Spaces:
Configuration error
Configuration error
Commit ·
3624d0b
1
Parent(s): 73a2d91
feat(RCLane): switch to C++ for runtime modeling
Browse files- .gitignore +8 -0
- README.md +24 -0
- benchmark_realtime.py +216 -0
- bev.py +361 -0
- build_tensorrt_engine.py +133 -0
- cpp/CMakeLists.txt +67 -0
- cpp/README.md +73 -0
- cpp/THIRD_PARTY_NOTICES.md +19 -0
- cpp/include/bev.hpp +57 -0
- cpp/include/decoder.hpp +80 -0
- cpp/include/preprocess.hpp +15 -0
- cpp/include/tensorrt_runner.hpp +50 -0
- cpp/render_cpp_results.py +188 -0
- cpp/src/bev.cpp +352 -0
- cpp/src/decoder.cpp +796 -0
- cpp/src/main.cpp +528 -0
- cpp/src/preprocess.cpp +119 -0
- cpp/src/tensorrt_runner.cpp +313 -0
- dataset.py +17 -0
- decode.py +638 -36
- requirements.txt +3 -0
- test_video_bev_onnx.py +781 -0
- test_video_onnx.py +189 -41
.gitignore
CHANGED
|
@@ -19,6 +19,14 @@ checkpoints/
|
|
| 19 |
*.pt
|
| 20 |
*.onnx
|
| 21 |
*.ckpt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# python
|
| 24 |
__pycache__/
|
|
|
|
| 19 |
*.pt
|
| 20 |
*.onnx
|
| 21 |
*.ckpt
|
| 22 |
+
*.engine
|
| 23 |
+
*.profile
|
| 24 |
+
*.timing
|
| 25 |
+
exports/
|
| 26 |
+
cpp/build*/
|
| 27 |
+
|
| 28 |
+
# local videos / visualization outputs
|
| 29 |
+
*.mp4
|
| 30 |
|
| 31 |
# python
|
| 32 |
__pycache__/
|
README.md
CHANGED
|
@@ -84,8 +84,32 @@ python eval_checkpoints.py --data-root data/dataset \
|
|
| 84 |
# Export a checkpoint and verify its outputs with ONNX Runtime
|
| 85 |
python export_onnx.py --checkpoint checkpoints/rclane_b0_e19.pth \
|
| 86 |
--output exports/rclane_b0_e19.onnx --check-runtime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
```
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
For CUDA inference, use the CUDA 13 ONNX Runtime build pinned in
|
| 90 |
`requirements.txt`. Disable TF32 when exact lane decisions matter:
|
| 91 |
|
|
|
|
| 84 |
# Export a checkpoint and verify its outputs with ONNX Runtime
|
| 85 |
python export_onnx.py --checkpoint checkpoints/rclane_b0_e19.pth \
|
| 86 |
--output exports/rclane_b0_e19.onnx --check-runtime
|
| 87 |
+
|
| 88 |
+
# Build/cache a device-specific TensorRT FP16 engine and compare it to CUDA FP32
|
| 89 |
+
python build_tensorrt_engine.py --model exports/rclane_b0_e19.onnx \
|
| 90 |
+
--cache-dir exports/trt_cache
|
| 91 |
+
|
| 92 |
+
# Benchmark the sequential production core (preprocess + engine + 1024-seed
|
| 93 |
+
# decode + raw-model BEV) without visualization/video-I/O
|
| 94 |
+
python benchmark_realtime.py --model exports/rclane_b0_e19.onnx \
|
| 95 |
+
--video raw_Town04_Opt_20260714_093110.mp4 --provider tensorrt \
|
| 96 |
+
--trt-cache-dir exports/trt_cache --cpu-threads 8 --max-seeds 1024 \
|
| 97 |
+
--max-frames 300 --report runs/realtime_benchmark_1024seeds.json
|
| 98 |
+
|
| 99 |
+
# Render raw decoded lanes in BEV and export one cubic per detected lane.
|
| 100 |
+
# No lane is synthesized or forced parallel; cubics are clipped to camera FOV.
|
| 101 |
+
python test_video_bev_onnx.py --model exports/rclane_b0_e19.onnx \
|
| 102 |
+
--video raw_Town04_Opt_20260714_093110.mp4 --provider tensorrt \
|
| 103 |
+
--trt-cache-dir exports/trt_cache --decode-cpu-threads 8 \
|
| 104 |
+
--decode-max-seeds 1024 --output runs/video_bev_e19.mp4
|
| 105 |
```
|
| 106 |
|
| 107 |
+
TensorRT cache files are tied to the TensorRT version and GPU compute
|
| 108 |
+
capability. Rebuild the cache on a different deployment GPU. The real-time
|
| 109 |
+
benchmark deliberately reports camera/video acquisition and visualization
|
| 110 |
+
separately: its core latency is the latency relevant to the ADAS algorithm,
|
| 111 |
+
whereas rendering and MP4 encoding are offline diagnostics.
|
| 112 |
+
|
| 113 |
For CUDA inference, use the CUDA 13 ONNX Runtime build pinned in
|
| 114 |
`requirements.txt`. Disable TF32 when exact lane decisions matter:
|
| 115 |
|
benchmark_realtime.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Benchmark the optimized RCLane -> decode -> BEV core pipeline.
|
| 2 |
+
|
| 3 |
+
Rendering, video writing and source-frame acquisition are reported separately
|
| 4 |
+
from the sequential per-frame ADAS latency. Each frame completes inference,
|
| 5 |
+
decode and raw-model BEV projection before the next frame is processed.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from types import SimpleNamespace
|
| 14 |
+
|
| 15 |
+
import cv2
|
| 16 |
+
import numba
|
| 17 |
+
import numpy as np
|
| 18 |
+
|
| 19 |
+
from bev import BevRange, CameraCalibration
|
| 20 |
+
from dataset import normalize_image_numpy
|
| 21 |
+
from decode import decode, warmup_decode_backend
|
| 22 |
+
from test_video_bev_onnx import clip_lane_results_to_funnel, lane_to_record
|
| 23 |
+
from test_video_onnx import (
|
| 24 |
+
MODEL_HEIGHT,
|
| 25 |
+
MODEL_WIDTH,
|
| 26 |
+
OUTPUT_NAMES,
|
| 27 |
+
create_session,
|
| 28 |
+
softmax_foreground,
|
| 29 |
+
timing_summary,
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_args():
|
| 34 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 35 |
+
parser.add_argument("--model", required=True)
|
| 36 |
+
parser.add_argument("--video", required=True)
|
| 37 |
+
parser.add_argument("--provider", choices=("tensorrt", "cuda"),
|
| 38 |
+
default="tensorrt")
|
| 39 |
+
parser.add_argument("--trt-cache-dir", default="exports/trt_cache")
|
| 40 |
+
parser.add_argument("--start-frame", type=int, default=0)
|
| 41 |
+
parser.add_argument("--max-frames", type=int, default=300)
|
| 42 |
+
parser.add_argument("--cpu-threads", type=int, default=8)
|
| 43 |
+
parser.add_argument("--max-seeds", type=int, default=1024)
|
| 44 |
+
parser.add_argument("--report", default="runs/realtime_benchmark.json")
|
| 45 |
+
return parser.parse_args()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def projection_args():
|
| 49 |
+
return SimpleNamespace(funnel_margin=0.10)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def postprocess(outputs, calibration, bev_range, config, max_seeds):
|
| 53 |
+
started = time.perf_counter()
|
| 54 |
+
stage = time.perf_counter()
|
| 55 |
+
seg_prob = softmax_foreground(outputs[0])[0]
|
| 56 |
+
softmax_ms = (time.perf_counter() - stage) * 1000.0
|
| 57 |
+
stage = time.perf_counter()
|
| 58 |
+
lanes = decode(
|
| 59 |
+
seg_prob, outputs[1][0], outputs[2][0], outputs[3][0], outputs[4][0],
|
| 60 |
+
max_seeds=max_seeds,
|
| 61 |
+
nms_max_lanes=128,
|
| 62 |
+
max_output_lanes=4,
|
| 63 |
+
crawl_backend="numba",
|
| 64 |
+
point_nms_backend="numba",
|
| 65 |
+
)
|
| 66 |
+
decode_ms = (time.perf_counter() - stage) * 1000.0
|
| 67 |
+
stage = time.perf_counter()
|
| 68 |
+
lane_results = [
|
| 69 |
+
lane_to_record(lane, calibration, bev_range, 0.5) for lane in lanes
|
| 70 |
+
]
|
| 71 |
+
funnel = clip_lane_results_to_funnel(lane_results, config)
|
| 72 |
+
bev_ms = (time.perf_counter() - stage) * 1000.0
|
| 73 |
+
return {
|
| 74 |
+
"softmax_ms": softmax_ms,
|
| 75 |
+
"decode_ms": decode_ms,
|
| 76 |
+
"bev_ms": bev_ms,
|
| 77 |
+
"postprocess_ms": (time.perf_counter() - started) * 1000.0,
|
| 78 |
+
"lane_count": len(lanes),
|
| 79 |
+
"funnel_clipped_lanes": len(funnel["clipped_lanes"]),
|
| 80 |
+
"funnel_rejected_lanes": len(funnel["rejected_lanes"]),
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_frames(path, start_frame, max_frames):
|
| 85 |
+
capture = cv2.VideoCapture(str(path))
|
| 86 |
+
if not capture.isOpened():
|
| 87 |
+
raise RuntimeError(f"cannot open video: {path}")
|
| 88 |
+
capture.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
| 89 |
+
frames = []
|
| 90 |
+
read_timings = []
|
| 91 |
+
for _ in range(max_frames):
|
| 92 |
+
started = time.perf_counter()
|
| 93 |
+
ok, frame = capture.read()
|
| 94 |
+
read_timings.append((time.perf_counter() - started) * 1000.0)
|
| 95 |
+
if not ok:
|
| 96 |
+
break
|
| 97 |
+
frames.append(frame)
|
| 98 |
+
capture.release()
|
| 99 |
+
if not frames:
|
| 100 |
+
raise RuntimeError("video produced no benchmark frames")
|
| 101 |
+
return frames, read_timings[:len(frames)]
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def summarize_results(results):
|
| 105 |
+
return {
|
| 106 |
+
name: timing_summary([result[name] for result in results])
|
| 107 |
+
for name in ("softmax_ms", "decode_ms", "bev_ms", "postprocess_ms")
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def main():
|
| 112 |
+
args = parse_args()
|
| 113 |
+
if args.start_frame < 0 or args.max_frames <= 0:
|
| 114 |
+
raise ValueError("start-frame must be >=0 and max-frames positive")
|
| 115 |
+
if args.cpu_threads <= 0 or args.cpu_threads > numba.config.NUMBA_NUM_THREADS:
|
| 116 |
+
raise ValueError(
|
| 117 |
+
f"cpu-threads must be in [1, {numba.config.NUMBA_NUM_THREADS}]"
|
| 118 |
+
)
|
| 119 |
+
if args.max_seeds <= 0:
|
| 120 |
+
raise ValueError("max-seeds must be positive")
|
| 121 |
+
model_path = Path(args.model).expanduser().resolve()
|
| 122 |
+
video_path = Path(args.video).expanduser().resolve()
|
| 123 |
+
report_path = Path(args.report).expanduser().resolve()
|
| 124 |
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
| 125 |
+
|
| 126 |
+
cv2.setNumThreads(1)
|
| 127 |
+
numba.set_num_threads(args.cpu_threads)
|
| 128 |
+
warmup_decode_backend("numba")
|
| 129 |
+
session = create_session(
|
| 130 |
+
model_path, args.provider, False, args.trt_cache_dir
|
| 131 |
+
)
|
| 132 |
+
warmup = np.zeros((1, 3, MODEL_HEIGHT, MODEL_WIDTH), np.float32)
|
| 133 |
+
for _ in range(10):
|
| 134 |
+
session.run(list(OUTPUT_NAMES), {"images": warmup})
|
| 135 |
+
frames, read_timings = load_frames(
|
| 136 |
+
video_path, args.start_frame, args.max_frames
|
| 137 |
+
)
|
| 138 |
+
calibration = CameraCalibration()
|
| 139 |
+
bev_range = BevRange()
|
| 140 |
+
config = projection_args()
|
| 141 |
+
|
| 142 |
+
sequential_results = []
|
| 143 |
+
preprocess_timings = []
|
| 144 |
+
inference_timings = []
|
| 145 |
+
total_timings = []
|
| 146 |
+
for frame in frames:
|
| 147 |
+
total_started = time.perf_counter()
|
| 148 |
+
stage = time.perf_counter()
|
| 149 |
+
images = normalize_image_numpy(frame, MODEL_WIDTH, MODEL_HEIGHT)
|
| 150 |
+
preprocess_timings.append((time.perf_counter() - stage) * 1000.0)
|
| 151 |
+
stage = time.perf_counter()
|
| 152 |
+
outputs = session.run(list(OUTPUT_NAMES), {"images": images})
|
| 153 |
+
inference_timings.append((time.perf_counter() - stage) * 1000.0)
|
| 154 |
+
sequential_results.append(postprocess(
|
| 155 |
+
outputs, calibration, bev_range, config, args.max_seeds
|
| 156 |
+
))
|
| 157 |
+
total_timings.append((time.perf_counter() - total_started) * 1000.0)
|
| 158 |
+
|
| 159 |
+
report = {
|
| 160 |
+
"model": str(model_path),
|
| 161 |
+
"video": str(video_path),
|
| 162 |
+
"provider": session.get_providers()[0],
|
| 163 |
+
"frames": len(frames),
|
| 164 |
+
"start_frame": args.start_frame,
|
| 165 |
+
"cpu_threads": args.cpu_threads,
|
| 166 |
+
"max_seeds": args.max_seeds,
|
| 167 |
+
"rendering_included": False,
|
| 168 |
+
"source_read_ms": timing_summary(read_timings),
|
| 169 |
+
"sequential": {
|
| 170 |
+
"preprocess_ms": timing_summary(preprocess_timings),
|
| 171 |
+
"inference_ms": timing_summary(inference_timings),
|
| 172 |
+
**summarize_results(sequential_results),
|
| 173 |
+
"core_pipeline_ms": timing_summary(total_timings),
|
| 174 |
+
"fps_from_median_latency": 1000.0 / np.median(total_timings),
|
| 175 |
+
},
|
| 176 |
+
"lane_count": {
|
| 177 |
+
"mean": float(np.mean([
|
| 178 |
+
result["lane_count"] for result in sequential_results
|
| 179 |
+
])),
|
| 180 |
+
"min": int(min(
|
| 181 |
+
result["lane_count"] for result in sequential_results
|
| 182 |
+
)),
|
| 183 |
+
"max": int(max(
|
| 184 |
+
result["lane_count"] for result in sequential_results
|
| 185 |
+
)),
|
| 186 |
+
},
|
| 187 |
+
"bev_projection": {
|
| 188 |
+
"mode": "raw_model_projection",
|
| 189 |
+
"parallel_assumption": False,
|
| 190 |
+
"synthetic_lanes": False,
|
| 191 |
+
"funnel_clipped_lane_fits": int(sum(
|
| 192 |
+
result["funnel_clipped_lanes"]
|
| 193 |
+
for result in sequential_results
|
| 194 |
+
)),
|
| 195 |
+
"funnel_rejected_lane_fits": int(sum(
|
| 196 |
+
result["funnel_rejected_lanes"]
|
| 197 |
+
for result in sequential_results
|
| 198 |
+
)),
|
| 199 |
+
},
|
| 200 |
+
}
|
| 201 |
+
temporary = report_path.with_suffix(report_path.suffix + ".tmp")
|
| 202 |
+
with temporary.open("w") as handle:
|
| 203 |
+
json.dump(report, handle, indent=2)
|
| 204 |
+
handle.write("\n")
|
| 205 |
+
os.replace(temporary, report_path)
|
| 206 |
+
print(f"report: {report_path}")
|
| 207 |
+
print(
|
| 208 |
+
"sequential median={:.3f}ms ({:.2f} FPS)".format(
|
| 209 |
+
report["sequential"]["core_pipeline_ms"]["median_ms"],
|
| 210 |
+
report["sequential"]["fps_from_median_latency"],
|
| 211 |
+
)
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
main()
|
bev.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ego-centric inverse-perspective mapping and cubic lane fitting.
|
| 2 |
+
|
| 3 |
+
The public coordinate convention is ADAS-style:
|
| 4 |
+
|
| 5 |
+
X: forward from the ego origin (metres)
|
| 6 |
+
Y: left of ego (metres)
|
| 7 |
+
|
| 8 |
+
Z is used only internally to intersect an image ray with the local road plane
|
| 9 |
+
``Z = 0``. The returned BEV lane points and cubic models are strictly 2-D.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from dataclasses import dataclass
|
| 13 |
+
from functools import lru_cache
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass(frozen=True)
|
| 19 |
+
class CameraCalibration:
|
| 20 |
+
width: int = 1920
|
| 21 |
+
height: int = 1080
|
| 22 |
+
horizontal_fov_deg: float = 30.0
|
| 23 |
+
fx: float = 3582.768775
|
| 24 |
+
fy: float = 3582.768775
|
| 25 |
+
cx: float = 960.0
|
| 26 |
+
cy: float = 540.0
|
| 27 |
+
camera_to_vehicle: tuple = (
|
| 28 |
+
(0.997564, 0.0, 0.069756, 1.0),
|
| 29 |
+
(0.0, 1.0, 0.0, 0.0),
|
| 30 |
+
(-0.069756, 0.0, 0.997564, 1.8),
|
| 31 |
+
(0.0, 0.0, 0.0, 1.0),
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
@property
|
| 35 |
+
def intrinsic(self):
|
| 36 |
+
return np.array(
|
| 37 |
+
((self.fx, 0.0, self.cx),
|
| 38 |
+
(0.0, self.fy, self.cy),
|
| 39 |
+
(0.0, 0.0, 1.0)),
|
| 40 |
+
dtype=np.float64,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
@property
|
| 44 |
+
def camera_to_vehicle_matrix(self):
|
| 45 |
+
return np.asarray(self.camera_to_vehicle, dtype=np.float64)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True)
|
| 49 |
+
class BevRange:
|
| 50 |
+
x_min: float = 0.0
|
| 51 |
+
x_max: float = 300.0
|
| 52 |
+
y_min: float = -85.0
|
| 53 |
+
y_max: float = 85.0
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@lru_cache(maxsize=8)
|
| 57 |
+
def _projection_constants(calibration):
|
| 58 |
+
optical_to_ue = np.array(
|
| 59 |
+
((0.0, 0.0, 1.0),
|
| 60 |
+
(1.0, 0.0, 0.0),
|
| 61 |
+
(0.0, -1.0, 0.0)),
|
| 62 |
+
dtype=np.float64,
|
| 63 |
+
)
|
| 64 |
+
mount = calibration.camera_to_vehicle_matrix
|
| 65 |
+
return (
|
| 66 |
+
np.linalg.inv(calibration.intrinsic),
|
| 67 |
+
optical_to_ue,
|
| 68 |
+
mount[:3, 3].copy(),
|
| 69 |
+
mount[:3, :3].copy(),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def pixels_to_ground(pixels, calibration=CameraCalibration(),
|
| 74 |
+
bev_range=BevRange()):
|
| 75 |
+
"""Project raw-image pixels onto the local ground plane.
|
| 76 |
+
|
| 77 |
+
Args:
|
| 78 |
+
pixels: ``(N, 2)`` raw-image coordinates ``(u, v)``.
|
| 79 |
+
calibration: pinhole intrinsics and fixed camera-to-vehicle mount.
|
| 80 |
+
bev_range: accepted metric ROI.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
``(points_xy, valid_mask)``. ``points_xy`` contains only valid points in
|
| 84 |
+
ADAS coordinates ``(X forward, Y left)``. ``valid_mask`` indexes the
|
| 85 |
+
input array and is useful for carrying per-point scores through IPM.
|
| 86 |
+
"""
|
| 87 |
+
pixels = np.asarray(pixels, dtype=np.float64)
|
| 88 |
+
if pixels.size == 0:
|
| 89 |
+
return np.empty((0, 2), dtype=np.float64), np.zeros(0, dtype=bool)
|
| 90 |
+
if pixels.ndim != 2 or pixels.shape[1] != 2:
|
| 91 |
+
raise ValueError("pixels must have shape (N, 2)")
|
| 92 |
+
|
| 93 |
+
finite = np.isfinite(pixels).all(axis=1)
|
| 94 |
+
homogeneous = np.column_stack((pixels, np.ones(len(pixels))))
|
| 95 |
+
inverse_intrinsic, optical_to_ue, origin_vehicle, rotation = (
|
| 96 |
+
_projection_constants(calibration)
|
| 97 |
+
)
|
| 98 |
+
rays_optical = (inverse_intrinsic @ homogeneous.T).T
|
| 99 |
+
|
| 100 |
+
# CARLA/UE camera axes are X forward, Y right, Z up. OpenCV optical axes
|
| 101 |
+
# are x right, y down, z forward: optical -> UE = (z, x, -y).
|
| 102 |
+
rays_camera_ue = (optical_to_ue @ rays_optical.T).T
|
| 103 |
+
rays_vehicle = (rotation @ rays_camera_ue.T).T
|
| 104 |
+
dz = rays_vehicle[:, 2]
|
| 105 |
+
with np.errstate(divide="ignore", invalid="ignore"):
|
| 106 |
+
scale = -origin_vehicle[2] / dz
|
| 107 |
+
ground_vehicle = origin_vehicle + scale[:, None] * rays_vehicle
|
| 108 |
+
|
| 109 |
+
# CARLA Y points right; public BEV Y points left.
|
| 110 |
+
x_forward = ground_vehicle[:, 0]
|
| 111 |
+
y_left = -ground_vehicle[:, 1]
|
| 112 |
+
valid = (
|
| 113 |
+
finite
|
| 114 |
+
& np.isfinite(ground_vehicle).all(axis=1)
|
| 115 |
+
& (dz < -1e-8)
|
| 116 |
+
& (scale > 0.0)
|
| 117 |
+
& (x_forward >= bev_range.x_min)
|
| 118 |
+
& (x_forward <= bev_range.x_max)
|
| 119 |
+
& (y_left >= bev_range.y_min)
|
| 120 |
+
& (y_left <= bev_range.y_max)
|
| 121 |
+
)
|
| 122 |
+
return np.column_stack((x_forward[valid], y_left[valid])), valid
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def ground_to_pixels(points_xy, calibration=CameraCalibration()):
|
| 126 |
+
"""Project ADAS ground points to raw pixels (used for calibration tests)."""
|
| 127 |
+
points_xy = np.asarray(points_xy, dtype=np.float64)
|
| 128 |
+
if points_xy.ndim != 2 or points_xy.shape[1] != 2:
|
| 129 |
+
raise ValueError("points_xy must have shape (N, 2)")
|
| 130 |
+
# ADAS Y-left -> CARLA Y-right.
|
| 131 |
+
points_vehicle = np.column_stack(
|
| 132 |
+
(points_xy[:, 0], -points_xy[:, 1], np.zeros(len(points_xy)))
|
| 133 |
+
)
|
| 134 |
+
mount = calibration.camera_to_vehicle_matrix
|
| 135 |
+
rotation_vehicle_to_camera = mount[:3, :3].T
|
| 136 |
+
points_camera_ue = (
|
| 137 |
+
rotation_vehicle_to_camera
|
| 138 |
+
@ (points_vehicle - mount[:3, 3]).T
|
| 139 |
+
).T
|
| 140 |
+
points_optical = np.column_stack(
|
| 141 |
+
(points_camera_ue[:, 1], -points_camera_ue[:, 2], points_camera_ue[:, 0])
|
| 142 |
+
)
|
| 143 |
+
projected = (calibration.intrinsic @ points_optical.T).T
|
| 144 |
+
with np.errstate(divide="ignore", invalid="ignore"):
|
| 145 |
+
return projected[:, :2] / projected[:, 2:3]
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def model_lane_to_ground(lane, model_width=800, model_height=320,
|
| 149 |
+
calibration=CameraCalibration(),
|
| 150 |
+
bev_range=BevRange()):
|
| 151 |
+
"""Convert one decoded RCLane polyline into metric BEV points + scores."""
|
| 152 |
+
if len(lane.points) == 0:
|
| 153 |
+
return np.empty((0, 2)), np.empty(0)
|
| 154 |
+
lane_points = np.asarray(lane.points, dtype=np.float64)
|
| 155 |
+
raw_pixels = lane_points[:, :2].copy()
|
| 156 |
+
raw_pixels[:, 0] *= calibration.width / float(model_width)
|
| 157 |
+
raw_pixels[:, 1] *= calibration.height / float(model_height)
|
| 158 |
+
ground, valid = pixels_to_ground(raw_pixels, calibration, bev_range)
|
| 159 |
+
return ground, lane_points[valid, 2]
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _weighted_lstsq(design, targets, weights):
|
| 163 |
+
root_weight = np.sqrt(np.maximum(weights, 1e-8))
|
| 164 |
+
return np.linalg.lstsq(
|
| 165 |
+
design * root_weight[:, None], targets * root_weight, rcond=None
|
| 166 |
+
)[0]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def fit_cubic_lane(points_xy, point_scores=None, min_points=6,
|
| 170 |
+
huber_delta_m=0.30, iterations=5):
|
| 171 |
+
"""Robustly fit ``Y(X) = c0 + c1 X + c2 X^2 + c3 X^3``.
|
| 172 |
+
|
| 173 |
+
X is normalized internally for numerical stability out to 300 metres. The
|
| 174 |
+
returned coefficients are converted back to metric X and ordered
|
| 175 |
+
``[c0, c1, c2, c3]``.
|
| 176 |
+
"""
|
| 177 |
+
points = np.asarray(points_xy, dtype=np.float64)
|
| 178 |
+
if points.ndim != 2 or points.shape[1] != 2:
|
| 179 |
+
raise ValueError("points_xy must have shape (N, 2)")
|
| 180 |
+
finite = np.isfinite(points).all(axis=1)
|
| 181 |
+
points = points[finite]
|
| 182 |
+
if len(points) < min_points:
|
| 183 |
+
return None
|
| 184 |
+
order = np.argsort(points[:, 0])
|
| 185 |
+
x = points[order, 0]
|
| 186 |
+
y = points[order, 1]
|
| 187 |
+
if np.ptp(x) < 1.0:
|
| 188 |
+
return None
|
| 189 |
+
|
| 190 |
+
if point_scores is None:
|
| 191 |
+
base_weights = np.ones(len(x), dtype=np.float64)
|
| 192 |
+
else:
|
| 193 |
+
scores = np.asarray(point_scores, dtype=np.float64)[finite][order]
|
| 194 |
+
base_weights = np.clip(scores, 0.05, 1.0)
|
| 195 |
+
|
| 196 |
+
center = float((x.min() + x.max()) * 0.5)
|
| 197 |
+
scale = float(max((x.max() - x.min()) * 0.5, 1.0))
|
| 198 |
+
z = (x - center) / scale
|
| 199 |
+
design = np.column_stack((np.ones(len(z)), z, z ** 2, z ** 3))
|
| 200 |
+
weights = base_weights.copy()
|
| 201 |
+
coefficients_normalized = _weighted_lstsq(design, y, weights)
|
| 202 |
+
for _ in range(iterations):
|
| 203 |
+
residual = y - design @ coefficients_normalized
|
| 204 |
+
robust_weight = np.minimum(
|
| 205 |
+
1.0, huber_delta_m / np.maximum(np.abs(residual), 1e-8)
|
| 206 |
+
)
|
| 207 |
+
weights = base_weights * robust_weight
|
| 208 |
+
coefficients_normalized = _weighted_lstsq(design, y, weights)
|
| 209 |
+
|
| 210 |
+
residual = y - design @ coefficients_normalized
|
| 211 |
+
inliers = np.abs(residual) <= max(huber_delta_m, 2.5 * np.median(np.abs(residual)))
|
| 212 |
+
if np.count_nonzero(inliers) >= min_points:
|
| 213 |
+
coefficients_normalized = _weighted_lstsq(
|
| 214 |
+
design[inliers], y[inliers], base_weights[inliers]
|
| 215 |
+
)
|
| 216 |
+
else:
|
| 217 |
+
inliers = np.ones(len(x), dtype=bool)
|
| 218 |
+
|
| 219 |
+
# Compose p((X - center) / scale) and return ascending metric coefficients.
|
| 220 |
+
normalized_polynomial = np.polynomial.Polynomial(coefficients_normalized)
|
| 221 |
+
metric_argument = np.polynomial.Polynomial((-center / scale, 1.0 / scale))
|
| 222 |
+
metric_polynomial = normalized_polynomial(metric_argument)
|
| 223 |
+
coefficients = np.zeros(4, dtype=np.float64)
|
| 224 |
+
coefficients[:len(metric_polynomial.coef)] = metric_polynomial.coef
|
| 225 |
+
|
| 226 |
+
fitted = np.polynomial.polynomial.polyval(x[inliers], coefficients)
|
| 227 |
+
rmse = float(np.sqrt(np.mean((y[inliers] - fitted) ** 2)))
|
| 228 |
+
return {
|
| 229 |
+
"coefficients": coefficients,
|
| 230 |
+
"x_min": float(x[inliers].min()),
|
| 231 |
+
"x_max": float(x[inliers].max()),
|
| 232 |
+
"rmse": rmse,
|
| 233 |
+
"point_count": int(len(x)),
|
| 234 |
+
"inlier_count": int(np.count_nonzero(inliers)),
|
| 235 |
+
"inlier_ratio": float(np.mean(inliers)),
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def evaluate_cubic(coefficients, x):
|
| 240 |
+
return np.polynomial.polynomial.polyval(
|
| 241 |
+
np.asarray(x, dtype=np.float64), np.asarray(coefficients, dtype=np.float64)
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def _copy_fit(fit):
|
| 246 |
+
copied = dict(fit)
|
| 247 |
+
copied["coefficients"] = np.asarray(
|
| 248 |
+
fit["coefficients"], dtype=np.float64
|
| 249 |
+
).copy()
|
| 250 |
+
return copied
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def clip_cubic_fit_to_funnel(fit, camera_x_m=1.0,
|
| 254 |
+
horizontal_fov_deg=30.0, margin_m=0.10,
|
| 255 |
+
sample_step_m=0.5, minimum_span_m=1.0):
|
| 256 |
+
"""Restrict a cubic's declared domain to the visible camera funnel.
|
| 257 |
+
|
| 258 |
+
The polynomial coefficients are left unchanged. Only the longest
|
| 259 |
+
contiguous, physically visible X interval is exported, which makes the
|
| 260 |
+
``x_domain_m`` contract explicit and prevents a renderer from drawing an
|
| 261 |
+
otherwise valid cubic after it leaves the camera footprint.
|
| 262 |
+
"""
|
| 263 |
+
copied = _copy_fit(fit)
|
| 264 |
+
x_min = float(fit["x_min"])
|
| 265 |
+
x_max = float(fit["x_max"])
|
| 266 |
+
report = {
|
| 267 |
+
"original_x_domain_m": [x_min, x_max],
|
| 268 |
+
"x_domain_m": None,
|
| 269 |
+
"clipped": False,
|
| 270 |
+
"valid": False,
|
| 271 |
+
}
|
| 272 |
+
if x_max - x_min < minimum_span_m:
|
| 273 |
+
return None, report
|
| 274 |
+
count = max(3, int(np.ceil((x_max - x_min) / sample_step_m)) + 1)
|
| 275 |
+
x = np.linspace(x_min, x_max, count)
|
| 276 |
+
y = evaluate_cubic(fit["coefficients"], x)
|
| 277 |
+
half_width = np.maximum(0.0, x - camera_x_m) * np.tan(
|
| 278 |
+
np.deg2rad(horizontal_fov_deg * 0.5)
|
| 279 |
+
)
|
| 280 |
+
valid = (
|
| 281 |
+
np.isfinite(y)
|
| 282 |
+
& (x >= camera_x_m)
|
| 283 |
+
& (np.abs(y) <= half_width + margin_m)
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
runs = []
|
| 287 |
+
start = None
|
| 288 |
+
for index, value in enumerate(valid):
|
| 289 |
+
if value and start is None:
|
| 290 |
+
start = index
|
| 291 |
+
if start is not None and (not value or index == len(valid) - 1):
|
| 292 |
+
end = index if value and index == len(valid) - 1 else index - 1
|
| 293 |
+
if x[end] - x[start] >= minimum_span_m:
|
| 294 |
+
runs.append((start, end))
|
| 295 |
+
start = None
|
| 296 |
+
if not runs:
|
| 297 |
+
return None, report
|
| 298 |
+
start, end = max(runs, key=lambda run: x[run[1]] - x[run[0]])
|
| 299 |
+
copied["x_min"] = float(x[start])
|
| 300 |
+
copied["x_max"] = float(x[end])
|
| 301 |
+
report.update({
|
| 302 |
+
"x_domain_m": [copied["x_min"], copied["x_max"]],
|
| 303 |
+
"clipped": bool(
|
| 304 |
+
copied["x_min"] > x_min + 1e-6
|
| 305 |
+
or copied["x_max"] < x_max - 1e-6
|
| 306 |
+
),
|
| 307 |
+
"valid": True,
|
| 308 |
+
})
|
| 309 |
+
return copied, report
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def _self_test():
|
| 313 |
+
calibration = CameraCalibration()
|
| 314 |
+
bev_range = BevRange()
|
| 315 |
+
ground = np.array(
|
| 316 |
+
((5.0, 0.0), (20.0, 3.5), (50.0, -3.5), (150.0, 2.0),
|
| 317 |
+
(299.0, 0.0)),
|
| 318 |
+
dtype=np.float64,
|
| 319 |
+
)
|
| 320 |
+
pixels = ground_to_pixels(ground, calibration)
|
| 321 |
+
reconstructed, valid = pixels_to_ground(pixels, calibration, bev_range)
|
| 322 |
+
assert valid.all()
|
| 323 |
+
assert np.allclose(reconstructed, ground, atol=1e-5), (
|
| 324 |
+
reconstructed, ground
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
x = np.linspace(5.0, 180.0, 80)
|
| 328 |
+
truth = np.array((1.8, 1.5e-2, -1.2e-4, 3.0e-7))
|
| 329 |
+
y = evaluate_cubic(truth, x)
|
| 330 |
+
y[20] += 4.0
|
| 331 |
+
fitted = fit_cubic_lane(np.column_stack((x, y)))
|
| 332 |
+
assert fitted is not None
|
| 333 |
+
prediction = evaluate_cubic(fitted["coefficients"], x)
|
| 334 |
+
clean = np.ones(len(x), dtype=bool)
|
| 335 |
+
clean[20] = False
|
| 336 |
+
clean_rmse = np.sqrt(np.mean(
|
| 337 |
+
(prediction[clean] - evaluate_cubic(truth, x[clean])) ** 2
|
| 338 |
+
))
|
| 339 |
+
assert clean_rmse < 0.02
|
| 340 |
+
|
| 341 |
+
runaway_fit = {
|
| 342 |
+
"coefficients": np.array((-5.0, 0.4, -0.013, 1.15e-4)),
|
| 343 |
+
"x_min": 10.0, "x_max": 130.0, "rmse": 0.1,
|
| 344 |
+
"point_count": 50, "inlier_count": 50, "inlier_ratio": 1.0,
|
| 345 |
+
}
|
| 346 |
+
source_coefficients = runaway_fit["coefficients"].copy()
|
| 347 |
+
clipped, funnel_report = clip_cubic_fit_to_funnel(runaway_fit)
|
| 348 |
+
assert clipped is not None and funnel_report["clipped"]
|
| 349 |
+
assert clipped["x_max"] < runaway_fit["x_max"]
|
| 350 |
+
assert np.array_equal(clipped["coefficients"], source_coefficients)
|
| 351 |
+
check_x = np.linspace(clipped["x_min"], clipped["x_max"], 300)
|
| 352 |
+
check_y = evaluate_cubic(clipped["coefficients"], check_x)
|
| 353 |
+
funnel_half_width = (check_x - 1.0) * np.tan(np.deg2rad(15.0))
|
| 354 |
+
assert np.all(np.abs(check_y) <= funnel_half_width + 0.11)
|
| 355 |
+
print("OK -- pixel/ground projection round trip")
|
| 356 |
+
print("OK -- robust metric cubic lane fit")
|
| 357 |
+
print("OK -- raw cubic funnel clipping preserves coefficients")
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
if __name__ == "__main__":
|
| 361 |
+
_self_test()
|
build_tensorrt_engine.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build/cache the RCLane TensorRT FP16 engine and verify its outputs.
|
| 2 |
+
|
| 3 |
+
TensorRT is used through ONNX Runtime's TensorRT Execution Provider so the
|
| 4 |
+
cached ``.engine`` remains consumable by the same runtime pipeline. The script
|
| 5 |
+
refuses silent provider fallback and records numerical/performance comparisons
|
| 6 |
+
against CUDA FP32.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from test_video_onnx import OUTPUT_NAMES, create_session, timing_summary
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def parse_args():
|
| 21 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 22 |
+
parser.add_argument("--model", required=True)
|
| 23 |
+
parser.add_argument("--cache-dir", default="exports/trt_cache")
|
| 24 |
+
parser.add_argument("--report", default=None)
|
| 25 |
+
parser.add_argument("--warmup", type=int, default=10)
|
| 26 |
+
parser.add_argument("--iterations", type=int, default=100)
|
| 27 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 28 |
+
return parser.parse_args()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def benchmark(session, images, warmup, iterations):
|
| 32 |
+
for _ in range(warmup):
|
| 33 |
+
session.run(list(OUTPUT_NAMES), {"images": images})
|
| 34 |
+
timings = []
|
| 35 |
+
outputs = None
|
| 36 |
+
for _ in range(iterations):
|
| 37 |
+
started = time.perf_counter()
|
| 38 |
+
outputs = session.run(list(OUTPUT_NAMES), {"images": images})
|
| 39 |
+
timings.append((time.perf_counter() - started) * 1000.0)
|
| 40 |
+
return outputs, timing_summary(timings)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main():
|
| 44 |
+
args = parse_args()
|
| 45 |
+
if args.warmup < 0 or args.iterations <= 0:
|
| 46 |
+
raise ValueError("warmup must be non-negative and iterations positive")
|
| 47 |
+
model_path = Path(args.model).expanduser().resolve()
|
| 48 |
+
cache_dir = Path(args.cache_dir).expanduser().resolve()
|
| 49 |
+
report_path = (
|
| 50 |
+
Path(args.report).expanduser().resolve()
|
| 51 |
+
if args.report else cache_dir / "build_report.json"
|
| 52 |
+
)
|
| 53 |
+
if not model_path.is_file():
|
| 54 |
+
raise FileNotFoundError(model_path)
|
| 55 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 56 |
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
|
| 58 |
+
rng = np.random.default_rng(args.seed)
|
| 59 |
+
images = rng.normal(size=(1, 3, 320, 800)).astype(np.float32)
|
| 60 |
+
|
| 61 |
+
build_started = time.perf_counter()
|
| 62 |
+
trt_session = create_session(
|
| 63 |
+
model_path, "tensorrt", allow_tf32=False, trt_cache_dir=cache_dir
|
| 64 |
+
)
|
| 65 |
+
build_seconds = time.perf_counter() - build_started
|
| 66 |
+
if trt_session.get_providers()[0] != "TensorrtExecutionProvider":
|
| 67 |
+
raise RuntimeError("TensorRT provider silently fell back")
|
| 68 |
+
cuda_session = create_session(model_path, "cuda", allow_tf32=False)
|
| 69 |
+
|
| 70 |
+
trt_outputs, trt_timing = benchmark(
|
| 71 |
+
trt_session, images, args.warmup, args.iterations
|
| 72 |
+
)
|
| 73 |
+
cuda_outputs, cuda_timing = benchmark(
|
| 74 |
+
cuda_session, images, args.warmup, args.iterations
|
| 75 |
+
)
|
| 76 |
+
comparisons = {}
|
| 77 |
+
for name, trt_output, cuda_output in zip(
|
| 78 |
+
OUTPUT_NAMES, trt_outputs, cuda_outputs
|
| 79 |
+
):
|
| 80 |
+
difference = trt_output.astype(np.float64) - cuda_output.astype(
|
| 81 |
+
np.float64
|
| 82 |
+
)
|
| 83 |
+
comparisons[name] = {
|
| 84 |
+
"shape": list(trt_output.shape),
|
| 85 |
+
"max_abs": float(np.max(np.abs(difference))),
|
| 86 |
+
"mean_abs": float(np.mean(np.abs(difference))),
|
| 87 |
+
"rmse": float(np.sqrt(np.mean(difference ** 2))),
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
cache_files = []
|
| 91 |
+
for path in sorted(cache_dir.iterdir()):
|
| 92 |
+
if path.is_file():
|
| 93 |
+
cache_files.append({
|
| 94 |
+
"path": str(path),
|
| 95 |
+
"size_bytes": path.stat().st_size,
|
| 96 |
+
})
|
| 97 |
+
engines = [
|
| 98 |
+
item for item in cache_files if item["path"].endswith(".engine")
|
| 99 |
+
]
|
| 100 |
+
if not engines:
|
| 101 |
+
raise RuntimeError(f"TensorRT did not create an engine in {cache_dir}")
|
| 102 |
+
|
| 103 |
+
report = {
|
| 104 |
+
"model": str(model_path),
|
| 105 |
+
"provider": trt_session.get_providers()[0],
|
| 106 |
+
"precision": "fp16",
|
| 107 |
+
"input_shape": list(images.shape),
|
| 108 |
+
"build_or_cache_load_seconds": build_seconds,
|
| 109 |
+
"tensorrt_timing": trt_timing,
|
| 110 |
+
"cuda_fp32_timing": cuda_timing,
|
| 111 |
+
"speedup_from_median": (
|
| 112 |
+
cuda_timing["median_ms"] / trt_timing["median_ms"]
|
| 113 |
+
),
|
| 114 |
+
"output_comparison_to_cuda_fp32": comparisons,
|
| 115 |
+
"cache_files": cache_files,
|
| 116 |
+
}
|
| 117 |
+
temporary = report_path.with_suffix(report_path.suffix + ".tmp")
|
| 118 |
+
with temporary.open("w") as handle:
|
| 119 |
+
json.dump(report, handle, indent=2)
|
| 120 |
+
handle.write("\n")
|
| 121 |
+
os.replace(temporary, report_path)
|
| 122 |
+
print(f"TensorRT engine OK: {engines[0]['path']}")
|
| 123 |
+
print(f"report: {report_path}")
|
| 124 |
+
print(
|
| 125 |
+
"median inference: TensorRT={:.3f}ms CUDA={:.3f}ms speedup={:.2f}x".format(
|
| 126 |
+
trt_timing["median_ms"], cuda_timing["median_ms"],
|
| 127 |
+
report["speedup_from_median"],
|
| 128 |
+
)
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
if __name__ == "__main__":
|
| 133 |
+
main()
|
cpp/CMakeLists.txt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cmake_minimum_required(VERSION 3.20)
|
| 2 |
+
project(rclane_runtime LANGUAGES CXX)
|
| 3 |
+
|
| 4 |
+
set(CMAKE_CXX_STANDARD 17)
|
| 5 |
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
| 6 |
+
set(CMAKE_CXX_EXTENSIONS OFF)
|
| 7 |
+
|
| 8 |
+
if(NOT CMAKE_BUILD_TYPE)
|
| 9 |
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
| 10 |
+
endif()
|
| 11 |
+
|
| 12 |
+
find_path(TENSORRT_INCLUDE_DIR NvInferRuntime.h
|
| 13 |
+
HINTS "$ENV{TENSORRT_ROOT}/include"
|
| 14 |
+
PATHS /usr/include /usr/include/aarch64-linux-gnu /usr/local/TensorRT/include
|
| 15 |
+
)
|
| 16 |
+
find_library(TENSORRT_LIBRARY NAMES nvinfer libnvinfer.so.10
|
| 17 |
+
HINTS "$ENV{TENSORRT_ROOT}/lib" "$ENV{TENSORRT_ROOT}/lib64"
|
| 18 |
+
PATHS /usr/lib /usr/lib/x86_64-linux-gnu /usr/lib/aarch64-linux-gnu
|
| 19 |
+
)
|
| 20 |
+
find_path(CUDA_INCLUDE_DIR cuda_runtime_api.h
|
| 21 |
+
HINTS "$ENV{CUDA_HOME}/include" "$ENV{CUDA_PATH}/include"
|
| 22 |
+
PATHS /usr/local/cuda/include /usr/include
|
| 23 |
+
)
|
| 24 |
+
find_library(CUDART_LIBRARY NAMES cudart libcudart.so.13 libcudart.so.12
|
| 25 |
+
HINTS "$ENV{CUDA_HOME}/lib64" "$ENV{CUDA_PATH}/lib64"
|
| 26 |
+
PATHS /usr/local/cuda/lib64 /usr/lib/x86_64-linux-gnu
|
| 27 |
+
/usr/lib/aarch64-linux-gnu
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
foreach(required_path
|
| 31 |
+
TENSORRT_INCLUDE_DIR TENSORRT_LIBRARY CUDA_INCLUDE_DIR CUDART_LIBRARY)
|
| 32 |
+
if(NOT ${required_path})
|
| 33 |
+
message(FATAL_ERROR
|
| 34 |
+
"${required_path} was not found; pass -D${required_path}=..."
|
| 35 |
+
)
|
| 36 |
+
endif()
|
| 37 |
+
endforeach()
|
| 38 |
+
|
| 39 |
+
find_package(OpenMP REQUIRED)
|
| 40 |
+
|
| 41 |
+
add_executable(rclane_runtime
|
| 42 |
+
src/bev.cpp
|
| 43 |
+
src/decoder.cpp
|
| 44 |
+
src/main.cpp
|
| 45 |
+
src/preprocess.cpp
|
| 46 |
+
src/tensorrt_runner.cpp
|
| 47 |
+
)
|
| 48 |
+
target_include_directories(rclane_runtime PRIVATE
|
| 49 |
+
include
|
| 50 |
+
"${TENSORRT_INCLUDE_DIR}"
|
| 51 |
+
"${CUDA_INCLUDE_DIR}"
|
| 52 |
+
)
|
| 53 |
+
target_link_libraries(rclane_runtime PRIVATE
|
| 54 |
+
"${TENSORRT_LIBRARY}"
|
| 55 |
+
"${CUDART_LIBRARY}"
|
| 56 |
+
OpenMP::OpenMP_CXX
|
| 57 |
+
dl
|
| 58 |
+
)
|
| 59 |
+
target_compile_options(rclane_runtime PRIVATE
|
| 60 |
+
-O3 -march=native -Wall -Wextra -Wpedantic
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
get_filename_component(TENSORRT_LIBRARY_DIR "${TENSORRT_LIBRARY}" DIRECTORY)
|
| 64 |
+
get_filename_component(CUDART_LIBRARY_DIR "${CUDART_LIBRARY}" DIRECTORY)
|
| 65 |
+
set_target_properties(rclane_runtime PROPERTIES
|
| 66 |
+
BUILD_RPATH "${TENSORRT_LIBRARY_DIR};${CUDART_LIBRARY_DIR}"
|
| 67 |
+
)
|
cpp/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RCLane native C++ runtime
|
| 2 |
+
|
| 3 |
+
This directory is the sequential deployment runtime. One frame completes
|
| 4 |
+
preprocessing, TensorRT inference, 1024-seed decode and raw-model BEV export
|
| 5 |
+
before the next frame begins. There is no inter-frame overlap. Rendering,
|
| 6 |
+
source-video decoding and video writing are outside the measured core latency.
|
| 7 |
+
|
| 8 |
+
The BEV output is a one-to-one projection of decoded model lanes. It does not
|
| 9 |
+
force curves to be parallel and does not synthesize missing lanes. The funnel
|
| 10 |
+
guard may only shorten a cubic's valid X domain; it never changes coefficients.
|
| 11 |
+
|
| 12 |
+
The build intentionally consumes TensorRT/CUDA SDK headers and shared libraries
|
| 13 |
+
from the target machine; serialized TensorRT engines must be rebuilt per GPU.
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
cmake -S cpp -B cpp/build -G "Unix Makefiles" \
|
| 17 |
+
-DTENSORRT_INCLUDE_DIR=/path/to/TensorRT/include \
|
| 18 |
+
-DTENSORRT_LIBRARY=/path/to/libnvinfer.so \
|
| 19 |
+
-DCUDA_INCLUDE_DIR=/path/to/cuda/include \
|
| 20 |
+
-DCUDART_LIBRARY=/path/to/libcudart.so
|
| 21 |
+
cmake --build cpp/build -j8
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
TensorRT and CUDA are auto-discovered in standard x86/Jetson locations. The
|
| 25 |
+
explicit `-D` paths above are useful for Python-wheel installations.
|
| 26 |
+
|
| 27 |
+
## One-frame parity harness
|
| 28 |
+
|
| 29 |
+
The runtime accepts either a preprocessed float32 NCHW tensor or a raw
|
| 30 |
+
1920x1080 BGR frame. It can dump maps, decoded image lanes and metric BEV
|
| 31 |
+
cubics for comparison with Python:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
cpp/build/rclane_runtime \
|
| 35 |
+
--engine exports/trt_cache/model.engine \
|
| 36 |
+
--input-bgr /tmp/frame.bgr \
|
| 37 |
+
--dump-prefix /tmp/cpp \
|
| 38 |
+
--lanes-json /tmp/cpp_lanes.json \
|
| 39 |
+
--bev-json /tmp/cpp_bev.json \
|
| 40 |
+
--threads 8
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Sequential full-video benchmark
|
| 44 |
+
|
| 45 |
+
Pipe decoded BGR frames from FFmpeg. The reported `core_pipeline` contains
|
| 46 |
+
only preprocess + TensorRT (including transfers) + decode + BEV/cubic/funnel.
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
ffmpeg -loglevel error -i raw_Town04_Opt_20260714_093110.mp4 \
|
| 50 |
+
-f rawvideo -pix_fmt bgr24 - | \
|
| 51 |
+
cpp/build/rclane_runtime \
|
| 52 |
+
--engine exports/trt_cache/model.engine \
|
| 53 |
+
--raw-bgr-stdin --source-width 1920 --source-height 1080 \
|
| 54 |
+
--threads 8 --warmup 10 --timing-warmup 5 \
|
| 55 |
+
--frames-jsonl runs/cpp_final_lanes.jsonl \
|
| 56 |
+
--report runs/cpp_benchmark_final_full.json
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Render those saved C++ results later without rerunning inference or affecting
|
| 60 |
+
the measured pipeline latency:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
python cpp/render_cpp_results.py \
|
| 64 |
+
--video raw_Town04_Opt_20260714_093110.mp4 \
|
| 65 |
+
--results runs/cpp_final_lanes.jsonl \
|
| 66 |
+
--benchmark-report runs/cpp_benchmark_final_full.json \
|
| 67 |
+
--output runs/cpp_final_render_h264.mp4
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
On the development RTX 3050/i5-13420H machine, the complete 1768-frame final
|
| 71 |
+
video measured 13.39 ms median and 14.15 ms p95 core latency (74.67 FPS median)
|
| 72 |
+
with 1024 seeds and 8 CPU threads. TensorRT engines are GPU-architecture
|
| 73 |
+
specific and must be rebuilt on the deployment target.
|
cpp/THIRD_PARTY_NOTICES.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Third-party notices
|
| 2 |
+
|
| 3 |
+
The native runtime links against NVIDIA TensorRT and the CUDA Runtime supplied
|
| 4 |
+
by the deployment environment. Those libraries are not distributed in this
|
| 5 |
+
repository and remain subject to NVIDIA's respective licenses.
|
| 6 |
+
|
| 7 |
+
Small compatibility routines were independently ported from these upstream
|
| 8 |
+
implementations so C++ reproduces the established Python pipeline:
|
| 9 |
+
|
| 10 |
+
- NumPy `npysort/aquicksort`, used to reproduce the tie ordering of
|
| 11 |
+
`numpy.argsort`: NumPy is distributed under the BSD 3-Clause license.
|
| 12 |
+
Source: <https://github.com/numpy/numpy/blob/v1.26.4/numpy/core/src/npysort/quicksort.cpp>
|
| 13 |
+
- OpenCV fixed-point bilinear resize arithmetic, used to reproduce
|
| 14 |
+
`cv2.dnn.blobFromImage`: OpenCV 4.11 is distributed under the Apache License
|
| 15 |
+
2.0. Source:
|
| 16 |
+
<https://github.com/opencv/opencv/blob/4.11.0/modules/imgproc/src/resize.cpp>
|
| 17 |
+
|
| 18 |
+
The full upstream license texts and copyright notices are available at the
|
| 19 |
+
linked projects.
|
cpp/include/bev.hpp
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "decoder.hpp"
|
| 4 |
+
|
| 5 |
+
#include <array>
|
| 6 |
+
#include <cstddef>
|
| 7 |
+
#include <string>
|
| 8 |
+
#include <vector>
|
| 9 |
+
|
| 10 |
+
namespace rclane {
|
| 11 |
+
|
| 12 |
+
struct GroundPoint {
|
| 13 |
+
double x{};
|
| 14 |
+
double y{};
|
| 15 |
+
double score{};
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
struct CubicFit {
|
| 19 |
+
std::array<double, 4> coefficients{};
|
| 20 |
+
double x_min{};
|
| 21 |
+
double x_max{};
|
| 22 |
+
double rmse{};
|
| 23 |
+
std::size_t point_count{};
|
| 24 |
+
std::size_t inlier_count{};
|
| 25 |
+
bool valid{};
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
struct BevLane {
|
| 29 |
+
int lane_id{};
|
| 30 |
+
std::string role;
|
| 31 |
+
double score{};
|
| 32 |
+
std::vector<GroundPoint> points;
|
| 33 |
+
CubicFit fit;
|
| 34 |
+
bool fit_accepted{};
|
| 35 |
+
bool funnel_clipped{};
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
struct BevConfig {
|
| 39 |
+
double x_min{0.0};
|
| 40 |
+
double x_max{300.0};
|
| 41 |
+
double y_min{-85.0};
|
| 42 |
+
double y_max{85.0};
|
| 43 |
+
double maximum_rmse{0.5};
|
| 44 |
+
double funnel_margin{0.10};
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
std::vector<BevLane> project_lanes_to_bev(
|
| 48 |
+
const std::vector<Lane>& lanes,
|
| 49 |
+
const BevConfig& config = {}
|
| 50 |
+
);
|
| 51 |
+
|
| 52 |
+
void write_bev_json(
|
| 53 |
+
const std::string& path,
|
| 54 |
+
const std::vector<BevLane>& lanes
|
| 55 |
+
);
|
| 56 |
+
|
| 57 |
+
} // namespace rclane
|
cpp/include/decoder.hpp
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include "tensorrt_runner.hpp"
|
| 4 |
+
|
| 5 |
+
#include <cstddef>
|
| 6 |
+
#include <string>
|
| 7 |
+
#include <unordered_map>
|
| 8 |
+
#include <vector>
|
| 9 |
+
|
| 10 |
+
namespace rclane {
|
| 11 |
+
|
| 12 |
+
struct LanePoint {
|
| 13 |
+
float x{};
|
| 14 |
+
float y{};
|
| 15 |
+
float score{};
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
struct Lane {
|
| 19 |
+
int width{};
|
| 20 |
+
int height{};
|
| 21 |
+
std::vector<LanePoint> points;
|
| 22 |
+
double score_sum{};
|
| 23 |
+
int lane_id{};
|
| 24 |
+
std::string role;
|
| 25 |
+
bool ego_boundary{};
|
| 26 |
+
int lateral_rank{};
|
| 27 |
+
|
| 28 |
+
double score() const;
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
struct DecoderConfig {
|
| 32 |
+
float step_length{10.0F};
|
| 33 |
+
float segmentation_threshold{0.5F};
|
| 34 |
+
float seed_threshold{0.5F};
|
| 35 |
+
int seed_min_distance{2};
|
| 36 |
+
float score_threshold{0.10F};
|
| 37 |
+
float iou_threshold{0.5F};
|
| 38 |
+
int max_seeds{1024};
|
| 39 |
+
int nms_max_lanes{128};
|
| 40 |
+
float nms_scale{0.25F};
|
| 41 |
+
int lane_width{15};
|
| 42 |
+
int max_output_lanes{4};
|
| 43 |
+
float ego_x{400.0F};
|
| 44 |
+
float ego_min_score_ratio{0.5F};
|
| 45 |
+
int threads{8};
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
struct DecodeStatistics {
|
| 49 |
+
std::size_t foreground_pixels{};
|
| 50 |
+
std::size_t seeds{};
|
| 51 |
+
std::size_t crawled_candidates{};
|
| 52 |
+
std::size_t nms_candidates{};
|
| 53 |
+
std::size_t nms_survivors{};
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
std::vector<float> softmax_foreground(const Tensor& segmentation_logits);
|
| 57 |
+
|
| 58 |
+
std::vector<Lane> decode(
|
| 59 |
+
const std::vector<float>& segmentation_probability,
|
| 60 |
+
const Tensor& up_arrow,
|
| 61 |
+
const Tensor& down_arrow,
|
| 62 |
+
const Tensor& up_bound,
|
| 63 |
+
const Tensor& down_bound,
|
| 64 |
+
const DecoderConfig& config = {},
|
| 65 |
+
DecodeStatistics* statistics = nullptr
|
| 66 |
+
);
|
| 67 |
+
|
| 68 |
+
std::vector<Lane> decode_outputs(
|
| 69 |
+
const std::unordered_map<std::string, Tensor>& outputs,
|
| 70 |
+
const DecoderConfig& config = {},
|
| 71 |
+
DecodeStatistics* statistics = nullptr
|
| 72 |
+
);
|
| 73 |
+
|
| 74 |
+
void write_lanes_json(
|
| 75 |
+
const std::string& path,
|
| 76 |
+
const std::vector<Lane>& lanes,
|
| 77 |
+
const DecodeStatistics* statistics = nullptr
|
| 78 |
+
);
|
| 79 |
+
|
| 80 |
+
} // namespace rclane
|
cpp/include/preprocess.hpp
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <cstdint>
|
| 4 |
+
#include <vector>
|
| 5 |
+
|
| 6 |
+
namespace rclane {
|
| 7 |
+
|
| 8 |
+
void normalize_bgr_to_nchw(
|
| 9 |
+
const std::uint8_t* bgr,
|
| 10 |
+
int source_width,
|
| 11 |
+
int source_height,
|
| 12 |
+
std::vector<float>& destination
|
| 13 |
+
);
|
| 14 |
+
|
| 15 |
+
} // namespace rclane
|
cpp/include/tensorrt_runner.hpp
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <cstddef>
|
| 4 |
+
#include <cstdint>
|
| 5 |
+
#include <memory>
|
| 6 |
+
#include <string>
|
| 7 |
+
#include <unordered_map>
|
| 8 |
+
#include <vector>
|
| 9 |
+
|
| 10 |
+
namespace rclane {
|
| 11 |
+
|
| 12 |
+
struct Tensor {
|
| 13 |
+
std::vector<std::int64_t> shape;
|
| 14 |
+
std::vector<float> values;
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
struct InferenceTiming {
|
| 18 |
+
double mean_ms{};
|
| 19 |
+
double median_ms{};
|
| 20 |
+
double p95_ms{};
|
| 21 |
+
double min_ms{};
|
| 22 |
+
double max_ms{};
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
class TensorRTRunner {
|
| 26 |
+
public:
|
| 27 |
+
explicit TensorRTRunner(const std::string& engine_path);
|
| 28 |
+
~TensorRTRunner();
|
| 29 |
+
|
| 30 |
+
TensorRTRunner(const TensorRTRunner&) = delete;
|
| 31 |
+
TensorRTRunner& operator=(const TensorRTRunner&) = delete;
|
| 32 |
+
TensorRTRunner(TensorRTRunner&&) noexcept;
|
| 33 |
+
TensorRTRunner& operator=(TensorRTRunner&&) noexcept;
|
| 34 |
+
|
| 35 |
+
const std::vector<std::int64_t>& input_shape() const;
|
| 36 |
+
std::size_t input_elements() const;
|
| 37 |
+
std::unordered_map<std::string, Tensor> infer(const float* input);
|
| 38 |
+
const std::unordered_map<std::string, Tensor>& infer_reuse(
|
| 39 |
+
const float* input
|
| 40 |
+
);
|
| 41 |
+
InferenceTiming benchmark(
|
| 42 |
+
const float* input, int warmup, int iterations
|
| 43 |
+
);
|
| 44 |
+
|
| 45 |
+
private:
|
| 46 |
+
class Impl;
|
| 47 |
+
std::unique_ptr<Impl> impl_;
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
} // namespace rclane
|
cpp/render_cpp_results.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render native C++ decoded lanes in a separate, untimed video pass."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from collections import deque
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import cv2
|
| 12 |
+
import imageio_ffmpeg
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 16 |
+
|
| 17 |
+
from decode import Lane
|
| 18 |
+
from bev import BevRange, CameraCalibration
|
| 19 |
+
from test_video_bev_onnx import draw_bev, make_composite
|
| 20 |
+
from test_video_onnx import draw_predictions
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args():
|
| 24 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 25 |
+
parser.add_argument("--video", required=True)
|
| 26 |
+
parser.add_argument("--results", required=True)
|
| 27 |
+
parser.add_argument("--output", required=True)
|
| 28 |
+
parser.add_argument("--benchmark-report", default=None)
|
| 29 |
+
return parser.parse_args()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_lane(payload):
|
| 33 |
+
lane = Lane(800, 320)
|
| 34 |
+
lane.points = np.asarray(payload["points"], dtype=np.float32)
|
| 35 |
+
lane._score_sum = float(payload["score"]) * len(lane.points)
|
| 36 |
+
lane.lane_id = int(payload["lane_id"])
|
| 37 |
+
lane.lane_role = payload["role"]
|
| 38 |
+
lane.is_ego_boundary = lane.lane_role in ("ego_left", "ego_right")
|
| 39 |
+
lane.lateral_rank = None
|
| 40 |
+
return lane
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def load_bev_result(payload):
|
| 44 |
+
fit_payload = payload["fit"]
|
| 45 |
+
fit = None
|
| 46 |
+
if payload["fit_accepted"] and fit_payload is not None:
|
| 47 |
+
fit = {
|
| 48 |
+
"coefficients": np.asarray(
|
| 49 |
+
fit_payload["coefficients"], dtype=np.float64
|
| 50 |
+
),
|
| 51 |
+
"x_min": float(fit_payload["x_min"]),
|
| 52 |
+
"x_max": float(fit_payload["x_max"]),
|
| 53 |
+
"rmse": float(fit_payload["rmse"]),
|
| 54 |
+
"point_count": int(fit_payload["point_count"]),
|
| 55 |
+
"inlier_count": int(fit_payload["inlier_count"]),
|
| 56 |
+
}
|
| 57 |
+
lane_id = int(payload["lane_id"])
|
| 58 |
+
record = {
|
| 59 |
+
"lane_id": f"P{lane_id}",
|
| 60 |
+
"lane_index": lane_id,
|
| 61 |
+
"role": payload["role"],
|
| 62 |
+
"score": float(payload["score"]),
|
| 63 |
+
"valid_fit": fit is not None,
|
| 64 |
+
}
|
| 65 |
+
if fit_payload is not None:
|
| 66 |
+
record["rmse_m"] = float(fit_payload["rmse"])
|
| 67 |
+
points = np.asarray(payload["points"], dtype=np.float64)
|
| 68 |
+
return {
|
| 69 |
+
"record": record,
|
| 70 |
+
"points": points[:, :2] if points.size else np.empty((0, 2)),
|
| 71 |
+
"fit": fit,
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def funnel_report(bev_payloads):
|
| 76 |
+
return {
|
| 77 |
+
"mode": "raw_model_projection",
|
| 78 |
+
"parallel_assumption": False,
|
| 79 |
+
"synthetic_lanes": False,
|
| 80 |
+
"clipped_lanes": [
|
| 81 |
+
f"P{item['lane_id']}" for item in bev_payloads
|
| 82 |
+
if item["funnel_clipped"]
|
| 83 |
+
],
|
| 84 |
+
"rejected_lanes": [
|
| 85 |
+
f"P{item['lane_id']}" for item in bev_payloads
|
| 86 |
+
if item["fit"] is not None and not item["fit_accepted"]
|
| 87 |
+
],
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def main():
|
| 92 |
+
args = parse_args()
|
| 93 |
+
video_path = Path(args.video).expanduser().resolve()
|
| 94 |
+
results_path = Path(args.results).expanduser().resolve()
|
| 95 |
+
output_path = Path(args.output).expanduser().resolve()
|
| 96 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 97 |
+
calibration = CameraCalibration()
|
| 98 |
+
bev_range = BevRange()
|
| 99 |
+
|
| 100 |
+
capture = cv2.VideoCapture(str(video_path))
|
| 101 |
+
if not capture.isOpened():
|
| 102 |
+
raise RuntimeError(f"cannot open video: {video_path}")
|
| 103 |
+
fps = float(capture.get(cv2.CAP_PROP_FPS))
|
| 104 |
+
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 105 |
+
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 106 |
+
temporary = output_path.with_name(output_path.stem + ".mp4v.mp4")
|
| 107 |
+
writer = cv2.VideoWriter(
|
| 108 |
+
str(temporary), cv2.VideoWriter_fourcc(*"mp4v"), fps,
|
| 109 |
+
(width, height),
|
| 110 |
+
)
|
| 111 |
+
if not writer.isOpened():
|
| 112 |
+
capture.release()
|
| 113 |
+
raise RuntimeError(f"cannot create temporary video: {temporary}")
|
| 114 |
+
|
| 115 |
+
rendered = 0
|
| 116 |
+
rolling_core_ms = deque(maxlen=20)
|
| 117 |
+
try:
|
| 118 |
+
with results_path.open() as results:
|
| 119 |
+
for line in results:
|
| 120 |
+
payload = json.loads(line)
|
| 121 |
+
ok, frame = capture.read()
|
| 122 |
+
if not ok:
|
| 123 |
+
raise RuntimeError(
|
| 124 |
+
f"video ended before result frame {payload['frame_index']}"
|
| 125 |
+
)
|
| 126 |
+
lanes = [load_lane(item) for item in payload["lanes"]]
|
| 127 |
+
bev_payloads = payload["bev_lanes"]
|
| 128 |
+
lane_results = [
|
| 129 |
+
load_bev_result(item) for item in bev_payloads
|
| 130 |
+
]
|
| 131 |
+
guard = funnel_report(bev_payloads)
|
| 132 |
+
timing = payload["timing"]
|
| 133 |
+
rolling_core_ms.append(float(timing["core_ms"]))
|
| 134 |
+
rolling_fps = 1000.0 / max(
|
| 135 |
+
float(np.mean(rolling_core_ms)), 1e-9
|
| 136 |
+
)
|
| 137 |
+
draw_predictions(frame, lanes)
|
| 138 |
+
bev_canvas = draw_bev(
|
| 139 |
+
lane_results, bev_range, calibration, guard
|
| 140 |
+
)
|
| 141 |
+
composite = make_composite(
|
| 142 |
+
frame, bev_canvas, int(payload["frame_index"]),
|
| 143 |
+
sum(item["fit"] is not None for item in lane_results),
|
| 144 |
+
float(timing["core_ms"]), guard,
|
| 145 |
+
result_generation_fps=rolling_fps,
|
| 146 |
+
)
|
| 147 |
+
detail = (
|
| 148 |
+
f"C++ current={timing['core_ms']:.1f}ms | "
|
| 149 |
+
f"infer={timing['inference_ms']:.1f} "
|
| 150 |
+
f"decode={timing['decode_ms']:.1f} "
|
| 151 |
+
f"BEV-result={timing['bev_result_ms']:.2f}ms | "
|
| 152 |
+
"draw/encode excluded"
|
| 153 |
+
)
|
| 154 |
+
cv2.putText(
|
| 155 |
+
composite, detail, (664, 101),
|
| 156 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.50, (0, 0, 0), 4,
|
| 157 |
+
cv2.LINE_AA,
|
| 158 |
+
)
|
| 159 |
+
cv2.putText(
|
| 160 |
+
composite, detail, (664, 101),
|
| 161 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.50, (255, 255, 255), 1,
|
| 162 |
+
cv2.LINE_AA,
|
| 163 |
+
)
|
| 164 |
+
writer.write(composite)
|
| 165 |
+
rendered += 1
|
| 166 |
+
if rendered % 200 == 0:
|
| 167 |
+
print(f"rendered {rendered} frames", flush=True)
|
| 168 |
+
finally:
|
| 169 |
+
capture.release()
|
| 170 |
+
writer.release()
|
| 171 |
+
|
| 172 |
+
encoded = output_path.with_name(output_path.stem + ".h264.tmp.mp4")
|
| 173 |
+
subprocess.run(
|
| 174 |
+
[
|
| 175 |
+
imageio_ffmpeg.get_ffmpeg_exe(), "-y", "-loglevel", "error",
|
| 176 |
+
"-i", str(temporary), "-c:v", "libx264", "-preset", "fast",
|
| 177 |
+
"-crf", "18", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
|
| 178 |
+
str(encoded),
|
| 179 |
+
],
|
| 180 |
+
check=True,
|
| 181 |
+
)
|
| 182 |
+
os.replace(encoded, output_path)
|
| 183 |
+
temporary.unlink(missing_ok=True)
|
| 184 |
+
print(f"rendered video: {output_path} ({rendered} frames)")
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
main()
|
cpp/src/bev.cpp
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "bev.hpp"
|
| 2 |
+
|
| 3 |
+
#include <algorithm>
|
| 4 |
+
#include <array>
|
| 5 |
+
#include <cmath>
|
| 6 |
+
#include <fstream>
|
| 7 |
+
#include <iomanip>
|
| 8 |
+
#include <limits>
|
| 9 |
+
#include <stdexcept>
|
| 10 |
+
#include <vector>
|
| 11 |
+
|
| 12 |
+
namespace rclane {
|
| 13 |
+
namespace {
|
| 14 |
+
|
| 15 |
+
constexpr double kFx = 3582.768775;
|
| 16 |
+
constexpr double kFy = 3582.768775;
|
| 17 |
+
constexpr double kCx = 960.0;
|
| 18 |
+
constexpr double kCy = 540.0;
|
| 19 |
+
constexpr double kRawWidth = 1920.0;
|
| 20 |
+
constexpr double kRawHeight = 1080.0;
|
| 21 |
+
constexpr double kModelWidth = 800.0;
|
| 22 |
+
constexpr double kModelHeight = 320.0;
|
| 23 |
+
constexpr double kCameraX = 1.0;
|
| 24 |
+
constexpr double kCameraZ = 1.8;
|
| 25 |
+
constexpr double kCosPitch = 0.997564;
|
| 26 |
+
constexpr double kSinDown = 0.069756;
|
| 27 |
+
constexpr double kPi = 3.14159265358979323846;
|
| 28 |
+
|
| 29 |
+
bool solve_four_by_four(
|
| 30 |
+
std::array<std::array<double, 4>, 4> matrix,
|
| 31 |
+
std::array<double, 4> target,
|
| 32 |
+
std::array<double, 4>& solution
|
| 33 |
+
) {
|
| 34 |
+
for (int column = 0; column < 4; ++column) {
|
| 35 |
+
int pivot = column;
|
| 36 |
+
for (int row = column + 1; row < 4; ++row) {
|
| 37 |
+
if (std::abs(matrix[row][column]) > std::abs(matrix[pivot][column])) {
|
| 38 |
+
pivot = row;
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
if (std::abs(matrix[pivot][column]) < 1e-12) {
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
std::swap(matrix[pivot], matrix[column]);
|
| 45 |
+
std::swap(target[pivot], target[column]);
|
| 46 |
+
const double divisor = matrix[column][column];
|
| 47 |
+
for (int index = column; index < 4; ++index) {
|
| 48 |
+
matrix[column][index] /= divisor;
|
| 49 |
+
}
|
| 50 |
+
target[column] /= divisor;
|
| 51 |
+
for (int row = 0; row < 4; ++row) {
|
| 52 |
+
if (row == column) {
|
| 53 |
+
continue;
|
| 54 |
+
}
|
| 55 |
+
const double factor = matrix[row][column];
|
| 56 |
+
for (int index = column; index < 4; ++index) {
|
| 57 |
+
matrix[row][index] -= factor * matrix[column][index];
|
| 58 |
+
}
|
| 59 |
+
target[row] -= factor * target[column];
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
solution = target;
|
| 63 |
+
return true;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
bool weighted_fit(
|
| 67 |
+
const std::vector<double>& z,
|
| 68 |
+
const std::vector<double>& y,
|
| 69 |
+
const std::vector<double>& weights,
|
| 70 |
+
const std::vector<std::uint8_t>* include,
|
| 71 |
+
std::array<double, 4>& coefficients
|
| 72 |
+
) {
|
| 73 |
+
std::array<std::array<double, 4>, 4> normal{};
|
| 74 |
+
std::array<double, 4> target{};
|
| 75 |
+
for (std::size_t index = 0; index < z.size(); ++index) {
|
| 76 |
+
if (include != nullptr && (*include)[index] == 0U) {
|
| 77 |
+
continue;
|
| 78 |
+
}
|
| 79 |
+
const std::array<double, 4> row{1.0, z[index], z[index] * z[index],
|
| 80 |
+
z[index] * z[index] * z[index]};
|
| 81 |
+
for (int i = 0; i < 4; ++i) {
|
| 82 |
+
target[i] += weights[index] * row[i] * y[index];
|
| 83 |
+
for (int j = 0; j < 4; ++j) {
|
| 84 |
+
normal[i][j] += weights[index] * row[i] * row[j];
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
return solve_four_by_four(normal, target, coefficients);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
double evaluate(const std::array<double, 4>& c, double x) {
|
| 92 |
+
return ((c[3] * x + c[2]) * x + c[1]) * x + c[0];
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
std::vector<GroundPoint> project_lane(
|
| 96 |
+
const Lane& lane, const BevConfig& config
|
| 97 |
+
) {
|
| 98 |
+
std::vector<GroundPoint> ground;
|
| 99 |
+
ground.reserve(lane.points.size());
|
| 100 |
+
for (const auto& point : lane.points) {
|
| 101 |
+
const double u = point.x * (kRawWidth / kModelWidth);
|
| 102 |
+
const double v = point.y * (kRawHeight / kModelHeight);
|
| 103 |
+
const double ray_right = (u - kCx) / kFx;
|
| 104 |
+
const double ray_down = (v - kCy) / kFy;
|
| 105 |
+
// OpenCV optical -> UE camera is (forward,right,up)=(1,x,-y),
|
| 106 |
+
// followed by the fixed camera-to-vehicle pitch rotation.
|
| 107 |
+
const double ray_x = kCosPitch - kSinDown * ray_down;
|
| 108 |
+
const double ray_y = ray_right;
|
| 109 |
+
const double ray_z = -kSinDown - kCosPitch * ray_down;
|
| 110 |
+
if (!(ray_z < -1e-8)) {
|
| 111 |
+
continue;
|
| 112 |
+
}
|
| 113 |
+
const double scale = -kCameraZ / ray_z;
|
| 114 |
+
if (!(scale > 0.0) || !std::isfinite(scale)) {
|
| 115 |
+
continue;
|
| 116 |
+
}
|
| 117 |
+
const double x = kCameraX + scale * ray_x;
|
| 118 |
+
const double y_left = -scale * ray_y;
|
| 119 |
+
if (std::isfinite(x) && std::isfinite(y_left)
|
| 120 |
+
&& x >= config.x_min && x <= config.x_max
|
| 121 |
+
&& y_left >= config.y_min && y_left <= config.y_max) {
|
| 122 |
+
ground.push_back({x, y_left, point.score});
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
return ground;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
CubicFit fit_cubic(const std::vector<GroundPoint>& input) {
|
| 129 |
+
CubicFit fit;
|
| 130 |
+
fit.point_count = input.size();
|
| 131 |
+
if (input.size() < 6) {
|
| 132 |
+
return fit;
|
| 133 |
+
}
|
| 134 |
+
std::vector<GroundPoint> points = input;
|
| 135 |
+
std::sort(points.begin(), points.end(), [](const GroundPoint& lhs,
|
| 136 |
+
const GroundPoint& rhs) {
|
| 137 |
+
return lhs.x < rhs.x;
|
| 138 |
+
});
|
| 139 |
+
const double minimum_x = points.front().x;
|
| 140 |
+
const double maximum_x = points.back().x;
|
| 141 |
+
if (maximum_x - minimum_x < 1.0) {
|
| 142 |
+
return fit;
|
| 143 |
+
}
|
| 144 |
+
const double center = (minimum_x + maximum_x) * 0.5;
|
| 145 |
+
const double scale = std::max((maximum_x - minimum_x) * 0.5, 1.0);
|
| 146 |
+
std::vector<double> z(points.size());
|
| 147 |
+
std::vector<double> y(points.size());
|
| 148 |
+
std::vector<double> base(points.size());
|
| 149 |
+
for (std::size_t index = 0; index < points.size(); ++index) {
|
| 150 |
+
z[index] = (points[index].x - center) / scale;
|
| 151 |
+
y[index] = points[index].y;
|
| 152 |
+
base[index] = std::clamp(points[index].score, 0.05, 1.0);
|
| 153 |
+
}
|
| 154 |
+
std::vector<double> weights = base;
|
| 155 |
+
std::array<double, 4> normalized{};
|
| 156 |
+
if (!weighted_fit(z, y, weights, nullptr, normalized)) {
|
| 157 |
+
return fit;
|
| 158 |
+
}
|
| 159 |
+
for (int iteration = 0; iteration < 3; ++iteration) {
|
| 160 |
+
for (std::size_t index = 0; index < points.size(); ++index) {
|
| 161 |
+
const double residual = y[index] - evaluate(normalized, z[index]);
|
| 162 |
+
const double robust = std::min(
|
| 163 |
+
1.0, 0.30 / std::max(std::abs(residual), 1e-8)
|
| 164 |
+
);
|
| 165 |
+
weights[index] = base[index] * robust;
|
| 166 |
+
}
|
| 167 |
+
if (!weighted_fit(z, y, weights, nullptr, normalized)) {
|
| 168 |
+
return fit;
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
std::vector<double> absolute_residual(points.size());
|
| 172 |
+
for (std::size_t index = 0; index < points.size(); ++index) {
|
| 173 |
+
absolute_residual[index] = std::abs(
|
| 174 |
+
y[index] - evaluate(normalized, z[index])
|
| 175 |
+
);
|
| 176 |
+
}
|
| 177 |
+
std::vector<double> sorted_residual = absolute_residual;
|
| 178 |
+
std::sort(sorted_residual.begin(), sorted_residual.end());
|
| 179 |
+
const std::size_t middle = sorted_residual.size() / 2;
|
| 180 |
+
const double median_residual = (sorted_residual.size() & 1U) != 0U
|
| 181 |
+
? sorted_residual[middle]
|
| 182 |
+
: (sorted_residual[middle - 1] + sorted_residual[middle]) * 0.5;
|
| 183 |
+
const double threshold = std::max(0.30, 2.5 * median_residual);
|
| 184 |
+
std::vector<std::uint8_t> inliers(points.size(), 0U);
|
| 185 |
+
std::size_t inlier_count = 0;
|
| 186 |
+
for (std::size_t index = 0; index < points.size(); ++index) {
|
| 187 |
+
inliers[index] = static_cast<std::uint8_t>(
|
| 188 |
+
absolute_residual[index] <= threshold
|
| 189 |
+
);
|
| 190 |
+
inlier_count += inliers[index];
|
| 191 |
+
}
|
| 192 |
+
if (inlier_count >= 6) {
|
| 193 |
+
if (!weighted_fit(z, y, base, &inliers, normalized)) {
|
| 194 |
+
return fit;
|
| 195 |
+
}
|
| 196 |
+
} else {
|
| 197 |
+
std::fill(inliers.begin(), inliers.end(), 1U);
|
| 198 |
+
inlier_count = points.size();
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const double inverse_scale = 1.0 / scale;
|
| 202 |
+
const double argument_constant = -center * inverse_scale;
|
| 203 |
+
const double argument_linear = inverse_scale;
|
| 204 |
+
fit.coefficients[0] = normalized[0]
|
| 205 |
+
+ normalized[1] * argument_constant
|
| 206 |
+
+ normalized[2] * argument_constant * argument_constant
|
| 207 |
+
+ normalized[3] * argument_constant * argument_constant * argument_constant;
|
| 208 |
+
fit.coefficients[1] = normalized[1] * argument_linear
|
| 209 |
+
+ 2.0 * normalized[2] * argument_constant * argument_linear
|
| 210 |
+
+ 3.0 * normalized[3] * argument_constant * argument_constant * argument_linear;
|
| 211 |
+
fit.coefficients[2] = normalized[2] * argument_linear * argument_linear
|
| 212 |
+
+ 3.0 * normalized[3] * argument_constant * argument_linear * argument_linear;
|
| 213 |
+
fit.coefficients[3] = normalized[3] * argument_linear * argument_linear * argument_linear;
|
| 214 |
+
|
| 215 |
+
fit.x_min = std::numeric_limits<double>::infinity();
|
| 216 |
+
fit.x_max = -std::numeric_limits<double>::infinity();
|
| 217 |
+
double square_error = 0.0;
|
| 218 |
+
for (std::size_t index = 0; index < points.size(); ++index) {
|
| 219 |
+
if (inliers[index] == 0U) {
|
| 220 |
+
continue;
|
| 221 |
+
}
|
| 222 |
+
fit.x_min = std::min(fit.x_min, points[index].x);
|
| 223 |
+
fit.x_max = std::max(fit.x_max, points[index].x);
|
| 224 |
+
const double residual = points[index].y
|
| 225 |
+
- evaluate(fit.coefficients, points[index].x);
|
| 226 |
+
square_error += residual * residual;
|
| 227 |
+
}
|
| 228 |
+
fit.inlier_count = inlier_count;
|
| 229 |
+
fit.rmse = std::sqrt(square_error / static_cast<double>(inlier_count));
|
| 230 |
+
fit.valid = true;
|
| 231 |
+
return fit;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
bool clip_to_funnel(CubicFit& fit, double margin, bool& clipped) {
|
| 235 |
+
clipped = false;
|
| 236 |
+
if (!fit.valid || fit.x_max - fit.x_min < 1.0) {
|
| 237 |
+
return false;
|
| 238 |
+
}
|
| 239 |
+
const int count = std::max(
|
| 240 |
+
3, static_cast<int>(std::ceil((fit.x_max - fit.x_min) / 0.5)) + 1
|
| 241 |
+
);
|
| 242 |
+
std::vector<double> x(static_cast<std::size_t>(count));
|
| 243 |
+
std::vector<std::uint8_t> valid(static_cast<std::size_t>(count), 0U);
|
| 244 |
+
const double tangent = std::tan(15.0 * kPi / 180.0);
|
| 245 |
+
for (int index = 0; index < count; ++index) {
|
| 246 |
+
x[static_cast<std::size_t>(index)] = fit.x_min
|
| 247 |
+
+ (fit.x_max - fit.x_min) * static_cast<double>(index)
|
| 248 |
+
/ static_cast<double>(count - 1);
|
| 249 |
+
const double y = evaluate(fit.coefficients, x[static_cast<std::size_t>(index)]);
|
| 250 |
+
const double half_width = std::max(
|
| 251 |
+
0.0, x[static_cast<std::size_t>(index)] - kCameraX
|
| 252 |
+
) * tangent;
|
| 253 |
+
valid[static_cast<std::size_t>(index)] = static_cast<std::uint8_t>(
|
| 254 |
+
std::isfinite(y)
|
| 255 |
+
&& x[static_cast<std::size_t>(index)] >= kCameraX
|
| 256 |
+
&& std::abs(y) <= half_width + margin
|
| 257 |
+
);
|
| 258 |
+
}
|
| 259 |
+
int best_start = -1;
|
| 260 |
+
int best_end = -1;
|
| 261 |
+
int start = -1;
|
| 262 |
+
for (int index = 0; index < count; ++index) {
|
| 263 |
+
if (valid[static_cast<std::size_t>(index)] != 0U && start < 0) {
|
| 264 |
+
start = index;
|
| 265 |
+
}
|
| 266 |
+
if (start >= 0 && (valid[static_cast<std::size_t>(index)] == 0U
|
| 267 |
+
|| index == count - 1)) {
|
| 268 |
+
const int end = valid[static_cast<std::size_t>(index)] != 0U
|
| 269 |
+
&& index == count - 1 ? index : index - 1;
|
| 270 |
+
if (x[static_cast<std::size_t>(end)] - x[static_cast<std::size_t>(start)] >= 1.0
|
| 271 |
+
&& (best_start < 0
|
| 272 |
+
|| x[static_cast<std::size_t>(end)] - x[static_cast<std::size_t>(start)]
|
| 273 |
+
> x[static_cast<std::size_t>(best_end)] - x[static_cast<std::size_t>(best_start)])) {
|
| 274 |
+
best_start = start;
|
| 275 |
+
best_end = end;
|
| 276 |
+
}
|
| 277 |
+
start = -1;
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
if (best_start < 0) {
|
| 281 |
+
return false;
|
| 282 |
+
}
|
| 283 |
+
const double original_min = fit.x_min;
|
| 284 |
+
const double original_max = fit.x_max;
|
| 285 |
+
fit.x_min = x[static_cast<std::size_t>(best_start)];
|
| 286 |
+
fit.x_max = x[static_cast<std::size_t>(best_end)];
|
| 287 |
+
clipped = fit.x_min > original_min + 1e-6
|
| 288 |
+
|| fit.x_max < original_max - 1e-6;
|
| 289 |
+
return true;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
} // namespace
|
| 293 |
+
|
| 294 |
+
std::vector<BevLane> project_lanes_to_bev(
|
| 295 |
+
const std::vector<Lane>& lanes,
|
| 296 |
+
const BevConfig& config
|
| 297 |
+
) {
|
| 298 |
+
std::vector<BevLane> result;
|
| 299 |
+
result.reserve(lanes.size());
|
| 300 |
+
for (const Lane& lane : lanes) {
|
| 301 |
+
BevLane output;
|
| 302 |
+
output.lane_id = lane.lane_id;
|
| 303 |
+
output.role = lane.role;
|
| 304 |
+
output.score = lane.score();
|
| 305 |
+
output.points = project_lane(lane, config);
|
| 306 |
+
output.fit = fit_cubic(output.points);
|
| 307 |
+
output.fit_accepted = output.fit.valid
|
| 308 |
+
&& output.fit.rmse <= config.maximum_rmse;
|
| 309 |
+
if (output.fit_accepted) {
|
| 310 |
+
output.fit_accepted = clip_to_funnel(
|
| 311 |
+
output.fit, config.funnel_margin, output.funnel_clipped
|
| 312 |
+
);
|
| 313 |
+
}
|
| 314 |
+
result.push_back(std::move(output));
|
| 315 |
+
}
|
| 316 |
+
return result;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
void write_bev_json(
|
| 320 |
+
const std::string& path,
|
| 321 |
+
const std::vector<BevLane>& lanes
|
| 322 |
+
) {
|
| 323 |
+
std::ofstream stream(path);
|
| 324 |
+
if (!stream) {
|
| 325 |
+
throw std::runtime_error("cannot write BEV JSON: " + path);
|
| 326 |
+
}
|
| 327 |
+
stream << std::setprecision(12) << "{\n \"mode\": \"raw_model_projection\",\n"
|
| 328 |
+
<< " \"parallel_assumption\": false,\n"
|
| 329 |
+
<< " \"synthetic_lanes\": false,\n \"lanes\": [\n";
|
| 330 |
+
for (std::size_t index = 0; index < lanes.size(); ++index) {
|
| 331 |
+
const BevLane& lane = lanes[index];
|
| 332 |
+
stream << " {\"lane_id\": " << lane.lane_id
|
| 333 |
+
<< ", \"role\": \"" << lane.role
|
| 334 |
+
<< "\", \"score\": " << lane.score
|
| 335 |
+
<< ", \"projected_point_count\": " << lane.points.size()
|
| 336 |
+
<< ", \"valid_fit\": " << (lane.fit_accepted ? "true" : "false");
|
| 337 |
+
if (lane.fit.valid) {
|
| 338 |
+
stream << ", \"coefficients_c0_to_c3\": ["
|
| 339 |
+
<< lane.fit.coefficients[0] << ',' << lane.fit.coefficients[1]
|
| 340 |
+
<< ',' << lane.fit.coefficients[2] << ',' << lane.fit.coefficients[3]
|
| 341 |
+
<< "], \"x_domain_m\": [" << lane.fit.x_min << ',' << lane.fit.x_max
|
| 342 |
+
<< "], \"rmse_m\": " << lane.fit.rmse
|
| 343 |
+
<< ", \"inlier_count\": " << lane.fit.inlier_count
|
| 344 |
+
<< ", \"funnel_clipped\": "
|
| 345 |
+
<< (lane.funnel_clipped ? "true" : "false");
|
| 346 |
+
}
|
| 347 |
+
stream << '}' << (index + 1 == lanes.size() ? "\n" : ",\n");
|
| 348 |
+
}
|
| 349 |
+
stream << " ]\n}\n";
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
} // namespace rclane
|
cpp/src/decoder.cpp
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "decoder.hpp"
|
| 2 |
+
|
| 3 |
+
#include <algorithm>
|
| 4 |
+
#include <cmath>
|
| 5 |
+
#include <cstdint>
|
| 6 |
+
#include <fstream>
|
| 7 |
+
#include <iomanip>
|
| 8 |
+
#include <limits>
|
| 9 |
+
#include <numeric>
|
| 10 |
+
#include <stdexcept>
|
| 11 |
+
#include <unordered_map>
|
| 12 |
+
#include <utility>
|
| 13 |
+
|
| 14 |
+
#include <omp.h>
|
| 15 |
+
|
| 16 |
+
namespace rclane {
|
| 17 |
+
namespace {
|
| 18 |
+
|
| 19 |
+
constexpr int kMapWidth = 800;
|
| 20 |
+
constexpr int kMapHeight = 320;
|
| 21 |
+
|
| 22 |
+
struct Seed {
|
| 23 |
+
int x{};
|
| 24 |
+
int y{};
|
| 25 |
+
float probability{};
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
std::vector<std::size_t> numpy_float_argquicksort(
|
| 29 |
+
const std::vector<Seed>& candidates
|
| 30 |
+
) {
|
| 31 |
+
// Partition structure follows NumPy's BSD-licensed npysort aquicksort;
|
| 32 |
+
// see cpp/THIRD_PARTY_NOTICES.md.
|
| 33 |
+
// Match NumPy 1.26's default np.argsort quicksort, including its
|
| 34 |
+
// deterministic (but unstable) ordering of equal saturated probabilities.
|
| 35 |
+
// This matters because the segmentation map contains long probability=1
|
| 36 |
+
// plateaus and greedy point-NMS consumes candidates in argsort order.
|
| 37 |
+
std::vector<std::size_t> order(candidates.size());
|
| 38 |
+
std::iota(order.begin(), order.end(), std::size_t{0});
|
| 39 |
+
if (order.size() <= 1) {
|
| 40 |
+
return order;
|
| 41 |
+
}
|
| 42 |
+
const auto key = [&candidates](std::size_t index) {
|
| 43 |
+
return -candidates[index].probability;
|
| 44 |
+
};
|
| 45 |
+
const auto less = [&key](std::size_t lhs, std::size_t rhs) {
|
| 46 |
+
return key(lhs) < key(rhs);
|
| 47 |
+
};
|
| 48 |
+
struct Partition {
|
| 49 |
+
std::ptrdiff_t left{};
|
| 50 |
+
std::ptrdiff_t right{};
|
| 51 |
+
int depth{};
|
| 52 |
+
};
|
| 53 |
+
std::vector<Partition> stack;
|
| 54 |
+
stack.reserve(128);
|
| 55 |
+
std::ptrdiff_t left = 0;
|
| 56 |
+
std::ptrdiff_t right = static_cast<std::ptrdiff_t>(order.size() - 1);
|
| 57 |
+
int most_significant_bit = 0;
|
| 58 |
+
for (std::size_t size = order.size(); size > 1; size >>= 1U) {
|
| 59 |
+
++most_significant_bit;
|
| 60 |
+
}
|
| 61 |
+
int depth = most_significant_bit * 2;
|
| 62 |
+
for (;;) {
|
| 63 |
+
if (depth < 0) {
|
| 64 |
+
// NumPy switches to arg-heapsort here. This branch is not reached
|
| 65 |
+
// by the map sizes/distributions used by RCLane; retain a safe
|
| 66 |
+
// deterministic fallback for adversarial inputs.
|
| 67 |
+
std::sort(
|
| 68 |
+
order.begin() + left, order.begin() + right + 1, less
|
| 69 |
+
);
|
| 70 |
+
goto pop_partition;
|
| 71 |
+
}
|
| 72 |
+
while (right - left > 15) {
|
| 73 |
+
const std::ptrdiff_t middle = left + ((right - left) >> 1);
|
| 74 |
+
if (less(order[static_cast<std::size_t>(middle)],
|
| 75 |
+
order[static_cast<std::size_t>(left)])) {
|
| 76 |
+
std::swap(order[static_cast<std::size_t>(middle)],
|
| 77 |
+
order[static_cast<std::size_t>(left)]);
|
| 78 |
+
}
|
| 79 |
+
if (less(order[static_cast<std::size_t>(right)],
|
| 80 |
+
order[static_cast<std::size_t>(middle)])) {
|
| 81 |
+
std::swap(order[static_cast<std::size_t>(right)],
|
| 82 |
+
order[static_cast<std::size_t>(middle)]);
|
| 83 |
+
}
|
| 84 |
+
if (less(order[static_cast<std::size_t>(middle)],
|
| 85 |
+
order[static_cast<std::size_t>(left)])) {
|
| 86 |
+
std::swap(order[static_cast<std::size_t>(middle)],
|
| 87 |
+
order[static_cast<std::size_t>(left)]);
|
| 88 |
+
}
|
| 89 |
+
const float pivot = key(order[static_cast<std::size_t>(middle)]);
|
| 90 |
+
std::ptrdiff_t i = left;
|
| 91 |
+
std::ptrdiff_t j = right - 1;
|
| 92 |
+
std::swap(order[static_cast<std::size_t>(middle)],
|
| 93 |
+
order[static_cast<std::size_t>(j)]);
|
| 94 |
+
for (;;) {
|
| 95 |
+
do {
|
| 96 |
+
++i;
|
| 97 |
+
} while (key(order[static_cast<std::size_t>(i)]) < pivot);
|
| 98 |
+
do {
|
| 99 |
+
--j;
|
| 100 |
+
} while (pivot < key(order[static_cast<std::size_t>(j)]));
|
| 101 |
+
if (i >= j) {
|
| 102 |
+
break;
|
| 103 |
+
}
|
| 104 |
+
std::swap(order[static_cast<std::size_t>(i)],
|
| 105 |
+
order[static_cast<std::size_t>(j)]);
|
| 106 |
+
}
|
| 107 |
+
std::swap(order[static_cast<std::size_t>(i)],
|
| 108 |
+
order[static_cast<std::size_t>(right - 1)]);
|
| 109 |
+
--depth;
|
| 110 |
+
if (i - left < right - i) {
|
| 111 |
+
stack.push_back({i + 1, right, depth});
|
| 112 |
+
right = i - 1;
|
| 113 |
+
} else {
|
| 114 |
+
stack.push_back({left, i - 1, depth});
|
| 115 |
+
left = i + 1;
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
for (std::ptrdiff_t i = left + 1; i <= right; ++i) {
|
| 119 |
+
const std::size_t value = order[static_cast<std::size_t>(i)];
|
| 120 |
+
std::ptrdiff_t position = i;
|
| 121 |
+
std::ptrdiff_t previous = i - 1;
|
| 122 |
+
while (position > left
|
| 123 |
+
&& key(value) < key(order[static_cast<std::size_t>(previous)])) {
|
| 124 |
+
order[static_cast<std::size_t>(position--)]
|
| 125 |
+
= order[static_cast<std::size_t>(previous--)];
|
| 126 |
+
}
|
| 127 |
+
order[static_cast<std::size_t>(position)] = value;
|
| 128 |
+
}
|
| 129 |
+
pop_partition:
|
| 130 |
+
if (stack.empty()) {
|
| 131 |
+
break;
|
| 132 |
+
}
|
| 133 |
+
const Partition next = stack.back();
|
| 134 |
+
stack.pop_back();
|
| 135 |
+
left = next.left;
|
| 136 |
+
right = next.right;
|
| 137 |
+
depth = next.depth;
|
| 138 |
+
}
|
| 139 |
+
return order;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
float median(std::vector<float> values) {
|
| 143 |
+
if (values.empty()) {
|
| 144 |
+
return 0.0F;
|
| 145 |
+
}
|
| 146 |
+
const std::size_t middle = values.size() / 2;
|
| 147 |
+
std::nth_element(values.begin(), values.begin() + middle, values.end());
|
| 148 |
+
const float upper = values[middle];
|
| 149 |
+
if ((values.size() & 1U) != 0U) {
|
| 150 |
+
return upper;
|
| 151 |
+
}
|
| 152 |
+
const float lower = *std::max_element(
|
| 153 |
+
values.begin(), values.begin() + middle
|
| 154 |
+
);
|
| 155 |
+
return (lower + upper) * 0.5F;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
double quantile(std::vector<float> values, double fraction) {
|
| 159 |
+
if (values.empty()) {
|
| 160 |
+
return std::numeric_limits<double>::quiet_NaN();
|
| 161 |
+
}
|
| 162 |
+
std::sort(values.begin(), values.end());
|
| 163 |
+
const double position = fraction * static_cast<double>(values.size() - 1);
|
| 164 |
+
const auto lower = static_cast<std::size_t>(std::floor(position));
|
| 165 |
+
const auto upper = static_cast<std::size_t>(std::ceil(position));
|
| 166 |
+
const double blend = position - static_cast<double>(lower);
|
| 167 |
+
return static_cast<double>(values[lower]) * (1.0 - blend)
|
| 168 |
+
+ static_cast<double>(values[upper]) * blend;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
std::vector<Seed> select_seeds(
|
| 172 |
+
const std::vector<float>& probability,
|
| 173 |
+
const DecoderConfig& config,
|
| 174 |
+
DecodeStatistics* statistics
|
| 175 |
+
) {
|
| 176 |
+
std::vector<Seed> candidates;
|
| 177 |
+
candidates.reserve(probability.size() / 20);
|
| 178 |
+
for (int y = 0; y < kMapHeight; ++y) {
|
| 179 |
+
for (int x = 0; x < kMapWidth; ++x) {
|
| 180 |
+
const float value = probability[static_cast<std::size_t>(
|
| 181 |
+
y * kMapWidth + x
|
| 182 |
+
)];
|
| 183 |
+
if (value > config.seed_threshold) {
|
| 184 |
+
candidates.push_back({x, y, value});
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
if (statistics != nullptr) {
|
| 189 |
+
statistics->foreground_pixels = candidates.size();
|
| 190 |
+
}
|
| 191 |
+
const auto order = numpy_float_argquicksort(candidates);
|
| 192 |
+
|
| 193 |
+
std::vector<std::uint8_t> taken(
|
| 194 |
+
static_cast<std::size_t>(kMapWidth * kMapHeight), 0
|
| 195 |
+
);
|
| 196 |
+
std::vector<Seed> selected;
|
| 197 |
+
selected.reserve(static_cast<std::size_t>(config.max_seeds));
|
| 198 |
+
for (const std::size_t candidate_index : order) {
|
| 199 |
+
const Seed& candidate = candidates[candidate_index];
|
| 200 |
+
const std::size_t position = static_cast<std::size_t>(
|
| 201 |
+
candidate.y * kMapWidth + candidate.x
|
| 202 |
+
);
|
| 203 |
+
if (taken[position] != 0U) {
|
| 204 |
+
continue;
|
| 205 |
+
}
|
| 206 |
+
selected.push_back(candidate);
|
| 207 |
+
const int y0 = std::max(0, candidate.y - config.seed_min_distance);
|
| 208 |
+
const int y1 = std::min(
|
| 209 |
+
kMapHeight - 1, candidate.y + config.seed_min_distance
|
| 210 |
+
);
|
| 211 |
+
const int x0 = std::max(0, candidate.x - config.seed_min_distance);
|
| 212 |
+
const int x1 = std::min(
|
| 213 |
+
kMapWidth - 1, candidate.x + config.seed_min_distance
|
| 214 |
+
);
|
| 215 |
+
for (int y = y0; y <= y1; ++y) {
|
| 216 |
+
for (int x = x0; x <= x1; ++x) {
|
| 217 |
+
taken[static_cast<std::size_t>(y * kMapWidth + x)] = 1U;
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
if (static_cast<int>(selected.size()) >= config.max_seeds) {
|
| 221 |
+
break;
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
if (statistics != nullptr) {
|
| 225 |
+
statistics->seeds = selected.size();
|
| 226 |
+
}
|
| 227 |
+
return selected;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
std::vector<LanePoint> crawl(
|
| 231 |
+
const Seed& seed,
|
| 232 |
+
const std::vector<float>& probability,
|
| 233 |
+
const std::vector<float>& arrow,
|
| 234 |
+
const std::vector<float>& bound,
|
| 235 |
+
const DecoderConfig& config
|
| 236 |
+
) {
|
| 237 |
+
std::vector<LanePoint> points;
|
| 238 |
+
points.reserve(48);
|
| 239 |
+
int cx = seed.x;
|
| 240 |
+
int cy = seed.y;
|
| 241 |
+
double remain_square_sum = 0.0;
|
| 242 |
+
int remain_count = 0;
|
| 243 |
+
const std::size_t channel_elements = static_cast<std::size_t>(
|
| 244 |
+
kMapWidth * kMapHeight
|
| 245 |
+
);
|
| 246 |
+
for (int index = 0; index < kMapHeight; ++index) {
|
| 247 |
+
const std::size_t current = static_cast<std::size_t>(
|
| 248 |
+
cy * kMapWidth + cx
|
| 249 |
+
);
|
| 250 |
+
if (probability[current] > config.segmentation_threshold) {
|
| 251 |
+
const double remain = static_cast<double>(bound[current]) * 100.0
|
| 252 |
+
/ static_cast<double>(config.step_length)
|
| 253 |
+
+ static_cast<double>(index);
|
| 254 |
+
remain_square_sum += remain * remain;
|
| 255 |
+
++remain_count;
|
| 256 |
+
}
|
| 257 |
+
const float dx = arrow[current];
|
| 258 |
+
const float dy = arrow[channel_elements + current];
|
| 259 |
+
const float norm = std::sqrt(dx * dx + dy * dy);
|
| 260 |
+
if (norm == 0.0F || !std::isfinite(norm)) {
|
| 261 |
+
break;
|
| 262 |
+
}
|
| 263 |
+
cx = static_cast<int>(std::floor(
|
| 264 |
+
static_cast<float>(cx) + dx / norm * config.step_length
|
| 265 |
+
));
|
| 266 |
+
cy = static_cast<int>(std::floor(
|
| 267 |
+
static_cast<float>(cy) + dy / norm * config.step_length
|
| 268 |
+
));
|
| 269 |
+
if (cx < 0 || cx >= kMapWidth || cy < 0 || cy >= kMapHeight) {
|
| 270 |
+
break;
|
| 271 |
+
}
|
| 272 |
+
const float score = probability[static_cast<std::size_t>(
|
| 273 |
+
cy * kMapWidth + cx
|
| 274 |
+
)];
|
| 275 |
+
points.push_back({static_cast<float>(cx), static_cast<float>(cy), score});
|
| 276 |
+
const double remaining = remain_count > 0
|
| 277 |
+
? std::sqrt(remain_square_sum / static_cast<double>(remain_count))
|
| 278 |
+
: 1.0;
|
| 279 |
+
if (score > config.segmentation_threshold) {
|
| 280 |
+
continue;
|
| 281 |
+
}
|
| 282 |
+
if (static_cast<double>(index) > remaining * 0.75) {
|
| 283 |
+
break;
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
return points;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
double reference_x(const Lane& lane) {
|
| 290 |
+
if (lane.points.empty()) {
|
| 291 |
+
return std::numeric_limits<double>::infinity();
|
| 292 |
+
}
|
| 293 |
+
if (lane.points.size() < 2) {
|
| 294 |
+
return lane.points.front().x;
|
| 295 |
+
}
|
| 296 |
+
std::vector<float> y_values;
|
| 297 |
+
y_values.reserve(lane.points.size());
|
| 298 |
+
for (const auto& point : lane.points) {
|
| 299 |
+
if (std::isfinite(point.x) && std::isfinite(point.y)) {
|
| 300 |
+
y_values.push_back(point.y);
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
if (y_values.size() < 2) {
|
| 304 |
+
return lane.points.front().x;
|
| 305 |
+
}
|
| 306 |
+
const double cutoff = quantile(y_values, 0.6);
|
| 307 |
+
double sum_x = 0.0;
|
| 308 |
+
double sum_y = 0.0;
|
| 309 |
+
double min_y = std::numeric_limits<double>::infinity();
|
| 310 |
+
double max_y = -std::numeric_limits<double>::infinity();
|
| 311 |
+
std::vector<const LanePoint*> lower;
|
| 312 |
+
for (const auto& point : lane.points) {
|
| 313 |
+
if (std::isfinite(point.x) && std::isfinite(point.y)
|
| 314 |
+
&& static_cast<double>(point.y) >= cutoff) {
|
| 315 |
+
lower.push_back(&point);
|
| 316 |
+
sum_x += point.x;
|
| 317 |
+
sum_y += point.y;
|
| 318 |
+
min_y = std::min(min_y, static_cast<double>(point.y));
|
| 319 |
+
max_y = std::max(max_y, static_cast<double>(point.y));
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
const auto bottom_x = [&lane]() {
|
| 323 |
+
return static_cast<double>(std::max_element(
|
| 324 |
+
lane.points.begin(), lane.points.end(),
|
| 325 |
+
[](const LanePoint& lhs, const LanePoint& rhs) {
|
| 326 |
+
return lhs.y < rhs.y;
|
| 327 |
+
}
|
| 328 |
+
)->x);
|
| 329 |
+
};
|
| 330 |
+
if (lower.size() < 2 || max_y - min_y < 1.0) {
|
| 331 |
+
return bottom_x();
|
| 332 |
+
}
|
| 333 |
+
const double mean_x = sum_x / static_cast<double>(lower.size());
|
| 334 |
+
const double mean_y = sum_y / static_cast<double>(lower.size());
|
| 335 |
+
double numerator = 0.0;
|
| 336 |
+
double denominator = 0.0;
|
| 337 |
+
for (const auto* point : lower) {
|
| 338 |
+
const double centered_y = static_cast<double>(point->y) - mean_y;
|
| 339 |
+
numerator += centered_y * (static_cast<double>(point->x) - mean_x);
|
| 340 |
+
denominator += centered_y * centered_y;
|
| 341 |
+
}
|
| 342 |
+
if (denominator <= 1e-6) {
|
| 343 |
+
return bottom_x();
|
| 344 |
+
}
|
| 345 |
+
return mean_x + numerator / denominator
|
| 346 |
+
* (static_cast<double>(lane.height - 1) - mean_y);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
std::vector<int> preselect_candidates(
|
| 350 |
+
const std::vector<Lane>& lanes,
|
| 351 |
+
const std::vector<int>& score_order,
|
| 352 |
+
int max_lanes
|
| 353 |
+
) {
|
| 354 |
+
if (static_cast<int>(score_order.size()) <= max_lanes) {
|
| 355 |
+
return score_order;
|
| 356 |
+
}
|
| 357 |
+
struct Bucket {
|
| 358 |
+
int key{};
|
| 359 |
+
std::vector<int> indices;
|
| 360 |
+
};
|
| 361 |
+
std::vector<Bucket> buckets;
|
| 362 |
+
for (const int index : score_order) {
|
| 363 |
+
const Lane& lane = lanes[static_cast<std::size_t>(index)];
|
| 364 |
+
std::vector<float> y;
|
| 365 |
+
y.reserve(lane.points.size());
|
| 366 |
+
for (const auto& point : lane.points) {
|
| 367 |
+
y.push_back(point.y);
|
| 368 |
+
}
|
| 369 |
+
const float median_y = median(std::move(y));
|
| 370 |
+
std::vector<float> lower_x;
|
| 371 |
+
for (const auto& point : lane.points) {
|
| 372 |
+
if (point.y >= median_y) {
|
| 373 |
+
lower_x.push_back(point.x);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
const int key = static_cast<int>(std::floor(median(lower_x) / 16.0F));
|
| 377 |
+
auto found = std::find_if(
|
| 378 |
+
buckets.begin(), buckets.end(),
|
| 379 |
+
[key](const Bucket& bucket) { return bucket.key == key; }
|
| 380 |
+
);
|
| 381 |
+
if (found == buckets.end()) {
|
| 382 |
+
buckets.push_back({key, {index}});
|
| 383 |
+
} else {
|
| 384 |
+
found->indices.push_back(index);
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
std::vector<int> selected;
|
| 388 |
+
selected.reserve(static_cast<std::size_t>(max_lanes));
|
| 389 |
+
for (std::size_t rank = 0; static_cast<int>(selected.size()) < max_lanes;
|
| 390 |
+
++rank) {
|
| 391 |
+
bool progressed = false;
|
| 392 |
+
for (const auto& bucket : buckets) {
|
| 393 |
+
if (rank < bucket.indices.size()) {
|
| 394 |
+
selected.push_back(bucket.indices[rank]);
|
| 395 |
+
progressed = true;
|
| 396 |
+
if (static_cast<int>(selected.size()) == max_lanes) {
|
| 397 |
+
break;
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
if (!progressed) {
|
| 402 |
+
break;
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
std::stable_sort(selected.begin(), selected.end(), [&lanes](int lhs, int rhs) {
|
| 406 |
+
return lanes[static_cast<std::size_t>(lhs)].score()
|
| 407 |
+
> lanes[static_cast<std::size_t>(rhs)].score();
|
| 408 |
+
});
|
| 409 |
+
return selected;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
void draw_disk(
|
| 413 |
+
std::vector<std::uint8_t>& mask, int width, int height,
|
| 414 |
+
int cx, int cy, int radius
|
| 415 |
+
) {
|
| 416 |
+
for (int y = std::max(0, cy - radius); y <= std::min(height - 1, cy + radius); ++y) {
|
| 417 |
+
for (int x = std::max(0, cx - radius); x <= std::min(width - 1, cx + radius); ++x) {
|
| 418 |
+
const int dx = x - cx;
|
| 419 |
+
const int dy = y - cy;
|
| 420 |
+
if (dx * dx + dy * dy <= radius * radius) {
|
| 421 |
+
mask[static_cast<std::size_t>(y * width + x)] = 1U;
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
std::vector<std::uint64_t> rasterize(
|
| 428 |
+
const Lane& lane, float scale, int lane_width, int width, int height
|
| 429 |
+
) {
|
| 430 |
+
std::vector<std::uint8_t> pixels(
|
| 431 |
+
static_cast<std::size_t>(width * height), 0U
|
| 432 |
+
);
|
| 433 |
+
if (lane.points.size() < 2) {
|
| 434 |
+
return std::vector<std::uint64_t>(
|
| 435 |
+
(pixels.size() + 63U) / 64U, 0U
|
| 436 |
+
);
|
| 437 |
+
}
|
| 438 |
+
const int thickness = std::max(1, static_cast<int>(std::lround(
|
| 439 |
+
static_cast<double>(lane_width) * scale
|
| 440 |
+
)));
|
| 441 |
+
const int radius = std::max(1, thickness / 2);
|
| 442 |
+
for (std::size_t index = 1; index < lane.points.size(); ++index) {
|
| 443 |
+
int x0 = std::clamp(static_cast<int>(lane.points[index - 1].x * scale), 0, width - 1);
|
| 444 |
+
int y0 = std::clamp(static_cast<int>(lane.points[index - 1].y * scale), 0, height - 1);
|
| 445 |
+
const int x1 = std::clamp(static_cast<int>(lane.points[index].x * scale), 0, width - 1);
|
| 446 |
+
const int y1 = std::clamp(static_cast<int>(lane.points[index].y * scale), 0, height - 1);
|
| 447 |
+
const int dx = std::abs(x1 - x0);
|
| 448 |
+
const int sx = x0 < x1 ? 1 : -1;
|
| 449 |
+
const int dy = -std::abs(y1 - y0);
|
| 450 |
+
const int sy = y0 < y1 ? 1 : -1;
|
| 451 |
+
int error = dx + dy;
|
| 452 |
+
for (;;) {
|
| 453 |
+
draw_disk(pixels, width, height, x0, y0, radius);
|
| 454 |
+
if (x0 == x1 && y0 == y1) {
|
| 455 |
+
break;
|
| 456 |
+
}
|
| 457 |
+
const int twice = 2 * error;
|
| 458 |
+
if (twice >= dy) {
|
| 459 |
+
error += dy;
|
| 460 |
+
x0 += sx;
|
| 461 |
+
}
|
| 462 |
+
if (twice <= dx) {
|
| 463 |
+
error += dx;
|
| 464 |
+
y0 += sy;
|
| 465 |
+
}
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
std::vector<std::uint64_t> mask((pixels.size() + 63U) / 64U, 0U);
|
| 469 |
+
for (std::size_t index = 0; index < pixels.size(); ++index) {
|
| 470 |
+
if (pixels[index] != 0U) {
|
| 471 |
+
mask[index / 64U] |= std::uint64_t{1} << (index % 64U);
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
return mask;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
std::vector<Lane> nms(
|
| 478 |
+
std::vector<Lane> lanes,
|
| 479 |
+
const DecoderConfig& config,
|
| 480 |
+
DecodeStatistics* statistics
|
| 481 |
+
) {
|
| 482 |
+
std::vector<int> order(lanes.size());
|
| 483 |
+
std::iota(order.begin(), order.end(), 0);
|
| 484 |
+
std::stable_sort(order.begin(), order.end(), [&lanes](int lhs, int rhs) {
|
| 485 |
+
return lanes[static_cast<std::size_t>(lhs)].score()
|
| 486 |
+
> lanes[static_cast<std::size_t>(rhs)].score();
|
| 487 |
+
});
|
| 488 |
+
order = preselect_candidates(lanes, order, config.nms_max_lanes);
|
| 489 |
+
if (statistics != nullptr) {
|
| 490 |
+
statistics->nms_candidates = order.size();
|
| 491 |
+
}
|
| 492 |
+
const int width = std::max(1, static_cast<int>(std::lround(
|
| 493 |
+
static_cast<double>(kMapWidth) * config.nms_scale
|
| 494 |
+
)));
|
| 495 |
+
const int height = std::max(1, static_cast<int>(std::lround(
|
| 496 |
+
static_cast<double>(kMapHeight) * config.nms_scale
|
| 497 |
+
)));
|
| 498 |
+
std::vector<std::vector<std::uint64_t>> masks;
|
| 499 |
+
std::vector<int> areas;
|
| 500 |
+
masks.reserve(order.size());
|
| 501 |
+
areas.reserve(order.size());
|
| 502 |
+
for (const int index : order) {
|
| 503 |
+
masks.push_back(rasterize(
|
| 504 |
+
lanes[static_cast<std::size_t>(index)], config.nms_scale,
|
| 505 |
+
config.lane_width, width, height
|
| 506 |
+
));
|
| 507 |
+
int area = 0;
|
| 508 |
+
for (const std::uint64_t word : masks.back()) {
|
| 509 |
+
area += __builtin_popcountll(word);
|
| 510 |
+
}
|
| 511 |
+
areas.push_back(area);
|
| 512 |
+
}
|
| 513 |
+
std::vector<std::uint8_t> suppressed(order.size(), 0U);
|
| 514 |
+
std::vector<Lane> kept;
|
| 515 |
+
for (std::size_t i = 0; i < order.size(); ++i) {
|
| 516 |
+
if (suppressed[i] != 0U) {
|
| 517 |
+
continue;
|
| 518 |
+
}
|
| 519 |
+
kept.push_back(std::move(lanes[static_cast<std::size_t>(order[i])])) ;
|
| 520 |
+
for (std::size_t j = i + 1; j < order.size(); ++j) {
|
| 521 |
+
if (suppressed[j] != 0U) {
|
| 522 |
+
continue;
|
| 523 |
+
}
|
| 524 |
+
int intersection = 0;
|
| 525 |
+
for (std::size_t word = 0; word < masks[i].size(); ++word) {
|
| 526 |
+
intersection += __builtin_popcountll(
|
| 527 |
+
masks[i][word] & masks[j][word]
|
| 528 |
+
);
|
| 529 |
+
}
|
| 530 |
+
const int union_area = areas[i] + areas[j] - intersection;
|
| 531 |
+
if (union_area > 0
|
| 532 |
+
&& static_cast<double>(intersection) / union_area
|
| 533 |
+
>= config.iou_threshold) {
|
| 534 |
+
suppressed[j] = 1U;
|
| 535 |
+
}
|
| 536 |
+
}
|
| 537 |
+
}
|
| 538 |
+
if (statistics != nullptr) {
|
| 539 |
+
statistics->nms_survivors = kept.size();
|
| 540 |
+
}
|
| 541 |
+
return kept;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
void assign_roles(std::vector<Lane>& lanes, double ego_x) {
|
| 545 |
+
std::vector<Lane*> left;
|
| 546 |
+
std::vector<Lane*> right;
|
| 547 |
+
std::unordered_map<const Lane*, double> references;
|
| 548 |
+
for (auto& lane : lanes) {
|
| 549 |
+
lane.lane_id = std::numeric_limits<int>::min();
|
| 550 |
+
lane.role.clear();
|
| 551 |
+
lane.ego_boundary = false;
|
| 552 |
+
lane.lateral_rank = 0;
|
| 553 |
+
references[&lane] = reference_x(lane);
|
| 554 |
+
(references[&lane] < ego_x ? left : right).push_back(&lane);
|
| 555 |
+
}
|
| 556 |
+
const auto near_ego = [&references, ego_x](const Lane* lhs, const Lane* rhs) {
|
| 557 |
+
const double dl = std::abs(references[lhs] - ego_x);
|
| 558 |
+
const double dr = std::abs(references[rhs] - ego_x);
|
| 559 |
+
return dl != dr ? dl < dr : lhs->score() > rhs->score();
|
| 560 |
+
};
|
| 561 |
+
std::sort(left.begin(), left.end(), near_ego);
|
| 562 |
+
std::sort(right.begin(), right.end(), near_ego);
|
| 563 |
+
for (std::size_t index = 0; index < left.size(); ++index) {
|
| 564 |
+
const int rank = static_cast<int>(index + 1);
|
| 565 |
+
left[index]->lane_id = 2 - rank;
|
| 566 |
+
left[index]->lateral_rank = -rank;
|
| 567 |
+
left[index]->ego_boundary = rank == 1;
|
| 568 |
+
left[index]->role = rank == 1 ? "ego_left" : "left_" + std::to_string(rank);
|
| 569 |
+
}
|
| 570 |
+
for (std::size_t index = 0; index < right.size(); ++index) {
|
| 571 |
+
const int rank = static_cast<int>(index + 1);
|
| 572 |
+
right[index]->lane_id = 1 + rank;
|
| 573 |
+
right[index]->lateral_rank = rank;
|
| 574 |
+
right[index]->ego_boundary = rank == 1;
|
| 575 |
+
right[index]->role = rank == 1 ? "ego_right" : "right_" + std::to_string(rank);
|
| 576 |
+
}
|
| 577 |
+
std::sort(lanes.begin(), lanes.end(), [](const Lane& lhs, const Lane& rhs) {
|
| 578 |
+
return lhs.lane_id < rhs.lane_id;
|
| 579 |
+
});
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
std::vector<Lane> select_ego_lanes(
|
| 583 |
+
std::vector<Lane> lanes, const DecoderConfig& config
|
| 584 |
+
) {
|
| 585 |
+
if (lanes.empty()) {
|
| 586 |
+
return {};
|
| 587 |
+
}
|
| 588 |
+
std::vector<int> pool(lanes.size());
|
| 589 |
+
std::iota(pool.begin(), pool.end(), 0);
|
| 590 |
+
std::vector<double> references(lanes.size());
|
| 591 |
+
for (std::size_t index = 0; index < lanes.size(); ++index) {
|
| 592 |
+
references[index] = reference_x(lanes[index]);
|
| 593 |
+
}
|
| 594 |
+
if (static_cast<int>(lanes.size()) > config.max_output_lanes) {
|
| 595 |
+
const double best = std::max_element(
|
| 596 |
+
lanes.begin(), lanes.end(), [](const Lane& lhs, const Lane& rhs) {
|
| 597 |
+
return lhs.score() < rhs.score();
|
| 598 |
+
}
|
| 599 |
+
)->score();
|
| 600 |
+
std::vector<int> reliable;
|
| 601 |
+
for (const int index : pool) {
|
| 602 |
+
if (lanes[static_cast<std::size_t>(index)].score()
|
| 603 |
+
>= best * config.ego_min_score_ratio) {
|
| 604 |
+
reliable.push_back(index);
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
if (static_cast<int>(reliable.size()) >= config.max_output_lanes) {
|
| 608 |
+
pool = std::move(reliable);
|
| 609 |
+
}
|
| 610 |
+
}
|
| 611 |
+
const auto proximity = [&lanes, &references, &config](int lhs, int rhs) {
|
| 612 |
+
const double dl = std::abs(references[static_cast<std::size_t>(lhs)] - config.ego_x);
|
| 613 |
+
const double dr = std::abs(references[static_cast<std::size_t>(rhs)] - config.ego_x);
|
| 614 |
+
return dl != dr ? dl < dr
|
| 615 |
+
: lanes[static_cast<std::size_t>(lhs)].score()
|
| 616 |
+
> lanes[static_cast<std::size_t>(rhs)].score();
|
| 617 |
+
};
|
| 618 |
+
std::vector<int> left;
|
| 619 |
+
std::vector<int> right;
|
| 620 |
+
for (const int index : pool) {
|
| 621 |
+
(references[static_cast<std::size_t>(index)] < config.ego_x
|
| 622 |
+
? left : right).push_back(index);
|
| 623 |
+
}
|
| 624 |
+
std::sort(left.begin(), left.end(), proximity);
|
| 625 |
+
std::sort(right.begin(), right.end(), proximity);
|
| 626 |
+
std::vector<int> selected;
|
| 627 |
+
if (config.max_output_lanes == 4) {
|
| 628 |
+
selected.insert(selected.end(), left.begin(), left.begin() + std::min<std::size_t>(2, left.size()));
|
| 629 |
+
selected.insert(selected.end(), right.begin(), right.begin() + std::min<std::size_t>(2, right.size()));
|
| 630 |
+
} else {
|
| 631 |
+
std::sort(pool.begin(), pool.end(), proximity);
|
| 632 |
+
pool.resize(std::min<std::size_t>(pool.size(), static_cast<std::size_t>(config.max_output_lanes)));
|
| 633 |
+
selected = std::move(pool);
|
| 634 |
+
}
|
| 635 |
+
std::vector<Lane> result;
|
| 636 |
+
result.reserve(selected.size());
|
| 637 |
+
for (const int index : selected) {
|
| 638 |
+
result.push_back(std::move(lanes[static_cast<std::size_t>(index)]));
|
| 639 |
+
}
|
| 640 |
+
assign_roles(result, config.ego_x);
|
| 641 |
+
return result;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
const Tensor& require_output(
|
| 645 |
+
const std::unordered_map<std::string, Tensor>& outputs,
|
| 646 |
+
const std::string& name
|
| 647 |
+
) {
|
| 648 |
+
const auto found = outputs.find(name);
|
| 649 |
+
if (found == outputs.end()) {
|
| 650 |
+
throw std::runtime_error("missing TensorRT output: " + name);
|
| 651 |
+
}
|
| 652 |
+
return found->second;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
} // namespace
|
| 656 |
+
|
| 657 |
+
double Lane::score() const {
|
| 658 |
+
return points.empty() ? 0.0
|
| 659 |
+
: score_sum / static_cast<double>(points.size());
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
std::vector<float> softmax_foreground(const Tensor& logits) {
|
| 663 |
+
const std::size_t plane = static_cast<std::size_t>(kMapWidth * kMapHeight);
|
| 664 |
+
if (logits.values.size() != plane * 2) {
|
| 665 |
+
throw std::invalid_argument("seg_map must have shape (1,2,320,800)");
|
| 666 |
+
}
|
| 667 |
+
std::vector<float> probability(plane);
|
| 668 |
+
#pragma omp parallel for schedule(static)
|
| 669 |
+
for (std::int64_t index = 0; index < static_cast<std::int64_t>(plane); ++index) {
|
| 670 |
+
const float difference = logits.values[static_cast<std::size_t>(index)]
|
| 671 |
+
- logits.values[plane + static_cast<std::size_t>(index)];
|
| 672 |
+
probability[static_cast<std::size_t>(index)]
|
| 673 |
+
= 1.0F / (1.0F + std::exp(difference));
|
| 674 |
+
}
|
| 675 |
+
return probability;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
std::vector<Lane> decode(
|
| 679 |
+
const std::vector<float>& probability,
|
| 680 |
+
const Tensor& up_arrow,
|
| 681 |
+
const Tensor& down_arrow,
|
| 682 |
+
const Tensor& up_bound,
|
| 683 |
+
const Tensor& down_bound,
|
| 684 |
+
const DecoderConfig& config,
|
| 685 |
+
DecodeStatistics* statistics
|
| 686 |
+
) {
|
| 687 |
+
const std::size_t plane = static_cast<std::size_t>(kMapWidth * kMapHeight);
|
| 688 |
+
if (probability.size() != plane
|
| 689 |
+
|| up_arrow.values.size() != plane * 2
|
| 690 |
+
|| down_arrow.values.size() != plane * 2
|
| 691 |
+
|| up_bound.values.size() != plane * 2
|
| 692 |
+
|| down_bound.values.size() != plane * 2) {
|
| 693 |
+
throw std::invalid_argument("decoder map shape mismatch");
|
| 694 |
+
}
|
| 695 |
+
if (statistics != nullptr) {
|
| 696 |
+
*statistics = {};
|
| 697 |
+
}
|
| 698 |
+
omp_set_num_threads(config.threads);
|
| 699 |
+
const auto seeds = select_seeds(probability, config, statistics);
|
| 700 |
+
std::vector<std::vector<LanePoint>> up(seeds.size());
|
| 701 |
+
std::vector<std::vector<LanePoint>> down(seeds.size());
|
| 702 |
+
#pragma omp parallel for schedule(static)
|
| 703 |
+
for (std::int64_t index = 0; index < static_cast<std::int64_t>(seeds.size()); ++index) {
|
| 704 |
+
up[static_cast<std::size_t>(index)] = crawl(
|
| 705 |
+
seeds[static_cast<std::size_t>(index)], probability,
|
| 706 |
+
up_arrow.values, up_bound.values, config
|
| 707 |
+
);
|
| 708 |
+
down[static_cast<std::size_t>(index)] = crawl(
|
| 709 |
+
seeds[static_cast<std::size_t>(index)], probability,
|
| 710 |
+
down_arrow.values, down_bound.values, config
|
| 711 |
+
);
|
| 712 |
+
}
|
| 713 |
+
std::vector<Lane> candidates;
|
| 714 |
+
candidates.reserve(seeds.size());
|
| 715 |
+
for (std::size_t index = 0; index < seeds.size(); ++index) {
|
| 716 |
+
const std::size_t count = up[index].size() + down[index].size();
|
| 717 |
+
if (count <= 1) {
|
| 718 |
+
continue;
|
| 719 |
+
}
|
| 720 |
+
Lane lane;
|
| 721 |
+
lane.width = kMapWidth;
|
| 722 |
+
lane.height = kMapHeight;
|
| 723 |
+
lane.points.reserve(count);
|
| 724 |
+
for (auto point = up[index].rbegin(); point != up[index].rend(); ++point) {
|
| 725 |
+
lane.points.push_back(*point);
|
| 726 |
+
lane.score_sum += point->score;
|
| 727 |
+
}
|
| 728 |
+
for (const auto& point : down[index]) {
|
| 729 |
+
lane.points.push_back(point);
|
| 730 |
+
lane.score_sum += point.score;
|
| 731 |
+
}
|
| 732 |
+
if (lane.score() >= config.score_threshold) {
|
| 733 |
+
candidates.push_back(std::move(lane));
|
| 734 |
+
}
|
| 735 |
+
}
|
| 736 |
+
if (statistics != nullptr) {
|
| 737 |
+
statistics->crawled_candidates = candidates.size();
|
| 738 |
+
}
|
| 739 |
+
auto kept = nms(std::move(candidates), config, statistics);
|
| 740 |
+
return select_ego_lanes(std::move(kept), config);
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
std::vector<Lane> decode_outputs(
|
| 744 |
+
const std::unordered_map<std::string, Tensor>& outputs,
|
| 745 |
+
const DecoderConfig& config,
|
| 746 |
+
DecodeStatistics* statistics
|
| 747 |
+
) {
|
| 748 |
+
const auto probability = softmax_foreground(require_output(outputs, "seg_map"));
|
| 749 |
+
return decode(
|
| 750 |
+
probability,
|
| 751 |
+
require_output(outputs, "up_arrow"),
|
| 752 |
+
require_output(outputs, "down_arrow"),
|
| 753 |
+
require_output(outputs, "up_bound"),
|
| 754 |
+
require_output(outputs, "down_bound"),
|
| 755 |
+
config,
|
| 756 |
+
statistics
|
| 757 |
+
);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
void write_lanes_json(
|
| 761 |
+
const std::string& path,
|
| 762 |
+
const std::vector<Lane>& lanes,
|
| 763 |
+
const DecodeStatistics* statistics
|
| 764 |
+
) {
|
| 765 |
+
std::ofstream stream(path);
|
| 766 |
+
if (!stream) {
|
| 767 |
+
throw std::runtime_error("cannot write lanes JSON: " + path);
|
| 768 |
+
}
|
| 769 |
+
stream << std::setprecision(9) << "{\n";
|
| 770 |
+
if (statistics != nullptr) {
|
| 771 |
+
stream << " \"statistics\": {\"foreground_pixels\": "
|
| 772 |
+
<< statistics->foreground_pixels << ", \"seeds\": "
|
| 773 |
+
<< statistics->seeds << ", \"crawled_candidates\": "
|
| 774 |
+
<< statistics->crawled_candidates << ", \"nms_candidates\": "
|
| 775 |
+
<< statistics->nms_candidates << ", \"nms_survivors\": "
|
| 776 |
+
<< statistics->nms_survivors << "},\n";
|
| 777 |
+
}
|
| 778 |
+
stream << " \"lanes\": [\n";
|
| 779 |
+
for (std::size_t lane_index = 0; lane_index < lanes.size(); ++lane_index) {
|
| 780 |
+
const Lane& lane = lanes[lane_index];
|
| 781 |
+
stream << " {\"lane_id\": " << lane.lane_id
|
| 782 |
+
<< ", \"role\": \"" << lane.role
|
| 783 |
+
<< "\", \"score\": " << lane.score() << ", \"points\": [";
|
| 784 |
+
for (std::size_t point_index = 0; point_index < lane.points.size(); ++point_index) {
|
| 785 |
+
const auto& point = lane.points[point_index];
|
| 786 |
+
if (point_index != 0) {
|
| 787 |
+
stream << ',';
|
| 788 |
+
}
|
| 789 |
+
stream << '[' << point.x << ',' << point.y << ',' << point.score << ']';
|
| 790 |
+
}
|
| 791 |
+
stream << "]}" << (lane_index + 1 == lanes.size() ? "\n" : ",\n");
|
| 792 |
+
}
|
| 793 |
+
stream << " ]\n}\n";
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
} // namespace rclane
|
cpp/src/main.cpp
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "tensorrt_runner.hpp"
|
| 2 |
+
#include "decoder.hpp"
|
| 3 |
+
#include "bev.hpp"
|
| 4 |
+
#include "preprocess.hpp"
|
| 5 |
+
|
| 6 |
+
#include <fstream>
|
| 7 |
+
#include <algorithm>
|
| 8 |
+
#include <chrono>
|
| 9 |
+
#include <cmath>
|
| 10 |
+
#include <iomanip>
|
| 11 |
+
#include <iostream>
|
| 12 |
+
#include <numeric>
|
| 13 |
+
#include <stdexcept>
|
| 14 |
+
#include <string>
|
| 15 |
+
#include <unordered_map>
|
| 16 |
+
#include <vector>
|
| 17 |
+
|
| 18 |
+
namespace {
|
| 19 |
+
|
| 20 |
+
struct Arguments {
|
| 21 |
+
std::string engine;
|
| 22 |
+
std::string input;
|
| 23 |
+
std::string input_bgr;
|
| 24 |
+
std::string dump_prefix;
|
| 25 |
+
std::string lanes_json;
|
| 26 |
+
std::string bev_json;
|
| 27 |
+
std::string report;
|
| 28 |
+
std::string frames_jsonl;
|
| 29 |
+
bool raw_bgr_stdin{false};
|
| 30 |
+
int source_width{1920};
|
| 31 |
+
int source_height{1080};
|
| 32 |
+
int max_frames{0};
|
| 33 |
+
int timing_warmup{5};
|
| 34 |
+
int warmup{10};
|
| 35 |
+
int iterations{100};
|
| 36 |
+
int threads{8};
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
Arguments parse_arguments(int argc, char** argv) {
|
| 40 |
+
Arguments args;
|
| 41 |
+
for (int index = 1; index < argc; ++index) {
|
| 42 |
+
const std::string option = argv[index];
|
| 43 |
+
const auto value = [&]() -> std::string {
|
| 44 |
+
if (++index >= argc) {
|
| 45 |
+
throw std::invalid_argument("missing value for " + option);
|
| 46 |
+
}
|
| 47 |
+
return argv[index];
|
| 48 |
+
};
|
| 49 |
+
if (option == "--engine") {
|
| 50 |
+
args.engine = value();
|
| 51 |
+
} else if (option == "--input-nchw") {
|
| 52 |
+
args.input = value();
|
| 53 |
+
} else if (option == "--input-bgr") {
|
| 54 |
+
args.input_bgr = value();
|
| 55 |
+
} else if (option == "--raw-bgr-stdin") {
|
| 56 |
+
args.raw_bgr_stdin = true;
|
| 57 |
+
} else if (option == "--dump-prefix") {
|
| 58 |
+
args.dump_prefix = value();
|
| 59 |
+
} else if (option == "--lanes-json") {
|
| 60 |
+
args.lanes_json = value();
|
| 61 |
+
} else if (option == "--bev-json") {
|
| 62 |
+
args.bev_json = value();
|
| 63 |
+
} else if (option == "--report") {
|
| 64 |
+
args.report = value();
|
| 65 |
+
} else if (option == "--frames-jsonl") {
|
| 66 |
+
args.frames_jsonl = value();
|
| 67 |
+
} else if (option == "--source-width") {
|
| 68 |
+
args.source_width = std::stoi(value());
|
| 69 |
+
} else if (option == "--source-height") {
|
| 70 |
+
args.source_height = std::stoi(value());
|
| 71 |
+
} else if (option == "--max-frames") {
|
| 72 |
+
args.max_frames = std::stoi(value());
|
| 73 |
+
} else if (option == "--timing-warmup") {
|
| 74 |
+
args.timing_warmup = std::stoi(value());
|
| 75 |
+
} else if (option == "--warmup") {
|
| 76 |
+
args.warmup = std::stoi(value());
|
| 77 |
+
} else if (option == "--iterations") {
|
| 78 |
+
args.iterations = std::stoi(value());
|
| 79 |
+
} else if (option == "--threads") {
|
| 80 |
+
args.threads = std::stoi(value());
|
| 81 |
+
} else {
|
| 82 |
+
throw std::invalid_argument("unknown argument: " + option);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
const int input_modes = static_cast<int>(!args.input.empty())
|
| 86 |
+
+ static_cast<int>(!args.input_bgr.empty())
|
| 87 |
+
+ static_cast<int>(args.raw_bgr_stdin);
|
| 88 |
+
if (args.engine.empty() || input_modes != 1
|
| 89 |
+
|| args.source_width <= 0 || args.source_height <= 0
|
| 90 |
+
|| args.timing_warmup < 0 || args.max_frames < 0) {
|
| 91 |
+
throw std::invalid_argument(
|
| 92 |
+
"usage: rclane_runtime --engine model.engine "
|
| 93 |
+
"(--input-nchw frame.f32 | --input-bgr frame.bgr | "
|
| 94 |
+
"--raw-bgr-stdin) "
|
| 95 |
+
"[--dump-prefix output]"
|
| 96 |
+
);
|
| 97 |
+
}
|
| 98 |
+
return args;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
std::vector<float> read_floats(
|
| 102 |
+
const std::string& path, std::size_t expected
|
| 103 |
+
) {
|
| 104 |
+
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
| 105 |
+
if (!stream) {
|
| 106 |
+
throw std::runtime_error("cannot open input: " + path);
|
| 107 |
+
}
|
| 108 |
+
const auto byte_count = static_cast<std::size_t>(stream.tellg());
|
| 109 |
+
if (byte_count != expected * sizeof(float)) {
|
| 110 |
+
throw std::runtime_error(
|
| 111 |
+
"input byte count mismatch: expected "
|
| 112 |
+
+ std::to_string(expected * sizeof(float)) + ", got "
|
| 113 |
+
+ std::to_string(byte_count)
|
| 114 |
+
);
|
| 115 |
+
}
|
| 116 |
+
std::vector<float> values(expected);
|
| 117 |
+
stream.seekg(0);
|
| 118 |
+
stream.read(
|
| 119 |
+
reinterpret_cast<char*>(values.data()),
|
| 120 |
+
static_cast<std::streamsize>(byte_count)
|
| 121 |
+
);
|
| 122 |
+
return values;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
std::vector<std::uint8_t> read_bytes(
|
| 126 |
+
const std::string& path, std::size_t expected
|
| 127 |
+
) {
|
| 128 |
+
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
| 129 |
+
if (!stream) {
|
| 130 |
+
throw std::runtime_error("cannot open input: " + path);
|
| 131 |
+
}
|
| 132 |
+
const auto byte_count = static_cast<std::size_t>(stream.tellg());
|
| 133 |
+
if (byte_count != expected) {
|
| 134 |
+
throw std::runtime_error(
|
| 135 |
+
"BGR byte count mismatch: expected " + std::to_string(expected)
|
| 136 |
+
+ ", got " + std::to_string(byte_count)
|
| 137 |
+
);
|
| 138 |
+
}
|
| 139 |
+
std::vector<std::uint8_t> bytes(expected);
|
| 140 |
+
stream.seekg(0);
|
| 141 |
+
stream.read(reinterpret_cast<char*>(bytes.data()),
|
| 142 |
+
static_cast<std::streamsize>(byte_count));
|
| 143 |
+
return bytes;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
void dump_outputs(
|
| 147 |
+
const std::string& prefix,
|
| 148 |
+
const std::unordered_map<std::string, rclane::Tensor>& outputs
|
| 149 |
+
) {
|
| 150 |
+
if (prefix.empty()) {
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
for (const auto& [name, tensor] : outputs) {
|
| 154 |
+
const std::string path = prefix + "." + name + ".f32";
|
| 155 |
+
std::ofstream stream(path, std::ios::binary);
|
| 156 |
+
if (!stream) {
|
| 157 |
+
throw std::runtime_error("cannot write output: " + path);
|
| 158 |
+
}
|
| 159 |
+
stream.write(
|
| 160 |
+
reinterpret_cast<const char*>(tensor.values.data()),
|
| 161 |
+
static_cast<std::streamsize>(
|
| 162 |
+
tensor.values.size() * sizeof(float)
|
| 163 |
+
)
|
| 164 |
+
);
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
void dump_input(const std::string& prefix, const std::vector<float>& input) {
|
| 169 |
+
if (prefix.empty()) {
|
| 170 |
+
return;
|
| 171 |
+
}
|
| 172 |
+
std::ofstream stream(prefix + ".input.f32", std::ios::binary);
|
| 173 |
+
if (!stream) {
|
| 174 |
+
throw std::runtime_error("cannot write normalized input");
|
| 175 |
+
}
|
| 176 |
+
stream.write(
|
| 177 |
+
reinterpret_cast<const char*>(input.data()),
|
| 178 |
+
static_cast<std::streamsize>(input.size() * sizeof(float))
|
| 179 |
+
);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
struct TimingSummary {
|
| 183 |
+
double mean{};
|
| 184 |
+
double median{};
|
| 185 |
+
double p95{};
|
| 186 |
+
double minimum{};
|
| 187 |
+
double maximum{};
|
| 188 |
+
};
|
| 189 |
+
|
| 190 |
+
TimingSummary summarize(std::vector<double> values) {
|
| 191 |
+
if (values.empty()) {
|
| 192 |
+
throw std::runtime_error("no timed frames after warmup");
|
| 193 |
+
}
|
| 194 |
+
std::sort(values.begin(), values.end());
|
| 195 |
+
const auto percentile = [&values](double fraction) {
|
| 196 |
+
const auto position = static_cast<std::size_t>(std::floor(
|
| 197 |
+
fraction * static_cast<double>(values.size() - 1)
|
| 198 |
+
));
|
| 199 |
+
return values[position];
|
| 200 |
+
};
|
| 201 |
+
return {
|
| 202 |
+
std::accumulate(values.begin(), values.end(), 0.0)
|
| 203 |
+
/ static_cast<double>(values.size()),
|
| 204 |
+
percentile(0.5), percentile(0.95), values.front(), values.back(),
|
| 205 |
+
};
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
double milliseconds(
|
| 209 |
+
std::chrono::steady_clock::time_point start,
|
| 210 |
+
std::chrono::steady_clock::time_point stop
|
| 211 |
+
) {
|
| 212 |
+
return std::chrono::duration<double, std::milli>(stop - start).count();
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
void write_stream_report(
|
| 216 |
+
const std::string& path,
|
| 217 |
+
std::size_t frames,
|
| 218 |
+
std::size_t timed_frames,
|
| 219 |
+
int threads,
|
| 220 |
+
const TimingSummary& read,
|
| 221 |
+
const TimingSummary& preprocess,
|
| 222 |
+
const TimingSummary& inference,
|
| 223 |
+
const TimingSummary& decode,
|
| 224 |
+
const TimingSummary& bev,
|
| 225 |
+
const TimingSummary& core,
|
| 226 |
+
double mean_lanes
|
| 227 |
+
) {
|
| 228 |
+
if (path.empty()) {
|
| 229 |
+
return;
|
| 230 |
+
}
|
| 231 |
+
std::ofstream stream(path);
|
| 232 |
+
if (!stream) {
|
| 233 |
+
throw std::runtime_error("cannot write benchmark report: " + path);
|
| 234 |
+
}
|
| 235 |
+
const auto timing = [&stream](const char* name, const TimingSummary& value,
|
| 236 |
+
bool comma) {
|
| 237 |
+
stream << " \"" << name << "\": {\"mean_ms\": " << value.mean
|
| 238 |
+
<< ", \"median_ms\": " << value.median
|
| 239 |
+
<< ", \"p95_ms\": " << value.p95
|
| 240 |
+
<< ", \"min_ms\": " << value.minimum
|
| 241 |
+
<< ", \"max_ms\": " << value.maximum << '}'
|
| 242 |
+
<< (comma ? ",\n" : "\n");
|
| 243 |
+
};
|
| 244 |
+
stream << std::setprecision(12)
|
| 245 |
+
<< "{\n \"runtime\": \"native_tensorrt_cpp\",\n"
|
| 246 |
+
<< " \"sequential_per_frame\": true,\n"
|
| 247 |
+
<< " \"frame_overlap\": false,\n"
|
| 248 |
+
<< " \"rendering_included\": false,\n"
|
| 249 |
+
<< " \"video_writing_included\": false,\n"
|
| 250 |
+
<< " \"bev_mode\": \"raw_model_projection\",\n"
|
| 251 |
+
<< " \"parallel_assumption\": false,\n"
|
| 252 |
+
<< " \"synthetic_lanes\": false,\n"
|
| 253 |
+
<< " \"frames\": " << frames << ",\n"
|
| 254 |
+
<< " \"timed_frames\": " << timed_frames << ",\n"
|
| 255 |
+
<< " \"decode_threads\": " << threads << ",\n"
|
| 256 |
+
<< " \"mean_output_lanes\": " << mean_lanes << ",\n"
|
| 257 |
+
<< " \"timing\": {\n";
|
| 258 |
+
timing("source_read", read, true);
|
| 259 |
+
timing("preprocess", preprocess, true);
|
| 260 |
+
timing("inference_with_transfers", inference, true);
|
| 261 |
+
timing("decode", decode, true);
|
| 262 |
+
timing("bev_cubic_funnel", bev, true);
|
| 263 |
+
timing("core_pipeline", core, false);
|
| 264 |
+
stream << " },\n \"fps_from_core_median_latency\": "
|
| 265 |
+
<< 1000.0 / core.median << "\n}\n";
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
int run_raw_stream(
|
| 269 |
+
const Arguments& args,
|
| 270 |
+
rclane::TensorRTRunner& runner
|
| 271 |
+
) {
|
| 272 |
+
const std::size_t frame_bytes = static_cast<std::size_t>(
|
| 273 |
+
args.source_width * args.source_height * 3
|
| 274 |
+
);
|
| 275 |
+
std::vector<std::uint8_t> frame(frame_bytes);
|
| 276 |
+
std::vector<float> input(runner.input_elements(), 0.0F);
|
| 277 |
+
// Warm only TensorRT with a neutral tensor; actual frame timings discard
|
| 278 |
+
// the first timing_warmup frames so OpenMP and allocator startup are absent.
|
| 279 |
+
for (int index = 0; index < args.warmup; ++index) {
|
| 280 |
+
runner.infer_reuse(input.data());
|
| 281 |
+
}
|
| 282 |
+
rclane::DecoderConfig decoder_config;
|
| 283 |
+
decoder_config.threads = args.threads;
|
| 284 |
+
std::vector<double> read_samples;
|
| 285 |
+
std::vector<double> preprocess_samples;
|
| 286 |
+
std::vector<double> inference_samples;
|
| 287 |
+
std::vector<double> decode_samples;
|
| 288 |
+
std::vector<double> bev_samples;
|
| 289 |
+
std::vector<double> core_samples;
|
| 290 |
+
std::size_t frames = 0;
|
| 291 |
+
std::size_t timed_frames = 0;
|
| 292 |
+
double lane_sum = 0.0;
|
| 293 |
+
std::ofstream frame_results;
|
| 294 |
+
if (!args.frames_jsonl.empty()) {
|
| 295 |
+
frame_results.open(args.frames_jsonl);
|
| 296 |
+
if (!frame_results) {
|
| 297 |
+
throw std::runtime_error(
|
| 298 |
+
"cannot write frame results: " + args.frames_jsonl
|
| 299 |
+
);
|
| 300 |
+
}
|
| 301 |
+
frame_results << std::setprecision(9);
|
| 302 |
+
}
|
| 303 |
+
for (;;) {
|
| 304 |
+
if (args.max_frames > 0 && static_cast<int>(frames) >= args.max_frames) {
|
| 305 |
+
break;
|
| 306 |
+
}
|
| 307 |
+
const auto read_start = std::chrono::steady_clock::now();
|
| 308 |
+
std::cin.read(
|
| 309 |
+
reinterpret_cast<char*>(frame.data()),
|
| 310 |
+
static_cast<std::streamsize>(frame.size())
|
| 311 |
+
);
|
| 312 |
+
const auto read_stop = std::chrono::steady_clock::now();
|
| 313 |
+
if (std::cin.gcount() == 0) {
|
| 314 |
+
break;
|
| 315 |
+
}
|
| 316 |
+
if (std::cin.gcount() != static_cast<std::streamsize>(frame.size())) {
|
| 317 |
+
throw std::runtime_error("partial BGR frame on stdin");
|
| 318 |
+
}
|
| 319 |
+
const auto core_start = std::chrono::steady_clock::now();
|
| 320 |
+
const auto preprocess_start = core_start;
|
| 321 |
+
rclane::normalize_bgr_to_nchw(
|
| 322 |
+
frame.data(), args.source_width, args.source_height, input
|
| 323 |
+
);
|
| 324 |
+
const auto preprocess_stop = std::chrono::steady_clock::now();
|
| 325 |
+
const auto inference_start = preprocess_stop;
|
| 326 |
+
const auto& outputs = runner.infer_reuse(input.data());
|
| 327 |
+
const auto inference_stop = std::chrono::steady_clock::now();
|
| 328 |
+
const auto decode_start = inference_stop;
|
| 329 |
+
rclane::DecodeStatistics decode_statistics;
|
| 330 |
+
const auto lanes = rclane::decode_outputs(
|
| 331 |
+
outputs, decoder_config, &decode_statistics
|
| 332 |
+
);
|
| 333 |
+
const auto decode_stop = std::chrono::steady_clock::now();
|
| 334 |
+
const auto bev_lanes = rclane::project_lanes_to_bev(lanes);
|
| 335 |
+
const auto bev_stop = std::chrono::steady_clock::now();
|
| 336 |
+
const double frame_preprocess_ms = milliseconds(
|
| 337 |
+
preprocess_start, preprocess_stop
|
| 338 |
+
);
|
| 339 |
+
const double frame_inference_ms = milliseconds(
|
| 340 |
+
inference_start, inference_stop
|
| 341 |
+
);
|
| 342 |
+
const double frame_decode_ms = milliseconds(decode_start, decode_stop);
|
| 343 |
+
const double frame_bev_ms = milliseconds(decode_stop, bev_stop);
|
| 344 |
+
const double frame_core_ms = milliseconds(core_start, bev_stop);
|
| 345 |
+
// Serialization is deliberately after bev_stop, outside core latency.
|
| 346 |
+
if (frame_results) {
|
| 347 |
+
frame_results << "{\"frame_index\":" << frames
|
| 348 |
+
<< ",\"timing\":{\"preprocess_ms\":"
|
| 349 |
+
<< frame_preprocess_ms
|
| 350 |
+
<< ",\"inference_ms\":" << frame_inference_ms
|
| 351 |
+
<< ",\"decode_ms\":" << frame_decode_ms
|
| 352 |
+
<< ",\"bev_result_ms\":" << frame_bev_ms
|
| 353 |
+
<< ",\"core_ms\":" << frame_core_ms << "}"
|
| 354 |
+
<< ",\"lanes\":[";
|
| 355 |
+
for (std::size_t lane_index = 0; lane_index < lanes.size();
|
| 356 |
+
++lane_index) {
|
| 357 |
+
const auto& lane = lanes[lane_index];
|
| 358 |
+
if (lane_index != 0U) {
|
| 359 |
+
frame_results << ',';
|
| 360 |
+
}
|
| 361 |
+
frame_results << "{\"lane_id\":" << lane.lane_id
|
| 362 |
+
<< ",\"role\":\"" << lane.role
|
| 363 |
+
<< "\",\"score\":" << lane.score()
|
| 364 |
+
<< ",\"points\":[";
|
| 365 |
+
for (std::size_t point_index = 0;
|
| 366 |
+
point_index < lane.points.size(); ++point_index) {
|
| 367 |
+
const auto& point = lane.points[point_index];
|
| 368 |
+
if (point_index != 0U) {
|
| 369 |
+
frame_results << ',';
|
| 370 |
+
}
|
| 371 |
+
frame_results << '[' << point.x << ',' << point.y << ','
|
| 372 |
+
<< point.score << ']';
|
| 373 |
+
}
|
| 374 |
+
frame_results << "]}";
|
| 375 |
+
}
|
| 376 |
+
frame_results << "],\"bev_lanes\":[";
|
| 377 |
+
for (std::size_t lane_index = 0;
|
| 378 |
+
lane_index < bev_lanes.size(); ++lane_index) {
|
| 379 |
+
const auto& lane = bev_lanes[lane_index];
|
| 380 |
+
if (lane_index != 0U) {
|
| 381 |
+
frame_results << ',';
|
| 382 |
+
}
|
| 383 |
+
frame_results << "{\"lane_id\":" << lane.lane_id
|
| 384 |
+
<< ",\"role\":\"" << lane.role
|
| 385 |
+
<< "\",\"score\":" << lane.score
|
| 386 |
+
<< ",\"fit_accepted\":"
|
| 387 |
+
<< (lane.fit_accepted ? "true" : "false")
|
| 388 |
+
<< ",\"funnel_clipped\":"
|
| 389 |
+
<< (lane.funnel_clipped ? "true" : "false")
|
| 390 |
+
<< ",\"points\":[";
|
| 391 |
+
for (std::size_t point_index = 0;
|
| 392 |
+
point_index < lane.points.size(); ++point_index) {
|
| 393 |
+
const auto& point = lane.points[point_index];
|
| 394 |
+
if (point_index != 0U) {
|
| 395 |
+
frame_results << ',';
|
| 396 |
+
}
|
| 397 |
+
frame_results << '[' << point.x << ',' << point.y << ','
|
| 398 |
+
<< point.score << ']';
|
| 399 |
+
}
|
| 400 |
+
frame_results << ']';
|
| 401 |
+
if (lane.fit.valid) {
|
| 402 |
+
frame_results << ",\"fit\":{\"coefficients\":["
|
| 403 |
+
<< lane.fit.coefficients[0] << ','
|
| 404 |
+
<< lane.fit.coefficients[1] << ','
|
| 405 |
+
<< lane.fit.coefficients[2] << ','
|
| 406 |
+
<< lane.fit.coefficients[3]
|
| 407 |
+
<< "],\"x_min\":" << lane.fit.x_min
|
| 408 |
+
<< ",\"x_max\":" << lane.fit.x_max
|
| 409 |
+
<< ",\"rmse\":" << lane.fit.rmse
|
| 410 |
+
<< ",\"point_count\":"
|
| 411 |
+
<< lane.fit.point_count
|
| 412 |
+
<< ",\"inlier_count\":"
|
| 413 |
+
<< lane.fit.inlier_count << '}';
|
| 414 |
+
} else {
|
| 415 |
+
frame_results << ",\"fit\":null";
|
| 416 |
+
}
|
| 417 |
+
frame_results << '}';
|
| 418 |
+
}
|
| 419 |
+
frame_results << "]}\n";
|
| 420 |
+
}
|
| 421 |
+
lane_sum += static_cast<double>(lanes.size());
|
| 422 |
+
if (static_cast<int>(frames) >= args.timing_warmup) {
|
| 423 |
+
read_samples.push_back(milliseconds(read_start, read_stop));
|
| 424 |
+
preprocess_samples.push_back(frame_preprocess_ms);
|
| 425 |
+
inference_samples.push_back(frame_inference_ms);
|
| 426 |
+
decode_samples.push_back(frame_decode_ms);
|
| 427 |
+
bev_samples.push_back(frame_bev_ms);
|
| 428 |
+
core_samples.push_back(frame_core_ms);
|
| 429 |
+
++timed_frames;
|
| 430 |
+
}
|
| 431 |
+
++frames;
|
| 432 |
+
if (frames % 100U == 0U) {
|
| 433 |
+
std::cerr << "processed " << frames << " frames\n";
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
if (frames == 0 || timed_frames == 0) {
|
| 437 |
+
throw std::runtime_error("raw stream produced no timed frames");
|
| 438 |
+
}
|
| 439 |
+
const auto read = summarize(std::move(read_samples));
|
| 440 |
+
const auto preprocess = summarize(std::move(preprocess_samples));
|
| 441 |
+
const auto inference = summarize(std::move(inference_samples));
|
| 442 |
+
const auto decode = summarize(std::move(decode_samples));
|
| 443 |
+
const auto bev = summarize(std::move(bev_samples));
|
| 444 |
+
const auto core = summarize(std::move(core_samples));
|
| 445 |
+
write_stream_report(
|
| 446 |
+
args.report, frames, timed_frames, args.threads, read, preprocess,
|
| 447 |
+
inference, decode, bev, core, lane_sum / static_cast<double>(frames)
|
| 448 |
+
);
|
| 449 |
+
std::cout << std::fixed << std::setprecision(3)
|
| 450 |
+
<< "C++ sequential raw-BEV benchmark (render/write excluded)\n"
|
| 451 |
+
<< "frames=" << frames << " timed=" << timed_frames
|
| 452 |
+
<< " threads=" << args.threads << '\n'
|
| 453 |
+
<< "preprocess median=" << preprocess.median << "ms\n"
|
| 454 |
+
<< "inference+D2H median=" << inference.median << "ms\n"
|
| 455 |
+
<< "decode median=" << decode.median << "ms\n"
|
| 456 |
+
<< "BEV median=" << bev.median << "ms\n"
|
| 457 |
+
<< "core median=" << core.median << "ms p95=" << core.p95
|
| 458 |
+
<< "ms FPS=" << (1000.0 / core.median) << '\n';
|
| 459 |
+
return 0;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
} // namespace
|
| 463 |
+
|
| 464 |
+
int main(int argc, char** argv) {
|
| 465 |
+
try {
|
| 466 |
+
const auto args = parse_arguments(argc, argv);
|
| 467 |
+
rclane::TensorRTRunner runner(args.engine);
|
| 468 |
+
if (args.raw_bgr_stdin) {
|
| 469 |
+
return run_raw_stream(args, runner);
|
| 470 |
+
}
|
| 471 |
+
std::vector<float> input;
|
| 472 |
+
if (!args.input.empty()) {
|
| 473 |
+
input = read_floats(args.input, runner.input_elements());
|
| 474 |
+
} else {
|
| 475 |
+
const auto bgr = read_bytes(
|
| 476 |
+
args.input_bgr, static_cast<std::size_t>(1920 * 1080 * 3)
|
| 477 |
+
);
|
| 478 |
+
rclane::normalize_bgr_to_nchw(bgr.data(), 1920, 1080, input);
|
| 479 |
+
}
|
| 480 |
+
dump_input(args.dump_prefix, input);
|
| 481 |
+
const auto outputs = runner.infer(input.data());
|
| 482 |
+
dump_outputs(args.dump_prefix, outputs);
|
| 483 |
+
rclane::DecoderConfig decoder_config;
|
| 484 |
+
decoder_config.threads = args.threads;
|
| 485 |
+
rclane::DecodeStatistics decode_statistics;
|
| 486 |
+
const auto lanes = rclane::decode_outputs(
|
| 487 |
+
outputs, decoder_config, &decode_statistics
|
| 488 |
+
);
|
| 489 |
+
if (!args.lanes_json.empty()) {
|
| 490 |
+
rclane::write_lanes_json(
|
| 491 |
+
args.lanes_json, lanes, &decode_statistics
|
| 492 |
+
);
|
| 493 |
+
}
|
| 494 |
+
const auto bev_lanes = rclane::project_lanes_to_bev(lanes);
|
| 495 |
+
if (!args.bev_json.empty()) {
|
| 496 |
+
rclane::write_bev_json(args.bev_json, bev_lanes);
|
| 497 |
+
}
|
| 498 |
+
const auto timing = runner.benchmark(
|
| 499 |
+
input.data(), args.warmup, args.iterations
|
| 500 |
+
);
|
| 501 |
+
std::cout << std::fixed << std::setprecision(3)
|
| 502 |
+
<< "TensorRT C++ inference: mean=" << timing.mean_ms
|
| 503 |
+
<< "ms median=" << timing.median_ms
|
| 504 |
+
<< "ms p95=" << timing.p95_ms
|
| 505 |
+
<< "ms min=" << timing.min_ms
|
| 506 |
+
<< "ms max=" << timing.max_ms
|
| 507 |
+
<< "ms FPS=" << (1000.0 / timing.median_ms) << '\n';
|
| 508 |
+
for (const auto& [name, tensor] : outputs) {
|
| 509 |
+
std::cout << name << " elements=" << tensor.values.size() << '\n';
|
| 510 |
+
}
|
| 511 |
+
std::cout << "decode seeds=" << decode_statistics.seeds
|
| 512 |
+
<< " candidates=" << decode_statistics.crawled_candidates
|
| 513 |
+
<< " NMS=" << decode_statistics.nms_candidates
|
| 514 |
+
<< "->" << decode_statistics.nms_survivors
|
| 515 |
+
<< " output_lanes=" << lanes.size() << '\n';
|
| 516 |
+
std::cout << "BEV lanes=" << bev_lanes.size() << " valid_cubics="
|
| 517 |
+
<< std::count_if(
|
| 518 |
+
bev_lanes.begin(), bev_lanes.end(),
|
| 519 |
+
[](const rclane::BevLane& lane) {
|
| 520 |
+
return lane.fit_accepted;
|
| 521 |
+
}
|
| 522 |
+
) << '\n';
|
| 523 |
+
return 0;
|
| 524 |
+
} catch (const std::exception& error) {
|
| 525 |
+
std::cerr << "error: " << error.what() << '\n';
|
| 526 |
+
return 1;
|
| 527 |
+
}
|
| 528 |
+
}
|
cpp/src/preprocess.cpp
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "preprocess.hpp"
|
| 2 |
+
|
| 3 |
+
#include <algorithm>
|
| 4 |
+
#include <cmath>
|
| 5 |
+
#include <cstddef>
|
| 6 |
+
#include <cstdint>
|
| 7 |
+
#include <stdexcept>
|
| 8 |
+
|
| 9 |
+
#include <omp.h>
|
| 10 |
+
|
| 11 |
+
namespace rclane {
|
| 12 |
+
|
| 13 |
+
void normalize_bgr_to_nchw(
|
| 14 |
+
const std::uint8_t* bgr,
|
| 15 |
+
int source_width,
|
| 16 |
+
int source_height,
|
| 17 |
+
std::vector<float>& destination
|
| 18 |
+
) {
|
| 19 |
+
constexpr int output_width = 800;
|
| 20 |
+
constexpr int output_height = 320;
|
| 21 |
+
constexpr float mean[3]{0.485F, 0.456F, 0.406F};
|
| 22 |
+
constexpr float standard_deviation[3]{0.229F, 0.224F, 0.225F};
|
| 23 |
+
if (bgr == nullptr || source_width <= 0 || source_height <= 0) {
|
| 24 |
+
throw std::invalid_argument("invalid BGR source image");
|
| 25 |
+
}
|
| 26 |
+
const std::size_t plane = static_cast<std::size_t>(
|
| 27 |
+
output_width * output_height
|
| 28 |
+
);
|
| 29 |
+
destination.resize(plane * 3);
|
| 30 |
+
constexpr int coefficient_scale = 1 << 11;
|
| 31 |
+
const double scale_x = static_cast<double>(source_width)
|
| 32 |
+
/ static_cast<double>(output_width);
|
| 33 |
+
const double scale_y = static_cast<double>(source_height)
|
| 34 |
+
/ static_cast<double>(output_height);
|
| 35 |
+
#pragma omp parallel for schedule(static)
|
| 36 |
+
for (int output_y = 0; output_y < output_height; ++output_y) {
|
| 37 |
+
const float source_y = static_cast<float>(
|
| 38 |
+
(static_cast<double>(output_y) + 0.5) * scale_y - 0.5
|
| 39 |
+
);
|
| 40 |
+
int y0 = static_cast<int>(std::floor(source_y));
|
| 41 |
+
float wy = source_y - static_cast<float>(y0);
|
| 42 |
+
if (y0 < 0) {
|
| 43 |
+
y0 = 0;
|
| 44 |
+
wy = 0.0F;
|
| 45 |
+
}
|
| 46 |
+
int y1 = std::min(y0 + 1, source_height - 1);
|
| 47 |
+
if (y0 >= source_height - 1) {
|
| 48 |
+
y0 = source_height - 1;
|
| 49 |
+
y1 = y0;
|
| 50 |
+
wy = 0.0F;
|
| 51 |
+
}
|
| 52 |
+
const int beta1 = static_cast<int>(std::nearbyint(
|
| 53 |
+
wy * static_cast<float>(coefficient_scale)
|
| 54 |
+
));
|
| 55 |
+
const int beta0 = static_cast<int>(std::nearbyint(
|
| 56 |
+
(1.0F - wy) * static_cast<float>(coefficient_scale)
|
| 57 |
+
));
|
| 58 |
+
for (int output_x = 0; output_x < output_width; ++output_x) {
|
| 59 |
+
const float source_x = static_cast<float>(
|
| 60 |
+
(static_cast<double>(output_x) + 0.5) * scale_x - 0.5
|
| 61 |
+
);
|
| 62 |
+
int x0 = static_cast<int>(std::floor(source_x));
|
| 63 |
+
float wx = source_x - static_cast<float>(x0);
|
| 64 |
+
if (x0 < 0) {
|
| 65 |
+
x0 = 0;
|
| 66 |
+
wx = 0.0F;
|
| 67 |
+
}
|
| 68 |
+
int x1 = std::min(x0 + 1, source_width - 1);
|
| 69 |
+
if (x0 >= source_width - 1) {
|
| 70 |
+
x0 = source_width - 1;
|
| 71 |
+
x1 = x0;
|
| 72 |
+
wx = 0.0F;
|
| 73 |
+
}
|
| 74 |
+
const int alpha1 = static_cast<int>(std::nearbyint(
|
| 75 |
+
wx * static_cast<float>(coefficient_scale)
|
| 76 |
+
));
|
| 77 |
+
const int alpha0 = static_cast<int>(std::nearbyint(
|
| 78 |
+
(1.0F - wx) * static_cast<float>(coefficient_scale)
|
| 79 |
+
));
|
| 80 |
+
const std::size_t output = static_cast<std::size_t>(
|
| 81 |
+
output_y * output_width + output_x
|
| 82 |
+
);
|
| 83 |
+
const std::size_t top_left = static_cast<std::size_t>(
|
| 84 |
+
(y0 * source_width + x0) * 3
|
| 85 |
+
);
|
| 86 |
+
const std::size_t top_right = static_cast<std::size_t>(
|
| 87 |
+
(y0 * source_width + x1) * 3
|
| 88 |
+
);
|
| 89 |
+
const std::size_t bottom_left = static_cast<std::size_t>(
|
| 90 |
+
(y1 * source_width + x0) * 3
|
| 91 |
+
);
|
| 92 |
+
const std::size_t bottom_right = static_cast<std::size_t>(
|
| 93 |
+
(y1 * source_width + x1) * 3
|
| 94 |
+
);
|
| 95 |
+
for (int rgb_channel = 0; rgb_channel < 3; ++rgb_channel) {
|
| 96 |
+
const int bgr_channel = 2 - rgb_channel;
|
| 97 |
+
const int top = static_cast<int>(bgr[top_left + bgr_channel]) * alpha0
|
| 98 |
+
+ static_cast<int>(bgr[top_right + bgr_channel]) * alpha1;
|
| 99 |
+
const int bottom = static_cast<int>(bgr[bottom_left + bgr_channel]) * alpha0
|
| 100 |
+
+ static_cast<int>(bgr[bottom_right + bgr_channel]) * alpha1;
|
| 101 |
+
// Match OpenCV 4.11's Apache-licensed VResizeLinear<8u>; see
|
| 102 |
+
// cpp/THIRD_PARTY_NOTICES.md. Its
|
| 103 |
+
// fixed-point shifts intentionally happen before accumulation.
|
| 104 |
+
const int pixel_integer = (
|
| 105 |
+
((beta0 * (top >> 4)) >> 16)
|
| 106 |
+
+ ((beta1 * (bottom >> 4)) >> 16) + 2
|
| 107 |
+
) >> 2;
|
| 108 |
+
const float pixel = static_cast<float>(
|
| 109 |
+
std::clamp(pixel_integer, 0, 255)
|
| 110 |
+
);
|
| 111 |
+
destination[static_cast<std::size_t>(rgb_channel) * plane + output]
|
| 112 |
+
= (pixel * (1.0F / 255.0F) - mean[rgb_channel])
|
| 113 |
+
/ standard_deviation[rgb_channel];
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
} // namespace rclane
|
cpp/src/tensorrt_runner.cpp
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "tensorrt_runner.hpp"
|
| 2 |
+
|
| 3 |
+
#include <NvInferRuntime.h>
|
| 4 |
+
#include <cuda_runtime_api.h>
|
| 5 |
+
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <chrono>
|
| 8 |
+
#include <cmath>
|
| 9 |
+
#include <fstream>
|
| 10 |
+
#include <limits>
|
| 11 |
+
#include <numeric>
|
| 12 |
+
#include <stdexcept>
|
| 13 |
+
#include <utility>
|
| 14 |
+
|
| 15 |
+
namespace rclane {
|
| 16 |
+
namespace {
|
| 17 |
+
|
| 18 |
+
class Logger final : public nvinfer1::ILogger {
|
| 19 |
+
public:
|
| 20 |
+
void log(Severity severity, const char* message) noexcept override {
|
| 21 |
+
if (severity <= Severity::kWARNING) {
|
| 22 |
+
// TensorRT owns message storage; copying is unnecessary here.
|
| 23 |
+
last_message_ = message == nullptr ? "" : message;
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const std::string& last_message() const { return last_message_; }
|
| 28 |
+
|
| 29 |
+
private:
|
| 30 |
+
std::string last_message_;
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
void check_cuda(cudaError_t status, const char* operation) {
|
| 34 |
+
if (status != cudaSuccess) {
|
| 35 |
+
throw std::runtime_error(
|
| 36 |
+
std::string(operation) + ": " + cudaGetErrorString(status)
|
| 37 |
+
);
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
std::vector<char> read_binary(const std::string& path) {
|
| 42 |
+
std::ifstream stream(path, std::ios::binary | std::ios::ate);
|
| 43 |
+
if (!stream) {
|
| 44 |
+
throw std::runtime_error("cannot open TensorRT engine: " + path);
|
| 45 |
+
}
|
| 46 |
+
const auto end = stream.tellg();
|
| 47 |
+
if (end <= 0) {
|
| 48 |
+
throw std::runtime_error("empty TensorRT engine: " + path);
|
| 49 |
+
}
|
| 50 |
+
std::vector<char> bytes(static_cast<std::size_t>(end));
|
| 51 |
+
stream.seekg(0);
|
| 52 |
+
stream.read(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
| 53 |
+
if (!stream) {
|
| 54 |
+
throw std::runtime_error("cannot read TensorRT engine: " + path);
|
| 55 |
+
}
|
| 56 |
+
return bytes;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
std::vector<std::int64_t> dimensions(const nvinfer1::Dims& dims) {
|
| 60 |
+
std::vector<std::int64_t> shape;
|
| 61 |
+
shape.reserve(static_cast<std::size_t>(dims.nbDims));
|
| 62 |
+
for (int index = 0; index < dims.nbDims; ++index) {
|
| 63 |
+
if (dims.d[index] <= 0) {
|
| 64 |
+
throw std::runtime_error("dynamic/invalid TensorRT shape");
|
| 65 |
+
}
|
| 66 |
+
shape.push_back(dims.d[index]);
|
| 67 |
+
}
|
| 68 |
+
return shape;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
std::size_t element_count(const std::vector<std::int64_t>& shape) {
|
| 72 |
+
return std::accumulate(
|
| 73 |
+
shape.begin(), shape.end(), std::size_t{1},
|
| 74 |
+
[](std::size_t total, std::int64_t value) {
|
| 75 |
+
return total * static_cast<std::size_t>(value);
|
| 76 |
+
}
|
| 77 |
+
);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
struct DeviceTensor {
|
| 81 |
+
std::string name;
|
| 82 |
+
bool input{};
|
| 83 |
+
std::vector<std::int64_t> shape;
|
| 84 |
+
std::size_t elements{};
|
| 85 |
+
void* device{};
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
+
} // namespace
|
| 89 |
+
|
| 90 |
+
class TensorRTRunner::Impl {
|
| 91 |
+
public:
|
| 92 |
+
explicit Impl(const std::string& engine_path) {
|
| 93 |
+
const auto bytes = read_binary(engine_path);
|
| 94 |
+
runtime_.reset(nvinfer1::createInferRuntime(logger_));
|
| 95 |
+
if (!runtime_) {
|
| 96 |
+
throw std::runtime_error("createInferRuntime failed");
|
| 97 |
+
}
|
| 98 |
+
engine_.reset(runtime_->deserializeCudaEngine(
|
| 99 |
+
bytes.data(), bytes.size()
|
| 100 |
+
));
|
| 101 |
+
if (!engine_) {
|
| 102 |
+
throw std::runtime_error(
|
| 103 |
+
"deserializeCudaEngine failed: " + logger_.last_message()
|
| 104 |
+
);
|
| 105 |
+
}
|
| 106 |
+
context_.reset(engine_->createExecutionContext());
|
| 107 |
+
if (!context_) {
|
| 108 |
+
throw std::runtime_error("createExecutionContext failed");
|
| 109 |
+
}
|
| 110 |
+
check_cuda(cudaStreamCreate(&stream_), "cudaStreamCreate");
|
| 111 |
+
|
| 112 |
+
const int tensor_count = engine_->getNbIOTensors();
|
| 113 |
+
for (int index = 0; index < tensor_count; ++index) {
|
| 114 |
+
const char* tensor_name = engine_->getIOTensorName(index);
|
| 115 |
+
if (engine_->getTensorDataType(tensor_name)
|
| 116 |
+
!= nvinfer1::DataType::kFLOAT) {
|
| 117 |
+
throw std::runtime_error(
|
| 118 |
+
std::string("non-FP32 engine tensor: ") + tensor_name
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
+
DeviceTensor tensor;
|
| 122 |
+
tensor.name = tensor_name;
|
| 123 |
+
tensor.input = engine_->getTensorIOMode(tensor_name)
|
| 124 |
+
== nvinfer1::TensorIOMode::kINPUT;
|
| 125 |
+
tensor.shape = dimensions(engine_->getTensorShape(tensor_name));
|
| 126 |
+
tensor.elements = element_count(tensor.shape);
|
| 127 |
+
check_cuda(
|
| 128 |
+
cudaMalloc(&tensor.device, tensor.elements * sizeof(float)),
|
| 129 |
+
"cudaMalloc"
|
| 130 |
+
);
|
| 131 |
+
if (!context_->setTensorAddress(tensor.name.c_str(), tensor.device)) {
|
| 132 |
+
throw std::runtime_error(
|
| 133 |
+
"setTensorAddress failed for " + tensor.name
|
| 134 |
+
);
|
| 135 |
+
}
|
| 136 |
+
if (tensor.input) {
|
| 137 |
+
if (input_index_ >= 0) {
|
| 138 |
+
throw std::runtime_error("engine has multiple inputs");
|
| 139 |
+
}
|
| 140 |
+
input_index_ = static_cast<int>(tensors_.size());
|
| 141 |
+
}
|
| 142 |
+
tensors_.push_back(std::move(tensor));
|
| 143 |
+
}
|
| 144 |
+
if (input_index_ < 0) {
|
| 145 |
+
throw std::runtime_error("engine has no input tensor");
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
~Impl() {
|
| 150 |
+
for (auto& tensor : tensors_) {
|
| 151 |
+
if (tensor.device != nullptr) {
|
| 152 |
+
cudaFree(tensor.device);
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
if (stream_ != nullptr) {
|
| 156 |
+
cudaStreamDestroy(stream_);
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
void enqueue(const float* input) {
|
| 161 |
+
const auto& tensor = tensors_[static_cast<std::size_t>(input_index_)];
|
| 162 |
+
check_cuda(cudaMemcpyAsync(
|
| 163 |
+
tensor.device, input, tensor.elements * sizeof(float),
|
| 164 |
+
cudaMemcpyHostToDevice, stream_
|
| 165 |
+
), "cudaMemcpyAsync input");
|
| 166 |
+
if (!context_->enqueueV3(stream_)) {
|
| 167 |
+
throw std::runtime_error("TensorRT enqueueV3 failed");
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
void synchronize() {
|
| 172 |
+
check_cuda(cudaStreamSynchronize(stream_), "cudaStreamSynchronize");
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
std::unordered_map<std::string, Tensor> infer(const float* input) {
|
| 176 |
+
enqueue(input);
|
| 177 |
+
std::unordered_map<std::string, Tensor> output;
|
| 178 |
+
for (const auto& tensor : tensors_) {
|
| 179 |
+
if (tensor.input) {
|
| 180 |
+
continue;
|
| 181 |
+
}
|
| 182 |
+
Tensor host;
|
| 183 |
+
host.shape = tensor.shape;
|
| 184 |
+
host.values.resize(tensor.elements);
|
| 185 |
+
check_cuda(cudaMemcpyAsync(
|
| 186 |
+
host.values.data(), tensor.device,
|
| 187 |
+
tensor.elements * sizeof(float), cudaMemcpyDeviceToHost,
|
| 188 |
+
stream_
|
| 189 |
+
), "cudaMemcpyAsync output");
|
| 190 |
+
output.emplace(tensor.name, std::move(host));
|
| 191 |
+
}
|
| 192 |
+
synchronize();
|
| 193 |
+
return output;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
const std::unordered_map<std::string, Tensor>& infer_reuse(
|
| 197 |
+
const float* input
|
| 198 |
+
) {
|
| 199 |
+
if (host_outputs_.empty()) {
|
| 200 |
+
for (const auto& tensor : tensors_) {
|
| 201 |
+
if (tensor.input) {
|
| 202 |
+
continue;
|
| 203 |
+
}
|
| 204 |
+
Tensor host;
|
| 205 |
+
host.shape = tensor.shape;
|
| 206 |
+
host.values.resize(tensor.elements);
|
| 207 |
+
host_outputs_.emplace(tensor.name, std::move(host));
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
enqueue(input);
|
| 211 |
+
for (const auto& tensor : tensors_) {
|
| 212 |
+
if (tensor.input) {
|
| 213 |
+
continue;
|
| 214 |
+
}
|
| 215 |
+
auto& host = host_outputs_.at(tensor.name);
|
| 216 |
+
check_cuda(cudaMemcpyAsync(
|
| 217 |
+
host.values.data(), tensor.device,
|
| 218 |
+
tensor.elements * sizeof(float), cudaMemcpyDeviceToHost,
|
| 219 |
+
stream_
|
| 220 |
+
), "cudaMemcpyAsync reusable output");
|
| 221 |
+
}
|
| 222 |
+
synchronize();
|
| 223 |
+
return host_outputs_;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
InferenceTiming benchmark(const float* input, int warmup, int iterations) {
|
| 227 |
+
if (warmup < 0 || iterations <= 0) {
|
| 228 |
+
throw std::invalid_argument("invalid benchmark iteration count");
|
| 229 |
+
}
|
| 230 |
+
for (int index = 0; index < warmup; ++index) {
|
| 231 |
+
enqueue(input);
|
| 232 |
+
synchronize();
|
| 233 |
+
}
|
| 234 |
+
std::vector<double> samples;
|
| 235 |
+
samples.reserve(static_cast<std::size_t>(iterations));
|
| 236 |
+
for (int index = 0; index < iterations; ++index) {
|
| 237 |
+
const auto started = std::chrono::steady_clock::now();
|
| 238 |
+
enqueue(input);
|
| 239 |
+
synchronize();
|
| 240 |
+
const auto stopped = std::chrono::steady_clock::now();
|
| 241 |
+
samples.push_back(std::chrono::duration<double, std::milli>(
|
| 242 |
+
stopped - started
|
| 243 |
+
).count());
|
| 244 |
+
}
|
| 245 |
+
std::sort(samples.begin(), samples.end());
|
| 246 |
+
const auto percentile = [&samples](double fraction) {
|
| 247 |
+
const auto position = static_cast<std::size_t>(std::floor(
|
| 248 |
+
fraction * static_cast<double>(samples.size() - 1)
|
| 249 |
+
));
|
| 250 |
+
return samples[position];
|
| 251 |
+
};
|
| 252 |
+
const double sum = std::accumulate(samples.begin(), samples.end(), 0.0);
|
| 253 |
+
return {
|
| 254 |
+
sum / static_cast<double>(samples.size()),
|
| 255 |
+
percentile(0.50),
|
| 256 |
+
percentile(0.95),
|
| 257 |
+
samples.front(),
|
| 258 |
+
samples.back(),
|
| 259 |
+
};
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
const std::vector<std::int64_t>& input_shape() const {
|
| 263 |
+
return tensors_[static_cast<std::size_t>(input_index_)].shape;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
std::size_t input_elements() const {
|
| 267 |
+
return tensors_[static_cast<std::size_t>(input_index_)].elements;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
private:
|
| 271 |
+
Logger logger_;
|
| 272 |
+
struct RuntimeDeleter {
|
| 273 |
+
template <typename T>
|
| 274 |
+
void operator()(T* pointer) const { delete pointer; }
|
| 275 |
+
};
|
| 276 |
+
std::unique_ptr<nvinfer1::IRuntime, RuntimeDeleter> runtime_;
|
| 277 |
+
std::unique_ptr<nvinfer1::ICudaEngine, RuntimeDeleter> engine_;
|
| 278 |
+
std::unique_ptr<nvinfer1::IExecutionContext, RuntimeDeleter> context_;
|
| 279 |
+
cudaStream_t stream_{};
|
| 280 |
+
std::vector<DeviceTensor> tensors_;
|
| 281 |
+
std::unordered_map<std::string, Tensor> host_outputs_;
|
| 282 |
+
int input_index_{-1};
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
TensorRTRunner::TensorRTRunner(const std::string& engine_path)
|
| 286 |
+
: impl_(std::make_unique<Impl>(engine_path)) {}
|
| 287 |
+
TensorRTRunner::~TensorRTRunner() = default;
|
| 288 |
+
TensorRTRunner::TensorRTRunner(TensorRTRunner&&) noexcept = default;
|
| 289 |
+
TensorRTRunner& TensorRTRunner::operator=(TensorRTRunner&&) noexcept = default;
|
| 290 |
+
|
| 291 |
+
const std::vector<std::int64_t>& TensorRTRunner::input_shape() const {
|
| 292 |
+
return impl_->input_shape();
|
| 293 |
+
}
|
| 294 |
+
std::size_t TensorRTRunner::input_elements() const {
|
| 295 |
+
return impl_->input_elements();
|
| 296 |
+
}
|
| 297 |
+
std::unordered_map<std::string, Tensor> TensorRTRunner::infer(
|
| 298 |
+
const float* input
|
| 299 |
+
) {
|
| 300 |
+
return impl_->infer(input);
|
| 301 |
+
}
|
| 302 |
+
const std::unordered_map<std::string, Tensor>& TensorRTRunner::infer_reuse(
|
| 303 |
+
const float* input
|
| 304 |
+
) {
|
| 305 |
+
return impl_->infer_reuse(input);
|
| 306 |
+
}
|
| 307 |
+
InferenceTiming TensorRTRunner::benchmark(
|
| 308 |
+
const float* input, int warmup, int iterations
|
| 309 |
+
) {
|
| 310 |
+
return impl_->benchmark(input, warmup, iterations);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
} // namespace rclane
|
dataset.py
CHANGED
|
@@ -33,6 +33,23 @@ def normalize_image(img_bgr, W, H):
|
|
| 33 |
return torch.from_numpy(np.ascontiguousarray(x.transpose(2, 0, 1)))
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def sparse_from_dense(gt):
|
| 37 |
"""Keep only foreground pixels of the dense GT maps."""
|
| 38 |
ys, xs = np.where(gt["seg_map"] > 0.5)
|
|
|
|
| 33 |
return torch.from_numpy(np.ascontiguousarray(x.transpose(2, 0, 1)))
|
| 34 |
|
| 35 |
|
| 36 |
+
def normalize_image_numpy(img_bgr, W, H):
|
| 37 |
+
"""Fast inference-only BGR uint8 -> normalized ``(1, 3, H, W)`` NumPy.
|
| 38 |
+
|
| 39 |
+
``cv2.dnn.blobFromImage`` performs resize, RGB channel swap and NCHW
|
| 40 |
+
packing in compiled code. The result is numerically equivalent to
|
| 41 |
+
:func:`normalize_image` within float32 rounding, while avoiding the
|
| 42 |
+
temporary HWC float image and Torch tensor wrapper.
|
| 43 |
+
"""
|
| 44 |
+
images = cv2.dnn.blobFromImage(
|
| 45 |
+
img_bgr, scalefactor=1.0 / 255.0, size=(W, H), mean=(0, 0, 0),
|
| 46 |
+
swapRB=True, crop=False,
|
| 47 |
+
)
|
| 48 |
+
images[0] -= _MEAN.reshape(3, 1, 1)
|
| 49 |
+
images[0] /= _STD.reshape(3, 1, 1)
|
| 50 |
+
return images
|
| 51 |
+
|
| 52 |
+
|
| 53 |
def sparse_from_dense(gt):
|
| 54 |
"""Keep only foreground pixels of the dense GT maps."""
|
| 55 |
ys, xs = np.where(gt["seg_map"] > 0.5)
|
decode.py
CHANGED
|
@@ -26,6 +26,44 @@ Map layout (matches encode.py / rclane.py, channel-first):
|
|
| 26 |
import numpy as np
|
| 27 |
import cv2
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# --------------------------------------------------------------------------- #
|
| 31 |
# Lane container (replaces FloatLengthLine + PointSelf)
|
|
@@ -36,32 +74,51 @@ class Lane:
|
|
| 36 |
self.height = height
|
| 37 |
self.points = [] # list of (x, y, score)
|
| 38 |
self._score_sum = 0.0
|
| 39 |
-
self.lane_id = None
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
def append(self, x, y, score):
|
|
|
|
|
|
|
| 42 |
self.points.append((float(x), float(y), float(score)))
|
| 43 |
self._score_sum += float(score)
|
| 44 |
|
| 45 |
def reverse(self):
|
| 46 |
-
self.points.
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def __len__(self):
|
| 49 |
return len(self.points)
|
| 50 |
|
| 51 |
@property
|
| 52 |
def score(self):
|
| 53 |
-
if
|
| 54 |
return 0.0
|
| 55 |
return self._score_sum / len(self.points)
|
| 56 |
|
| 57 |
def concat(self, other):
|
| 58 |
out = Lane(self.width, self.height)
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
out._score_sum = self._score_sum + other._score_sum
|
| 61 |
return out
|
| 62 |
|
| 63 |
def xy(self):
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
def iou(self, other, lane_width=15):
|
| 67 |
"""Rasterize both lanes (cv2.line, width 15) and return mask IoU."""
|
|
@@ -83,13 +140,28 @@ class Lane:
|
|
| 83 |
# --------------------------------------------------------------------------- #
|
| 84 |
# seeding
|
| 85 |
# --------------------------------------------------------------------------- #
|
| 86 |
-
def point_nms(prob, thr=0.5, min_dist=2, max_seeds=1024
|
|
|
|
| 87 |
"""Greedy point-NMS: keep highest-prob foreground pixels >= min_dist apart."""
|
| 88 |
H, W = prob.shape
|
| 89 |
ys, xs = np.where(prob > thr)
|
| 90 |
if len(ys) == 0:
|
| 91 |
return []
|
| 92 |
order = np.argsort(-prob[ys, xs])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
taken = np.zeros((H, W), dtype=bool)
|
| 94 |
seeds = []
|
| 95 |
r = min_dist
|
|
@@ -144,6 +216,341 @@ def decode_branch(cx, cy, semantic_fine, arrow, bound, step_length, seg_threshol
|
|
| 144 |
return lane
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
# --------------------------------------------------------------------------- #
|
| 148 |
# post-processing
|
| 149 |
# --------------------------------------------------------------------------- #
|
|
@@ -255,14 +662,46 @@ def _bottom_x(lane):
|
|
| 255 |
return float(xy[int(np.argmax(xy[:, 1])), 0])
|
| 256 |
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
def order_lanes(lanes):
|
| 259 |
-
"""Sort lanes left-to-right and
|
| 260 |
|
| 261 |
RCLane is anchor-free: `decode` emits lane instances in score order with no
|
| 262 |
-
inherent identity
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
frames. Returns a new list; also sets `.lane_id` on each Lane in place.
|
| 266 |
"""
|
| 267 |
ordered = sorted(lanes, key=_bottom_x)
|
| 268 |
for i, ln in enumerate(ordered):
|
|
@@ -270,6 +709,72 @@ def order_lanes(lanes):
|
|
| 270 |
return ordered
|
| 271 |
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
def select_ego_lanes(lanes, max_lanes=4, ego_x=None,
|
| 274 |
min_score_ratio=0.5, balance_sides=True):
|
| 275 |
"""Keep the closest reliable lane boundaries around the ego vehicle.
|
|
@@ -282,24 +787,45 @@ def select_ego_lanes(lanes, max_lanes=4, ego_x=None,
|
|
| 282 |
|
| 283 |
For the usual four-lane output, ``balance_sides`` reserves two slots on
|
| 284 |
either side of the camera centre when possible. Any unfilled slots are
|
| 285 |
-
taken from the remaining closest candidates.
|
| 286 |
-
|
|
|
|
| 287 |
"""
|
| 288 |
if max_lanes is None:
|
| 289 |
-
return
|
| 290 |
if max_lanes <= 0:
|
| 291 |
raise ValueError("max_lanes must be positive or None")
|
| 292 |
if not 0.0 <= min_score_ratio <= 1.0:
|
| 293 |
raise ValueError("min_score_ratio must be in [0, 1]")
|
| 294 |
|
| 295 |
ordered = order_lanes(lanes)
|
| 296 |
-
if
|
| 297 |
-
return
|
| 298 |
-
|
| 299 |
if ego_x is None:
|
| 300 |
ego_x = ordered[0].width / 2.0
|
| 301 |
ego_x = float(ego_x)
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
best_score = max(lane.score for lane in ordered)
|
| 304 |
reliable = [
|
| 305 |
lane for lane in ordered
|
|
@@ -308,18 +834,20 @@ def select_ego_lanes(lanes, max_lanes=4, ego_x=None,
|
|
| 308 |
# Never let the reliability gate force the output below the requested cap.
|
| 309 |
pool = reliable if len(reliable) >= max_lanes else ordered
|
| 310 |
|
|
|
|
|
|
|
| 311 |
def proximity_key(lane):
|
| 312 |
-
return (abs(
|
| 313 |
|
| 314 |
ranked = sorted(pool, key=proximity_key)
|
| 315 |
selected = []
|
| 316 |
if balance_sides and max_lanes >= 2:
|
| 317 |
left = sorted(
|
| 318 |
-
(lane for lane in pool if
|
| 319 |
key=proximity_key,
|
| 320 |
)
|
| 321 |
right = sorted(
|
| 322 |
-
(lane for lane in pool if
|
| 323 |
key=proximity_key,
|
| 324 |
)
|
| 325 |
left_slots = max_lanes // 2
|
|
@@ -327,13 +855,14 @@ def select_ego_lanes(lanes, max_lanes=4, ego_x=None,
|
|
| 327 |
selected.extend(left[:left_slots])
|
| 328 |
selected.extend(right[:right_slots])
|
| 329 |
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
|
|
|
| 335 |
|
| 336 |
-
return
|
| 337 |
|
| 338 |
|
| 339 |
# --------------------------------------------------------------------------- #
|
|
@@ -344,7 +873,9 @@ def decode(seg_prob, up_arrow, down_arrow, up_bound, down_bound,
|
|
| 344 |
score_thresh=0.10, iou_thresh=0.5, seed_threshold=None,
|
| 345 |
max_seeds=1024, nms_max_lanes=128, nms_scale=0.25,
|
| 346 |
sort_lanes=True, max_output_lanes=4, ego_x=None,
|
| 347 |
-
ego_min_score_ratio=0.5, balance_ego_sides=True
|
|
|
|
|
|
|
| 348 |
"""
|
| 349 |
Args:
|
| 350 |
seg_prob: (H, W) foreground probability.
|
|
@@ -361,17 +892,49 @@ def decode(seg_prob, up_arrow, down_arrow, up_bound, down_bound,
|
|
| 361 |
H, W = seg_prob.shape
|
| 362 |
if seed_threshold is None:
|
| 363 |
seed_threshold = seg_threshold
|
| 364 |
-
seeds = point_nms(
|
|
|
|
|
|
|
|
|
|
| 365 |
ub0, db0 = up_bound[0], down_bound[0] # bound channel 0 (both channels equal)
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
lines = thresh_line(lines, score_thresh)
|
| 377 |
lines = iou_nms(lines, iou_thresh, max_lanes=nms_max_lanes,
|
|
@@ -486,4 +1049,43 @@ if __name__ == "__main__":
|
|
| 486 |
f"ego selector kept the wrong lanes: {ego_xs}"
|
| 487 |
)
|
| 488 |
assert [lane.lane_id for lane in ego_lanes] == [0, 1, 2, 3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
print("OK -- ego post-processing keeps four reliable nearby lanes.")
|
|
|
|
| 26 |
import numpy as np
|
| 27 |
import cv2
|
| 28 |
|
| 29 |
+
try:
|
| 30 |
+
from numba import njit, prange
|
| 31 |
+
except ImportError: # portable fallback for environments without the JIT
|
| 32 |
+
njit = None
|
| 33 |
+
prange = range
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if njit is not None:
|
| 37 |
+
@njit(cache=True)
|
| 38 |
+
def _greedy_seed_select_numba(sorted_x, sorted_y, height, width,
|
| 39 |
+
radius, max_seeds):
|
| 40 |
+
taken = np.zeros((height, width), dtype=np.uint8)
|
| 41 |
+
limit = len(sorted_x) if max_seeds < 0 else min(
|
| 42 |
+
len(sorted_x), max_seeds
|
| 43 |
+
)
|
| 44 |
+
seeds = np.empty((limit, 2), dtype=np.int32)
|
| 45 |
+
seed_count = 0
|
| 46 |
+
for candidate in range(len(sorted_x)):
|
| 47 |
+
x = int(sorted_x[candidate])
|
| 48 |
+
y = int(sorted_y[candidate])
|
| 49 |
+
if taken[y, x] != 0:
|
| 50 |
+
continue
|
| 51 |
+
seeds[seed_count, 0] = x
|
| 52 |
+
seeds[seed_count, 1] = y
|
| 53 |
+
seed_count += 1
|
| 54 |
+
y_start = max(0, y - radius)
|
| 55 |
+
y_stop = min(height, y + radius + 1)
|
| 56 |
+
x_start = max(0, x - radius)
|
| 57 |
+
x_stop = min(width, x + radius + 1)
|
| 58 |
+
for yy in range(y_start, y_stop):
|
| 59 |
+
for xx in range(x_start, x_stop):
|
| 60 |
+
taken[yy, xx] = 1
|
| 61 |
+
if max_seeds >= 0 and seed_count >= max_seeds:
|
| 62 |
+
break
|
| 63 |
+
return seeds[:seed_count]
|
| 64 |
+
else:
|
| 65 |
+
_greedy_seed_select_numba = None
|
| 66 |
+
|
| 67 |
|
| 68 |
# --------------------------------------------------------------------------- #
|
| 69 |
# Lane container (replaces FloatLengthLine + PointSelf)
|
|
|
|
| 74 |
self.height = height
|
| 75 |
self.points = [] # list of (x, y, score)
|
| 76 |
self._score_sum = 0.0
|
| 77 |
+
self.lane_id = None
|
| 78 |
+
self.lane_role = None
|
| 79 |
+
self.is_ego_boundary = False
|
| 80 |
+
self.lateral_rank = None
|
| 81 |
|
| 82 |
def append(self, x, y, score):
|
| 83 |
+
if isinstance(self.points, np.ndarray):
|
| 84 |
+
self.points = [tuple(map(float, point)) for point in self.points]
|
| 85 |
self.points.append((float(x), float(y), float(score)))
|
| 86 |
self._score_sum += float(score)
|
| 87 |
|
| 88 |
def reverse(self):
|
| 89 |
+
if isinstance(self.points, np.ndarray):
|
| 90 |
+
self.points = self.points[::-1].copy()
|
| 91 |
+
else:
|
| 92 |
+
self.points.reverse()
|
| 93 |
|
| 94 |
def __len__(self):
|
| 95 |
return len(self.points)
|
| 96 |
|
| 97 |
@property
|
| 98 |
def score(self):
|
| 99 |
+
if len(self.points) == 0:
|
| 100 |
return 0.0
|
| 101 |
return self._score_sum / len(self.points)
|
| 102 |
|
| 103 |
def concat(self, other):
|
| 104 |
out = Lane(self.width, self.height)
|
| 105 |
+
if isinstance(self.points, np.ndarray) or isinstance(
|
| 106 |
+
other.points, np.ndarray
|
| 107 |
+
):
|
| 108 |
+
out.points = np.concatenate((
|
| 109 |
+
np.asarray(self.points, dtype=np.float32),
|
| 110 |
+
np.asarray(other.points, dtype=np.float32),
|
| 111 |
+
))
|
| 112 |
+
else:
|
| 113 |
+
out.points = self.points + other.points
|
| 114 |
out._score_sum = self._score_sum + other._score_sum
|
| 115 |
return out
|
| 116 |
|
| 117 |
def xy(self):
|
| 118 |
+
points = np.asarray(self.points, dtype=np.float32)
|
| 119 |
+
if points.size == 0:
|
| 120 |
+
return np.empty((0, 2), dtype=np.float32)
|
| 121 |
+
return points[:, :2]
|
| 122 |
|
| 123 |
def iou(self, other, lane_width=15):
|
| 124 |
"""Rasterize both lanes (cv2.line, width 15) and return mask IoU."""
|
|
|
|
| 140 |
# --------------------------------------------------------------------------- #
|
| 141 |
# seeding
|
| 142 |
# --------------------------------------------------------------------------- #
|
| 143 |
+
def point_nms(prob, thr=0.5, min_dist=2, max_seeds=1024,
|
| 144 |
+
backend="auto"):
|
| 145 |
"""Greedy point-NMS: keep highest-prob foreground pixels >= min_dist apart."""
|
| 146 |
H, W = prob.shape
|
| 147 |
ys, xs = np.where(prob > thr)
|
| 148 |
if len(ys) == 0:
|
| 149 |
return []
|
| 150 |
order = np.argsort(-prob[ys, xs])
|
| 151 |
+
selected_backend = backend
|
| 152 |
+
if backend == "auto":
|
| 153 |
+
selected_backend = (
|
| 154 |
+
"numba" if _greedy_seed_select_numba is not None else "python"
|
| 155 |
+
)
|
| 156 |
+
if selected_backend == "numba":
|
| 157 |
+
limit = -1 if max_seeds is None else int(max_seeds)
|
| 158 |
+
return _greedy_seed_select_numba(
|
| 159 |
+
np.ascontiguousarray(xs[order], dtype=np.int32),
|
| 160 |
+
np.ascontiguousarray(ys[order], dtype=np.int32),
|
| 161 |
+
H, W, int(min_dist), limit,
|
| 162 |
+
)
|
| 163 |
+
if selected_backend != "python":
|
| 164 |
+
raise ValueError("point-NMS backend must be auto, numba, or python")
|
| 165 |
taken = np.zeros((H, W), dtype=bool)
|
| 166 |
seeds = []
|
| 167 |
r = min_dist
|
|
|
|
| 216 |
return lane
|
| 217 |
|
| 218 |
|
| 219 |
+
def decode_branches_batch(seeds, semantic_fine, arrow, bound, step_length,
|
| 220 |
+
seg_threshold):
|
| 221 |
+
"""Vectorized equivalent of :func:`decode_branch` for many seeds.
|
| 222 |
+
|
| 223 |
+
The relay walk is inherently sequential along each lane, but every seed at
|
| 224 |
+
a given crawl step is independent. Advancing all active seeds with NumPy
|
| 225 |
+
gathers removes the expensive Python ``seeds x steps`` nested loop while
|
| 226 |
+
retaining the original stopping rule and integer pixel trajectory.
|
| 227 |
+
|
| 228 |
+
Returns:
|
| 229 |
+
``(points, lengths)`` where ``points`` has shape ``(N, H, 3)`` and each
|
| 230 |
+
valid prefix stores ``(x, y, score)`` exactly like ``Lane.points``.
|
| 231 |
+
"""
|
| 232 |
+
H, W = semantic_fine.shape
|
| 233 |
+
seed_array = np.asarray(seeds, dtype=np.int32)
|
| 234 |
+
if seed_array.size == 0:
|
| 235 |
+
return np.empty((0, H, 3), dtype=np.float32), np.zeros(0, np.int32)
|
| 236 |
+
if seed_array.ndim != 2 or seed_array.shape[1] != 2:
|
| 237 |
+
raise ValueError("seeds must have shape (N, 2)")
|
| 238 |
+
|
| 239 |
+
count = len(seed_array)
|
| 240 |
+
cx = seed_array[:, 0].copy()
|
| 241 |
+
cy = seed_array[:, 1].copy()
|
| 242 |
+
active = np.ones(count, dtype=bool)
|
| 243 |
+
lengths = np.zeros(count, dtype=np.int32)
|
| 244 |
+
remain_sq_sum = np.zeros(count, dtype=np.float64)
|
| 245 |
+
remain_count = np.zeros(count, dtype=np.int32)
|
| 246 |
+
points = np.empty((count, H, 3), dtype=np.float32)
|
| 247 |
+
arrow_dx, arrow_dy = arrow[0], arrow[1]
|
| 248 |
+
|
| 249 |
+
for index in range(H):
|
| 250 |
+
active_ids = np.flatnonzero(active)
|
| 251 |
+
if len(active_ids) == 0:
|
| 252 |
+
break
|
| 253 |
+
current_x = cx[active_ids]
|
| 254 |
+
current_y = cy[active_ids]
|
| 255 |
+
current_score = semantic_fine[current_y, current_x]
|
| 256 |
+
foreground = current_score > seg_threshold
|
| 257 |
+
if np.any(foreground):
|
| 258 |
+
foreground_ids = active_ids[foreground]
|
| 259 |
+
remain = (
|
| 260 |
+
bound[current_y[foreground], current_x[foreground]]
|
| 261 |
+
* 100.0 / step_length + index
|
| 262 |
+
)
|
| 263 |
+
remain_sq_sum[foreground_ids] += remain * remain
|
| 264 |
+
remain_count[foreground_ids] += 1
|
| 265 |
+
|
| 266 |
+
dx = arrow_dx[current_y, current_x]
|
| 267 |
+
dy = arrow_dy[current_y, current_x]
|
| 268 |
+
norm = np.sqrt(dx * dx + dy * dy)
|
| 269 |
+
movable = np.isfinite(norm) & (norm != 0.0)
|
| 270 |
+
if np.any(~movable):
|
| 271 |
+
active[active_ids[~movable]] = False
|
| 272 |
+
active_ids = active_ids[movable]
|
| 273 |
+
if len(active_ids) == 0:
|
| 274 |
+
continue
|
| 275 |
+
next_x = np.floor(
|
| 276 |
+
cx[active_ids] + dx[movable] / norm[movable] * step_length
|
| 277 |
+
).astype(np.int32)
|
| 278 |
+
next_y = np.floor(
|
| 279 |
+
cy[active_ids] + dy[movable] / norm[movable] * step_length
|
| 280 |
+
).astype(np.int32)
|
| 281 |
+
in_bounds = (
|
| 282 |
+
(next_x >= 0) & (next_x < W) & (next_y >= 0) & (next_y < H)
|
| 283 |
+
)
|
| 284 |
+
if np.any(~in_bounds):
|
| 285 |
+
active[active_ids[~in_bounds]] = False
|
| 286 |
+
active_ids = active_ids[in_bounds]
|
| 287 |
+
if len(active_ids) == 0:
|
| 288 |
+
continue
|
| 289 |
+
next_x = next_x[in_bounds]
|
| 290 |
+
next_y = next_y[in_bounds]
|
| 291 |
+
cx[active_ids] = next_x
|
| 292 |
+
cy[active_ids] = next_y
|
| 293 |
+
next_score = semantic_fine[next_y, next_x]
|
| 294 |
+
points[active_ids, index, 0] = next_x
|
| 295 |
+
points[active_ids, index, 1] = next_y
|
| 296 |
+
points[active_ids, index, 2] = next_score
|
| 297 |
+
lengths[active_ids] = index + 1
|
| 298 |
+
|
| 299 |
+
has_remaining = remain_count[active_ids] > 0
|
| 300 |
+
remaining = np.ones(len(active_ids), dtype=np.float64)
|
| 301 |
+
remaining[has_remaining] = np.sqrt(
|
| 302 |
+
remain_sq_sum[active_ids[has_remaining]]
|
| 303 |
+
/ remain_count[active_ids[has_remaining]]
|
| 304 |
+
)
|
| 305 |
+
stop = (
|
| 306 |
+
(next_score <= seg_threshold)
|
| 307 |
+
& (index > remaining * 0.75)
|
| 308 |
+
)
|
| 309 |
+
if np.any(stop):
|
| 310 |
+
active[active_ids[stop]] = False
|
| 311 |
+
return points, lengths
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
if njit is not None:
|
| 315 |
+
@njit(cache=True, parallel=True)
|
| 316 |
+
def _decode_branches_numba_impl(seeds, semantic_fine, arrow, bound,
|
| 317 |
+
step_length, seg_threshold):
|
| 318 |
+
"""Parallel scalar relay walks compiled to native CPU code."""
|
| 319 |
+
height, width = semantic_fine.shape
|
| 320 |
+
seed_count = len(seeds)
|
| 321 |
+
points = np.empty((seed_count, height, 3), dtype=np.float32)
|
| 322 |
+
lengths = np.zeros(seed_count, dtype=np.int32)
|
| 323 |
+
for seed_index in prange(seed_count):
|
| 324 |
+
cx = int(seeds[seed_index, 0])
|
| 325 |
+
cy = int(seeds[seed_index, 1])
|
| 326 |
+
remain_sq_sum = 0.0
|
| 327 |
+
remain_count = 0
|
| 328 |
+
for index in range(height):
|
| 329 |
+
if semantic_fine[cy, cx] > seg_threshold:
|
| 330 |
+
remain = (
|
| 331 |
+
bound[cy, cx] * 100.0 / step_length + index
|
| 332 |
+
)
|
| 333 |
+
remain_sq_sum += remain * remain
|
| 334 |
+
remain_count += 1
|
| 335 |
+
|
| 336 |
+
dx = arrow[0, cy, cx]
|
| 337 |
+
dy = arrow[1, cy, cx]
|
| 338 |
+
norm = np.sqrt(dx * dx + dy * dy)
|
| 339 |
+
if norm == 0.0 or not np.isfinite(norm):
|
| 340 |
+
break
|
| 341 |
+
cx = int(np.floor(cx + dx / norm * step_length))
|
| 342 |
+
cy = int(np.floor(cy + dy / norm * step_length))
|
| 343 |
+
if not (0 <= cx < width and 0 <= cy < height):
|
| 344 |
+
break
|
| 345 |
+
|
| 346 |
+
score = semantic_fine[cy, cx]
|
| 347 |
+
points[seed_index, index, 0] = cx
|
| 348 |
+
points[seed_index, index, 1] = cy
|
| 349 |
+
points[seed_index, index, 2] = score
|
| 350 |
+
lengths[seed_index] = index + 1
|
| 351 |
+
|
| 352 |
+
ret = (
|
| 353 |
+
np.sqrt(remain_sq_sum / remain_count)
|
| 354 |
+
if remain_count else 1.0
|
| 355 |
+
)
|
| 356 |
+
if score > seg_threshold:
|
| 357 |
+
continue
|
| 358 |
+
if index > ret * 0.75:
|
| 359 |
+
break
|
| 360 |
+
return points, lengths
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
@njit(cache=True, parallel=True)
|
| 364 |
+
def _candidate_metadata_numba_impl(up_points, up_lengths,
|
| 365 |
+
down_points, down_lengths, bin_px):
|
| 366 |
+
seed_count = len(up_lengths)
|
| 367 |
+
scores = np.zeros(seed_count, dtype=np.float64)
|
| 368 |
+
bins = np.zeros(seed_count, dtype=np.int32)
|
| 369 |
+
total_lengths = up_lengths + down_lengths
|
| 370 |
+
for seed_index in prange(seed_count):
|
| 371 |
+
up_length = int(up_lengths[seed_index])
|
| 372 |
+
down_length = int(down_lengths[seed_index])
|
| 373 |
+
total_length = up_length + down_length
|
| 374 |
+
if total_length <= 1:
|
| 375 |
+
continue
|
| 376 |
+
y_values = np.empty(total_length, dtype=np.float32)
|
| 377 |
+
score_sum = 0.0
|
| 378 |
+
position = 0
|
| 379 |
+
for point_index in range(up_length):
|
| 380 |
+
score_sum += up_points[seed_index, point_index, 2]
|
| 381 |
+
y_values[position] = up_points[seed_index, point_index, 1]
|
| 382 |
+
position += 1
|
| 383 |
+
for point_index in range(down_length):
|
| 384 |
+
score_sum += down_points[seed_index, point_index, 2]
|
| 385 |
+
y_values[position] = down_points[seed_index, point_index, 1]
|
| 386 |
+
position += 1
|
| 387 |
+
scores[seed_index] = score_sum / total_length
|
| 388 |
+
median_y = np.median(y_values)
|
| 389 |
+
lower_x = np.empty(total_length, dtype=np.float32)
|
| 390 |
+
lower_count = 0
|
| 391 |
+
for point_index in range(up_length):
|
| 392 |
+
if up_points[seed_index, point_index, 1] >= median_y:
|
| 393 |
+
lower_x[lower_count] = up_points[
|
| 394 |
+
seed_index, point_index, 0
|
| 395 |
+
]
|
| 396 |
+
lower_count += 1
|
| 397 |
+
for point_index in range(down_length):
|
| 398 |
+
if down_points[seed_index, point_index, 1] >= median_y:
|
| 399 |
+
lower_x[lower_count] = down_points[
|
| 400 |
+
seed_index, point_index, 0
|
| 401 |
+
]
|
| 402 |
+
lower_count += 1
|
| 403 |
+
bins[seed_index] = int(
|
| 404 |
+
np.median(lower_x[:lower_count]) // bin_px
|
| 405 |
+
)
|
| 406 |
+
return total_lengths, scores, bins
|
| 407 |
+
else:
|
| 408 |
+
_decode_branches_numba_impl = None
|
| 409 |
+
_candidate_metadata_numba_impl = None
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
def decode_branches_numba(seeds, semantic_fine, arrow, bound, step_length,
|
| 413 |
+
seg_threshold):
|
| 414 |
+
if _decode_branches_numba_impl is None:
|
| 415 |
+
raise RuntimeError(
|
| 416 |
+
"Numba crawl requested but numba is not installed; "
|
| 417 |
+
"install requirements.txt or use crawl_backend='numpy'"
|
| 418 |
+
)
|
| 419 |
+
seed_array = np.asarray(seeds, dtype=np.int32)
|
| 420 |
+
if seed_array.size == 0:
|
| 421 |
+
height = semantic_fine.shape[0]
|
| 422 |
+
return (
|
| 423 |
+
np.empty((0, height, 3), dtype=np.float32),
|
| 424 |
+
np.zeros(0, dtype=np.int32),
|
| 425 |
+
)
|
| 426 |
+
return _decode_branches_numba_impl(
|
| 427 |
+
seed_array,
|
| 428 |
+
np.ascontiguousarray(semantic_fine, dtype=np.float32),
|
| 429 |
+
np.ascontiguousarray(arrow, dtype=np.float32),
|
| 430 |
+
np.ascontiguousarray(bound, dtype=np.float32),
|
| 431 |
+
float(step_length),
|
| 432 |
+
float(seg_threshold),
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def warmup_decode_backend(crawl_backend="auto"):
|
| 437 |
+
"""Compile the optional Numba backend before latency measurements."""
|
| 438 |
+
backend = crawl_backend
|
| 439 |
+
if crawl_backend == "auto":
|
| 440 |
+
backend = "numba" if njit is not None else "numpy"
|
| 441 |
+
if backend != "numba":
|
| 442 |
+
return backend
|
| 443 |
+
semantic = np.zeros((8, 8), dtype=np.float32)
|
| 444 |
+
arrow = np.zeros((2, 8, 8), dtype=np.float32)
|
| 445 |
+
arrow[1] = 1.0
|
| 446 |
+
bound = np.zeros((8, 8), dtype=np.float32)
|
| 447 |
+
points, lengths = decode_branches_numba(
|
| 448 |
+
[(4, 4)], semantic, arrow, bound, 1, 0.5
|
| 449 |
+
)
|
| 450 |
+
_candidate_metadata_numba_impl(
|
| 451 |
+
points, lengths, points, lengths, 16
|
| 452 |
+
)
|
| 453 |
+
_greedy_seed_select_numba(
|
| 454 |
+
np.array((4,), dtype=np.int32),
|
| 455 |
+
np.array((4,), dtype=np.int32),
|
| 456 |
+
8, 8, 2, 1,
|
| 457 |
+
)
|
| 458 |
+
return backend
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def configure_decode_threads(thread_count=8):
|
| 462 |
+
"""Set Numba's relay-crawl worker count and return the applied value."""
|
| 463 |
+
if njit is None:
|
| 464 |
+
return 1
|
| 465 |
+
import numba
|
| 466 |
+
maximum = int(numba.config.NUMBA_NUM_THREADS)
|
| 467 |
+
if not 1 <= int(thread_count) <= maximum:
|
| 468 |
+
raise ValueError(f"decode threads must be in [1, {maximum}]")
|
| 469 |
+
numba.set_num_threads(int(thread_count))
|
| 470 |
+
return numba.get_num_threads()
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
def _candidate_metadata_numpy(up_points, up_lengths,
|
| 474 |
+
down_points, down_lengths, bin_px):
|
| 475 |
+
total_lengths = up_lengths + down_lengths
|
| 476 |
+
scores = np.zeros(len(total_lengths), dtype=np.float64)
|
| 477 |
+
bins = np.zeros(len(total_lengths), dtype=np.int32)
|
| 478 |
+
for seed_index, total_length in enumerate(total_lengths):
|
| 479 |
+
if total_length <= 1:
|
| 480 |
+
continue
|
| 481 |
+
up = up_points[seed_index, :up_lengths[seed_index]]
|
| 482 |
+
down = down_points[seed_index, :down_lengths[seed_index]]
|
| 483 |
+
merged = np.concatenate((up, down))
|
| 484 |
+
scores[seed_index] = merged[:, 2].sum(dtype=np.float64) / len(merged)
|
| 485 |
+
median_y = np.median(merged[:, 1])
|
| 486 |
+
lower = merged[merged[:, 1] >= median_y]
|
| 487 |
+
bins[seed_index] = int(np.median(lower[:, 0]) // bin_px)
|
| 488 |
+
return total_lengths, scores, bins
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def _preselect_batched_candidates(up_points, up_lengths,
|
| 492 |
+
down_points, down_lengths, score_threshold,
|
| 493 |
+
max_lanes, backend, bin_px=16):
|
| 494 |
+
metadata = (
|
| 495 |
+
_candidate_metadata_numba_impl
|
| 496 |
+
if backend == "numba" else _candidate_metadata_numpy
|
| 497 |
+
)
|
| 498 |
+
total_lengths, scores, bins = metadata(
|
| 499 |
+
up_points, up_lengths, down_points, down_lengths, bin_px
|
| 500 |
+
)
|
| 501 |
+
valid = np.flatnonzero(
|
| 502 |
+
(total_lengths > 1) & (scores >= score_threshold)
|
| 503 |
+
)
|
| 504 |
+
order = valid[np.argsort(-scores[valid], kind="stable")]
|
| 505 |
+
if max_lanes is None or len(order) <= max_lanes:
|
| 506 |
+
return order
|
| 507 |
+
|
| 508 |
+
buckets = {}
|
| 509 |
+
for candidate in order:
|
| 510 |
+
buckets.setdefault(int(bins[candidate]), []).append(int(candidate))
|
| 511 |
+
keys = list(buckets)
|
| 512 |
+
positions = {key: 0 for key in keys}
|
| 513 |
+
selected = []
|
| 514 |
+
while len(selected) < max_lanes:
|
| 515 |
+
progressed = False
|
| 516 |
+
for key in keys:
|
| 517 |
+
position = positions[key]
|
| 518 |
+
if position < len(buckets[key]):
|
| 519 |
+
selected.append(buckets[key][position])
|
| 520 |
+
positions[key] += 1
|
| 521 |
+
progressed = True
|
| 522 |
+
if len(selected) >= max_lanes:
|
| 523 |
+
break
|
| 524 |
+
if not progressed:
|
| 525 |
+
break
|
| 526 |
+
return np.asarray(
|
| 527 |
+
sorted(selected, key=lambda index: scores[index], reverse=True),
|
| 528 |
+
dtype=np.int32,
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def _lines_from_batched_crawls(up_points, up_lengths,
|
| 533 |
+
down_points, down_lengths, width, height,
|
| 534 |
+
candidate_indices=None):
|
| 535 |
+
lines = []
|
| 536 |
+
if candidate_indices is None:
|
| 537 |
+
candidate_indices = range(len(up_lengths))
|
| 538 |
+
for seed_index in candidate_indices:
|
| 539 |
+
up_length = int(up_lengths[seed_index])
|
| 540 |
+
down_length = int(down_lengths[seed_index])
|
| 541 |
+
if up_length + down_length <= 1:
|
| 542 |
+
continue
|
| 543 |
+
merged = np.concatenate((
|
| 544 |
+
up_points[seed_index, :up_length][::-1],
|
| 545 |
+
down_points[seed_index, :down_length],
|
| 546 |
+
))
|
| 547 |
+
lane = Lane(width, height)
|
| 548 |
+
lane.points = merged
|
| 549 |
+
lane._score_sum = float(merged[:, 2].sum(dtype=np.float64))
|
| 550 |
+
lines.append(lane)
|
| 551 |
+
return lines
|
| 552 |
+
|
| 553 |
+
|
| 554 |
# --------------------------------------------------------------------------- #
|
| 555 |
# post-processing
|
| 556 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 662 |
return float(xy[int(np.argmax(xy[:, 1])), 0])
|
| 663 |
|
| 664 |
|
| 665 |
+
def _ego_reference_x(lane, target_y=None):
|
| 666 |
+
"""Estimate where a boundary meets the near-camera reference row.
|
| 667 |
+
|
| 668 |
+
On a sharp bend, multiple boundaries can leave through the same image side.
|
| 669 |
+
Comparing their last visible x then becomes ambiguous (both are about 0 or
|
| 670 |
+
``width - 1``), and a short outer boundary can be mistaken for the ego-lane
|
| 671 |
+
boundary. Extrapolating the lower 40% of each polyline to a common row keeps
|
| 672 |
+
their lateral order after they leave the image.
|
| 673 |
+
"""
|
| 674 |
+
xy = lane.xy()
|
| 675 |
+
if len(xy) < 2:
|
| 676 |
+
return _bottom_x(lane)
|
| 677 |
+
xy = xy[np.isfinite(xy).all(axis=1)]
|
| 678 |
+
if len(xy) < 2:
|
| 679 |
+
return _bottom_x(lane)
|
| 680 |
+
if target_y is None:
|
| 681 |
+
target_y = lane.height - 1.0
|
| 682 |
+
|
| 683 |
+
cutoff = np.quantile(xy[:, 1], 0.6)
|
| 684 |
+
lower = xy[xy[:, 1] >= cutoff]
|
| 685 |
+
if len(lower) < 2 or np.ptp(lower[:, 1]) < 1.0:
|
| 686 |
+
return _bottom_x(lane)
|
| 687 |
+
|
| 688 |
+
ys = lower[:, 1]
|
| 689 |
+
xs = lower[:, 0]
|
| 690 |
+
centered_y = ys - ys.mean()
|
| 691 |
+
denominator = float(np.dot(centered_y, centered_y))
|
| 692 |
+
if denominator <= 1e-6:
|
| 693 |
+
return _bottom_x(lane)
|
| 694 |
+
slope = float(np.dot(centered_y, xs - xs.mean()) / denominator)
|
| 695 |
+
return float(xs.mean() + slope * (float(target_y) - ys.mean()))
|
| 696 |
+
|
| 697 |
+
|
| 698 |
def order_lanes(lanes):
|
| 699 |
+
"""Sort lanes left-to-right and assign a frame-local index.
|
| 700 |
|
| 701 |
RCLane is anchor-free: `decode` emits lane instances in score order with no
|
| 702 |
+
inherent identity. This helper only establishes spatial order inside one
|
| 703 |
+
frame; it must not be used as a persistent video identity because a missing
|
| 704 |
+
outer lane would shift every following index.
|
|
|
|
| 705 |
"""
|
| 706 |
ordered = sorted(lanes, key=_bottom_x)
|
| 707 |
for i, ln in enumerate(ordered):
|
|
|
|
| 709 |
return ordered
|
| 710 |
|
| 711 |
|
| 712 |
+
def assign_ego_lane_roles(lanes, ego_x=None):
|
| 713 |
+
"""Assign stable semantic IDs relative to the ego vehicle.
|
| 714 |
+
|
| 715 |
+
IDs describe a lane boundary's role rather than its position in a variable
|
| 716 |
+
length list:
|
| 717 |
+
|
| 718 |
+
* P1: nearest boundary left of ego (current-lane left boundary)
|
| 719 |
+
* P2: nearest boundary right of ego (current-lane right boundary)
|
| 720 |
+
* P0: next boundary to the left
|
| 721 |
+
* P3: next boundary to the right
|
| 722 |
+
|
| 723 |
+
Consequently P1/P2 do not become P0/P1 merely because an outer boundary is
|
| 724 |
+
temporarily missing. With the default four-lane cap, returned IDs are in
|
| 725 |
+
``[0, 3]``. More uncapped lanes continue outward with negative IDs on the
|
| 726 |
+
left and IDs greater than three on the right.
|
| 727 |
+
"""
|
| 728 |
+
if not lanes:
|
| 729 |
+
return []
|
| 730 |
+
if ego_x is None:
|
| 731 |
+
ego_x = lanes[0].width / 2.0
|
| 732 |
+
ego_x = float(ego_x)
|
| 733 |
+
|
| 734 |
+
for lane in lanes:
|
| 735 |
+
lane.lane_id = None
|
| 736 |
+
lane.lane_role = None
|
| 737 |
+
lane.is_ego_boundary = False
|
| 738 |
+
lane.lateral_rank = None
|
| 739 |
+
|
| 740 |
+
reference_x = {lane: _ego_reference_x(lane) for lane in lanes}
|
| 741 |
+
left = sorted(
|
| 742 |
+
(lane for lane in lanes if reference_x[lane] < ego_x),
|
| 743 |
+
key=lambda lane: (abs(reference_x[lane] - ego_x), -lane.score),
|
| 744 |
+
)
|
| 745 |
+
right = sorted(
|
| 746 |
+
(lane for lane in lanes if reference_x[lane] >= ego_x),
|
| 747 |
+
key=lambda lane: (abs(reference_x[lane] - ego_x), -lane.score),
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
for rank, lane in enumerate(left, 1):
|
| 751 |
+
lane.lane_id = 2 - rank # nearest left=P1, next=P0
|
| 752 |
+
lane.lateral_rank = -rank
|
| 753 |
+
lane.is_ego_boundary = rank == 1
|
| 754 |
+
lane.lane_role = "ego_left" if rank == 1 else f"left_{rank}"
|
| 755 |
+
for rank, lane in enumerate(right, 1):
|
| 756 |
+
lane.lane_id = 1 + rank # nearest right=P2, next=P3
|
| 757 |
+
lane.lateral_rank = rank
|
| 758 |
+
lane.is_ego_boundary = rank == 1
|
| 759 |
+
lane.lane_role = "ego_right" if rank == 1 else f"right_{rank}"
|
| 760 |
+
|
| 761 |
+
return sorted(lanes, key=lambda lane: lane.lane_id)
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def ego_lane_boundaries(lanes):
|
| 765 |
+
"""Return ``(left, right)`` boundaries of the lane containing ego.
|
| 766 |
+
|
| 767 |
+
Either value can be ``None`` when that side was not detected.
|
| 768 |
+
"""
|
| 769 |
+
left = next(
|
| 770 |
+
(lane for lane in lanes if lane.lane_role == "ego_left"), None
|
| 771 |
+
)
|
| 772 |
+
right = next(
|
| 773 |
+
(lane for lane in lanes if lane.lane_role == "ego_right"), None
|
| 774 |
+
)
|
| 775 |
+
return left, right
|
| 776 |
+
|
| 777 |
+
|
| 778 |
def select_ego_lanes(lanes, max_lanes=4, ego_x=None,
|
| 779 |
min_score_ratio=0.5, balance_sides=True):
|
| 780 |
"""Keep the closest reliable lane boundaries around the ego vehicle.
|
|
|
|
| 787 |
|
| 788 |
For the usual four-lane output, ``balance_sides`` reserves two slots on
|
| 789 |
either side of the camera centre when possible. Any unfilled slots are
|
| 790 |
+
taken from the remaining closest candidates. The returned IDs are semantic:
|
| 791 |
+
P1/P2 are the current-lane boundaries, while P0/P3 are the adjacent outer
|
| 792 |
+
boundaries. Missing outer lanes therefore do not shift the ego-lane IDs.
|
| 793 |
"""
|
| 794 |
if max_lanes is None:
|
| 795 |
+
return assign_ego_lane_roles(lanes, ego_x)
|
| 796 |
if max_lanes <= 0:
|
| 797 |
raise ValueError("max_lanes must be positive or None")
|
| 798 |
if not 0.0 <= min_score_ratio <= 1.0:
|
| 799 |
raise ValueError("min_score_ratio must be in [0, 1]")
|
| 800 |
|
| 801 |
ordered = order_lanes(lanes)
|
| 802 |
+
if not ordered:
|
| 803 |
+
return []
|
|
|
|
| 804 |
if ego_x is None:
|
| 805 |
ego_x = ordered[0].width / 2.0
|
| 806 |
ego_x = float(ego_x)
|
| 807 |
|
| 808 |
+
# The four-lane semantic contract has exactly two possible boundaries per
|
| 809 |
+
# side: P0/P1 on the left and P2/P3 on the right. If one side is missing,
|
| 810 |
+
# return fewer lanes instead of filling the gap with P4/P-1 farther out.
|
| 811 |
+
if len(ordered) <= max_lanes:
|
| 812 |
+
if balance_sides and max_lanes == 4:
|
| 813 |
+
reference_x = {lane: _ego_reference_x(lane) for lane in ordered}
|
| 814 |
+
|
| 815 |
+
def near_ego(lane):
|
| 816 |
+
return (abs(reference_x[lane] - ego_x), -lane.score)
|
| 817 |
+
|
| 818 |
+
left = sorted(
|
| 819 |
+
(lane for lane in ordered if reference_x[lane] < ego_x),
|
| 820 |
+
key=near_ego,
|
| 821 |
+
)
|
| 822 |
+
right = sorted(
|
| 823 |
+
(lane for lane in ordered if reference_x[lane] >= ego_x),
|
| 824 |
+
key=near_ego,
|
| 825 |
+
)
|
| 826 |
+
ordered = left[:2] + right[:2]
|
| 827 |
+
return assign_ego_lane_roles(ordered, ego_x)
|
| 828 |
+
|
| 829 |
best_score = max(lane.score for lane in ordered)
|
| 830 |
reliable = [
|
| 831 |
lane for lane in ordered
|
|
|
|
| 834 |
# Never let the reliability gate force the output below the requested cap.
|
| 835 |
pool = reliable if len(reliable) >= max_lanes else ordered
|
| 836 |
|
| 837 |
+
reference_x = {lane: _ego_reference_x(lane) for lane in pool}
|
| 838 |
+
|
| 839 |
def proximity_key(lane):
|
| 840 |
+
return (abs(reference_x[lane] - ego_x), -lane.score)
|
| 841 |
|
| 842 |
ranked = sorted(pool, key=proximity_key)
|
| 843 |
selected = []
|
| 844 |
if balance_sides and max_lanes >= 2:
|
| 845 |
left = sorted(
|
| 846 |
+
(lane for lane in pool if reference_x[lane] < ego_x),
|
| 847 |
key=proximity_key,
|
| 848 |
)
|
| 849 |
right = sorted(
|
| 850 |
+
(lane for lane in pool if reference_x[lane] >= ego_x),
|
| 851 |
key=proximity_key,
|
| 852 |
)
|
| 853 |
left_slots = max_lanes // 2
|
|
|
|
| 855 |
selected.extend(left[:left_slots])
|
| 856 |
selected.extend(right[:right_slots])
|
| 857 |
|
| 858 |
+
if not (balance_sides and max_lanes == 4):
|
| 859 |
+
for lane in ranked:
|
| 860 |
+
if lane not in selected:
|
| 861 |
+
selected.append(lane)
|
| 862 |
+
if len(selected) == max_lanes:
|
| 863 |
+
break
|
| 864 |
|
| 865 |
+
return assign_ego_lane_roles(selected[:max_lanes], ego_x)
|
| 866 |
|
| 867 |
|
| 868 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 873 |
score_thresh=0.10, iou_thresh=0.5, seed_threshold=None,
|
| 874 |
max_seeds=1024, nms_max_lanes=128, nms_scale=0.25,
|
| 875 |
sort_lanes=True, max_output_lanes=4, ego_x=None,
|
| 876 |
+
ego_min_score_ratio=0.5, balance_ego_sides=True,
|
| 877 |
+
batch_crawl=True, crawl_backend="auto",
|
| 878 |
+
point_nms_backend="auto"):
|
| 879 |
"""
|
| 880 |
Args:
|
| 881 |
seg_prob: (H, W) foreground probability.
|
|
|
|
| 892 |
H, W = seg_prob.shape
|
| 893 |
if seed_threshold is None:
|
| 894 |
seed_threshold = seg_threshold
|
| 895 |
+
seeds = point_nms(
|
| 896 |
+
seg_prob, seed_threshold, seed_min_dist, max_seeds,
|
| 897 |
+
backend=point_nms_backend,
|
| 898 |
+
)
|
| 899 |
ub0, db0 = up_bound[0], down_bound[0] # bound channel 0 (both channels equal)
|
| 900 |
|
| 901 |
+
if batch_crawl:
|
| 902 |
+
backend = crawl_backend
|
| 903 |
+
if crawl_backend == "auto":
|
| 904 |
+
backend = "numba" if njit is not None else "numpy"
|
| 905 |
+
if backend not in ("numba", "numpy"):
|
| 906 |
+
raise ValueError("crawl_backend must be auto, numba, or numpy")
|
| 907 |
+
crawl = (
|
| 908 |
+
decode_branches_numba if backend == "numba"
|
| 909 |
+
else decode_branches_batch
|
| 910 |
+
)
|
| 911 |
+
up_points, up_lengths = crawl(
|
| 912 |
+
seeds, seg_prob, up_arrow, ub0, step_length, seg_threshold
|
| 913 |
+
)
|
| 914 |
+
down_points, down_lengths = crawl(
|
| 915 |
+
seeds, seg_prob, down_arrow, db0, step_length, seg_threshold
|
| 916 |
+
)
|
| 917 |
+
candidate_indices = _preselect_batched_candidates(
|
| 918 |
+
up_points, up_lengths, down_points, down_lengths,
|
| 919 |
+
score_thresh, nms_max_lanes, backend,
|
| 920 |
+
)
|
| 921 |
+
lines = _lines_from_batched_crawls(
|
| 922 |
+
up_points, up_lengths, down_points, down_lengths, W, H,
|
| 923 |
+
candidate_indices,
|
| 924 |
+
)
|
| 925 |
+
else:
|
| 926 |
+
lines = []
|
| 927 |
+
for (x, y) in seeds:
|
| 928 |
+
up = decode_branch(
|
| 929 |
+
x, y, seg_prob, up_arrow, ub0, step_length, seg_threshold
|
| 930 |
+
)
|
| 931 |
+
down = decode_branch(
|
| 932 |
+
x, y, seg_prob, down_arrow, db0, step_length, seg_threshold
|
| 933 |
+
)
|
| 934 |
+
up.reverse()
|
| 935 |
+
full = up.concat(down)
|
| 936 |
+
if len(full) > 1:
|
| 937 |
+
lines.append(full)
|
| 938 |
|
| 939 |
lines = thresh_line(lines, score_thresh)
|
| 940 |
lines = iou_nms(lines, iou_thresh, max_lanes=nms_max_lanes,
|
|
|
|
| 1049 |
f"ego selector kept the wrong lanes: {ego_xs}"
|
| 1050 |
)
|
| 1051 |
assert [lane.lane_id for lane in ego_lanes] == [0, 1, 2, 3]
|
| 1052 |
+
ego_left, ego_right = ego_lane_boundaries(ego_lanes)
|
| 1053 |
+
assert ego_left is not None and int(_bottom_x(ego_left)) == 95
|
| 1054 |
+
assert ego_right is not None and int(_bottom_x(ego_right)) == 748
|
| 1055 |
+
|
| 1056 |
+
# Semantic IDs must not shift when an outer lane disappears. P1/P2 remain
|
| 1057 |
+
# the current-lane boundaries and P3 remains the next boundary on the right.
|
| 1058 |
+
missing_outer_left = select_ego_lanes(
|
| 1059 |
+
[vertical_lane(95, 0.94), vertical_lane(748, 0.89),
|
| 1060 |
+
vertical_lane(796, 0.81)],
|
| 1061 |
+
max_lanes=4,
|
| 1062 |
+
)
|
| 1063 |
+
assert [lane.lane_id for lane in missing_outer_left] == [1, 2, 3]
|
| 1064 |
+
missing_outer_right = select_ego_lanes(
|
| 1065 |
+
[vertical_lane(8, 0.93), vertical_lane(95, 0.94),
|
| 1066 |
+
vertical_lane(748, 0.89)],
|
| 1067 |
+
max_lanes=4,
|
| 1068 |
+
)
|
| 1069 |
+
assert [lane.lane_id for lane in missing_outer_right] == [0, 1, 2]
|
| 1070 |
+
|
| 1071 |
+
right_only = select_ego_lanes(
|
| 1072 |
+
[vertical_lane(500, 0.95), vertical_lane(600, 0.90),
|
| 1073 |
+
vertical_lane(700, 0.80)],
|
| 1074 |
+
max_lanes=4,
|
| 1075 |
+
)
|
| 1076 |
+
assert [lane.lane_id for lane in right_only] == [2, 3]
|
| 1077 |
+
|
| 1078 |
+
# Two right boundaries can both leave through x=width on a sharp curve.
|
| 1079 |
+
# The longer/nearer curve must remain P2 even if its last visible x is a
|
| 1080 |
+
# little farther right than the short outer curve's last x.
|
| 1081 |
+
near_right = Lane(800, 320)
|
| 1082 |
+
outer_right = Lane(800, 320)
|
| 1083 |
+
for x, y in ((700, 200), (740, 225), (780, 250)):
|
| 1084 |
+
near_right.append(x, y, 0.9)
|
| 1085 |
+
for x, y in ((700, 125), (750, 137.5), (790, 147.5)):
|
| 1086 |
+
outer_right.append(x, y, 0.8)
|
| 1087 |
+
curved_right = assign_ego_lane_roles([outer_right, near_right], ego_x=400)
|
| 1088 |
+
assert near_right.lane_id == 2 and near_right.lane_role == "ego_right"
|
| 1089 |
+
assert outer_right.lane_id == 3 and outer_right.lane_role == "right_2"
|
| 1090 |
+
assert [lane.lane_id for lane in curved_right] == [2, 3]
|
| 1091 |
print("OK -- ego post-processing keeps four reliable nearby lanes.")
|
requirements.txt
CHANGED
|
@@ -10,3 +10,6 @@ onnx>=1.16,<2.0
|
|
| 10 |
# `onnxruntime` package alongside this package; both expose the same module.
|
| 11 |
onnxruntime-gpu==1.27.0.dev20260511001
|
| 12 |
torch==2.13.0+cu130
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
# `onnxruntime` package alongside this package; both expose the same module.
|
| 11 |
onnxruntime-gpu==1.27.0.dev20260511001
|
| 12 |
torch==2.13.0+cu130
|
| 13 |
+
# TensorRT 10 is required because ONNX Runtime's provider links libnvinfer.so.10.
|
| 14 |
+
tensorrt-cu13==10.16.1.11
|
| 15 |
+
numba>=0.61,<0.63
|
test_video_bev_onnx.py
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run RCLane ONNX and export ego-centric cubic lane models in BEV.
|
| 2 |
+
|
| 3 |
+
Each JSONL frame stores one metric polynomial per visible marking:
|
| 4 |
+
|
| 5 |
+
Y(X) = c0 + c1*X + c2*X^2 + c3*X^3
|
| 6 |
+
|
| 7 |
+
X points forward from ego and Y points left. Coefficients are valid only inside
|
| 8 |
+
the exported ``x_domain_m``; the implementation never extrapolates a detected
|
| 9 |
+
lane to the full 300 m visualization range.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import time
|
| 16 |
+
from collections import Counter
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
import cv2
|
| 20 |
+
import numpy as np
|
| 21 |
+
|
| 22 |
+
from bev import (
|
| 23 |
+
BevRange,
|
| 24 |
+
CameraCalibration,
|
| 25 |
+
clip_cubic_fit_to_funnel,
|
| 26 |
+
evaluate_cubic,
|
| 27 |
+
fit_cubic_lane,
|
| 28 |
+
model_lane_to_ground,
|
| 29 |
+
)
|
| 30 |
+
from dataset import normalize_image_numpy
|
| 31 |
+
from decode import configure_decode_threads, decode, warmup_decode_backend
|
| 32 |
+
from test_video_onnx import (
|
| 33 |
+
MODEL_HEIGHT,
|
| 34 |
+
MODEL_WIDTH,
|
| 35 |
+
OUTPUT_NAMES,
|
| 36 |
+
create_session,
|
| 37 |
+
draw_predictions,
|
| 38 |
+
softmax_foreground,
|
| 39 |
+
timing_summary,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
LANE_COLORS = {
|
| 44 |
+
0: (255, 255, 0),
|
| 45 |
+
1: (0, 255, 0),
|
| 46 |
+
2: (0, 165, 255),
|
| 47 |
+
3: (255, 255, 0),
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_args():
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 53 |
+
parser.add_argument("--model", required=True)
|
| 54 |
+
parser.add_argument("--video", required=True)
|
| 55 |
+
parser.add_argument("--output", default="runs/video_bev_e19.mp4")
|
| 56 |
+
parser.add_argument("--polynomials", default=None,
|
| 57 |
+
help="per-frame JSONL; defaults next to output")
|
| 58 |
+
parser.add_argument("--summary", default=None)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--provider", choices=("tensorrt", "cuda", "cpu"),
|
| 61 |
+
default="tensorrt",
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument("--trt-cache-dir", default="exports/trt_cache")
|
| 64 |
+
parser.add_argument("--allow-tf32", action="store_true")
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"--start-frame", type=int, default=0,
|
| 67 |
+
help="zero-based source frame at which processing starts",
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument("--max-frames", type=int, default=None)
|
| 70 |
+
parser.add_argument("--x-min", type=float, default=0.0)
|
| 71 |
+
parser.add_argument("--x-max", type=float, default=300.0)
|
| 72 |
+
parser.add_argument("--y-min", type=float, default=-85.0)
|
| 73 |
+
parser.add_argument("--y-max", type=float, default=85.0)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--max-cubic-rmse", type=float, default=0.5,
|
| 76 |
+
help="reject a cubic visualization above this metric RMSE",
|
| 77 |
+
)
|
| 78 |
+
parser.add_argument("--decode-seg-threshold", type=float, default=0.5)
|
| 79 |
+
parser.add_argument("--decode-seed-threshold", type=float, default=None)
|
| 80 |
+
parser.add_argument("--decode-seed-min-dist", type=int, default=2)
|
| 81 |
+
parser.add_argument("--decode-score-thresh", type=float, default=0.10)
|
| 82 |
+
parser.add_argument("--decode-nms-iou", type=float, default=0.5)
|
| 83 |
+
parser.add_argument("--decode-max-seeds", type=int, default=1024)
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--decode-crawl-backend", choices=("auto", "numba", "numpy"),
|
| 86 |
+
default="auto",
|
| 87 |
+
)
|
| 88 |
+
parser.add_argument("--decode-cpu-threads", type=int, default=8)
|
| 89 |
+
parser.add_argument("--decode-nms-max-lanes", type=int, default=128)
|
| 90 |
+
parser.add_argument("--decode-nms-scale", type=float, default=0.25)
|
| 91 |
+
parser.add_argument("--max-ego-lanes", type=int, default=4)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--funnel-margin", type=float, default=0.10,
|
| 94 |
+
help="metric tolerance around the calibrated camera funnel",
|
| 95 |
+
)
|
| 96 |
+
return parser.parse_args()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def lane_to_record(lane, calibration, bev_range, max_cubic_rmse=0.5):
|
| 100 |
+
points, scores = model_lane_to_ground(
|
| 101 |
+
lane, MODEL_WIDTH, MODEL_HEIGHT, calibration, bev_range
|
| 102 |
+
)
|
| 103 |
+
fit = fit_cubic_lane(points, scores, iterations=3)
|
| 104 |
+
lane_id = int(lane.lane_id) if lane.lane_id is not None else None
|
| 105 |
+
valid_fit = fit is not None and fit["rmse"] <= max_cubic_rmse
|
| 106 |
+
record = {
|
| 107 |
+
"lane_id": f"P{lane_id}" if lane_id is not None else None,
|
| 108 |
+
"lane_index": lane_id,
|
| 109 |
+
"role": lane.lane_role,
|
| 110 |
+
"score": float(lane.score),
|
| 111 |
+
"projected_point_count": int(len(points)),
|
| 112 |
+
"valid_fit": valid_fit,
|
| 113 |
+
"fit_status": (
|
| 114 |
+
"ok" if valid_fit else
|
| 115 |
+
"rmse_above_limit" if fit is not None else
|
| 116 |
+
"insufficient_geometry"
|
| 117 |
+
),
|
| 118 |
+
}
|
| 119 |
+
if fit is not None:
|
| 120 |
+
record.update({
|
| 121 |
+
"polynomial": "Y(X)=c0+c1*X+c2*X^2+c3*X^3",
|
| 122 |
+
"coefficients_c0_to_c3": [
|
| 123 |
+
float(value) for value in fit["coefficients"]
|
| 124 |
+
],
|
| 125 |
+
"x_domain_m": [fit["x_min"], fit["x_max"]],
|
| 126 |
+
"rmse_m": fit["rmse"],
|
| 127 |
+
"fit_point_count": fit["point_count"],
|
| 128 |
+
"inlier_count": fit["inlier_count"],
|
| 129 |
+
"inlier_ratio": fit["inlier_ratio"],
|
| 130 |
+
})
|
| 131 |
+
return {
|
| 132 |
+
"record": record,
|
| 133 |
+
"points": points,
|
| 134 |
+
"fit": fit if valid_fit else None,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def clip_lane_results_to_funnel(lane_results, args):
|
| 139 |
+
"""Clip raw model-derived cubics without changing their geometry.
|
| 140 |
+
|
| 141 |
+
Every output lane corresponds one-to-one with a decoded image-space lane.
|
| 142 |
+
No lane is synthesized, shifted, reordered, or forced to be parallel. The
|
| 143 |
+
cubic coefficients remain unchanged; only the declared X domain may shrink
|
| 144 |
+
when the fitted curve leaves the calibrated camera footprint.
|
| 145 |
+
"""
|
| 146 |
+
calibration = CameraCalibration()
|
| 147 |
+
report = {
|
| 148 |
+
"mode": "raw_model_projection",
|
| 149 |
+
"parallel_assumption": False,
|
| 150 |
+
"synthetic_lanes": False,
|
| 151 |
+
"camera_x_m": float(calibration.camera_to_vehicle_matrix[0, 3]),
|
| 152 |
+
"horizontal_fov_deg": float(calibration.horizontal_fov_deg),
|
| 153 |
+
"margin_m": float(args.funnel_margin),
|
| 154 |
+
"clipped_lanes": [],
|
| 155 |
+
"rejected_lanes": [],
|
| 156 |
+
}
|
| 157 |
+
for result in lane_results:
|
| 158 |
+
fit = result["fit"]
|
| 159 |
+
record = result["record"]
|
| 160 |
+
if fit is None:
|
| 161 |
+
continue
|
| 162 |
+
original_coefficients = np.asarray(
|
| 163 |
+
fit["coefficients"], dtype=np.float64
|
| 164 |
+
).copy()
|
| 165 |
+
clipped_fit, clip_report = clip_cubic_fit_to_funnel(
|
| 166 |
+
fit,
|
| 167 |
+
camera_x_m=report["camera_x_m"],
|
| 168 |
+
horizontal_fov_deg=report["horizontal_fov_deg"],
|
| 169 |
+
margin_m=args.funnel_margin,
|
| 170 |
+
)
|
| 171 |
+
lane_label = record["lane_id"]
|
| 172 |
+
if clipped_fit is None:
|
| 173 |
+
result["fit"] = None
|
| 174 |
+
record.update({
|
| 175 |
+
"valid_fit": False,
|
| 176 |
+
"fit_status": "outside_camera_funnel",
|
| 177 |
+
"funnel_guard": clip_report,
|
| 178 |
+
})
|
| 179 |
+
report["rejected_lanes"].append(lane_label)
|
| 180 |
+
continue
|
| 181 |
+
if not np.array_equal(
|
| 182 |
+
original_coefficients, clipped_fit["coefficients"]
|
| 183 |
+
):
|
| 184 |
+
raise AssertionError("funnel clipping changed cubic coefficients")
|
| 185 |
+
result["fit"] = clipped_fit
|
| 186 |
+
record["x_domain_m"] = [
|
| 187 |
+
float(clipped_fit["x_min"]), float(clipped_fit["x_max"])
|
| 188 |
+
]
|
| 189 |
+
record["funnel_guard"] = clip_report
|
| 190 |
+
if clip_report["clipped"]:
|
| 191 |
+
report["clipped_lanes"].append(lane_label)
|
| 192 |
+
return report
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def _bev_pixel(x_forward, y_left, bev_range, bounds):
|
| 196 |
+
left, top, right, bottom = bounds
|
| 197 |
+
px = left + (bev_range.y_max - y_left) / (
|
| 198 |
+
bev_range.y_max - bev_range.y_min
|
| 199 |
+
) * (right - left)
|
| 200 |
+
py = top + (bev_range.x_max - x_forward) / (
|
| 201 |
+
bev_range.x_max - bev_range.x_min
|
| 202 |
+
) * (bottom - top)
|
| 203 |
+
return np.column_stack((px, py))
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def _draw_dashed_curve(canvas, points, color, thickness=4,
|
| 207 |
+
dash_points=7, gap_points=4):
|
| 208 |
+
stride = dash_points + gap_points
|
| 209 |
+
for start in range(0, len(points) - 1, stride):
|
| 210 |
+
segment = points[start:min(start + dash_points + 1, len(points))]
|
| 211 |
+
if len(segment) >= 2:
|
| 212 |
+
cv2.polylines(
|
| 213 |
+
canvas, [segment], False, (0, 0, 0), thickness + 4,
|
| 214 |
+
cv2.LINE_AA,
|
| 215 |
+
)
|
| 216 |
+
cv2.polylines(
|
| 217 |
+
canvas, [segment], False, color, thickness, cv2.LINE_AA,
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def draw_bev(lane_results, bev_range, calibration, funnel_report=None,
|
| 222 |
+
width=640, height=1080):
|
| 223 |
+
canvas = np.full((height, width, 3), (35, 19, 10), dtype=np.uint8)
|
| 224 |
+
bounds = (72, 62, width - 24, height - 70)
|
| 225 |
+
left, top, right, bottom = bounds
|
| 226 |
+
|
| 227 |
+
# Requested sensor-style ROI funnel: ego at the apex and the configured
|
| 228 |
+
# metric ROI at its far edge. It is a visualization of the output ROI, not
|
| 229 |
+
# a replacement for the calibrated camera ray/ground intersection.
|
| 230 |
+
camera_x = float(calibration.camera_to_vehicle_matrix[0, 3])
|
| 231 |
+
half_fov_radians = np.deg2rad(
|
| 232 |
+
calibration.horizontal_fov_deg * 0.5
|
| 233 |
+
)
|
| 234 |
+
fov_half_width = min(
|
| 235 |
+
bev_range.y_max,
|
| 236 |
+
max(0.0, bev_range.x_max - camera_x) * np.tan(half_fov_radians),
|
| 237 |
+
)
|
| 238 |
+
roi_metric = np.array(
|
| 239 |
+
((camera_x, 0.0),
|
| 240 |
+
(bev_range.x_max, fov_half_width),
|
| 241 |
+
(bev_range.x_max, -fov_half_width)),
|
| 242 |
+
dtype=np.float64,
|
| 243 |
+
)
|
| 244 |
+
roi_pixels = np.round(_bev_pixel(
|
| 245 |
+
roi_metric[:, 0], roi_metric[:, 1], bev_range, bounds
|
| 246 |
+
)).astype(np.int32)
|
| 247 |
+
overlay = canvas.copy()
|
| 248 |
+
cv2.fillPoly(overlay, [roi_pixels], (105, 74, 105), cv2.LINE_AA)
|
| 249 |
+
cv2.addWeighted(overlay, 0.72, canvas, 0.28, 0.0, canvas)
|
| 250 |
+
cv2.polylines(canvas, [roi_pixels], True, (150, 120, 165), 2, cv2.LINE_AA)
|
| 251 |
+
|
| 252 |
+
for x in np.arange(
|
| 253 |
+
np.ceil(bev_range.x_min / 50.0) * 50.0,
|
| 254 |
+
bev_range.x_max + 0.1,
|
| 255 |
+
50.0,
|
| 256 |
+
):
|
| 257 |
+
half_width = min(
|
| 258 |
+
fov_half_width,
|
| 259 |
+
max(0.0, x - camera_x) * np.tan(half_fov_radians),
|
| 260 |
+
)
|
| 261 |
+
y_at_left_edge = half_width
|
| 262 |
+
y_at_right_edge = -half_width
|
| 263 |
+
range_line = _bev_pixel(
|
| 264 |
+
np.array([x, x]),
|
| 265 |
+
np.array([y_at_left_edge, y_at_right_edge]),
|
| 266 |
+
bev_range,
|
| 267 |
+
bounds,
|
| 268 |
+
)
|
| 269 |
+
range_line = np.round(range_line).astype(np.int32)
|
| 270 |
+
row = int(round(range_line[0, 1]))
|
| 271 |
+
cv2.line(
|
| 272 |
+
canvas, tuple(range_line[0]), tuple(range_line[1]),
|
| 273 |
+
(118, 91, 120), 1, cv2.LINE_AA,
|
| 274 |
+
)
|
| 275 |
+
cv2.putText(
|
| 276 |
+
canvas, f"{x:.0f}m", (8, min(bottom, max(top + 12, row + 5))),
|
| 277 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.42, (205, 190, 210), 1,
|
| 278 |
+
cv2.LINE_AA,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
zero = _bev_pixel(
|
| 282 |
+
np.array([bev_range.x_min]), np.array([0.0]), bev_range, bounds
|
| 283 |
+
)[0].astype(int)
|
| 284 |
+
far_center = _bev_pixel(
|
| 285 |
+
np.array([bev_range.x_max]), np.array([0.0]), bev_range, bounds
|
| 286 |
+
)[0].astype(int)
|
| 287 |
+
for y0 in range(zero[1] - 12, far_center[1], -28):
|
| 288 |
+
y1 = max(far_center[1], y0 - 15)
|
| 289 |
+
cv2.line(canvas, (zero[0], y0), (far_center[0], y1),
|
| 290 |
+
(225, 215, 230), 2, cv2.LINE_AA)
|
| 291 |
+
ego = np.array(
|
| 292 |
+
((zero[0] - 10, bottom - 22), (zero[0] + 10, bottom - 22),
|
| 293 |
+
(zero[0] + 14, bottom), (zero[0] - 14, bottom)),
|
| 294 |
+
dtype=np.int32,
|
| 295 |
+
)
|
| 296 |
+
cv2.fillPoly(canvas, [ego], (238, 238, 238), cv2.LINE_AA)
|
| 297 |
+
cv2.polylines(canvas, [ego], True, (20, 20, 20), 2, cv2.LINE_AA)
|
| 298 |
+
|
| 299 |
+
formula_rows = []
|
| 300 |
+
for result in lane_results:
|
| 301 |
+
record = result["record"]
|
| 302 |
+
lane_id = record["lane_index"]
|
| 303 |
+
color = LANE_COLORS.get(lane_id, (220, 220, 220))
|
| 304 |
+
points = result["points"]
|
| 305 |
+
if len(points):
|
| 306 |
+
pixels = _bev_pixel(
|
| 307 |
+
points[:, 0], points[:, 1], bev_range, bounds
|
| 308 |
+
)
|
| 309 |
+
pixels = np.round(pixels).astype(np.int32)
|
| 310 |
+
for pixel in pixels[::max(1, len(pixels) // 40)]:
|
| 311 |
+
cv2.circle(canvas, tuple(pixel), 2, color, -1, cv2.LINE_AA)
|
| 312 |
+
|
| 313 |
+
fit = result["fit"]
|
| 314 |
+
if fit is None:
|
| 315 |
+
rmse = record.get("rmse_m")
|
| 316 |
+
reason = (
|
| 317 |
+
f"rejected rmse={rmse:.2f}m" if rmse is not None
|
| 318 |
+
else "insufficient geometry"
|
| 319 |
+
)
|
| 320 |
+
formula_rows.append((color, f"{record['lane_id']}: {reason}"))
|
| 321 |
+
continue
|
| 322 |
+
x = np.linspace(fit["x_min"], fit["x_max"], 160)
|
| 323 |
+
y = evaluate_cubic(fit["coefficients"], x)
|
| 324 |
+
valid = (
|
| 325 |
+
np.isfinite(y)
|
| 326 |
+
& (y >= bev_range.y_min)
|
| 327 |
+
& (y <= bev_range.y_max)
|
| 328 |
+
)
|
| 329 |
+
curve = _bev_pixel(x[valid], y[valid], bev_range, bounds)
|
| 330 |
+
if len(curve) >= 2:
|
| 331 |
+
curve = np.round(curve).astype(np.int32)
|
| 332 |
+
_draw_dashed_curve(canvas, curve, color)
|
| 333 |
+
cv2.putText(
|
| 334 |
+
canvas, record["lane_id"], tuple(curve[-1]),
|
| 335 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.48, color, 2, cv2.LINE_AA,
|
| 336 |
+
)
|
| 337 |
+
formula_rows.append((
|
| 338 |
+
color,
|
| 339 |
+
f"{record['lane_id']}: X={fit['x_min']:.1f}..{fit['x_max']:.1f}m "
|
| 340 |
+
f"rmse={fit['rmse']:.2f}m",
|
| 341 |
+
))
|
| 342 |
+
|
| 343 |
+
cv2.putText(
|
| 344 |
+
canvas,
|
| 345 |
+
f"CAMERA FOV {calibration.horizontal_fov_deg:.0f}deg: "
|
| 346 |
+
"X forward / Y left",
|
| 347 |
+
(20, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.54,
|
| 348 |
+
(255, 255, 255), 2, cv2.LINE_AA,
|
| 349 |
+
)
|
| 350 |
+
cv2.putText(
|
| 351 |
+
canvas, "RAW MODEL LANES | no synthetic/parallel prior",
|
| 352 |
+
(20, 52), cv2.FONT_HERSHEY_SIMPLEX, 0.42,
|
| 353 |
+
(80, 220, 255), 1, cv2.LINE_AA,
|
| 354 |
+
)
|
| 355 |
+
cv2.putText(
|
| 356 |
+
canvas, "Y left (+) Y right (-)",
|
| 357 |
+
(left, height - 16), cv2.FONT_HERSHEY_SIMPLEX, 0.38,
|
| 358 |
+
(210, 210, 210), 1, cv2.LINE_AA,
|
| 359 |
+
)
|
| 360 |
+
for row, (color, text) in enumerate(formula_rows[:4]):
|
| 361 |
+
cv2.putText(
|
| 362 |
+
canvas, text, (82, 82 + row * 18),
|
| 363 |
+
cv2.FONT_HERSHEY_SIMPLEX, 0.33, color, 1, cv2.LINE_AA,
|
| 364 |
+
)
|
| 365 |
+
return canvas
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _preprocess_and_infer(session, frame):
|
| 369 |
+
"""Run the producer stage; safe to execute in a dedicated GPU thread."""
|
| 370 |
+
stage = time.perf_counter()
|
| 371 |
+
images = normalize_image_numpy(frame, MODEL_WIDTH, MODEL_HEIGHT)
|
| 372 |
+
preprocess_ms = (time.perf_counter() - stage) * 1000.0
|
| 373 |
+
|
| 374 |
+
stage = time.perf_counter()
|
| 375 |
+
outputs = session.run(list(OUTPUT_NAMES), {"images": images})
|
| 376 |
+
inference_ms = (time.perf_counter() - stage) * 1000.0
|
| 377 |
+
return outputs, preprocess_ms, inference_ms
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def _decode_and_generate_bev_result(outputs, args, calibration, bev_range):
|
| 381 |
+
"""Generate metric BEV data without drawing, compositing, or file I/O."""
|
| 382 |
+
stage = time.perf_counter()
|
| 383 |
+
lanes = decode(
|
| 384 |
+
softmax_foreground(outputs[0])[0], outputs[1][0], outputs[2][0],
|
| 385 |
+
outputs[3][0], outputs[4][0],
|
| 386 |
+
seg_threshold=args.decode_seg_threshold,
|
| 387 |
+
seed_threshold=args.decode_seed_threshold,
|
| 388 |
+
seed_min_dist=args.decode_seed_min_dist,
|
| 389 |
+
score_thresh=args.decode_score_thresh,
|
| 390 |
+
iou_thresh=args.decode_nms_iou,
|
| 391 |
+
max_seeds=args.decode_max_seeds,
|
| 392 |
+
nms_max_lanes=args.decode_nms_max_lanes,
|
| 393 |
+
nms_scale=args.decode_nms_scale,
|
| 394 |
+
max_output_lanes=args.max_ego_lanes,
|
| 395 |
+
crawl_backend=args.decode_crawl_backend,
|
| 396 |
+
)
|
| 397 |
+
decode_ms = (time.perf_counter() - stage) * 1000.0
|
| 398 |
+
|
| 399 |
+
stage = time.perf_counter()
|
| 400 |
+
lane_results = [
|
| 401 |
+
lane_to_record(lane, calibration, bev_range, args.max_cubic_rmse)
|
| 402 |
+
for lane in lanes
|
| 403 |
+
]
|
| 404 |
+
funnel_report = clip_lane_results_to_funnel(lane_results, args)
|
| 405 |
+
valid_records = [
|
| 406 |
+
result["record"] for result in lane_results
|
| 407 |
+
if result["record"]["valid_fit"]
|
| 408 |
+
]
|
| 409 |
+
bev_result_ms = (time.perf_counter() - stage) * 1000.0
|
| 410 |
+
return {
|
| 411 |
+
"lanes": lanes,
|
| 412 |
+
"lane_results": lane_results,
|
| 413 |
+
"funnel_report": funnel_report,
|
| 414 |
+
"valid_records": valid_records,
|
| 415 |
+
"decode_ms": decode_ms,
|
| 416 |
+
"bev_result_ms": bev_result_ms,
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
def generate_bev_results(capture, target_frames, start_frame, session, args,
|
| 421 |
+
calibration, bev_range):
|
| 422 |
+
"""Generate ordered BEV results sequentially, one complete frame at a time.
|
| 423 |
+
|
| 424 |
+
No BEV/camera drawing, compositing, video writing, or JSON serialization is
|
| 425 |
+
performed inside this timed region.
|
| 426 |
+
"""
|
| 427 |
+
timings = {
|
| 428 |
+
"preprocess": [],
|
| 429 |
+
"inference": [],
|
| 430 |
+
"decode": [],
|
| 431 |
+
"bev_result": [],
|
| 432 |
+
"pipeline": [],
|
| 433 |
+
}
|
| 434 |
+
results = []
|
| 435 |
+
generation_started = time.perf_counter()
|
| 436 |
+
|
| 437 |
+
def finish(frame_index, inference_payload):
|
| 438 |
+
outputs, preprocess_ms, inference_ms = inference_payload
|
| 439 |
+
bundle = _decode_and_generate_bev_result(
|
| 440 |
+
outputs, args, calibration, bev_range
|
| 441 |
+
)
|
| 442 |
+
compute_ms = (
|
| 443 |
+
preprocess_ms + inference_ms + bundle["decode_ms"]
|
| 444 |
+
+ bundle["bev_result_ms"]
|
| 445 |
+
)
|
| 446 |
+
bundle.update({
|
| 447 |
+
"frame_index": frame_index,
|
| 448 |
+
"preprocess_ms": preprocess_ms,
|
| 449 |
+
"inference_ms": inference_ms,
|
| 450 |
+
"compute_ms": compute_ms,
|
| 451 |
+
})
|
| 452 |
+
results.append(bundle)
|
| 453 |
+
timings["preprocess"].append(preprocess_ms)
|
| 454 |
+
timings["inference"].append(inference_ms)
|
| 455 |
+
timings["decode"].append(bundle["decode_ms"])
|
| 456 |
+
timings["bev_result"].append(bundle["bev_result_ms"])
|
| 457 |
+
timings["pipeline"].append(compute_ms)
|
| 458 |
+
processed = len(results)
|
| 459 |
+
if processed % 25 == 0 or processed == target_frames:
|
| 460 |
+
elapsed = time.perf_counter() - generation_started
|
| 461 |
+
print(
|
| 462 |
+
f"result {processed}/{target_frames} "
|
| 463 |
+
f"(source={frame_index}) | "
|
| 464 |
+
f"lanes={len(bundle['lanes'])} "
|
| 465 |
+
f"cubic={len(bundle['valid_records'])} "
|
| 466 |
+
f"decode={bundle['decode_ms']:.1f}ms "
|
| 467 |
+
f"bev-result={bundle['bev_result_ms']:.1f}ms "
|
| 468 |
+
f"throughput={processed / max(elapsed, 1e-9):.1f} FPS",
|
| 469 |
+
flush=True,
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
for offset in range(target_frames):
|
| 473 |
+
ok, frame = capture.read()
|
| 474 |
+
if not ok:
|
| 475 |
+
break
|
| 476 |
+
finish(
|
| 477 |
+
start_frame + offset,
|
| 478 |
+
_preprocess_and_infer(session, frame),
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
generation_seconds = time.perf_counter() - generation_started
|
| 482 |
+
if not results:
|
| 483 |
+
raise RuntimeError("input video produced no BEV results")
|
| 484 |
+
return results, timings, generation_seconds
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
def make_composite(frame, bev_canvas, frame_index, fit_count, pipeline_ms,
|
| 488 |
+
funnel_report=None, result_generation_fps=None):
|
| 489 |
+
output = np.zeros((1080, 1920, 3), dtype=np.uint8)
|
| 490 |
+
bev_width = 640
|
| 491 |
+
output[:, :bev_width] = cv2.resize(
|
| 492 |
+
bev_canvas, (bev_width, 1080), interpolation=cv2.INTER_AREA
|
| 493 |
+
)
|
| 494 |
+
camera_width = 1920 - bev_width
|
| 495 |
+
scaled_height = int(round(frame.shape[0] * camera_width / frame.shape[1]))
|
| 496 |
+
camera_view = cv2.resize(frame, (camera_width, scaled_height),
|
| 497 |
+
interpolation=cv2.INTER_AREA)
|
| 498 |
+
top = (1080 - scaled_height) // 2
|
| 499 |
+
output[top:top + scaled_height, bev_width:] = camera_view
|
| 500 |
+
cv2.line(output, (bev_width, 0), (bev_width, 1079), (255, 255, 255), 2)
|
| 501 |
+
if result_generation_fps is None:
|
| 502 |
+
result_generation_fps = 1000.0 / max(float(pipeline_ms), 1e-9)
|
| 503 |
+
runtime_label = (
|
| 504 |
+
f"frame={frame_index} | cubic lanes={fit_count} | "
|
| 505 |
+
f"BEV-result={result_generation_fps:.1f} FPS "
|
| 506 |
+
"(sequential, no render)"
|
| 507 |
+
)
|
| 508 |
+
cv2.putText(
|
| 509 |
+
output, runtime_label,
|
| 510 |
+
(bev_width + 24, 42), cv2.FONT_HERSHEY_SIMPLEX, 0.78,
|
| 511 |
+
(0, 0, 0), 5, cv2.LINE_AA,
|
| 512 |
+
)
|
| 513 |
+
cv2.putText(
|
| 514 |
+
output, runtime_label,
|
| 515 |
+
(bev_width + 24, 42), cv2.FONT_HERSHEY_SIMPLEX, 0.78,
|
| 516 |
+
(255, 255, 255), 2, cv2.LINE_AA,
|
| 517 |
+
)
|
| 518 |
+
cv2.putText(
|
| 519 |
+
output, "CAMERA: raw decode (not back-projected)",
|
| 520 |
+
(bev_width + 24, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 521 |
+
(0, 0, 0), 4, cv2.LINE_AA,
|
| 522 |
+
)
|
| 523 |
+
cv2.putText(
|
| 524 |
+
output, "CAMERA: raw decode (not back-projected)",
|
| 525 |
+
(bev_width + 24, 72), cv2.FONT_HERSHEY_SIMPLEX, 0.55,
|
| 526 |
+
(255, 255, 255), 1, cv2.LINE_AA,
|
| 527 |
+
)
|
| 528 |
+
return output
|
| 529 |
+
|
| 530 |
+
|
| 531 |
+
def main():
|
| 532 |
+
args = parse_args()
|
| 533 |
+
if args.max_frames is not None and args.max_frames <= 0:
|
| 534 |
+
raise ValueError("--max-frames must be positive")
|
| 535 |
+
if args.start_frame < 0:
|
| 536 |
+
raise ValueError("--start-frame must be non-negative")
|
| 537 |
+
if args.max_cubic_rmse <= 0:
|
| 538 |
+
raise ValueError("--max-cubic-rmse must be positive")
|
| 539 |
+
if not args.x_min < args.x_max or not args.y_min < args.y_max:
|
| 540 |
+
raise ValueError("invalid BEV range")
|
| 541 |
+
if args.funnel_margin < 0:
|
| 542 |
+
raise ValueError("--funnel-margin must be non-negative")
|
| 543 |
+
|
| 544 |
+
model_path = Path(args.model).expanduser().resolve()
|
| 545 |
+
video_path = Path(args.video).expanduser().resolve()
|
| 546 |
+
output_path = Path(args.output).expanduser().resolve()
|
| 547 |
+
polynomial_path = (
|
| 548 |
+
Path(args.polynomials).expanduser().resolve()
|
| 549 |
+
if args.polynomials else output_path.with_suffix(".lanes.jsonl")
|
| 550 |
+
)
|
| 551 |
+
summary_path = (
|
| 552 |
+
Path(args.summary).expanduser().resolve()
|
| 553 |
+
if args.summary else output_path.with_suffix(".json")
|
| 554 |
+
)
|
| 555 |
+
for path in (model_path, video_path):
|
| 556 |
+
if not path.is_file():
|
| 557 |
+
raise FileNotFoundError(path)
|
| 558 |
+
for path in (output_path, polynomial_path, summary_path):
|
| 559 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 560 |
+
|
| 561 |
+
calibration = CameraCalibration()
|
| 562 |
+
bev_range = BevRange(args.x_min, args.x_max, args.y_min, args.y_max)
|
| 563 |
+
cv2.setNumThreads(1)
|
| 564 |
+
configure_decode_threads(args.decode_cpu_threads)
|
| 565 |
+
session = create_session(
|
| 566 |
+
model_path, args.provider, args.allow_tf32, args.trt_cache_dir
|
| 567 |
+
)
|
| 568 |
+
warmup_decode_backend(args.decode_crawl_backend)
|
| 569 |
+
warmup = np.zeros((1, 3, MODEL_HEIGHT, MODEL_WIDTH), dtype=np.float32)
|
| 570 |
+
for _ in range(5):
|
| 571 |
+
session.run(list(OUTPUT_NAMES), {"images": warmup})
|
| 572 |
+
|
| 573 |
+
capture = cv2.VideoCapture(str(video_path))
|
| 574 |
+
if not capture.isOpened():
|
| 575 |
+
raise RuntimeError(f"cannot open video: {video_path}")
|
| 576 |
+
width = int(capture.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 577 |
+
height = int(capture.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
| 578 |
+
fps = float(capture.get(cv2.CAP_PROP_FPS) or 20.0)
|
| 579 |
+
source_frames = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 580 |
+
if (width, height) != (calibration.width, calibration.height):
|
| 581 |
+
capture.release()
|
| 582 |
+
raise ValueError(
|
| 583 |
+
f"calibration is {calibration.width}x{calibration.height}, "
|
| 584 |
+
f"video is {width}x{height}; crop/resize must be calibrated"
|
| 585 |
+
)
|
| 586 |
+
if args.start_frame >= source_frames:
|
| 587 |
+
capture.release()
|
| 588 |
+
raise ValueError(
|
| 589 |
+
f"--start-frame {args.start_frame} is outside {source_frames} frames"
|
| 590 |
+
)
|
| 591 |
+
if args.start_frame:
|
| 592 |
+
capture.set(cv2.CAP_PROP_POS_FRAMES, args.start_frame)
|
| 593 |
+
available_frames = source_frames - args.start_frame
|
| 594 |
+
target_frames = min(available_frames, args.max_frames or available_frames)
|
| 595 |
+
|
| 596 |
+
overall_started = time.perf_counter()
|
| 597 |
+
try:
|
| 598 |
+
results, timings, generation_seconds = generate_bev_results(
|
| 599 |
+
capture, target_frames, args.start_frame, session, args,
|
| 600 |
+
calibration, bev_range,
|
| 601 |
+
)
|
| 602 |
+
finally:
|
| 603 |
+
capture.release()
|
| 604 |
+
processed_frames = len(results)
|
| 605 |
+
result_generation_fps = processed_frames / generation_seconds
|
| 606 |
+
|
| 607 |
+
fit_counts = Counter()
|
| 608 |
+
funnel_clipped_lanes = Counter()
|
| 609 |
+
funnel_rejected_lanes = Counter()
|
| 610 |
+
lane_counts = []
|
| 611 |
+
for bundle in results:
|
| 612 |
+
valid_records = bundle["valid_records"]
|
| 613 |
+
funnel_report = bundle["funnel_report"]
|
| 614 |
+
for record in valid_records:
|
| 615 |
+
fit_counts[record["lane_id"]] += 1
|
| 616 |
+
funnel_clipped_lanes.update(
|
| 617 |
+
funnel_report["clipped_lanes"]
|
| 618 |
+
)
|
| 619 |
+
funnel_rejected_lanes.update(
|
| 620 |
+
funnel_report["rejected_lanes"]
|
| 621 |
+
)
|
| 622 |
+
lane_counts.append(len(bundle["lanes"]))
|
| 623 |
+
|
| 624 |
+
temporary_polynomials = polynomial_path.with_suffix(
|
| 625 |
+
polynomial_path.suffix + ".tmp"
|
| 626 |
+
)
|
| 627 |
+
with temporary_polynomials.open("w") as polynomial_file:
|
| 628 |
+
for bundle in results:
|
| 629 |
+
polynomial_file.write(json.dumps({
|
| 630 |
+
"frame_index": bundle["frame_index"],
|
| 631 |
+
"timestamp_seconds": bundle["frame_index"] / fps,
|
| 632 |
+
"coordinate_system": {"X": "forward_m", "Y": "left_m"},
|
| 633 |
+
"funnel_guard": bundle["funnel_report"],
|
| 634 |
+
"lanes": [
|
| 635 |
+
result["record"] for result in bundle["lane_results"]
|
| 636 |
+
],
|
| 637 |
+
}) + "\n")
|
| 638 |
+
os.replace(temporary_polynomials, polynomial_path)
|
| 639 |
+
|
| 640 |
+
# Rendering is a separate, untimed pass. Reopen the source video so none
|
| 641 |
+
# of these operations can affect the reported BEV-result throughput.
|
| 642 |
+
temporary_output = output_path.with_name(
|
| 643 |
+
output_path.stem + ".tmp" + output_path.suffix
|
| 644 |
+
)
|
| 645 |
+
render_capture = cv2.VideoCapture(str(video_path))
|
| 646 |
+
if not render_capture.isOpened():
|
| 647 |
+
raise RuntimeError(f"cannot reopen video for rendering: {video_path}")
|
| 648 |
+
if args.start_frame:
|
| 649 |
+
render_capture.set(cv2.CAP_PROP_POS_FRAMES, args.start_frame)
|
| 650 |
+
writer = cv2.VideoWriter(
|
| 651 |
+
str(temporary_output), cv2.VideoWriter_fourcc(*"mp4v"), fps,
|
| 652 |
+
(1920, 1080),
|
| 653 |
+
)
|
| 654 |
+
if not writer.isOpened():
|
| 655 |
+
render_capture.release()
|
| 656 |
+
raise RuntimeError(f"cannot open writer: {temporary_output}")
|
| 657 |
+
render_started = time.perf_counter()
|
| 658 |
+
try:
|
| 659 |
+
for position, bundle in enumerate(results, 1):
|
| 660 |
+
ok, frame = render_capture.read()
|
| 661 |
+
if not ok:
|
| 662 |
+
raise RuntimeError(
|
| 663 |
+
f"render pass stopped before frame {bundle['frame_index']}"
|
| 664 |
+
)
|
| 665 |
+
bev_canvas = draw_bev(
|
| 666 |
+
bundle["lane_results"], bev_range, calibration,
|
| 667 |
+
bundle["funnel_report"],
|
| 668 |
+
)
|
| 669 |
+
draw_predictions(frame, bundle["lanes"])
|
| 670 |
+
composite = make_composite(
|
| 671 |
+
frame, bev_canvas, bundle["frame_index"],
|
| 672 |
+
len(bundle["valid_records"]), bundle["compute_ms"],
|
| 673 |
+
bundle["funnel_report"],
|
| 674 |
+
result_generation_fps=result_generation_fps,
|
| 675 |
+
)
|
| 676 |
+
writer.write(composite)
|
| 677 |
+
if position % 100 == 0 or position == processed_frames:
|
| 678 |
+
print(
|
| 679 |
+
f"render {position}/{processed_frames} "
|
| 680 |
+
"(excluded from BEV-result FPS)",
|
| 681 |
+
flush=True,
|
| 682 |
+
)
|
| 683 |
+
finally:
|
| 684 |
+
render_capture.release()
|
| 685 |
+
writer.release()
|
| 686 |
+
render_seconds = time.perf_counter() - render_started
|
| 687 |
+
os.replace(temporary_output, output_path)
|
| 688 |
+
|
| 689 |
+
summary = {
|
| 690 |
+
"model": str(model_path),
|
| 691 |
+
"video": str(video_path),
|
| 692 |
+
"output_video": str(output_path),
|
| 693 |
+
"output_polynomials": str(polynomial_path),
|
| 694 |
+
"processed_frames": processed_frames,
|
| 695 |
+
"start_frame": args.start_frame,
|
| 696 |
+
"source_frames": source_frames,
|
| 697 |
+
"fps": fps,
|
| 698 |
+
"provider": args.provider,
|
| 699 |
+
"execution": {
|
| 700 |
+
"mode": "sequential_per_frame",
|
| 701 |
+
"result_generation_scope": (
|
| 702 |
+
"video read + normalize + inference + decode + metric "
|
| 703 |
+
"projection + cubic fit + camera-funnel clipping"
|
| 704 |
+
),
|
| 705 |
+
"excluded_from_result_fps": [
|
| 706 |
+
"draw_bev",
|
| 707 |
+
"draw_predictions",
|
| 708 |
+
"make_composite",
|
| 709 |
+
"video_encode_write",
|
| 710 |
+
"json_serialize_write",
|
| 711 |
+
],
|
| 712 |
+
"result_generation_wall_seconds": generation_seconds,
|
| 713 |
+
"result_generation_fps": result_generation_fps,
|
| 714 |
+
"render_wall_seconds": render_seconds,
|
| 715 |
+
},
|
| 716 |
+
"coordinate_system": {
|
| 717 |
+
"X": "forward from ego, metres",
|
| 718 |
+
"Y": "left of ego, metres",
|
| 719 |
+
"Z": "not exported; local road plane is Z=0",
|
| 720 |
+
},
|
| 721 |
+
"camera": {
|
| 722 |
+
"resolution": [calibration.width, calibration.height],
|
| 723 |
+
"horizontal_fov_deg": calibration.horizontal_fov_deg,
|
| 724 |
+
"intrinsic": calibration.intrinsic.tolist(),
|
| 725 |
+
"camera_to_vehicle": calibration.camera_to_vehicle_matrix.tolist(),
|
| 726 |
+
"distortion": None,
|
| 727 |
+
},
|
| 728 |
+
"bev_range_m": {
|
| 729 |
+
"X": [bev_range.x_min, bev_range.x_max],
|
| 730 |
+
"Y": [bev_range.y_min, bev_range.y_max],
|
| 731 |
+
},
|
| 732 |
+
"polynomial": {
|
| 733 |
+
"formula": "Y(X)=c0+c1*X+c2*X^2+c3*X^3",
|
| 734 |
+
"coefficient_order": ["c0", "c1", "c2", "c3"],
|
| 735 |
+
"max_accepted_rmse_m": args.max_cubic_rmse,
|
| 736 |
+
"fit_counts_by_lane": dict(sorted(fit_counts.items())),
|
| 737 |
+
},
|
| 738 |
+
"bev_projection": {
|
| 739 |
+
"mode": "raw_model_projection",
|
| 740 |
+
"parallel_assumption": False,
|
| 741 |
+
"synthetic_lanes": False,
|
| 742 |
+
"funnel_margin_m": args.funnel_margin,
|
| 743 |
+
"funnel_clipped_lane_fits": int(
|
| 744 |
+
sum(funnel_clipped_lanes.values())
|
| 745 |
+
),
|
| 746 |
+
"funnel_clipped_by_lane": dict(
|
| 747 |
+
sorted(funnel_clipped_lanes.items())
|
| 748 |
+
),
|
| 749 |
+
"funnel_rejected_lane_fits": int(
|
| 750 |
+
sum(funnel_rejected_lanes.values())
|
| 751 |
+
),
|
| 752 |
+
"funnel_rejected_by_lane": dict(
|
| 753 |
+
sorted(funnel_rejected_lanes.items())
|
| 754 |
+
),
|
| 755 |
+
},
|
| 756 |
+
"lane_count": {
|
| 757 |
+
"mean": float(np.mean(lane_counts)),
|
| 758 |
+
"min": int(min(lane_counts)),
|
| 759 |
+
"max": int(max(lane_counts)),
|
| 760 |
+
},
|
| 761 |
+
"timings": {
|
| 762 |
+
name: timing_summary(values) for name, values in timings.items()
|
| 763 |
+
},
|
| 764 |
+
"wall_time_seconds": time.perf_counter() - overall_started,
|
| 765 |
+
}
|
| 766 |
+
temporary_summary = summary_path.with_suffix(summary_path.suffix + ".tmp")
|
| 767 |
+
with temporary_summary.open("w") as handle:
|
| 768 |
+
json.dump(summary, handle, indent=2)
|
| 769 |
+
handle.write("\n")
|
| 770 |
+
os.replace(temporary_summary, summary_path)
|
| 771 |
+
print(f"video OK: {output_path}")
|
| 772 |
+
print(f"polynomials OK: {polynomial_path}")
|
| 773 |
+
print(f"summary: {summary_path}")
|
| 774 |
+
print(
|
| 775 |
+
f"BEV-result throughput: {result_generation_fps:.2f} FPS "
|
| 776 |
+
f"({generation_seconds:.3f}s, render excluded)"
|
| 777 |
+
)
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
if __name__ == "__main__":
|
| 781 |
+
main()
|
test_video_onnx.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
"""Run an RCLane ONNX model on a video and render predicted lanes + timing.
|
| 2 |
|
| 3 |
-
The input video may already contain ground-truth annotations.
|
| 4 |
-
|
| 5 |
-
from colored GT points.
|
| 6 |
"""
|
| 7 |
|
| 8 |
import argparse
|
|
@@ -16,8 +16,13 @@ import cv2
|
|
| 16 |
import numpy as np
|
| 17 |
import onnxruntime as ort
|
| 18 |
|
| 19 |
-
from dataset import
|
| 20 |
-
from decode import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
MODEL_HEIGHT = 320
|
|
@@ -32,77 +37,183 @@ OUTPUT_NAMES = (
|
|
| 32 |
|
| 33 |
|
| 34 |
def softmax_foreground(logits):
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
-
def create_session(model_path, provider, allow_tf32):
|
| 41 |
available = ort.get_available_providers()
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
raise RuntimeError(
|
| 45 |
-
"
|
| 46 |
-
"onnxruntime-gpu build from requirements.txt"
|
| 47 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
providers = [
|
| 49 |
(
|
| 50 |
-
"
|
| 51 |
{
|
| 52 |
"device_id": "0",
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
},
|
| 58 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"CPUExecutionProvider",
|
| 60 |
]
|
| 61 |
else:
|
| 62 |
providers = ["CPUExecutionProvider"]
|
| 63 |
|
| 64 |
session = ort.InferenceSession(str(model_path), providers=providers)
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
raise RuntimeError(
|
| 67 |
-
f"
|
|
|
|
| 68 |
)
|
| 69 |
return session
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def draw_predictions(frame, lanes):
|
| 73 |
height, width = frame.shape[:2]
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
for index, lane in enumerate(lanes):
|
| 77 |
-
points =
|
| 78 |
if len(points) < 2:
|
| 79 |
continue
|
| 80 |
-
points[:, 0] *= sx
|
| 81 |
-
points[:, 1] *= sy
|
| 82 |
-
points[:, 0] = np.clip(points[:, 0], 0, width - 1)
|
| 83 |
-
points[:, 1] = np.clip(points[:, 1], 0, height - 1)
|
| 84 |
points = np.round(points).astype(np.int32)
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
near = points[int(np.argmax(points[:, 1]))]
|
| 89 |
lane_id = lane.lane_id if lane.lane_id is not None else index
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
cv2.putText(
|
| 93 |
frame, label, position, cv2.FONT_HERSHEY_SIMPLEX,
|
| 94 |
0.65, (0, 0, 0), 5, cv2.LINE_AA,
|
| 95 |
)
|
| 96 |
cv2.putText(
|
| 97 |
frame, label, position, cv2.FONT_HERSHEY_SIMPLEX,
|
| 98 |
-
0.65,
|
| 99 |
)
|
| 100 |
|
| 101 |
|
| 102 |
def draw_runtime(frame, provider, lane_count, infer_ms, decode_ms, pipeline_ms,
|
| 103 |
-
rolling_pipeline_ms, source_fps, input_has_gt
|
|
|
|
| 104 |
height, width = frame.shape[:2]
|
| 105 |
-
box_width, box_height = min(690, width - 20),
|
| 106 |
left, top = width - box_width - 10, 10
|
| 107 |
overlay = frame.copy()
|
| 108 |
cv2.rectangle(
|
|
@@ -113,8 +224,11 @@ def draw_runtime(frame, provider, lane_count, infer_ms, decode_ms, pipeline_ms,
|
|
| 113 |
model_fps = 1000.0 / max(infer_ms, 1e-9)
|
| 114 |
pipeline_fps = 1000.0 / max(rolling_pipeline_ms, 1e-9)
|
| 115 |
realtime = "YES" if pipeline_fps >= source_fps else "NO"
|
|
|
|
|
|
|
| 116 |
lines = (
|
| 117 |
f"RCLane e19 ONNX {provider.upper()} | lanes={lane_count}",
|
|
|
|
| 118 |
f"infer {infer_ms:6.1f} ms ({model_fps:5.1f} FPS)",
|
| 119 |
f"decode {decode_ms:6.1f} ms | pipeline {pipeline_ms:6.1f} ms",
|
| 120 |
f"rolling pipeline {pipeline_fps:5.1f} FPS | realtime@{source_fps:g}: {realtime}",
|
|
@@ -126,9 +240,10 @@ def draw_runtime(frame, provider, lane_count, infer_ms, decode_ms, pipeline_ms,
|
|
| 126 |
2, cv2.LINE_AA,
|
| 127 |
)
|
| 128 |
|
|
|
|
| 129 |
legend = (
|
| 130 |
-
"GT: colored dots |
|
| 131 |
-
if input_has_gt else
|
| 132 |
)
|
| 133 |
cv2.putText(
|
| 134 |
frame, legend, (16, height - 20), cv2.FONT_HERSHEY_SIMPLEX,
|
|
@@ -158,7 +273,11 @@ def parse_args():
|
|
| 158 |
parser.add_argument("--output", default="runs/video_test.mp4")
|
| 159 |
parser.add_argument("--summary", default=None,
|
| 160 |
help="timing JSON; defaults next to output video")
|
| 161 |
-
parser.add_argument(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
parser.add_argument("--allow-tf32", action="store_true",
|
| 163 |
help="faster CUDA math with slightly larger numerical drift")
|
| 164 |
parser.add_argument("--input-has-gt", action="store_true",
|
|
@@ -177,6 +296,11 @@ def parse_args():
|
|
| 177 |
"--max-ego-lanes", type=int, default=4,
|
| 178 |
help="post-process to at most N reliable lanes nearest the ego vehicle",
|
| 179 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
return parser.parse_args()
|
| 181 |
|
| 182 |
|
|
@@ -204,7 +328,12 @@ def main():
|
|
| 204 |
output_path.stem + ".tmp" + output_path.suffix
|
| 205 |
)
|
| 206 |
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
warmup = np.zeros((1, 3, MODEL_HEIGHT, MODEL_WIDTH), dtype=np.float32)
|
| 209 |
for _ in range(5):
|
| 210 |
session.run(list(OUTPUT_NAMES), {"images": warmup})
|
|
@@ -228,6 +357,9 @@ def main():
|
|
| 228 |
|
| 229 |
timings = {"preprocess": [], "inference": [], "decode": [], "pipeline": []}
|
| 230 |
lane_counts = []
|
|
|
|
|
|
|
|
|
|
| 231 |
rolling = deque(maxlen=30)
|
| 232 |
started = time.perf_counter()
|
| 233 |
frame_index = 0
|
|
@@ -239,8 +371,7 @@ def main():
|
|
| 239 |
pipeline_start = time.perf_counter()
|
| 240 |
|
| 241 |
stage = time.perf_counter()
|
| 242 |
-
images =
|
| 243 |
-
images = images.unsqueeze(0).numpy()
|
| 244 |
preprocess_ms = (time.perf_counter() - stage) * 1000
|
| 245 |
|
| 246 |
stage = time.perf_counter()
|
|
@@ -266,6 +397,7 @@ def main():
|
|
| 266 |
nms_max_lanes=args.decode_nms_max_lanes,
|
| 267 |
nms_scale=args.decode_nms_scale,
|
| 268 |
max_output_lanes=args.max_ego_lanes,
|
|
|
|
| 269 |
)
|
| 270 |
decode_ms = (time.perf_counter() - stage) * 1000
|
| 271 |
pipeline_ms = (time.perf_counter() - pipeline_start) * 1000
|
|
@@ -276,12 +408,16 @@ def main():
|
|
| 276 |
timings["pipeline"].append(pipeline_ms)
|
| 277 |
lane_counts.append(len(lanes))
|
| 278 |
rolling.append(pipeline_ms)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
draw_predictions(frame, lanes)
|
| 281 |
draw_runtime(
|
| 282 |
frame, args.provider, len(lanes), inference_ms, decode_ms,
|
| 283 |
pipeline_ms, float(np.median(rolling)), source_fps,
|
| 284 |
-
args.input_has_gt,
|
| 285 |
)
|
| 286 |
writer.write(frame)
|
| 287 |
frame_index += 1
|
|
@@ -313,6 +449,12 @@ def main():
|
|
| 313 |
"max_ego_lanes": args.max_ego_lanes,
|
| 314 |
"min_score_ratio": 0.5,
|
| 315 |
"balance_sides": True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
},
|
| 317 |
"resolution": [width, height],
|
| 318 |
"source_fps": source_fps,
|
|
@@ -326,6 +468,12 @@ def main():
|
|
| 326 |
"min": int(min(lane_counts)),
|
| 327 |
"max": int(max(lane_counts)),
|
| 328 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
}
|
| 330 |
temporary_summary = summary_path.with_suffix(summary_path.suffix + ".tmp")
|
| 331 |
with temporary_summary.open("w") as handle:
|
|
|
|
| 1 |
"""Run an RCLane ONNX model on a video and render predicted lanes + timing.
|
| 2 |
|
| 3 |
+
The input video may already contain ground-truth annotations. Ego-lane boundaries
|
| 4 |
+
are highlighted in green/orange, other predictions in cyan, all with a black
|
| 5 |
+
outline so they remain distinguishable from colored GT points.
|
| 6 |
"""
|
| 7 |
|
| 8 |
import argparse
|
|
|
|
| 16 |
import numpy as np
|
| 17 |
import onnxruntime as ort
|
| 18 |
|
| 19 |
+
from dataset import normalize_image_numpy
|
| 20 |
+
from decode import (
|
| 21 |
+
configure_decode_threads,
|
| 22 |
+
decode,
|
| 23 |
+
ego_lane_boundaries,
|
| 24 |
+
warmup_decode_backend,
|
| 25 |
+
)
|
| 26 |
|
| 27 |
|
| 28 |
MODEL_HEIGHT = 320
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
def softmax_foreground(logits):
|
| 40 |
+
# Binary softmax: exp(l1)/(exp(l0)+exp(l1)) = sigmoid(l1-l0).
|
| 41 |
+
# This halves exponent work and avoids allocating a two-channel temporary.
|
| 42 |
+
difference = logits[:, 0] - logits[:, 1]
|
| 43 |
+
with np.errstate(over="ignore"):
|
| 44 |
+
return 1.0 / (1.0 + np.exp(difference))
|
| 45 |
|
| 46 |
|
| 47 |
+
def create_session(model_path, provider, allow_tf32, trt_cache_dir=None):
|
| 48 |
available = ort.get_available_providers()
|
| 49 |
+
cuda_options = {
|
| 50 |
+
"device_id": "0",
|
| 51 |
+
"use_tf32": "1" if allow_tf32 else "0",
|
| 52 |
+
"cudnn_conv_algo_search": "HEURISTIC",
|
| 53 |
+
"cudnn_conv_use_max_workspace": "1",
|
| 54 |
+
"do_copy_in_default_stream": "1",
|
| 55 |
+
}
|
| 56 |
+
if provider == "tensorrt":
|
| 57 |
+
if "TensorrtExecutionProvider" not in available:
|
| 58 |
raise RuntimeError(
|
| 59 |
+
"TensorrtExecutionProvider is unavailable in ONNX Runtime"
|
|
|
|
| 60 |
)
|
| 61 |
+
try:
|
| 62 |
+
import tensorrt # noqa: F401 - preloads libnvinfer for ORT
|
| 63 |
+
except ImportError as exc:
|
| 64 |
+
raise RuntimeError(
|
| 65 |
+
"TensorRT provider requested but tensorrt-cu13 is not installed"
|
| 66 |
+
) from exc
|
| 67 |
+
cache_dir = Path(
|
| 68 |
+
trt_cache_dir or Path(model_path).resolve().parent / "trt_cache"
|
| 69 |
+
)
|
| 70 |
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
| 71 |
+
shape = f"images:1x3x{MODEL_HEIGHT}x{MODEL_WIDTH}"
|
| 72 |
providers = [
|
| 73 |
(
|
| 74 |
+
"TensorrtExecutionProvider",
|
| 75 |
{
|
| 76 |
"device_id": "0",
|
| 77 |
+
"trt_fp16_enable": "True",
|
| 78 |
+
"trt_engine_cache_enable": "True",
|
| 79 |
+
"trt_engine_cache_path": str(cache_dir),
|
| 80 |
+
"trt_timing_cache_enable": "True",
|
| 81 |
+
"trt_timing_cache_path": str(cache_dir),
|
| 82 |
+
"trt_force_timing_cache": "True",
|
| 83 |
+
"trt_builder_optimization_level": "3",
|
| 84 |
+
"trt_max_workspace_size": str(2 * 1024 ** 3),
|
| 85 |
+
"trt_min_subgraph_size": "1",
|
| 86 |
+
"trt_profile_min_shapes": shape,
|
| 87 |
+
"trt_profile_opt_shapes": shape,
|
| 88 |
+
"trt_profile_max_shapes": shape,
|
| 89 |
},
|
| 90 |
),
|
| 91 |
+
("CUDAExecutionProvider", cuda_options),
|
| 92 |
+
"CPUExecutionProvider",
|
| 93 |
+
]
|
| 94 |
+
elif provider == "cuda":
|
| 95 |
+
if "CUDAExecutionProvider" not in available:
|
| 96 |
+
raise RuntimeError(
|
| 97 |
+
"CUDAExecutionProvider is unavailable; install the CUDA 13 "
|
| 98 |
+
"onnxruntime-gpu build from requirements.txt"
|
| 99 |
+
)
|
| 100 |
+
providers = [
|
| 101 |
+
("CUDAExecutionProvider", cuda_options),
|
| 102 |
"CPUExecutionProvider",
|
| 103 |
]
|
| 104 |
else:
|
| 105 |
providers = ["CPUExecutionProvider"]
|
| 106 |
|
| 107 |
session = ort.InferenceSession(str(model_path), providers=providers)
|
| 108 |
+
expected_provider = {
|
| 109 |
+
"cuda": "CUDAExecutionProvider",
|
| 110 |
+
"tensorrt": "TensorrtExecutionProvider",
|
| 111 |
+
}.get(provider)
|
| 112 |
+
if expected_provider and session.get_providers()[0] != expected_provider:
|
| 113 |
raise RuntimeError(
|
| 114 |
+
f"{expected_provider} was requested but session uses "
|
| 115 |
+
f"{session.get_providers()}"
|
| 116 |
)
|
| 117 |
return session
|
| 118 |
|
| 119 |
|
| 120 |
+
def _lane_points_in_frame(lane, width, height):
|
| 121 |
+
points = lane.xy().copy()
|
| 122 |
+
if len(points) < 2:
|
| 123 |
+
return np.empty((0, 2), dtype=np.float32)
|
| 124 |
+
points[:, 0] *= width / MODEL_WIDTH
|
| 125 |
+
points[:, 1] *= height / MODEL_HEIGHT
|
| 126 |
+
points[:, 0] = np.clip(points[:, 0], 0, width - 1)
|
| 127 |
+
points[:, 1] = np.clip(points[:, 1], 0, height - 1)
|
| 128 |
+
return points
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _x_at_rows(points, rows):
|
| 132 |
+
order = np.argsort(points[:, 1])
|
| 133 |
+
ys = points[order, 1]
|
| 134 |
+
xs = points[order, 0]
|
| 135 |
+
unique_y, inverse = np.unique(ys, return_inverse=True)
|
| 136 |
+
x_sum = np.zeros(len(unique_y), dtype=np.float64)
|
| 137 |
+
count = np.zeros(len(unique_y), dtype=np.float64)
|
| 138 |
+
np.add.at(x_sum, inverse, xs)
|
| 139 |
+
np.add.at(count, inverse, 1)
|
| 140 |
+
return np.interp(rows, unique_y, x_sum / count)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def draw_ego_corridor(frame, ego_left, ego_right):
|
| 144 |
+
"""Shade the visible region bounded by the two current-lane boundaries."""
|
| 145 |
+
if ego_left is None or ego_right is None:
|
| 146 |
+
return
|
| 147 |
+
height, width = frame.shape[:2]
|
| 148 |
+
left = _lane_points_in_frame(ego_left, width, height)
|
| 149 |
+
right = _lane_points_in_frame(ego_right, width, height)
|
| 150 |
+
if len(left) < 2 or len(right) < 2:
|
| 151 |
+
return
|
| 152 |
+
y_start = max(float(left[:, 1].min()), float(right[:, 1].min()))
|
| 153 |
+
y_stop = min(float(left[:, 1].max()), float(right[:, 1].max()))
|
| 154 |
+
if y_stop - y_start < 8:
|
| 155 |
+
return
|
| 156 |
+
rows = np.linspace(y_start, y_stop, 64)
|
| 157 |
+
left_x = _x_at_rows(left, rows)
|
| 158 |
+
right_x = _x_at_rows(right, rows)
|
| 159 |
+
valid = right_x > left_x
|
| 160 |
+
if np.count_nonzero(valid) < 2:
|
| 161 |
+
return
|
| 162 |
+
rows = rows[valid]
|
| 163 |
+
left_edge = np.column_stack((left_x[valid], rows))
|
| 164 |
+
right_edge = np.column_stack((right_x[valid], rows))[::-1]
|
| 165 |
+
polygon = np.round(np.vstack((left_edge, right_edge))).astype(np.int32)
|
| 166 |
+
overlay = frame.copy()
|
| 167 |
+
cv2.fillPoly(overlay, [polygon], (40, 140, 40), cv2.LINE_AA)
|
| 168 |
+
cv2.addWeighted(overlay, 0.18, frame, 0.82, 0, frame)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
def draw_predictions(frame, lanes):
|
| 172 |
height, width = frame.shape[:2]
|
| 173 |
+
ego_left, ego_right = ego_lane_boundaries(lanes)
|
| 174 |
+
draw_ego_corridor(frame, ego_left, ego_right)
|
| 175 |
+
|
| 176 |
for index, lane in enumerate(lanes):
|
| 177 |
+
points = _lane_points_in_frame(lane, width, height)
|
| 178 |
if len(points) < 2:
|
| 179 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
points = np.round(points).astype(np.int32)
|
| 181 |
+
role = getattr(lane, "lane_role", None)
|
| 182 |
+
if role == "ego_left":
|
| 183 |
+
color, role_text, thickness = (0, 255, 0), "EGO-L", 7
|
| 184 |
+
elif role == "ego_right":
|
| 185 |
+
color, role_text, thickness = (0, 165, 255), "EGO-R", 7
|
| 186 |
+
else:
|
| 187 |
+
color, role_text, thickness = (255, 255, 0), "", 5
|
| 188 |
+
cv2.polylines(
|
| 189 |
+
frame, [points], False, (0, 0, 0), thickness + 5, cv2.LINE_AA
|
| 190 |
+
)
|
| 191 |
+
cv2.polylines(frame, [points], False, color, thickness, cv2.LINE_AA)
|
| 192 |
|
| 193 |
near = points[int(np.argmax(points[:, 1]))]
|
| 194 |
lane_id = lane.lane_id if lane.lane_id is not None else index
|
| 195 |
+
role_label = f" {role_text}" if role_text else ""
|
| 196 |
+
label = f"P{lane_id}{role_label} {lane.score:.2f}"
|
| 197 |
+
text_size = cv2.getTextSize(
|
| 198 |
+
label, cv2.FONT_HERSHEY_SIMPLEX, 0.65, 2
|
| 199 |
+
)[0]
|
| 200 |
+
label_x = min(max(8, int(near[0]) + 8), width - text_size[0] - 8)
|
| 201 |
+
position = (label_x, max(32, int(near[1]) - 8))
|
| 202 |
cv2.putText(
|
| 203 |
frame, label, position, cv2.FONT_HERSHEY_SIMPLEX,
|
| 204 |
0.65, (0, 0, 0), 5, cv2.LINE_AA,
|
| 205 |
)
|
| 206 |
cv2.putText(
|
| 207 |
frame, label, position, cv2.FONT_HERSHEY_SIMPLEX,
|
| 208 |
+
0.65, color, 2, cv2.LINE_AA,
|
| 209 |
)
|
| 210 |
|
| 211 |
|
| 212 |
def draw_runtime(frame, provider, lane_count, infer_ms, decode_ms, pipeline_ms,
|
| 213 |
+
rolling_pipeline_ms, source_fps, input_has_gt, ego_left,
|
| 214 |
+
ego_right):
|
| 215 |
height, width = frame.shape[:2]
|
| 216 |
+
box_width, box_height = min(690, width - 20), 196
|
| 217 |
left, top = width - box_width - 10, 10
|
| 218 |
overlay = frame.copy()
|
| 219 |
cv2.rectangle(
|
|
|
|
| 224 |
model_fps = 1000.0 / max(infer_ms, 1e-9)
|
| 225 |
pipeline_fps = 1000.0 / max(rolling_pipeline_ms, 1e-9)
|
| 226 |
realtime = "YES" if pipeline_fps >= source_fps else "NO"
|
| 227 |
+
left_id = f"P{ego_left.lane_id}" if ego_left is not None else "missing"
|
| 228 |
+
right_id = f"P{ego_right.lane_id}" if ego_right is not None else "missing"
|
| 229 |
lines = (
|
| 230 |
f"RCLane e19 ONNX {provider.upper()} | lanes={lane_count}",
|
| 231 |
+
f"ego lane boundaries: {left_id} (L) | {right_id} (R)",
|
| 232 |
f"infer {infer_ms:6.1f} ms ({model_fps:5.1f} FPS)",
|
| 233 |
f"decode {decode_ms:6.1f} ms | pipeline {pipeline_ms:6.1f} ms",
|
| 234 |
f"rolling pipeline {pipeline_fps:5.1f} FPS | realtime@{source_fps:g}: {realtime}",
|
|
|
|
| 240 |
2, cv2.LINE_AA,
|
| 241 |
)
|
| 242 |
|
| 243 |
+
prediction_legend = "EGO-L: green | EGO-R: orange | other: cyan"
|
| 244 |
legend = (
|
| 245 |
+
f"GT: colored dots | {prediction_legend}"
|
| 246 |
+
if input_has_gt else prediction_legend
|
| 247 |
)
|
| 248 |
cv2.putText(
|
| 249 |
frame, legend, (16, height - 20), cv2.FONT_HERSHEY_SIMPLEX,
|
|
|
|
| 273 |
parser.add_argument("--output", default="runs/video_test.mp4")
|
| 274 |
parser.add_argument("--summary", default=None,
|
| 275 |
help="timing JSON; defaults next to output video")
|
| 276 |
+
parser.add_argument(
|
| 277 |
+
"--provider", choices=["tensorrt", "cuda", "cpu"],
|
| 278 |
+
default="tensorrt",
|
| 279 |
+
)
|
| 280 |
+
parser.add_argument("--trt-cache-dir", default="exports/trt_cache")
|
| 281 |
parser.add_argument("--allow-tf32", action="store_true",
|
| 282 |
help="faster CUDA math with slightly larger numerical drift")
|
| 283 |
parser.add_argument("--input-has-gt", action="store_true",
|
|
|
|
| 296 |
"--max-ego-lanes", type=int, default=4,
|
| 297 |
help="post-process to at most N reliable lanes nearest the ego vehicle",
|
| 298 |
)
|
| 299 |
+
parser.add_argument(
|
| 300 |
+
"--decode-crawl-backend", choices=("auto", "numba", "numpy"),
|
| 301 |
+
default="auto",
|
| 302 |
+
)
|
| 303 |
+
parser.add_argument("--decode-cpu-threads", type=int, default=8)
|
| 304 |
return parser.parse_args()
|
| 305 |
|
| 306 |
|
|
|
|
| 328 |
output_path.stem + ".tmp" + output_path.suffix
|
| 329 |
)
|
| 330 |
|
| 331 |
+
cv2.setNumThreads(1)
|
| 332 |
+
configure_decode_threads(args.decode_cpu_threads)
|
| 333 |
+
session = create_session(
|
| 334 |
+
model_path, args.provider, args.allow_tf32, args.trt_cache_dir
|
| 335 |
+
)
|
| 336 |
+
warmup_decode_backend(args.decode_crawl_backend)
|
| 337 |
warmup = np.zeros((1, 3, MODEL_HEIGHT, MODEL_WIDTH), dtype=np.float32)
|
| 338 |
for _ in range(5):
|
| 339 |
session.run(list(OUTPUT_NAMES), {"images": warmup})
|
|
|
|
| 357 |
|
| 358 |
timings = {"preprocess": [], "inference": [], "decode": [], "pipeline": []}
|
| 359 |
lane_counts = []
|
| 360 |
+
ego_left_frames = 0
|
| 361 |
+
ego_right_frames = 0
|
| 362 |
+
ego_pair_frames = 0
|
| 363 |
rolling = deque(maxlen=30)
|
| 364 |
started = time.perf_counter()
|
| 365 |
frame_index = 0
|
|
|
|
| 371 |
pipeline_start = time.perf_counter()
|
| 372 |
|
| 373 |
stage = time.perf_counter()
|
| 374 |
+
images = normalize_image_numpy(frame, MODEL_WIDTH, MODEL_HEIGHT)
|
|
|
|
| 375 |
preprocess_ms = (time.perf_counter() - stage) * 1000
|
| 376 |
|
| 377 |
stage = time.perf_counter()
|
|
|
|
| 397 |
nms_max_lanes=args.decode_nms_max_lanes,
|
| 398 |
nms_scale=args.decode_nms_scale,
|
| 399 |
max_output_lanes=args.max_ego_lanes,
|
| 400 |
+
crawl_backend=args.decode_crawl_backend,
|
| 401 |
)
|
| 402 |
decode_ms = (time.perf_counter() - stage) * 1000
|
| 403 |
pipeline_ms = (time.perf_counter() - pipeline_start) * 1000
|
|
|
|
| 408 |
timings["pipeline"].append(pipeline_ms)
|
| 409 |
lane_counts.append(len(lanes))
|
| 410 |
rolling.append(pipeline_ms)
|
| 411 |
+
ego_left, ego_right = ego_lane_boundaries(lanes)
|
| 412 |
+
ego_left_frames += int(ego_left is not None)
|
| 413 |
+
ego_right_frames += int(ego_right is not None)
|
| 414 |
+
ego_pair_frames += int(ego_left is not None and ego_right is not None)
|
| 415 |
|
| 416 |
draw_predictions(frame, lanes)
|
| 417 |
draw_runtime(
|
| 418 |
frame, args.provider, len(lanes), inference_ms, decode_ms,
|
| 419 |
pipeline_ms, float(np.median(rolling)), source_fps,
|
| 420 |
+
args.input_has_gt, ego_left, ego_right,
|
| 421 |
)
|
| 422 |
writer.write(frame)
|
| 423 |
frame_index += 1
|
|
|
|
| 449 |
"max_ego_lanes": args.max_ego_lanes,
|
| 450 |
"min_score_ratio": 0.5,
|
| 451 |
"balance_sides": True,
|
| 452 |
+
"lane_id_semantics": {
|
| 453 |
+
"P0": "next boundary left of ego lane",
|
| 454 |
+
"P1": "ego lane left boundary",
|
| 455 |
+
"P2": "ego lane right boundary",
|
| 456 |
+
"P3": "next boundary right of ego lane",
|
| 457 |
+
},
|
| 458 |
},
|
| 459 |
"resolution": [width, height],
|
| 460 |
"source_fps": source_fps,
|
|
|
|
| 468 |
"min": int(min(lane_counts)),
|
| 469 |
"max": int(max(lane_counts)),
|
| 470 |
},
|
| 471 |
+
"ego_lane_boundary_coverage": {
|
| 472 |
+
"left_frames": ego_left_frames,
|
| 473 |
+
"right_frames": ego_right_frames,
|
| 474 |
+
"pair_frames": ego_pair_frames,
|
| 475 |
+
"pair_rate": ego_pair_frames / frame_index,
|
| 476 |
+
},
|
| 477 |
}
|
| 478 |
temporary_summary = summary_path.with_suffix(summary_path.suffix + ".tmp")
|
| 479 |
with temporary_summary.open("w") as handle:
|