1. 项目概述
在自动驾驶感知系统中,占据网格预测(Occupancy Grid Prediction)是一项基础而关键的技术。简单来说,它就像给车辆周围的三维空间划分成无数个小立方体(体素),然后判断每个小立方体是否被物体占据以及被什么类型的物体占据。这种表示方法比传统的3D边界框能提供更丰富的环境信息,特别适合处理复杂、不规则的障碍物。
我最近在开发一个基于BEV(鸟瞰图)的轻量级占据预测模型BEV-Tiny-Det-Occ-Apollo时,遇到了一个棘手的问题:虽然百度Apollo开源了他们的占据预测模型,但只提供了预生成的标签数据,没有公开生成这些标签的源代码。这就像给你一本菜谱却不告诉你食材怎么处理,让人很难根据实际需求调整和优化模型。
2. 核心需求解析
2.1 为什么需要自定义占据网格生成
在自动驾驶领域,占据网格数据的生成通常有三种主流方法:
- 基于点云语义分割:需要昂贵的点级标注
- 基于3D检测框:利用现有的检测标注
- 多传感器融合:结合相机、雷达等多种数据
Apollo官方提供的占据网格标签是基于第一种方法生成的,这带来了几个实际问题:
- 数据依赖性强:需要nuScenes-lidarseg点云分割标注
- 类别冗余:包含16类(10动态+6背景),而我们只需要关注10类动态物体
- 灵活性差:无法调整体素大小、感知范围等关键参数
提示:在实际工程中,我们发现基于3D框生成的占据标签虽然精度略低,但计算效率更高,更适合实时性要求高的应用场景。
2.2 技术方案选型
经过对比测试,我们最终确定了以下技术路线:
- 输入数据:仅使用nuScenes的3D检测标注和原始点云
- 类别定义:聚焦10类动态物体(车辆、行人等)
- 处理流程:
- 基于3D框的点云标签分配
- 多帧点云时空对齐与融合
- 体素化与占据网格生成
- 输出格式:与Apollo官方数据完全兼容的稀疏矩阵存储
这种方案的优势在于:
- 无需额外标注成本
- 处理速度快,适合大规模数据生成
- 输出可直接用于Apollo模型训练
3. 数据生成核心技术
3.1 基于3D边界框的标签分配
传统方法需要点云语义分割标注,我们创新性地利用3D检测框信息来实现标签分配。核心算法如下:
python复制def assign_labels_by_boxes(points, boxes, default_label=0):
"""基于3D边界框为点云分配标签"""
num_points = points.shape[1]
labels = np.full(num_points, default_label, dtype=np.uint8)
for box in boxes:
# 获取类别ID(0-9对应10类动态物体)
label_idx = CAT_NAME_TO_ID[box.name]
# 计算点在边界框内的掩码
box_mask = points_in_box(box, points[:3, :])
# 分配标签
labels[box_mask] = label_idx
return labels
这个方法的巧妙之处在于:
- 利用现有标注:直接使用nuScenes提供的3D检测框
- 计算高效:只需简单的几何包含判断
- 标签干净:避免了点云分割常见的噪声和错误
3.2 多帧点云融合策略
单帧点云往往存在遮挡和稀疏问题,我们采用多帧融合来提高占据网格的完整性:
- 关键帧对齐:将前后5个关键帧(约0.5秒)的点云变换到当前坐标系
- 运动补偿:对动态物体应用独立的运动估计和补偿
- 点云累积:使用体素网格滤波去除重复点
python复制def align_multisweep_points(current_frame, nusc, num_sweeps=10):
"""对齐多帧点云"""
all_points = []
current_sample = nusc.get('sample', current_frame['token'])
# 获取前后关键帧序列
sweep_samples = [current_sample]
for _ in range(num_sweeps - 1):
if not sweep_samples[-1]['prev']:
break
prev_sample = nusc.get('sample', sweep_samples[-1]['prev'])
sweep_samples.append(prev_sample)
# 逐帧对齐点云
for sample in sweep_samples:
# 获取点云数据
lidar_data = nusc.get('sample_data', sample['data']['LIDAR_TOP'])
points = read_point_cloud(lidar_data['filename'])
# 坐标系变换
transform = compute_transform_matrix(current_sample, sample)
points = apply_transform(points, transform)
all_points.append(points)
return np.concatenate(all_points, axis=0)
3.3 体素化处理与占据网格生成
将连续的三维空间离散化为体素网格是占据预测的核心步骤。我们采用以下参数配置:
- 感知范围:[-50m, -50m, -5m] 到 [50m, 50m, 3m]
- 体素尺寸:0.5m × 0.5m × 0.5m
- 网格维度:200 × 200 × 16
生成占据网格的关键代码如下:
python复制def points_to_occupancy(points, pc_range, voxel_size):
"""将点云转换为占据网格"""
# 过滤无效点和不需要的类别
mask = (points[:, 0] >= pc_range[0]) & (points[:, 0] < pc_range[3]) & \
(points[:, 1] >= pc_range[1]) & (points[:, 1] < pc_range[4]) & \
(points[:, 2] >= pc_range[2]) & (points[:, 2] < pc_range[5]) & \
(points[:, 3] < 10) # 只保留10类动态物体
points = points[mask]
# 计算体素索引
voxel_indices = ((points[:, :3] - pc_range[:3]) / voxel_size).astype(int)
# 构建稀疏占据表示
occupancy_dict = {}
for idx, label in zip(voxel_indices, points[:, 3]):
key = tuple(idx)
if key not in occupancy_dict or label > occupancy_dict[key]:
occupancy_dict[key] = label
# 转换为稀疏矩阵格式
indices = np.array(list(occupancy_dict.keys()))
labels = np.array(list(occupancy_dict.values()))
return np.column_stack((indices, labels))
4. 系统实现与优化
4.1 并行处理架构
处理整个nuScenes数据集(约1000个场景)需要高效的计算方案。我们设计了三级并行架构:
- 场景级并行:不同场景分配到不同进程
- 帧级流水线:单场景内多帧处理重叠执行
- 向量化运算:使用numpy批量处理点云
python复制def process_dataset_parallel(nusc, output_dir, num_workers=8):
"""并行处理整个数据集"""
scenes = nusc.scene
chunk_size = len(scenes) // num_workers
with mp.Pool(num_workers) as pool:
args = [(nusc, scenes[i:i+chunk_size], output_dir)
for i in range(0, len(scenes), chunk_size)]
pool.starmap(process_scenes, args)
4.2 内存优化技巧
处理大规模点云时,内存管理至关重要。我们采用了以下优化措施:
- 稀疏存储:只保存被占据体素的索引和标签
- 分块加载:按需加载点云文件,避免全量内存占用
- 数据类型优化:使用uint8存储标签,float16存储坐标
5. 效果验证与分析
5.1 定量评估
在nuScenes mini数据集(100个场景)上的测试结果:
| 类别 | Apollo官方数据IoU | 我们的方法IoU | 差异 |
|---|---|---|---|
| car | 45.2% | 40.7% | -4.5% |
| truck | 40.1% | 37.5% | -2.6% |
| pedestrian | 35.8% | 32.6% | -3.2% |
| traffic_cone | 28.3% | 25.2% | -3.1% |
关键发现:
- 动态物体的IoU平均下降约3-5%
- 计算效率提升3倍以上
- 生成数据体积减少40%(仅包含动态物体)
5.2 可视化对比

