4、目标检测:基于深度学习的3D检测(PointPillars/VoxelNet)、传统方法(滑动窗口+特征匹配)
聊到3D目标检测,很多新手第一反应就是“上深度学习”。没错,现在PointPillars、VoxelNet这些模型确实很火,效果也好。但我想说一句:传统方法真的过时了吗?我在实际项目中遇到过好几次,算力受限或者数据量不够的时候,反而是滑动窗口+特征匹配这种“老办法”救了场。
这一章,咱们就把两种路子都捋一遍。你想想看,只有理解了传统方法的局限,你才能真正明白深度学习为什么能赢;反过来,知道深度学习的“坑”,你才会在资源紧张时想起传统方法这个备胎。
4.1 传统方法:滑动窗口 + 特征匹配
说白了,传统3D检测的思路跟2D图像上的滑动窗口差不多。只不过咱们是在3D空间里滑,而且特征不是像素,是点云的手工特征。
4.1.1 滑动窗口策略
嗯,这里要注意:3D空间的滑动窗口,计算量是指数级增长的。假设你在BEV(鸟瞰图)上做,窗口大小是1m×1m,步长0.5m,一个100m×100m的区域,你就要滑4万个窗口。如果再考虑高度方向……我建议你直接放弃。
所以实际工程中,我们一般会加一些先验知识来缩小搜索范围:
- 地面约束:先做地面分割,只在地面以上一定高度范围内搜索
- 尺寸先验:比如检测车辆,窗口尺寸固定为4m×2m×1.5m
- ROI过滤:只在高反射率区域或路沿附近滑动
4.1.2 手工特征设计
窗口滑完了,每个窗口里都是一堆点云。怎么判断里面有没有目标?这就需要手工特征了。常用的有这么几种:
| 特征名称 | 描述 | 适用场景 |
|---|---|---|
| 点云数量 | 窗口内点的总数 | 快速过滤空窗口 |
| 高度分布 | 点云在Z轴上的分布直方图 | 区分车辆与行人 |
| 反射强度统计 | 强度值的均值、方差 | 区分金属物体与植被 |
| 形状描述子 | 如VFH(视点特征直方图) | 精细分类 |
特征提取完,一般接一个SVM或者随机森林做分类。说白了,这就是一个“手工特征 + 浅层分类器”的pipeline。
4.1.3 传统方法的优缺点
- 优点:不需要GPU,不需要大量标注数据,可解释性强
- 缺点:召回率低,对遮挡和稀疏点云效果差,速度慢
4.2 深度学习方法:PointPillars
好,聊完传统方法,咱们来看看现在最主流的PointPillars。为什么先讲它?因为它在速度和精度之间取得了很好的平衡,工业界用得最多。
4.2.1 核心思想:把点云“压”成柱子
PointPillars的思路其实很粗暴:把3D点云在BEV平面上划分成一个个网格(柱子),每个柱子里的点云用PointNet提取特征,然后拼成一个伪图像,最后用2D检测头做检测。
为什么会这样设计?因为直接处理3D点云太慢了。VoxelNet用3D卷积,计算量爆炸。PointPillars把问题降维到2D,速度直接起飞。
4.2.2 网络结构
我习惯把PointPillars分成三个模块:
- Pillar Feature Net:每个柱子内的点云,用PointNet提取特征,输出一个C维的特征向量
- Backbone(2D CNN):把特征向量拼成伪图像(H×W×C),然后用2D卷积做特征提取
- Detection Head(SSD):用SSD的检测头输出3D边界框
代码实现上,Pillar Feature Net的核心就这几行:
# 伪代码:Pillar Feature Net
def pillar_feature_net(points, pillar_indices):
# points: [N, 4] (x, y, z, intensity)
# pillar_indices: [N] 每个点属于哪个柱子
# 1. 计算每个点相对于柱子中心的偏移
center = get_pillar_center(pillar_indices)
offset = points[:, :2] - center
# 2. 拼接特征:原始坐标 + 偏移 + 强度
features = torch.cat([points, offset], dim=1) # [N, 6]
# 3. 对每个柱子内的点做PointNet
pillar_features = scatter_max(features, pillar_indices) # [P, C]
return pillar_features
4.2.3 训练技巧
训练PointPillars时,有几个坑我踩过:
- 数据增强:一定要做全局旋转和翻转。我试过不做增强,验证集AP直接掉了5个点
- GT采样:把真实目标复制粘贴到场景中,增加正样本数量。这个技巧在KITTI上能提2-3个点
- 学习率:建议用余弦退火,初始学习率0.001,batch size 32
4.3 深度学习方法:VoxelNet
VoxelNet是PointPillars的前身,思路更直接:把点云划分成3D体素(voxel),然后用3D卷积处理。精度更高,但速度慢很多。
4.3.1 Voxel Feature Encoding(VFE)
每个体素内的点云,用VFE层提取特征。VFE的核心就是PointNet的变体:对每个点做MLP,然后做最大池化,得到体素级别的特征。
# 伪代码:VFE层
def VFE(voxel_points):
# voxel_points: [M, 4] 一个体素内的M个点
# 1. 逐点MLP
point_features = MLP(voxel_points) # [M, C]
# 2. 最大池化得到体素特征
voxel_feature = torch.max(point_features, dim=0) # [C]
# 3. 拼接逐点特征和体素特征
output = torch.cat([point_features, voxel_feature.expand(M, -1)], dim=1)
return output
4.3.2 3D卷积与RPN
VFE提取完特征后,得到一个4D张量(C×D×H×W)。然后接3D卷积层做下采样,最后用RPN(区域提议网络)输出检测结果。
这里有个问题:3D卷积的计算量是O(C×D×H×W×K³),K是卷积核大小。你想想看,如果体素分辨率是0.1m,一个100m×100m×4m的区域,D=40, H=1000, W=1000,这计算量……
4.4 两种方法的对比与选择
说了这么多,到底该用哪种?我个人的建议是这样的:
| 场景 | 推荐方法 | 理由 |
|---|---|---|
| 有GPU,追求精度 | VoxelNet | 3D卷积能捕捉更精细的几何结构 |
| 有GPU,追求实时性 | PointPillars | 速度比VoxelNet快10倍以上 |
| 无GPU,算力受限 | 传统方法 | 不需要深度学习框架,CPU就能跑 |
| 数据量少(<1000帧) | 传统方法 | 深度学习容易过拟合 |
嗯,这里还要补充一点:实际工程中,很多人会做“混合方案”。比如用传统方法做快速初筛,再用深度学习做精细检测。我在一个园区无人车项目里就这么干过,效果还不错。
最后说一句:不管你选哪种方法,数据预处理和标定永远是第一步。我见过太多人一上来就调模型,结果发现点云坐标系都没对齐——那真是白忙活。
公众号:蓝海资料掘金营,微信deep3321