#include "decoder.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace rclane { namespace { constexpr int kMapWidth = 800; constexpr int kMapHeight = 320; struct Seed { int x{}; int y{}; float probability{}; }; std::vector numpy_float_argquicksort( const std::vector& candidates ) { // Partition structure follows NumPy's BSD-licensed npysort aquicksort; // see cpp/THIRD_PARTY_NOTICES.md. // Match NumPy 1.26's default np.argsort quicksort, including its // deterministic (but unstable) ordering of equal saturated probabilities. // This matters because the segmentation map contains long probability=1 // plateaus and greedy point-NMS consumes candidates in argsort order. std::vector order(candidates.size()); std::iota(order.begin(), order.end(), std::size_t{0}); if (order.size() <= 1) { return order; } const auto key = [&candidates](std::size_t index) { return -candidates[index].probability; }; const auto less = [&key](std::size_t lhs, std::size_t rhs) { return key(lhs) < key(rhs); }; struct Partition { std::ptrdiff_t left{}; std::ptrdiff_t right{}; int depth{}; }; std::vector stack; stack.reserve(128); std::ptrdiff_t left = 0; std::ptrdiff_t right = static_cast(order.size() - 1); int most_significant_bit = 0; for (std::size_t size = order.size(); size > 1; size >>= 1U) { ++most_significant_bit; } int depth = most_significant_bit * 2; for (;;) { if (depth < 0) { // NumPy switches to arg-heapsort here. This branch is not reached // by the map sizes/distributions used by RCLane; retain a safe // deterministic fallback for adversarial inputs. std::sort( order.begin() + left, order.begin() + right + 1, less ); goto pop_partition; } while (right - left > 15) { const std::ptrdiff_t middle = left + ((right - left) >> 1); if (less(order[static_cast(middle)], order[static_cast(left)])) { std::swap(order[static_cast(middle)], order[static_cast(left)]); } if (less(order[static_cast(right)], order[static_cast(middle)])) { std::swap(order[static_cast(right)], order[static_cast(middle)]); } if (less(order[static_cast(middle)], order[static_cast(left)])) { std::swap(order[static_cast(middle)], order[static_cast(left)]); } const float pivot = key(order[static_cast(middle)]); std::ptrdiff_t i = left; std::ptrdiff_t j = right - 1; std::swap(order[static_cast(middle)], order[static_cast(j)]); for (;;) { do { ++i; } while (key(order[static_cast(i)]) < pivot); do { --j; } while (pivot < key(order[static_cast(j)])); if (i >= j) { break; } std::swap(order[static_cast(i)], order[static_cast(j)]); } std::swap(order[static_cast(i)], order[static_cast(right - 1)]); --depth; if (i - left < right - i) { stack.push_back({i + 1, right, depth}); right = i - 1; } else { stack.push_back({left, i - 1, depth}); left = i + 1; } } for (std::ptrdiff_t i = left + 1; i <= right; ++i) { const std::size_t value = order[static_cast(i)]; std::ptrdiff_t position = i; std::ptrdiff_t previous = i - 1; while (position > left && key(value) < key(order[static_cast(previous)])) { order[static_cast(position--)] = order[static_cast(previous--)]; } order[static_cast(position)] = value; } pop_partition: if (stack.empty()) { break; } const Partition next = stack.back(); stack.pop_back(); left = next.left; right = next.right; depth = next.depth; } return order; } float median(std::vector values) { if (values.empty()) { return 0.0F; } const std::size_t middle = values.size() / 2; std::nth_element(values.begin(), values.begin() + middle, values.end()); const float upper = values[middle]; if ((values.size() & 1U) != 0U) { return upper; } const float lower = *std::max_element( values.begin(), values.begin() + middle ); return (lower + upper) * 0.5F; } double quantile(std::vector values, double fraction) { if (values.empty()) { return std::numeric_limits::quiet_NaN(); } std::sort(values.begin(), values.end()); const double position = fraction * static_cast(values.size() - 1); const auto lower = static_cast(std::floor(position)); const auto upper = static_cast(std::ceil(position)); const double blend = position - static_cast(lower); return static_cast(values[lower]) * (1.0 - blend) + static_cast(values[upper]) * blend; } std::vector select_seeds( const std::vector& probability, const DecoderConfig& config, DecodeStatistics* statistics ) { std::vector candidates; candidates.reserve(probability.size() / 20); for (int y = 0; y < kMapHeight; ++y) { for (int x = 0; x < kMapWidth; ++x) { const float value = probability[static_cast( y * kMapWidth + x )]; if (value > config.seed_threshold) { candidates.push_back({x, y, value}); } } } if (statistics != nullptr) { statistics->foreground_pixels = candidates.size(); } const auto order = numpy_float_argquicksort(candidates); std::vector taken( static_cast(kMapWidth * kMapHeight), 0 ); std::vector selected; selected.reserve(static_cast(config.max_seeds)); for (const std::size_t candidate_index : order) { const Seed& candidate = candidates[candidate_index]; const std::size_t position = static_cast( candidate.y * kMapWidth + candidate.x ); if (taken[position] != 0U) { continue; } selected.push_back(candidate); const int y0 = std::max(0, candidate.y - config.seed_min_distance); const int y1 = std::min( kMapHeight - 1, candidate.y + config.seed_min_distance ); const int x0 = std::max(0, candidate.x - config.seed_min_distance); const int x1 = std::min( kMapWidth - 1, candidate.x + config.seed_min_distance ); for (int y = y0; y <= y1; ++y) { for (int x = x0; x <= x1; ++x) { taken[static_cast(y * kMapWidth + x)] = 1U; } } if (static_cast(selected.size()) >= config.max_seeds) { break; } } if (statistics != nullptr) { statistics->seeds = selected.size(); } return selected; } std::vector crawl( const Seed& seed, const std::vector& probability, const std::vector& arrow, const std::vector& bound, const DecoderConfig& config ) { std::vector points; points.reserve(48); int cx = seed.x; int cy = seed.y; double remain_square_sum = 0.0; int remain_count = 0; const std::size_t channel_elements = static_cast( kMapWidth * kMapHeight ); for (int index = 0; index < kMapHeight; ++index) { const std::size_t current = static_cast( cy * kMapWidth + cx ); if (probability[current] > config.segmentation_threshold) { const double remain = static_cast(bound[current]) * 100.0 / static_cast(config.step_length) + static_cast(index); remain_square_sum += remain * remain; ++remain_count; } const float dx = arrow[current]; const float dy = arrow[channel_elements + current]; const float norm = std::sqrt(dx * dx + dy * dy); if (norm == 0.0F || !std::isfinite(norm)) { break; } cx = static_cast(std::floor( static_cast(cx) + dx / norm * config.step_length )); cy = static_cast(std::floor( static_cast(cy) + dy / norm * config.step_length )); if (cx < 0 || cx >= kMapWidth || cy < 0 || cy >= kMapHeight) { break; } const float score = probability[static_cast( cy * kMapWidth + cx )]; points.push_back({static_cast(cx), static_cast(cy), score}); const double remaining = remain_count > 0 ? std::sqrt(remain_square_sum / static_cast(remain_count)) : 1.0; if (score > config.segmentation_threshold) { continue; } if (static_cast(index) > remaining * 0.75) { break; } } return points; } double reference_x(const Lane& lane) { if (lane.points.empty()) { return std::numeric_limits::infinity(); } if (lane.points.size() < 2) { return lane.points.front().x; } std::vector y_values; y_values.reserve(lane.points.size()); for (const auto& point : lane.points) { if (std::isfinite(point.x) && std::isfinite(point.y)) { y_values.push_back(point.y); } } if (y_values.size() < 2) { return lane.points.front().x; } const double cutoff = quantile(y_values, 0.6); double sum_x = 0.0; double sum_y = 0.0; double min_y = std::numeric_limits::infinity(); double max_y = -std::numeric_limits::infinity(); std::vector lower; for (const auto& point : lane.points) { if (std::isfinite(point.x) && std::isfinite(point.y) && static_cast(point.y) >= cutoff) { lower.push_back(&point); sum_x += point.x; sum_y += point.y; min_y = std::min(min_y, static_cast(point.y)); max_y = std::max(max_y, static_cast(point.y)); } } const auto bottom_x = [&lane]() { return static_cast(std::max_element( lane.points.begin(), lane.points.end(), [](const LanePoint& lhs, const LanePoint& rhs) { return lhs.y < rhs.y; } )->x); }; if (lower.size() < 2 || max_y - min_y < 1.0) { return bottom_x(); } const double mean_x = sum_x / static_cast(lower.size()); const double mean_y = sum_y / static_cast(lower.size()); double numerator = 0.0; double denominator = 0.0; for (const auto* point : lower) { const double centered_y = static_cast(point->y) - mean_y; numerator += centered_y * (static_cast(point->x) - mean_x); denominator += centered_y * centered_y; } if (denominator <= 1e-6) { return bottom_x(); } return mean_x + numerator / denominator * (static_cast(lane.height - 1) - mean_y); } std::vector preselect_candidates( const std::vector& lanes, const std::vector& score_order, int max_lanes ) { if (static_cast(score_order.size()) <= max_lanes) { return score_order; } struct Bucket { int key{}; std::vector indices; }; std::vector buckets; for (const int index : score_order) { const Lane& lane = lanes[static_cast(index)]; std::vector y; y.reserve(lane.points.size()); for (const auto& point : lane.points) { y.push_back(point.y); } const float median_y = median(std::move(y)); std::vector lower_x; for (const auto& point : lane.points) { if (point.y >= median_y) { lower_x.push_back(point.x); } } const int key = static_cast(std::floor(median(lower_x) / 16.0F)); auto found = std::find_if( buckets.begin(), buckets.end(), [key](const Bucket& bucket) { return bucket.key == key; } ); if (found == buckets.end()) { buckets.push_back({key, {index}}); } else { found->indices.push_back(index); } } std::vector selected; selected.reserve(static_cast(max_lanes)); for (std::size_t rank = 0; static_cast(selected.size()) < max_lanes; ++rank) { bool progressed = false; for (const auto& bucket : buckets) { if (rank < bucket.indices.size()) { selected.push_back(bucket.indices[rank]); progressed = true; if (static_cast(selected.size()) == max_lanes) { break; } } } if (!progressed) { break; } } std::stable_sort(selected.begin(), selected.end(), [&lanes](int lhs, int rhs) { return lanes[static_cast(lhs)].score() > lanes[static_cast(rhs)].score(); }); return selected; } void draw_disk( std::vector& mask, int width, int height, int cx, int cy, int radius ) { for (int y = std::max(0, cy - radius); y <= std::min(height - 1, cy + radius); ++y) { for (int x = std::max(0, cx - radius); x <= std::min(width - 1, cx + radius); ++x) { const int dx = x - cx; const int dy = y - cy; if (dx * dx + dy * dy <= radius * radius) { mask[static_cast(y * width + x)] = 1U; } } } } std::vector rasterize( const Lane& lane, float scale, int lane_width, int width, int height ) { std::vector pixels( static_cast(width * height), 0U ); if (lane.points.size() < 2) { return std::vector( (pixels.size() + 63U) / 64U, 0U ); } const int thickness = std::max(1, static_cast(std::lround( static_cast(lane_width) * scale ))); const int radius = std::max(1, thickness / 2); for (std::size_t index = 1; index < lane.points.size(); ++index) { int x0 = std::clamp(static_cast(lane.points[index - 1].x * scale), 0, width - 1); int y0 = std::clamp(static_cast(lane.points[index - 1].y * scale), 0, height - 1); const int x1 = std::clamp(static_cast(lane.points[index].x * scale), 0, width - 1); const int y1 = std::clamp(static_cast(lane.points[index].y * scale), 0, height - 1); const int dx = std::abs(x1 - x0); const int sx = x0 < x1 ? 1 : -1; const int dy = -std::abs(y1 - y0); const int sy = y0 < y1 ? 1 : -1; int error = dx + dy; for (;;) { draw_disk(pixels, width, height, x0, y0, radius); if (x0 == x1 && y0 == y1) { break; } const int twice = 2 * error; if (twice >= dy) { error += dy; x0 += sx; } if (twice <= dx) { error += dx; y0 += sy; } } } std::vector mask((pixels.size() + 63U) / 64U, 0U); for (std::size_t index = 0; index < pixels.size(); ++index) { if (pixels[index] != 0U) { mask[index / 64U] |= std::uint64_t{1} << (index % 64U); } } return mask; } std::vector nms( std::vector lanes, const DecoderConfig& config, DecodeStatistics* statistics ) { std::vector order(lanes.size()); std::iota(order.begin(), order.end(), 0); std::stable_sort(order.begin(), order.end(), [&lanes](int lhs, int rhs) { return lanes[static_cast(lhs)].score() > lanes[static_cast(rhs)].score(); }); order = preselect_candidates(lanes, order, config.nms_max_lanes); if (statistics != nullptr) { statistics->nms_candidates = order.size(); } const int width = std::max(1, static_cast(std::lround( static_cast(kMapWidth) * config.nms_scale ))); const int height = std::max(1, static_cast(std::lround( static_cast(kMapHeight) * config.nms_scale ))); std::vector> masks; std::vector areas; masks.reserve(order.size()); areas.reserve(order.size()); for (const int index : order) { masks.push_back(rasterize( lanes[static_cast(index)], config.nms_scale, config.lane_width, width, height )); int area = 0; for (const std::uint64_t word : masks.back()) { area += __builtin_popcountll(word); } areas.push_back(area); } std::vector suppressed(order.size(), 0U); std::vector kept; for (std::size_t i = 0; i < order.size(); ++i) { if (suppressed[i] != 0U) { continue; } kept.push_back(std::move(lanes[static_cast(order[i])])) ; for (std::size_t j = i + 1; j < order.size(); ++j) { if (suppressed[j] != 0U) { continue; } int intersection = 0; for (std::size_t word = 0; word < masks[i].size(); ++word) { intersection += __builtin_popcountll( masks[i][word] & masks[j][word] ); } const int union_area = areas[i] + areas[j] - intersection; if (union_area > 0 && static_cast(intersection) / union_area >= config.iou_threshold) { suppressed[j] = 1U; } } } if (statistics != nullptr) { statistics->nms_survivors = kept.size(); } return kept; } void assign_roles(std::vector& lanes, double ego_x) { std::vector left; std::vector right; std::unordered_map references; for (auto& lane : lanes) { lane.lane_id = std::numeric_limits::min(); lane.role.clear(); lane.ego_boundary = false; lane.lateral_rank = 0; references[&lane] = reference_x(lane); (references[&lane] < ego_x ? left : right).push_back(&lane); } const auto near_ego = [&references, ego_x](const Lane* lhs, const Lane* rhs) { const double dl = std::abs(references[lhs] - ego_x); const double dr = std::abs(references[rhs] - ego_x); return dl != dr ? dl < dr : lhs->score() > rhs->score(); }; std::sort(left.begin(), left.end(), near_ego); std::sort(right.begin(), right.end(), near_ego); for (std::size_t index = 0; index < left.size(); ++index) { const int rank = static_cast(index + 1); left[index]->lane_id = 2 - rank; left[index]->lateral_rank = -rank; left[index]->ego_boundary = rank == 1; left[index]->role = rank == 1 ? "ego_left" : "left_" + std::to_string(rank); } for (std::size_t index = 0; index < right.size(); ++index) { const int rank = static_cast(index + 1); right[index]->lane_id = 1 + rank; right[index]->lateral_rank = rank; right[index]->ego_boundary = rank == 1; right[index]->role = rank == 1 ? "ego_right" : "right_" + std::to_string(rank); } std::sort(lanes.begin(), lanes.end(), [](const Lane& lhs, const Lane& rhs) { return lhs.lane_id < rhs.lane_id; }); } std::vector select_ego_lanes( std::vector lanes, const DecoderConfig& config ) { if (lanes.empty()) { return {}; } std::vector pool(lanes.size()); std::iota(pool.begin(), pool.end(), 0); std::vector references(lanes.size()); for (std::size_t index = 0; index < lanes.size(); ++index) { references[index] = reference_x(lanes[index]); } if (static_cast(lanes.size()) > config.max_output_lanes) { const double best = std::max_element( lanes.begin(), lanes.end(), [](const Lane& lhs, const Lane& rhs) { return lhs.score() < rhs.score(); } )->score(); std::vector reliable; for (const int index : pool) { if (lanes[static_cast(index)].score() >= best * config.ego_min_score_ratio) { reliable.push_back(index); } } if (static_cast(reliable.size()) >= config.max_output_lanes) { pool = std::move(reliable); } } const auto proximity = [&lanes, &references, &config](int lhs, int rhs) { const double dl = std::abs(references[static_cast(lhs)] - config.ego_x); const double dr = std::abs(references[static_cast(rhs)] - config.ego_x); return dl != dr ? dl < dr : lanes[static_cast(lhs)].score() > lanes[static_cast(rhs)].score(); }; std::vector left; std::vector right; for (const int index : pool) { (references[static_cast(index)] < config.ego_x ? left : right).push_back(index); } std::sort(left.begin(), left.end(), proximity); std::sort(right.begin(), right.end(), proximity); std::vector selected; if (config.max_output_lanes == 4) { selected.insert(selected.end(), left.begin(), left.begin() + std::min(2, left.size())); selected.insert(selected.end(), right.begin(), right.begin() + std::min(2, right.size())); } else { std::sort(pool.begin(), pool.end(), proximity); pool.resize(std::min(pool.size(), static_cast(config.max_output_lanes))); selected = std::move(pool); } std::vector result; result.reserve(selected.size()); for (const int index : selected) { result.push_back(std::move(lanes[static_cast(index)])); } assign_roles(result, config.ego_x); return result; } const Tensor& require_output( const std::unordered_map& outputs, const std::string& name ) { const auto found = outputs.find(name); if (found == outputs.end()) { throw std::runtime_error("missing TensorRT output: " + name); } return found->second; } } // namespace double Lane::score() const { return points.empty() ? 0.0 : score_sum / static_cast(points.size()); } std::vector softmax_foreground(const Tensor& logits) { const std::size_t plane = static_cast(kMapWidth * kMapHeight); if (logits.values.size() != plane * 2) { throw std::invalid_argument("seg_map must have shape (1,2,320,800)"); } std::vector probability(plane); #pragma omp parallel for schedule(static) for (std::int64_t index = 0; index < static_cast(plane); ++index) { const float difference = logits.values[static_cast(index)] - logits.values[plane + static_cast(index)]; probability[static_cast(index)] = 1.0F / (1.0F + std::exp(difference)); } return probability; } std::vector decode( const std::vector& probability, const Tensor& up_arrow, const Tensor& down_arrow, const Tensor& up_bound, const Tensor& down_bound, const DecoderConfig& config, DecodeStatistics* statistics ) { const std::size_t plane = static_cast(kMapWidth * kMapHeight); if (probability.size() != plane || up_arrow.values.size() != plane * 2 || down_arrow.values.size() != plane * 2 || up_bound.values.size() != plane * 2 || down_bound.values.size() != plane * 2) { throw std::invalid_argument("decoder map shape mismatch"); } if (statistics != nullptr) { *statistics = {}; } omp_set_num_threads(config.threads); const auto seeds = select_seeds(probability, config, statistics); std::vector> up(seeds.size()); std::vector> down(seeds.size()); #pragma omp parallel for schedule(static) for (std::int64_t index = 0; index < static_cast(seeds.size()); ++index) { up[static_cast(index)] = crawl( seeds[static_cast(index)], probability, up_arrow.values, up_bound.values, config ); down[static_cast(index)] = crawl( seeds[static_cast(index)], probability, down_arrow.values, down_bound.values, config ); } std::vector candidates; candidates.reserve(seeds.size()); for (std::size_t index = 0; index < seeds.size(); ++index) { const std::size_t count = up[index].size() + down[index].size(); if (count <= 1) { continue; } Lane lane; lane.width = kMapWidth; lane.height = kMapHeight; lane.points.reserve(count); for (auto point = up[index].rbegin(); point != up[index].rend(); ++point) { lane.points.push_back(*point); lane.score_sum += point->score; } for (const auto& point : down[index]) { lane.points.push_back(point); lane.score_sum += point.score; } if (lane.score() >= config.score_threshold) { candidates.push_back(std::move(lane)); } } if (statistics != nullptr) { statistics->crawled_candidates = candidates.size(); } auto kept = nms(std::move(candidates), config, statistics); return select_ego_lanes(std::move(kept), config); } std::vector decode_outputs( const std::unordered_map& outputs, const DecoderConfig& config, DecodeStatistics* statistics ) { const auto probability = softmax_foreground(require_output(outputs, "seg_map")); return decode( probability, require_output(outputs, "up_arrow"), require_output(outputs, "down_arrow"), require_output(outputs, "up_bound"), require_output(outputs, "down_bound"), config, statistics ); } void write_lanes_json( const std::string& path, const std::vector& lanes, const DecodeStatistics* statistics ) { std::ofstream stream(path); if (!stream) { throw std::runtime_error("cannot write lanes JSON: " + path); } stream << std::setprecision(9) << "{\n"; if (statistics != nullptr) { stream << " \"statistics\": {\"foreground_pixels\": " << statistics->foreground_pixels << ", \"seeds\": " << statistics->seeds << ", \"crawled_candidates\": " << statistics->crawled_candidates << ", \"nms_candidates\": " << statistics->nms_candidates << ", \"nms_survivors\": " << statistics->nms_survivors << "},\n"; } stream << " \"lanes\": [\n"; for (std::size_t lane_index = 0; lane_index < lanes.size(); ++lane_index) { const Lane& lane = lanes[lane_index]; stream << " {\"lane_id\": " << lane.lane_id << ", \"role\": \"" << lane.role << "\", \"score\": " << lane.score() << ", \"points\": ["; for (std::size_t point_index = 0; point_index < lane.points.size(); ++point_index) { const auto& point = lane.points[point_index]; if (point_index != 0) { stream << ','; } stream << '[' << point.x << ',' << point.y << ',' << point.score << ']'; } stream << "]}" << (lane_index + 1 == lanes.size() ? "\n" : ",\n"); } stream << " ]\n}\n"; } } // namespace rclane