#include "bev.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace rclane { namespace { constexpr double kFx = 3582.768775; constexpr double kFy = 3582.768775; constexpr double kCx = 960.0; constexpr double kCy = 540.0; constexpr double kRawWidth = 1920.0; constexpr double kRawHeight = 1080.0; constexpr double kModelWidth = 800.0; constexpr double kModelHeight = 320.0; constexpr double kCameraX = 1.0; constexpr double kCameraZ = 1.8; constexpr double kCosPitch = 0.997564; constexpr double kSinDown = 0.069756; constexpr double kPi = 3.14159265358979323846; bool solve_four_by_four( std::array, 4> matrix, std::array target, std::array& solution ) { for (int column = 0; column < 4; ++column) { int pivot = column; for (int row = column + 1; row < 4; ++row) { if (std::abs(matrix[row][column]) > std::abs(matrix[pivot][column])) { pivot = row; } } if (std::abs(matrix[pivot][column]) < 1e-12) { return false; } std::swap(matrix[pivot], matrix[column]); std::swap(target[pivot], target[column]); const double divisor = matrix[column][column]; for (int index = column; index < 4; ++index) { matrix[column][index] /= divisor; } target[column] /= divisor; for (int row = 0; row < 4; ++row) { if (row == column) { continue; } const double factor = matrix[row][column]; for (int index = column; index < 4; ++index) { matrix[row][index] -= factor * matrix[column][index]; } target[row] -= factor * target[column]; } } solution = target; return true; } bool weighted_fit( const std::vector& z, const std::vector& y, const std::vector& weights, const std::vector* include, std::array& coefficients ) { std::array, 4> normal{}; std::array target{}; for (std::size_t index = 0; index < z.size(); ++index) { if (include != nullptr && (*include)[index] == 0U) { continue; } const std::array row{1.0, z[index], z[index] * z[index], z[index] * z[index] * z[index]}; for (int i = 0; i < 4; ++i) { target[i] += weights[index] * row[i] * y[index]; for (int j = 0; j < 4; ++j) { normal[i][j] += weights[index] * row[i] * row[j]; } } } return solve_four_by_four(normal, target, coefficients); } double evaluate(const std::array& c, double x) { return ((c[3] * x + c[2]) * x + c[1]) * x + c[0]; } std::vector project_lane( const Lane& lane, const BevConfig& config ) { std::vector ground; ground.reserve(lane.points.size()); for (const auto& point : lane.points) { const double u = point.x * (kRawWidth / kModelWidth); const double v = point.y * (kRawHeight / kModelHeight); const double ray_right = (u - kCx) / kFx; const double ray_down = (v - kCy) / kFy; // OpenCV optical -> UE camera is (forward,right,up)=(1,x,-y), // followed by the fixed camera-to-vehicle pitch rotation. const double ray_x = kCosPitch - kSinDown * ray_down; const double ray_y = ray_right; const double ray_z = -kSinDown - kCosPitch * ray_down; if (!(ray_z < -1e-8)) { continue; } const double scale = -kCameraZ / ray_z; if (!(scale > 0.0) || !std::isfinite(scale)) { continue; } const double x = kCameraX + scale * ray_x; const double y_left = -scale * ray_y; if (std::isfinite(x) && std::isfinite(y_left) && x >= config.x_min && x <= config.x_max && y_left >= config.y_min && y_left <= config.y_max) { ground.push_back({x, y_left, point.score}); } } return ground; } CubicFit fit_cubic(const std::vector& input) { CubicFit fit; fit.point_count = input.size(); if (input.size() < 6) { return fit; } std::vector points = input; std::sort(points.begin(), points.end(), [](const GroundPoint& lhs, const GroundPoint& rhs) { return lhs.x < rhs.x; }); const double minimum_x = points.front().x; const double maximum_x = points.back().x; if (maximum_x - minimum_x < 1.0) { return fit; } const double center = (minimum_x + maximum_x) * 0.5; const double scale = std::max((maximum_x - minimum_x) * 0.5, 1.0); std::vector z(points.size()); std::vector y(points.size()); std::vector base(points.size()); for (std::size_t index = 0; index < points.size(); ++index) { z[index] = (points[index].x - center) / scale; y[index] = points[index].y; base[index] = std::clamp(points[index].score, 0.05, 1.0); } std::vector weights = base; std::array normalized{}; if (!weighted_fit(z, y, weights, nullptr, normalized)) { return fit; } for (int iteration = 0; iteration < 3; ++iteration) { for (std::size_t index = 0; index < points.size(); ++index) { const double residual = y[index] - evaluate(normalized, z[index]); const double robust = std::min( 1.0, 0.30 / std::max(std::abs(residual), 1e-8) ); weights[index] = base[index] * robust; } if (!weighted_fit(z, y, weights, nullptr, normalized)) { return fit; } } std::vector absolute_residual(points.size()); for (std::size_t index = 0; index < points.size(); ++index) { absolute_residual[index] = std::abs( y[index] - evaluate(normalized, z[index]) ); } std::vector sorted_residual = absolute_residual; std::sort(sorted_residual.begin(), sorted_residual.end()); const std::size_t middle = sorted_residual.size() / 2; const double median_residual = (sorted_residual.size() & 1U) != 0U ? sorted_residual[middle] : (sorted_residual[middle - 1] + sorted_residual[middle]) * 0.5; const double threshold = std::max(0.30, 2.5 * median_residual); std::vector inliers(points.size(), 0U); std::size_t inlier_count = 0; for (std::size_t index = 0; index < points.size(); ++index) { inliers[index] = static_cast( absolute_residual[index] <= threshold ); inlier_count += inliers[index]; } if (inlier_count >= 6) { if (!weighted_fit(z, y, base, &inliers, normalized)) { return fit; } } else { std::fill(inliers.begin(), inliers.end(), 1U); inlier_count = points.size(); } const double inverse_scale = 1.0 / scale; const double argument_constant = -center * inverse_scale; const double argument_linear = inverse_scale; fit.coefficients[0] = normalized[0] + normalized[1] * argument_constant + normalized[2] * argument_constant * argument_constant + normalized[3] * argument_constant * argument_constant * argument_constant; fit.coefficients[1] = normalized[1] * argument_linear + 2.0 * normalized[2] * argument_constant * argument_linear + 3.0 * normalized[3] * argument_constant * argument_constant * argument_linear; fit.coefficients[2] = normalized[2] * argument_linear * argument_linear + 3.0 * normalized[3] * argument_constant * argument_linear * argument_linear; fit.coefficients[3] = normalized[3] * argument_linear * argument_linear * argument_linear; fit.x_min = std::numeric_limits::infinity(); fit.x_max = -std::numeric_limits::infinity(); double square_error = 0.0; for (std::size_t index = 0; index < points.size(); ++index) { if (inliers[index] == 0U) { continue; } fit.x_min = std::min(fit.x_min, points[index].x); fit.x_max = std::max(fit.x_max, points[index].x); const double residual = points[index].y - evaluate(fit.coefficients, points[index].x); square_error += residual * residual; } fit.inlier_count = inlier_count; fit.rmse = std::sqrt(square_error / static_cast(inlier_count)); fit.valid = true; return fit; } double evaluate_derivative(const std::array& c, double x) { return (3.0 * c[3] * x + 2.0 * c[2]) * x + c[1]; } double median(std::vector values) { if (values.empty()) { return std::numeric_limits::quiet_NaN(); } std::sort(values.begin(), values.end()); const std::size_t middle = values.size() / 2U; return (values.size() & 1U) != 0U ? values[middle] : (values[middle - 1U] + values[middle]) * 0.5; } std::vector shared_samples( const CubicFit& left, const CubicFit& right, double sample_step ) { const double x_min = std::max(left.x_min, right.x_min); const double x_max = std::min(left.x_max, right.x_max); if (x_max - x_min < sample_step) { return {}; } const int count = std::max( 3, static_cast(std::ceil((x_max - x_min) / sample_step)) + 1 ); std::vector result(static_cast(count)); for (int index = 0; index < count; ++index) { result[static_cast(index)] = x_min + (x_max - x_min) * static_cast(index) / static_cast(count - 1); } return result; } double normal_gap(const CubicFit& left, const CubicFit& right, double x) { const double mean_slope = 0.5 * ( evaluate_derivative(left.coefficients, x) + evaluate_derivative(right.coefficients, x) ); return (evaluate(left.coefficients, x) - evaluate(right.coefficients, x)) / std::sqrt(1.0 + mean_slope * mean_slope); } struct FitView { int lane_id{}; double score{}; const CubicFit* fit{}; }; std::vector fit_views(const std::vector& lanes) { std::vector result; for (const BevLane& lane : lanes) { if (lane.fit_accepted && lane.fit.valid) { result.push_back({lane.lane_id, lane.score, &lane.fit}); } } std::sort(result.begin(), result.end(), [](const FitView& lhs, const FitView& rhs) { return lhs.lane_id < rhs.lane_id; }); return result; } struct TopologyAnalysis { std::vector trigger_pairs; bool all_positive{true}; }; TopologyAnalysis analyze_topology( const std::vector& ordered, double lane_width, const BevConfig& config ) { TopologyAnalysis result; for (std::size_t index = 1; index < ordered.size(); ++index) { const FitView& left = ordered[index - 1U]; const FitView& right = ordered[index]; const int lane_steps = right.lane_id - left.lane_id; if (lane_steps <= 0) { continue; } const auto x = shared_samples( *left.fit, *right.fit, config.sample_step ); if (x.size() < 3U) { continue; } const double expected_gap = lane_width * lane_steps; const double trigger_gap = std::max( config.minimum_gap, config.trigger_gap_ratio * expected_gap ); bool crossing = false; double longest_bad_run = 0.0; int bad_start = -1; for (std::size_t sample = 0; sample < x.size(); ++sample) { const double gap = normal_gap(*left.fit, *right.fit, x[sample]); if (!std::isfinite(gap)) { if (bad_start >= 0) { const int end = static_cast(sample) - 1; if (end > bad_start) { longest_bad_run = std::max( longest_bad_run, x[static_cast(end)] - x[static_cast(bad_start)] ); } bad_start = -1; } continue; } crossing = crossing || gap <= 0.0; result.all_positive = result.all_positive && gap > 0.0; const bool bad = gap < trigger_gap; if (bad && bad_start < 0) { bad_start = static_cast(sample); } if (bad_start >= 0 && (!bad || sample + 1U == x.size())) { const int end = bad && sample + 1U == x.size() ? static_cast(sample) : static_cast(sample) - 1; if (end > bad_start) { longest_bad_run = std::max( longest_bad_run, x[static_cast(end)] - x[static_cast(bad_start)] ); } bad_start = -1; } } if (crossing || longest_bad_run >= config.minimum_bad_run) { result.trigger_pairs.push_back( "P" + std::to_string(left.lane_id) + "-P" + std::to_string(right.lane_id) ); } } return result; } std::pair estimate_lane_width( const std::vector& ordered, const BevConfig& config ) { std::vector candidates; for (std::size_t index = 1; index < ordered.size(); ++index) { const FitView& left = ordered[index - 1U]; const FitView& right = ordered[index]; const int lane_steps = right.lane_id - left.lane_id; if (lane_steps <= 0) { continue; } const auto x = shared_samples( *left.fit, *right.fit, config.sample_step ); if (x.size() < 3U) { continue; } const std::size_t near_count = std::max( 3U, static_cast(std::ceil(x.size() * 0.40)) ); std::vector plausible; for (std::size_t sample = 0; sample < std::min(near_count, x.size()); ++sample) { const double gap = normal_gap( *left.fit, *right.fit, x[sample] ) / lane_steps; if (std::isfinite(gap) && gap >= 2.4 && gap <= 5.0) { plausible.push_back(gap); } } if (!plausible.empty()) { candidates.push_back(median(std::move(plausible))); } } if (candidates.empty()) { return {config.nominal_lane_width, "nominal"}; } return { std::clamp(median(std::move(candidates)), 2.6, 4.5), "near_range_median", }; } double shared_domain_support( const FitView& candidate, const std::vector& ordered ) { double support = 0.0; for (const FitView& other : ordered) { if (&other == &candidate) { continue; } support += std::max( 0.0, std::min(candidate.fit->x_max, other.fit->x_max) - std::max(candidate.fit->x_min, other.fit->x_min) ); } return support; } CubicFit parallel_offset_fit( const CubicFit& reference, const CubicFit& target, double offset, double sample_step ) { const double x_min = std::max(reference.x_min, target.x_min); const double x_max = std::min(reference.x_max, target.x_max); const double span = x_max - x_min; if (span < 1.0) { return {}; } const int count = std::max( 16, static_cast(std::ceil(span / sample_step)) + 1 ); std::vector points; points.reserve(static_cast(count)); for (int index = 0; index < count; ++index) { const double x = x_min + span * static_cast(index) / static_cast(count - 1); const double y = evaluate(reference.coefficients, x); const double slope = evaluate_derivative(reference.coefficients, x); const double norm = std::sqrt(1.0 + slope * slope); points.push_back({ x - offset * slope / norm, y + offset / norm, 1.0, }); } return fit_cubic(points); } std::vector views_from_fits( const std::vector& source, const std::map& fits ) { std::vector result; result.reserve(source.size()); for (const FitView& view : source) { const auto found = fits.find(view.lane_id); if (found != fits.end()) { result.push_back({view.lane_id, view.score, &found->second}); } } return result; } BevLane* find_lane(std::vector& lanes, int lane_id) { const auto found = std::find_if( lanes.begin(), lanes.end(), [lane_id](const BevLane& lane) { return lane.lane_id == lane_id; } ); return found == lanes.end() ? nullptr : &*found; } void repair_parallel_lanes( std::vector& lanes, const BevConfig& config, bool force, BevTopologyReport& report ) { const auto ordered = fit_views(lanes); report.forced = force; report.activation = force ? "always_parallel" : "triggered"; if (ordered.size() < 2U) { return; } const auto [lane_width, width_source] = estimate_lane_width( ordered, config ); report.lane_width_m = lane_width; report.lane_width_source = width_source; const auto before = analyze_topology(ordered, lane_width, config); report.trigger_pairs = before.trigger_pairs; if (before.trigger_pairs.empty() && !force) { return; } const double required_min = std::min_element( ordered.begin(), ordered.end(), [](const FitView& lhs, const FitView& rhs) { return lhs.fit->x_min < rhs.fit->x_min; } )->fit->x_min; const double required_max = std::max_element( ordered.begin(), ordered.end(), [](const FitView& lhs, const FitView& rhs) { return lhs.fit->x_max < rhs.fit->x_max; } )->fit->x_max; const auto uncovered = [required_min, required_max](const FitView& view) { return std::max( std::max(0.0, view.fit->x_min - required_min), std::max(0.0, required_max - view.fit->x_max) ); }; const FitView* reference = nullptr; if (force) { reference = &*std::max_element( ordered.begin(), ordered.end(), [&ordered](const FitView& lhs, const FitView& rhs) { return std::make_tuple( shared_domain_support(lhs, ordered), lhs.score, lhs.fit->x_max - lhs.fit->x_min, -lhs.lane_id ) < std::make_tuple( shared_domain_support(rhs, ordered), rhs.score, rhs.fit->x_max - rhs.fit->x_min, -rhs.lane_id ); } ); report.reference_selection = "maximum_shared_domain_then_confidence"; } else { std::vector coverage; for (const FitView& view : ordered) { if (uncovered(view) <= config.maximum_reference_extrapolation) { coverage.push_back(&view); } } if (!coverage.empty()) { reference = *std::max_element( coverage.begin(), coverage.end(), [](const FitView* lhs, const FitView* rhs) { return std::make_pair(lhs->score, -lhs->lane_id) < std::make_pair(rhs->score, -rhs->lane_id); } ); report.reference_selection = "highest_confidence_with_domain_coverage"; } else { reference = &*std::min_element( ordered.begin(), ordered.end(), [&uncovered](const FitView& lhs, const FitView& rhs) { return std::make_tuple( uncovered(lhs), -lhs.score, lhs.lane_id ) < std::make_tuple( uncovered(rhs), -rhs.score, rhs.lane_id ); } ); report.reference_selection = "best_domain_coverage_then_confidence"; } } std::map offsets; std::map repaired; bool normal_ok = true; for (const FitView& view : ordered) { const double offset = ( reference->lane_id - view.lane_id ) * lane_width; offsets[view.lane_id] = offset; if (view.lane_id == reference->lane_id) { repaired[view.lane_id] = *reference->fit; } else { CubicFit fit = parallel_offset_fit( *reference->fit, *view.fit, offset, config.sample_step ); if (!fit.valid) { normal_ok = false; break; } repaired[view.lane_id] = fit; } } report.method = "normal_offset_cubic"; TopologyAnalysis after; if (normal_ok) { after = analyze_topology( views_from_fits(ordered, repaired), lane_width, config ); } else { after.trigger_pairs.push_back("normal_offset_fit_failed"); } std::set passthrough; if (!after.trigger_pairs.empty()) { report.method = "shared_shape_vertical_offset_fallback"; repaired.clear(); for (const FitView& view : ordered) { CubicFit fit = *view.fit; fit.x_min = std::max(reference->fit->x_min, view.fit->x_min); fit.x_max = std::min(reference->fit->x_max, view.fit->x_max); if (fit.x_max - fit.x_min < 1.0) { fit = *view.fit; passthrough.insert(view.lane_id); } else { fit.coefficients = reference->fit->coefficients; fit.coefficients[0] += offsets[view.lane_id]; fit.rmse = 0.0; } repaired[view.lane_id] = fit; } after = analyze_topology( views_from_fits(ordered, repaired), lane_width, config ); } report.reference_lane = reference->lane_id; report.reference_score = reference->score; report.validation_passed = after.trigger_pairs.empty() && after.all_positive; if (!report.validation_passed) { report.failure = "repaired topology did not pass ordering validation"; return; } for (const FitView& view : ordered) { if (passthrough.count(view.lane_id) != 0U) { continue; } BevLane* lane = find_lane(lanes, view.lane_id); if (lane == nullptr) { continue; } lane->source_fit = lane->fit; lane->has_source_fit = true; lane->fit = repaired.at(view.lane_id); lane->parallel_repaired = true; lane->parallel_reference_lane = reference->lane_id; lane->parallel_offset_m = offsets.at(view.lane_id); lane->parallel_repair_method = report.method; } report.applied = true; } std::string role_for_lane(int lane_id) { switch (lane_id) { case 0: return "left_2"; case 1: return "ego_left"; case 2: return "ego_right"; case 3: return "right_2"; default: return "lane_" + std::to_string(lane_id); } } void complete_four_lanes( std::vector& lanes, const BevConfig& config, BevTopologyReport& report ) { const auto ordered = fit_views(lanes); report.forced = true; report.funnel_bypassed = true; report.activation = "complete_four_parallel"; report.reference_selection = "maximum_shared_domain_then_confidence"; report.validation_passed = false; if (ordered.empty()) { report.failure = "no valid measured BEV fit is available"; return; } double lane_width = config.nominal_lane_width; std::string width_source = "nominal_single_reference"; if (ordered.size() >= 2U) { std::tie(lane_width, width_source) = estimate_lane_width( ordered, config ); report.trigger_pairs = analyze_topology( ordered, lane_width, config ).trigger_pairs; } report.lane_width_m = lane_width; report.lane_width_source = width_source; const FitView& reference = *std::max_element( ordered.begin(), ordered.end(), [&ordered](const FitView& lhs, const FitView& rhs) { return std::make_tuple( shared_domain_support(lhs, ordered), lhs.score, lhs.fit->x_max - lhs.fit->x_min, -lhs.lane_id ) < std::make_tuple( shared_domain_support(rhs, ordered), rhs.score, rhs.fit->x_max - rhs.fit->x_min, -rhs.lane_id ); } ); std::map completed; std::map offsets; bool normal_ok = true; for (int lane_id = 0; lane_id < 4; ++lane_id) { const double offset = (reference.lane_id - lane_id) * lane_width; offsets[lane_id] = offset; if (lane_id == reference.lane_id) { completed[lane_id] = *reference.fit; } else { CubicFit fit = parallel_offset_fit( *reference.fit, *reference.fit, offset, config.sample_step ); if (!fit.valid) { normal_ok = false; break; } completed[lane_id] = fit; } } std::vector completed_views; const auto rebuild_completed_views = [&]() { completed_views.clear(); for (int lane_id = 0; lane_id < 4; ++lane_id) { completed_views.push_back({ lane_id, 1.0, &completed.at(lane_id) }); } }; report.method = "normal_offset_cubic"; TopologyAnalysis after; if (normal_ok) { rebuild_completed_views(); after = analyze_topology(completed_views, lane_width, config); } else { after.trigger_pairs.push_back("normal_offset_fit_failed"); } if (!after.trigger_pairs.empty()) { report.method = "shared_shape_vertical_offset_fallback"; completed.clear(); for (int lane_id = 0; lane_id < 4; ++lane_id) { CubicFit fit = *reference.fit; fit.coefficients[0] += offsets[lane_id]; fit.rmse = 0.0; completed[lane_id] = fit; } rebuild_completed_views(); after = analyze_topology(completed_views, lane_width, config); } report.validation_passed = after.trigger_pairs.empty() && after.all_positive; report.reference_lane = reference.lane_id; report.reference_score = reference.score; if (!report.validation_passed) { report.failure = "completed topology did not pass ordering validation"; return; } std::vector output; output.reserve(4U); for (int lane_id = 0; lane_id < 4; ++lane_id) { const BevLane* measured = nullptr; for (const BevLane& lane : lanes) { if (lane.lane_id == lane_id && lane.fit_accepted) { measured = &lane; break; } } BevLane lane; if (measured != nullptr) { lane = *measured; lane.source_fit = measured->fit; lane.has_source_fit = true; } else { lane.lane_id = lane_id; lane.role = role_for_lane(lane_id); lane.score = 0.0; lane.synthetic = true; report.synthetic_lane_ids.push_back(lane_id); } lane.fit = completed.at(lane_id); lane.fit_accepted = true; lane.funnel_clipped = false; lane.parallel_repaired = true; lane.parallel_reference_lane = reference.lane_id; lane.parallel_offset_m = offsets.at(lane_id); lane.parallel_repair_method = report.method; output.push_back(std::move(lane)); } lanes = std::move(output); report.applied = true; } bool clip_to_funnel(CubicFit& fit, double margin, bool& clipped) { clipped = false; if (!fit.valid || fit.x_max - fit.x_min < 1.0) { return false; } const int count = std::max( 3, static_cast(std::ceil((fit.x_max - fit.x_min) / 0.5)) + 1 ); std::vector x(static_cast(count)); std::vector valid(static_cast(count), 0U); const double tangent = std::tan(15.0 * kPi / 180.0); for (int index = 0; index < count; ++index) { x[static_cast(index)] = fit.x_min + (fit.x_max - fit.x_min) * static_cast(index) / static_cast(count - 1); const double y = evaluate(fit.coefficients, x[static_cast(index)]); const double half_width = std::max( 0.0, x[static_cast(index)] - kCameraX ) * tangent; valid[static_cast(index)] = static_cast( std::isfinite(y) && x[static_cast(index)] >= kCameraX && std::abs(y) <= half_width + margin ); } int best_start = -1; int best_end = -1; int start = -1; for (int index = 0; index < count; ++index) { if (valid[static_cast(index)] != 0U && start < 0) { start = index; } if (start >= 0 && (valid[static_cast(index)] == 0U || index == count - 1)) { const int end = valid[static_cast(index)] != 0U && index == count - 1 ? index : index - 1; if (x[static_cast(end)] - x[static_cast(start)] >= 1.0 && (best_start < 0 || x[static_cast(end)] - x[static_cast(start)] > x[static_cast(best_end)] - x[static_cast(best_start)])) { best_start = start; best_end = end; } start = -1; } } if (best_start < 0) { return false; } const double original_min = fit.x_min; const double original_max = fit.x_max; fit.x_min = x[static_cast(best_start)]; fit.x_max = x[static_cast(best_end)]; clipped = fit.x_min > original_min + 1e-6 || fit.x_max < original_max - 1e-6; return true; } } // namespace const char* bev_mode_name(BevMode mode) { switch (mode) { case BevMode::Raw: return "raw"; case BevMode::Trigger: return "trigger"; case BevMode::AlwaysParallel: return "always-parallel"; case BevMode::CompleteFour: return "complete-four"; } throw std::invalid_argument("unknown BEV mode enum"); } BevMode parse_bev_mode(const std::string& value) { if (value == "raw" || value == "off" || value == "none") { return BevMode::Raw; } if (value == "trigger" || value == "triggered") { return BevMode::Trigger; } if (value == "always-parallel" || value == "always_parallel") { return BevMode::AlwaysParallel; } if (value == "complete-four" || value == "complete_four" || value == "complete4") { return BevMode::CompleteFour; } throw std::invalid_argument( "invalid --bev-mode '" + value + "' (expected raw, trigger, always-parallel, or complete-four)" ); } void apply_bev_mode( std::vector& lanes, const BevConfig& config, BevTopologyReport* topology ) { BevTopologyReport report; report.mode = config.mode; report.lane_width_m = config.nominal_lane_width; for (BevLane& lane : lanes) { lane.funnel_clipped = false; lane.parallel_repaired = false; lane.synthetic = false; lane.has_source_fit = false; lane.parallel_reference_lane = -1; lane.parallel_offset_m = 0.0; lane.parallel_repair_method.clear(); } switch (config.mode) { case BevMode::Raw: report.activation = "disabled"; break; case BevMode::Trigger: repair_parallel_lanes(lanes, config, false, report); break; case BevMode::AlwaysParallel: repair_parallel_lanes(lanes, config, true, report); break; case BevMode::CompleteFour: complete_four_lanes(lanes, config, report); break; } if (config.mode != BevMode::CompleteFour) { for (BevLane& lane : lanes) { if (!lane.fit_accepted) { continue; } lane.fit_accepted = clip_to_funnel( lane.fit, config.funnel_margin, lane.funnel_clipped ); } } if (topology != nullptr) { *topology = std::move(report); } } std::vector project_lanes_to_bev( const std::vector& lanes, const BevConfig& config, BevTopologyReport* topology ) { std::vector result; result.reserve(lanes.size()); for (const Lane& lane : lanes) { BevLane output; output.lane_id = lane.lane_id; output.role = lane.role; output.score = lane.score(); output.points = project_lane(lane, config); output.fit = fit_cubic(output.points); output.fit_accepted = output.fit.valid && output.fit.rmse <= config.maximum_rmse; result.push_back(std::move(output)); } apply_bev_mode(result, config, topology); return result; } std::string bev_topology_json(const BevTopologyReport& topology) { std::ostringstream stream; stream << std::setprecision(12) << "{\"mode\":\"" << bev_mode_name(topology.mode) << "\"" << ",\"parallel_assumption\":" << (topology.mode == BevMode::Raw ? "false" : "true") << ",\"applied\":" << (topology.applied ? "true" : "false") << ",\"forced\":" << (topology.forced ? "true" : "false") << ",\"validation_passed\":" << (topology.validation_passed ? "true" : "false") << ",\"funnel_bypassed\":" << (topology.funnel_bypassed ? "true" : "false") << ",\"activation\":\"" << topology.activation << "\"" << ",\"lane_width_m\":" << topology.lane_width_m << ",\"lane_width_source\":\"" << topology.lane_width_source << "\"" << ",\"reference_lane\":"; if (topology.reference_lane < 0) { stream << "null"; } else { stream << topology.reference_lane; } stream << ",\"reference_score\":"; if (topology.reference_lane < 0) { stream << "null"; } else { stream << topology.reference_score; } stream << ",\"reference_selection\":\"" << topology.reference_selection << "\"" << ",\"method\":\"" << topology.method << "\"" << ",\"trigger_pairs\":["; for (std::size_t index = 0; index < topology.trigger_pairs.size(); ++index) { if (index != 0U) { stream << ','; } stream << '\"' << topology.trigger_pairs[index] << '\"'; } stream << "],\"synthetic_lane_ids\":["; for (std::size_t index = 0; index < topology.synthetic_lane_ids.size(); ++index) { if (index != 0U) { stream << ','; } stream << topology.synthetic_lane_ids[index]; } stream << "],\"failure\":\"" << topology.failure << "\"}"; return stream.str(); } void write_bev_json( const std::string& path, const std::vector& lanes, const BevTopologyReport* topology ) { std::ofstream stream(path); if (!stream) { throw std::runtime_error("cannot write BEV JSON: " + path); } BevTopologyReport raw_report; const BevTopologyReport& report = topology == nullptr ? raw_report : *topology; stream << std::setprecision(12) << "{\n \"mode\": \"" << bev_mode_name(report.mode) << "\",\n" << " \"parallel_assumption\": " << (report.mode == BevMode::Raw ? "false" : "true") << ",\n" << " \"synthetic_lanes\": " << (report.synthetic_lane_ids.empty() ? "false" : "true") << ",\n \"topology\": " << bev_topology_json(report) << ",\n \"lanes\": [\n"; for (std::size_t index = 0; index < lanes.size(); ++index) { const BevLane& lane = lanes[index]; stream << " {\"lane_id\": " << lane.lane_id << ", \"role\": \"" << lane.role << "\", \"score\": " << lane.score << ", \"projected_point_count\": " << lane.points.size() << ", \"valid_fit\": " << (lane.fit_accepted ? "true" : "false") << ", \"synthetic\": " << (lane.synthetic ? "true" : "false") << ", \"parallel_repaired\": " << (lane.parallel_repaired ? "true" : "false"); if (lane.parallel_repaired) { stream << ", \"parallel_reference_lane\": " << lane.parallel_reference_lane << ", \"parallel_offset_m\": " << lane.parallel_offset_m << ", \"parallel_repair_method\": \"" << lane.parallel_repair_method << "\""; } if (lane.fit.valid) { stream << ", \"coefficients_c0_to_c3\": [" << lane.fit.coefficients[0] << ',' << lane.fit.coefficients[1] << ',' << lane.fit.coefficients[2] << ',' << lane.fit.coefficients[3] << "], \"x_domain_m\": [" << lane.fit.x_min << ',' << lane.fit.x_max << "], \"rmse_m\": " << lane.fit.rmse << ", \"inlier_count\": " << lane.fit.inlier_count << ", \"funnel_clipped\": " << (lane.funnel_clipped ? "true" : "false"); } if (lane.has_source_fit) { stream << ", \"source_coefficients_c0_to_c3\": [" << lane.source_fit.coefficients[0] << ',' << lane.source_fit.coefficients[1] << ',' << lane.source_fit.coefficients[2] << ',' << lane.source_fit.coefficients[3] << ']'; } stream << '}' << (index + 1 == lanes.size() ? "\n" : ",\n"); } stream << " ]\n}\n"; } } // namespace rclane