Perception › ROS 2 implementation › Acronym glossary
Port the models, prove the system
How the Python BEVFusion + SimpleTrack implementation becomes a composed, testable ROS 2 graph; where segmentation and occupancy fit; and which tests distinguish a faster system from a system that is merely wrong faster. Start with the architecture and message reference.
Symbols used on this page are defined in the Nomenclature → metrics.
1 · Map every Python contract to an executable node
| Reference pipeline | ROS 2 equivalent | Why this seam exists |
|---|---|---|
sensor_source_nuscenes.py | nuscenes_replay_node | Stamped sensor-topic replay for integration, never a GT input to perception. |
contracts.SensorFrame, _frame_to_data_dict() | SensorFrame + frame_sync_node + calibration cache | One sweep-anchored, immutable snapshot with valid-view mask and pose. |
| ImageAug3D, Normalize, point loading | image_ingest_node + lidar_decoder_node | Fused image preprocessing; packet accumulation and point-time de-skew before GPU encode. |
| BEVFusion camera backbone, depth and BEVPool | camera_encoder_node | Independent CUDA stream and measured stage deadline. |
| Voxel encoder, sparse convolution | lidar_encoder_node | Parallel with camera, common metric BEV geometry. |
| ConvFuser and TransFusion/CenterHead | bev_fuser_node + det_head_node | Fallback to LiDAR-only branch; isolated box decoding and head version. |
| Detection/Detections, global conversion | Detection3D/Detections3D + box transform | Explicit axes, dimensions, yaw and frame timestamp. |
| TrackerBank, MOTModel, Kalman, hit manager | tracker_node + per-class ClassTracker | Rotated GIoU, two-stage Hungarian matching, birth/COASTED/death and covariance. |
| Localization and sinks | External EKF/TF + viz_node + health_node | Map/odom continuity, status and failures observable to GNC. |
pipeline.py loop | ROS graph + executor/QoS + latest-only buffer | Explicit bounded callbacks rather than serial in-memory calls. |
Python remains excellent for training, rapid exploration and official devkit evaluation. A Python bridge can validate bring-up. The composed C++/TensorRT graph buys bounded queueing, observable per-link failures, shared GPU context and concurrency; it must earn complexity through measurement rather than assumption.
2 · Define a numerical contract before enabling TensorRT
| Arrow | Tensor / spatial invariant |
|---|---|
| Ingest → camera encoder | Six normalized FP16 images, $[6,3,256,704]$ NCHW; camera intrinsics updated for crop/resize. |
| Decoder → LiDAR encoder | $[N,5]$ FP32 $(x,y,z,intensity,\Delta t)$; past-sweep $\Delta t$ sign must match training. |
| Frame sync → encoders | Per-view LiDAR-to-image and augmentation $4\times4$ matrices, valid camera mask, ego pose at sweep time. |
| Camera / LiDAR → fuser | $[1,80,180,180]$ and $[1,256,180,180]$ FP16 BEV respectively, identical frame and 0.6 m cell resolution. |
| Fuser → heads | $[1,512,180,180]$ FP16; ready CUDA event and same-process buffer owner. |
| Detection → tracker | At most 300 $(x,y,z,l,w,h,yaw,score,label)$ boxes; compare ego-frame and transformed map-frame conventions. |
The reference point-cloud range is $[-54,-54,-5,54,54,3]$ m; voxel size $[.075,.075,.2]$ m yields a $1440\times1440\times40$ grid, downsampled by 8 to $180\times180$ BEV. LSS depth samples $1$ to $60$ m by $.5$ m. Assert $grid/8=bev_{H,W}$ at startup. Misinterpreting $(z,y,x)$ voxel order, changing the point time sign, swapping length/width, or treating a box bottom as center produces plausible pictures and wrong trajectories.
- Graph-only: synthetic input, stub backend, one container, 11 model/health nodes, no unexpected frame drops.
- Preprocess parity: same nuScenes frame, Python vs ROS FP16 image and $[N,5]$ point tensors; compare with a defined absolute tolerance.
- Encoder parity: same input into both, compare camera and LiDAR BEV features by numerical errors and per-channel cosine similarity; identify depth/voxel indexing errors.
- Detection parity: freeze confidence/NMS and compare box count, center, yaw, size, class/score.
- Tracking parity: inject identical detections into both trackers for a complete scene, compare association and lifecycle/ID sequence.
- Only then performance: FP16/TensorRT, pinned uploads, preallocated buffers, stream overlap, CUDA graphs. Re-run quality after every optimization.
3 · Segmentation is three different engineering problems
| Task | Output / consumer | Status and trade-off |
|---|---|---|
| Per-camera 2D semantic | Image-space $[H,W]$ classes for sensor QA and lane cues | Optional node. Lifting into metric space needs depth, extrinsics and ground assumptions; IPM fails on slopes. |
| BEV map segmentation | Ego-centric $[K,H,W]$ probabilities for drivable area/costmap | Shared BEV feature head in seg_head_node; lower priority than detection. |
| 3D occupancy | $[X,Y,Z,C]$ voxel state for height-aware collision | Proposed/stub, not shipped. Valuable for overhangs; expensive memory and inference. |
The six BEV map channels are drivable area, pedestrian crossing, walkway, stop line, carpark and divider. They overlap: use per-channel sigmoids, not mutually exclusive softmax (a crosswalk is also drivable). BevGrid is channel-first: $idx=cHW+rW+col$, with forward $x$ mapping to column and left $y$ to row. Publish a multi-channel probability grid and a separate planner-compatible single-channel drivable OccupancyGrid. Near a free/occupied decision boundary represent unknown rather than assert free. Document whether occupancy values encode free=0 / occupied=100 / unknown=-1 and when a grid is stale.
If image segmentation is needed, reuse GPU images; publish compact per-view labels at a lower rate on a low-priority stream, and undistort → infer depth → transform → rasterize before calling it a BEV map. A 200×200×16 voxel grid with 18 FP16 class channels is about 23 MB per frame; keep that on the GPU and downsample only for debugging. Example planning budget: detection head ~15 ms high priority, BEV segmentation ~8 ms lower priority, optional occupancy ~25 ms and six-camera 2D head ~20 ms; enabling all may exceed the 100 ms period. Profile on hardware rather than assuming stream overlap makes compute free.
Evaluate BEV with per-class IoU $IoU_c=TP_c/(TP_c+FP_c+FN_c)$ (also range slices), image boundaries with boundary F1, occupancy using occupied-voxel/ray IoU. A mean dominated by drivable road surface says little about rare stop-line misses or ramps.
4 · Debug from the first violated contract
| Symptom | First checks | Likely root cause |
|---|---|---|
| No topic data | ros2 topic info --verbose; endpoint names/remaps, offered/requested QoS | Reliable observer of best-effort stream, volatile vs transient local mismatch, unrouted group. |
| Latency grows each frame | Queue depth, stamp age, drop count, retransmit and per-callback traces | Reliable large images/clouds, stale backlog, subscriber blocked by group. |
| Standalone works; container fails | Forwarded NodeOptions, callback groups, stream/workspace ownership | Intra-process disabled, serialization or GPU context race. |
| Detections consistently displaced | Signed ego-frame error, calibration matrices and timestamp, box-convention tests | Resize/intrinsics drift, voxel order, pose age or length/width swap. |
| Frame-rate fine, tail age awful | Capture-to-publish histograms (p99/p99.9), CUDA event and executor trace | Thread wakeup jitter, GPU thermal stall or blocked stream dependency. |
ros2 node list
ros2 topic info /lidar/points --verbose
ros2 topic echo /lidar/points --qos-reliability best_effort --once
ros2 component list
ros2 param dump /det_head
ros2 run perception_py latency_report
ros2 topic echo /perception/statusTrace callback dispatch and publish-to-callback delay with ros2_tracing/LTTng (or CARET to reconstruct chains); correlate stamps to CUDA/NVTX ranges using Nsight Systems. Look for unexpected memcpy between BEV stages, no camera/LiDAR kernel overlap, steady-state cudaMalloc, device-wide synchronization and unpinned uploads. Profiling within a callback alone misses executor waiting: include capture and end-to-end age.
5 · A test pyramid that can falsify the design
| Layer | Contract / evidence |
|---|---|
| Unit, on any laptop | Box-frame conversion, sweep pairing under dead camera, buffer ownership, GPU host-reference kernels, tracker association/ID lifetimes, detection matcher and MOTA accumulator. |
| Synthetic integration, CPU stub | Compose the nodes, publish deterministic frames, verify stamps, partial-view path, QoS compatibility, no unwarranted queue growth and exact evaluator totals. |
| Dataset parity, offline GPU | Frozen nuScenes scenes: per-stage tensor tolerances, box and ID parity; run official evaluation separately from online approximations. |
| Hardware stress + fault injection | Stop a camera, kill LiDAR, lose PTP, impose 1% network loss, restart planner, slow GPU and fill recorder disk; measure transition-to-safe-output. |
colcon build --symlink-install --cmake-args -DPERCEPTION_WITH_CUDA=OFF
colcon test --event-handlers console_direct+
colcon test-result --verbose
ros2 launch perception_bringup full_system.launch.py backend:=stub source:=syntheticA successful CPU-stub latency test proves plumbing, not the real BEVFusion latency or accuracy. Run thermal and sustained-load tests on the deployment SoC; require quality and latency together. Route rosbag recording to a separate low-priority process; record sensor stamps, raw packets, calibrated state, outputs, status, per-stage latency and evaluation results if GT exists. Do not record process-local CUDA pointer topics: they cannot be meaningfully replayed. Include negative tests for QoS mismatches and lifecycle restart, not just sunny-day scenes.
6 · Explain it at senior level: challenge drills
For every answer give a choice, the alternative you rejected, a failure mode and a measurement. Practice these aloud:
- Six cameras + LiDAR: justify QoS for each link; why is a reliable 6 MB sweep often worse than one lost sweep?
- Why does a multithreaded executor sometimes serialize an entire node? Show explicit callback groups, affinity and a service deadlock fix.
- Distinguish intra-process move, shared fan-out, loaned SHM and a GPU handle; prove each rather than labeling everything “zero copy.”
- Whiteboard the Python SensorFrame-to-BEVFusion-to-tracker mapping; where do temporal calibration and per-point LiDAR offsets come from?
- Why is GNC not in the GPU container? Which topics and freshness limits form its contract, and how does it stop if perception dies?
- Why does BEV map segmentation use sigmoids? When is 3D occupancy worth its 23 MB per frame?
- mAP improved, but pedestrian recall at 40 m got worse. Which number limits speed? How do you find a threshold vs sensor vs calibration failure?
- MOTA .71, recall .88, IDF1 field .90: which definitions are actually implemented? Does this dashboard compute sequence-level IDF1?
- A LiDAR-only fallback “works” on a sunny dataset; how do you test spray, low-return pedestrians and sensor common-mode loss?
7 · Source guide: the complete ROS 2 study set
The tutorial's pages map to all twelve source chapters. Continue at the section that answers your next question:
| Source chapter | Web tutorial treatment |
|---|---|
| 01 · Core concepts | Middleware/time/TF, services/params. |
| 02 · Executors and callback groups | Dispatch, groups, priority and deadlocks. |
| 03 · QoS trade-offs | Sensor-by-sensor matrix and compatibility. |
| 04 · Zero copy | Four mechanisms and fixed-capacity SHM. |
| 05 · Perception architecture | Graph, requirements, nodes, all message types. |
| 06 · Composition and containers | Single process, lifecycle and scheduling. |
| 07 · GNC integration | Control contract, frames and degraded response. |
| 08 · BEVFusion ROS 2 mapping | Python → nodes and tensor parity. |
| 09 · Segmentation and BEV | 2D, BEV, occupancy and their budget. |
| 10 · Testing, profiling, debugging | Decision tree and traces/CI/fault injection. |
| 11 · Online evaluation | Complete metric mathematics and runtime errors. |
| 99 · Interview drills | Trade-off and failure-mode questions with links throughout these pages. |