LaserPerception v0.1.0 — PointPillars + TensorRT + ROS 2 with honest deployment benchmarks

LaserPerception v0.1.0 — PointPillars + TensorRT + ROS 2 with honest deployment benchmarks

I’ve released LaserPerception v0.1.0, an open-source LiDAR deployment project focused on reproducible PointPillars/TensorRT/ROS 2 inference rather than model training.

GitHub: GitHub - muhammadmahadazher/laserperception: Reproducible 3D LiDAR detection with TensorRT FP16, exact deterministic voxelization, and ROS 2. · GitHub
Release: Release LaserPerception v0.1.0 · muhammadmahadazher/laserperception · GitHub

Pipeline:

multi-sweep PointCloud2 → exact deterministic voxelization → TensorRT FP16 PointPillars → Detection3DArray → RViz/Foxglove

Representative W1 workload: current keyframe + 10 historical sweeps, 354,182 points, RTX 4060 Laptop GPU under WSL2.

  • 10 Hz: sustained cleanly, ~9.95 Hz output, zero measured drops
  • 15 Hz: not sustained, ~13.34 Hz useful output
  • 20 Hz: not sustained, ~10.83 Hz useful output

So the release deliberately does not claim a 20 Hz ROS pipeline.

A second result came from deterministic hard voxelization. I first tried the faster deterministic=False path, but it changed retained point subsets and produced observable detector differences, so I rejected it.

The replacement exact_fast path matched the official deterministic implementation:

  • 81/81 nuScenes mini_val samples bit-exact
  • 20/20 frozen detector samples with exact raw TensorRT outputs
  • no custom CUDA/C++
  • no TensorRT plugin

Representative hard-voxel layer measurements:

  • 238.9 ms → 1.76 ms
  • 261.9 ms → 1.92 ms

That’s roughly 136× at the hard-voxel layer only, not end-to-end.

I also posted the result to the original MMDetection3D voxelization discussion:

One thing I deliberately kept in the repo is the failed/corrected benchmark history: lighter scene-start workloads, rejected execution-boundary comparisons, the nondeterministic shortcut, and the failed 20 Hz target.

I’d be interested in how other ROS 2 perception teams report performance:

model runtime, callback latency, sensor-to-output loopback, or all three?

Cross-domain update: frozen PointPillars on KITTI Raw

I’ve completed a new cross-domain study for LaserPerception, this time taking the unchanged nuScenes-trained PointPillars deployment onto KITTI Raw without fine-tuning.

The main result surprised me:

Class H10 recall H5 recall
Car 0.242 0.727
Pedestrian 0.553 0.677

H10 means the current scan plus ten historical sweeps; H5 uses five. This is not evidence that “five sweeps is optimal.” H10→H5 changes temporal span, time_lag, accumulated point density, pillar population, and capacity pressure together, so I’m treating it as a compound ablation rather than assigning a single cause.

There were two useful failures before the final experiment as well.

First, my original KITTI pose oracle failed because I was comparing KITTI Raw synced OXTS against KITTI Odometry poses as if they were the same data product. They aren’t. I preserved that failure and rebuilt the oracle against the actual Raw-devkit semantics.

Second, KITTI exposed a deployment-contract problem: my deterministic voxelizer was allowed to retain 40,000 voxels, while the TensorRT engine profile only accepted 30,000. The first M6b run therefore stopped before network execution, with zero KITTI predictions. I built a separate 40k-profile engine from the byte-identical ONNX and re-proved TensorRT parity before allowing the evaluation to continue.

One preregistered hypothesis was that the 40k voxel cap itself was driving the poor H10 result. The data did not support that as the primary corpus-wide explanation. Overflow occurred on 68/428 H10 frames, but Car recall was ~0.231 on overflow frames versus ~0.245 on non-overflow frames.

A related preprocessing result was interesting: every discarded pillar was first touched by one of the three oldest sweeps. Nothing first touched by the current scan or history sweeps 1–7 was discarded under the deterministic first-occurrence ordering used here.

I wrote up the full experiment, including the stopped runs, engine remediation, reproducibility hashes, range results, annotation limitations, and real offline KITTI visualizations.

Technical note: Moving a Frozen nuScenes PointPillars Deployment to KITTI Raw

I’d especially be interested in how other ROS/perception teams define temporal history when moving a model between LiDARs with different acquisition rates. A fixed sweep count can represent a very different physical time window.

Final M6 update: a failed byte-exact ROS test turned into a representation-boundary result

I’ve now finished the KITTI Raw cross-domain validation work for LaserPerception, including the ROS 2 integration stage.

The original goal for that stage sounded simple: replay official KITTI Raw through PointCloud2 + time-aware TF + the live multi-sweep builder and require the resulting model-ready cloud to match the frozen offline reference byte-for-byte.

That test failed at the first frame containing history.

The difference was tiny—one float32-scale transform discrepancy—but I kept the failure rather than adding a tolerance and traced it through the full transform path.

The useful result was that the error was not coming from tf2.

KITTI’s serialized pose rotation was slightly non-orthonormal. ROS TF represents rotation as a unit quaternion, so converting that matrix into a legal TF rotation maps it onto an orthonormal SO(3) rotation. In the diagnostic ladder:

frozen matrix → WSL matrix → unit-quaternion projection → real tf2 → builder storage

the unit-quaternion representation produced the dominant float32 change, while real lookup_transform_full reproduced the projected rotation exactly at the float32 boundary.

The effect was also accumulation-dependent.

With one historical sweep, the projected path changed coordinate-bearing feature bytes but preserved the complete discrete voxel structure: same pillar keys, ordering, coors, num_points, and retained point membership.

At H10, enough tiny coordinate changes accumulated for six points in the diagnostic frame to cross voxel boundaries. That changed point membership, TensorRT outputs, and eventually produced 302 detections instead of the frozen offline result’s 301.

Because the original matrix literally cannot be transported unchanged through unit-quaternion TF, I preregistered a final protocol against an independently generated ROS-representable projected reference instead of weakening the original gate with a tolerance.

Final result:

  • 24/24 early/startup H10 targets byte-exact
  • 856/856 full H10/H5 corpus conditions byte-exact
  • 860/860 unique live ROS conditions byte-exact overall
  • 0 TF failures, rejected frames, or history resets
  • ten frozen detector sentinels passed the pre-existing parity-v2 semantic envelope
  • exported detections: 113 / 113
  • high-confidence matches: 81 / 81 in both directions
  • all continuous checks: 81 / 81
  • class mismatches: 0
  • continuous outliers: 0
  • final Detection3DArray conversion: 10 / 10

One other result I found useful: the projected and original offline model-ready hashes differ for all 856 conditions, but the point count is identical for 856/856—880,487,994 points on both sides. So the representation shift perturbed coordinates without changing the range-filter population anywhere in the frozen corpus.

The claim is deliberately narrow. This validates the recorded ROS 2 software path—PointCloud2 transport, timestamps, unit-quaternion TF, time-aware fixed-frame composition, live history selection, builder orchestration, and detector publication. It does not claim physical-LiDAR validation, real-time performance, or that ROS reproduces the original non-orthonormal matrix bytes.

Full write-up:

M6 technical note:

Final M6c result:

Repository:

I’d be interested in how other ROS perception teams handle this when validating a live TF pipeline against an offline reference whose source rotations are matrix-native or numerically off SO(3). Do you define the oracle before or after the TF-representable projection?