BEVFusion¶
BEVFusion is a camera-LiDAR 3D object detection model integrated under the detection3d task namespace. It combines a sparse-voxel LiDAR branch (hard voxelization + sparse 3D convolution encoder), a multiview image branch, a LiDAR-depth-guided DepthLSSTransform view transform, and a convolutional BEV fusion layer before the TransFusion detection head. The bundled experiments are LiDAR-only: they leave camera_network unset and skip the image branch entirely.
Summary¶
| Property | Value |
|---|---|
| Task | 3D object detection |
| Modality | LiDAR (camera-LiDAR supported by the model, no bundled experiment yet) |
| Input | Point cloud, optionally with synchronized multiview images |
| Output | 3D bounding boxes and class scores |
| Architecture | Sparse voxel encoder + multiview camera backbone/FPN + DepthLSS + fusion + TransFusion head |
| Datasets | T4Dataset, nuScenes |
Available Configurations¶
Experiment configs live under autoware_ml/configs/tasks/detection3d/bevfusion/<dataset>/ and are passed to the CLI as the path relative to tasks/. Every bundled leaf is LiDAR-only and sits under a bevfusion_lidar/ directory. Camera-LiDAR experiments will add a sibling directory that configures the camera branch and fuser.
| Leaf | Database | Weights |
|---|---|---|
nuscenes/bevfusion_lidar/lidar_<variant>_nuscenes_<range> |
nuScenes | Trained from scratch |
The leaf name spells out its variant: the voxel size, the backbone and neck, the batch size, the epoch count and the detection range, so the values live in one place, the config. The configs are layered so that each file owns one concern:
| File | Owns |
|---|---|
base.yaml |
Model architecture, voxel preprocessor, optimizer, trainer settings, and ONNX export modules |
<dataset>/base_<range>.yaml |
Point cloud range, evaluation ranges, proposal count, NMS and score thresholds, transforms |
<dataset>/bevfusion_lidar/base_lidar_<voxel>.yaml |
Voxel size, sparse encoder grid shapes, and head output stride |
<dataset>/bevfusion_lidar/lidar_<variant>.yaml |
Database selection, batch size, experiment name, epochs, and optional pretrained weights |
For T4Dataset the bundled configs stop at the bases t4dataset/base_120m.yaml and t4dataset/bevfusion_lidar/base_lidar_voxel0170_120m.yaml. They declare the classes the model trains and require the taxonomy that folds the labels of the database onto them, so a T4Dataset experiment lives in an external config root, see Configuration.
Training¶
The nuScenes config trains from scratch on the nuscenes/nuscenes_base database:
autoware-ml train --config-name detection3d/bevfusion/nuscenes/bevfusion_lidar/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m
For a pipeline validation run:
autoware-ml train \
--config-name detection3d/bevfusion/nuscenes/bevfusion_lidar/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m \
+trainer.fast_dev_run=true
Checkpoints are written to mlruns/<experiment_group_name>/<experiment_name>/<run_id>/artifacts/checkpoints/ as best.ckpt and last.ckpt. best.ckpt is the validation with the highest val/det3d/mAP, and early stopping watches the same metric. Resume an interrupted run with --resume-checkpoint pointing at last.ckpt.
Evaluation¶
autoware-ml test \
--config-name detection3d/bevfusion/nuscenes/bevfusion_lidar/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m \
--weights mlruns/bevfusion/NuScenesDataset-base-v1.0.0/nuscenes/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m/<run_id>/artifacts/checkpoints/best.ckpt
Evaluation runs on a single device by default so metrics are deterministic. Pass --use-config-devices to keep the trainer.devices from the config.
Deployment¶
autoware-ml deploy \
--config-name detection3d/bevfusion/nuscenes/bevfusion_lidar/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m \
--weights mlruns/bevfusion/NuScenesDataset-base-v1.0.0/nuscenes/lidar_voxel0075_second_secfpn_b16_30e_nuscenes_54m/<run_id>/artifacts/checkpoints/best.ckpt \
--release v1.0.0
The export produces the ONNX modules for autoware_universe/perception/autoware_bevfusion. LiDAR-only configurations export a single bevfusion_lidar.onnx main body restricted to the first three inputs below, and the runtime node consumes it unchanged. Camera-LiDAR models export two modules: bevfusion_image_backbone.onnx encodes raw uint8 multiview images (the training-time 1 / 255 normalization is baked into the graph) into image_feats, and bevfusion_camera_lidar.onnx is the main body consuming those features together with the LiDAR depth maps and precomputed bev_pool metadata. The camera-LiDAR main body is not consumed by the current runtime node: its inputs changed and the node has not been updated yet, see Runtime compatibility.
| Input Tensor | Description |
|---|---|
voxels |
Voxelized LiDAR features |
coors |
Voxel coordinates in (z, y, x) order, batch-free |
num_points_per_voxel |
Point count per voxel |
image_feats |
Image backbone features from the image backbone ONNX |
depth_maps |
Sparse LiDAR depth maps (num_cameras, 1, H, W) at the network image resolution, built by LiDARDepthSparseTransform |
geom_feats |
Precomputed BEV pooling coordinates |
kept |
Boolean mask for valid projected frustum points |
ranks |
Sorted BEV pooling ranks |
indices |
Sorting indices for pooled frustum features |
Every main-body module returns the runtime detection interface: bbox_pred with the raw regression channels (center, height, dim, rot, vel) per proposal, score with per-proposal confidences, and label_pred with per-proposal class labels. Metric-space decoding happens in the runtime node. The height channel is the z of the box gravity center, which is how autoware_bevfusion reads it.
Exports land in mlruns/<experiment_group_name>/<experiment_name>/<run_id>/artifacts/exports/ of a dedicated deploy run linked to the training run. Every module is stamped with its provenance and the --release version. Omit --release only for throwaway exports, which are stamped unversioned. TensorRT engine generation is disabled (deploy.tensorrt.enabled=false) because the runtime builds engines itself using the custom sparse-convolution and bev_pool plugins.
Runtime compatibility¶
Breaking change: camera-LiDAR main body inputs¶
The bevfusion_camera_lidar.onnx main body no longer takes points, lidar2image and img_aug_matrix. It takes depth_maps instead. Previously the graph projected the point cloud onto every camera itself. Now the sparse depth maps are built outside the graph by LiDARDepthSparseTransform (autoware_ml/transforms/camera_lidar/depth.py), both in the training pipeline and in the export sample. The kept input is now sized by num_frustum_points rather than num_kept.
| Main body input | Before (main) |
Now |
|---|---|---|
voxels, coors, num_points_per_voxel |
yes | yes |
image_feats |
yes | yes |
points |
yes | removed |
lidar2image |
yes | removed |
img_aug_matrix |
yes | removed |
depth_maps |
no | new |
geom_feats, kept, ranks, indices |
yes | yes |
autoware_bevfusion (lib/bevfusion_trt.cpp) still binds points, img_aug_matrix and lidar2image in fusion mode and does not build depth maps, so a camera-LiDAR model exported from this version does not load in the current node, and the camera-LiDAR ONNX published for the current node does not match this export. LiDAR-only exports are unaffected. No shipped experiment runs the camera branch yet, so nothing deployed is broken today, but the runtime must change before the first camera-LiDAR model from this repository ships.
A second behaviour change, outside the export ABI: a camera-only model (lidar_network: null) now feeds the image BEV straight into the detection head. The BEV backbone and neck (pts_backbone / pts_neck) live on the LiDAR branch and do not run without it, unlike upstream BEVFusion where every BEV map goes through them. A camera-only head therefore has to take the view-transform channels as in_channels. There is no camera-only export, so the runtime is not affected.
Planned runtime change (autoware_bevfusion)¶
The node has to build the depth maps that the graph used to build, with the same geometry it already uses for lidar2image and img_aug_matrix.
- Depth map kernel. Add a CUDA kernel in
lib/preprocess/preprocess_kernel.cuthat projects the sweep points (points_d_, the same buffer fed to voxelization) onto every camera and writes a(num_cameras, 1, roi_height, roi_width)float32 buffer, reproducingLiDARDepthSparseTransform.build_depth_maps. - Bindings. In
addCameraNetworkIO,addCameraProfileDims,setTensorAddressandconfigureTensorRTInputs, replacepoints,img_aug_matrixandlidar2imagewithdepth_maps(Dims{4, {num_cameras, 1, roi_height, roi_width}}, min = opt = max). Droppoints_d_from the engine inputs (it stays as the voxelization buffer) andimg_aug_matrix_d_from device memory, and keep the host-sideimg_aug_matrices_and thelidar2imagevalues for the kernel. Bindings are by name, so the new input order does not matter. - Pipeline. Allocate
depth_maps_d_(num_cameras * roi_height * roi_widthfloats) in fusion mode, clear it inclearDeviceMemory, and launch the kernel inpreProcessaftergenerateSweepPointsand beforeconfigureTensorRTInputs. No new ROS parameters are needed:num_cameras,roi_heightandroi_widthalready exist inml_package_bevfusion_camera_lidar.param.yaml. - ABI transition. Either gate the fusion bindings on the input names of the loaded network (
pointspresent means the old graph,depth_mapsthe new one) for one release, or bump the camera-LiDAR model package version and switch outright. The node README, the parameter schema andCHANGELOG.rstmust document the new input set. - Validation. Export a camera-LiDAR model from this repository, dump the
depth_mapsthe export sample used and compare them with the kernel output for the same frame and calibration (equal everywhere except duplicate-hit pixels), then comparebbox_pred/score/label_predbetween the PyTorch model and the TensorRT engine on a few frames.
Implementation¶
| Path | Description |
|---|---|
autoware_ml/models/detection3d/main_modules/bevfusion.py |
BEVFusion model wrapper |
autoware_ml/models/detection3d/main_modules/bevfusions/bevfusion_lidar.py |
LiDAR BEV network |
autoware_ml/models/detection3d/main_modules/bevfusions/bevfusion_camera.py |
Multiview image network |
autoware_ml/models/detection3d/main_modules/bevfusions/export_wrappers.py |
ONNX export wrappers |
autoware_ml/models/detection3d/view_transforms/depth_lss.py |
Multiview image-to-BEV transform |
autoware_ml/models/detection3d/main_modules/bevfusions/fuser.py |
Camera-LiDAR BEV fusion layer |
autoware_ml/models/detection3d/encoders/voxel.py |
Hard voxelization feature encoder |
autoware_ml/models/detection3d/encoders/sparse/sparse_encoder.py |
Sparse 3D convolution encoder |
autoware_ml/models/detection3d/backbones/second.py |
SECOND backbone |
autoware_ml/models/detection3d/necks/second_fpn.py |
SECONDFPN neck |
autoware_ml/models/detection3d/heads/transfusions/transfusion_head.py |
TransFusion detection head |
autoware_ml/models/common/backbones/resnet.py |
ResNet multiview image backbone |
autoware_ml/models/common/necks/lss_fpn.py |
Multiview image neck |
autoware_ml/models/detection3d/task_modules/ |
Shared assigners, costs, coders |
autoware_ml/datamodule/t4dataset/dataset.py |
T4Dataset records as a dataset |
autoware_ml/datamodule/nuscenes/dataset.py |
nuScenes dataset |
autoware_ml/datamodule/common/detection3d.py |
3D boxes of a record |
autoware_ml/transforms/camera/ |
Multiview image loading and augmentation |
autoware_ml/configs/datamodule/ |
Datamodule, dataset and transform configurations |
autoware_ml/preprocessing/detection3d/point_pillar_preprocessor.py |
Pillar preprocessing |
autoware_ml/configs/tasks/detection3d/bevfusion/ |
Experiment configurations |
Acknowledgment¶
The Autoware-ML BEVFusion implementation was ported from the official mmdetection3d project by OpenMMLab.
- Repository: https://github.com/open-mmlab/mmdetection3d
- License: Apache License 2.0
- Paper: Liu, Zhijian, et al. "BEVFusion: Multi-Task Multi-Sensor Fusion with Unified Bird's-Eye View Representation" ICRA, 2023.