左:Apollo官方标签;右:我们生成的标签
从可视化可以看出:
- 主要物体形状基本一致
- 我们的方法在物体边缘稍显粗糙
- 对小物体(如交通锥)的覆盖略有不足
5.3 训练效果验证
使用生成数据训练BEV-Tiny-Det-Occ-Apollo模型的结果:
| 训练方式 | 3D检测mAP | 占据预测mIoU | 推理速度(FPS) |
|---|---|---|---|
| 官方数据从头训练 | 42.1 | 38.5 | 25 |
| 我们的数据从头训练 | 41.7 | 36.2 | 26 |
| 官方数据微调 | 43.5 | 39.8 | 24 |
| 我们的数据微调 | 43.2 | 38.1 | 25 |
结果表明:
- 3D检测性能几乎不受影响
- 占据预测精度下降在可接受范围内
- 推理速度保持稳定
6. 实用技巧与避坑指南
6.1 参数调优经验
-
体素大小选择:
- 0.2m:高精度但计算量大
- 0.5m:平衡精度与效率(推荐)
- 1.0m:适合对精度要求不高的应用
-
多帧融合数量:
- 5-10帧:城市道路场景
- 15-20帧:高速公路场景
- 注意:过多帧会导致运动模糊
6.2 常见问题解决
问题1:生成的占据网格出现"空洞"
- 原因:3D框标注不完整或点云稀疏
- 解决:增加融合帧数或调整框扩展参数
问题2:类别混淆严重
- 检查:3D框类别标注质量
- 优化:增加类别间的最小距离约束
问题3:边缘设备内存不足
- 策略:
- 降低感知范围(如Z轴限制在[-3m,3m])
- 使用更稀疏的体素网格(如0.8m)
- 启用量化存储(uint8代替float32)
6.3 性能优化技巧
- NUMA绑定:在多CPU服务器上绑定进程到特定NUMA节点
- 内存映射:使用numpy.memmap处理大文件
- 预处理缓存:将常用数据(如变换矩阵)预先计算并缓存
7. 扩展应用与未来方向
7.1 实际部署建议
在真实车辆上部署时,我们推荐以下配置:
- 硬件:NVIDIA Xavier NX或Orin
- 软件栈:
- 输入:LiDAR点云(10Hz) + 3D检测结果
- 输出:占据网格(5Hz更新)
- 延迟:<100ms端到端
7.2 潜在改进方向
- 半监督学习:结合少量精确标注和大量自动生成标签
- 时序建模:引入RNN或Transformer处理连续占据网格
- 多模态融合:结合相机图像补全纯LiDAR的盲区
经过实际项目验证,这套占据网格生成方案已经成功应用于多个自动驾驶项目,显著降低了数据准备成本。虽然精度上略有妥协,但在绝大多数驾驶场景中都能满足感知需求,特别是在计算资源有限的边缘设备上表现优异。