做感知算法这些年,我最近把一套基于图像2D检测的老方案,整体迁移到了BEV(鸟瞰图)视角下,同时接入激光雷达和毫米波雷达,做了一套多模态融合的3D目标检测,骨干和检测头最终落在了YOLOv8系上。很多人一听“自动驾驶 + 3D检测”就会先想到各种大算力大模型,但实际上对中小团队和实车落地来说,如何把一个轻量、成熟、部署链路短的检测骨架改造成BEV多模态检测器,才是更现实的问题。这篇主要讲改造思路、三个模态在BEV网格里的融合方法,以及训练和部署时容易翻车的细节,适合正在做感知方案选型、或者打算从2D检测转向3D检测的同学参考。
1. 为什么是“BEV视角+多模态”,而不是堆更多的激光雷达
先说结论:BEV视角解决的问题不是“看得更远”,而是“让感知结果直接变成人类地图视角下的坐标”,多模态解决的则是“任何单一传感器在真实道路上都存在盲区”这件事。两者叠加,不是为了炫技,而是为了让检测框在规划模块眼里更可信。
1.1 单模态各自的天花板
每个传感器单独拿出来都有明确短板。相机方案纹理信息丰富,车道线、交通灯、车型颜色全靠它,但深度估计天生是病态问题——同一个目标在图像里可能只占几十个像素,距离稍微远一点,深度误差就是几米甚至十几米。激光雷达的几何精度很高,点云直接在三维空间里分布,但它是稀疏的,远处目标可能只有几个点,而且没有颜色和语义信息,雨雾天气衰减又严重。毫米波雷达速度和测距都很稳,穿透性也好,但角度分辨率低,点很稀疏,杂波多,很难单独勾勒出目标的精确轮廓。
所以在实车场景里,真正可靠的方案一定是融合。不是让某个传感器单独背锅,而是让相机提供“是什么”,激光雷达提供“在哪里”,毫米波雷达提供“速度有多快”,最后统一到同一个坐标系里。
1.2 BEV视角究竟解决了哪一类问题
BEV本质上是一种坐标系的统一。在图像视角下,目标的大小随距离变化,远处的车可能只有几十像素宽,近处的卡车却撑满整张图;而在BEV(自车正上方向下看的俯视视角)里,一辆车无论出现在前方20米还是50米,它的物理尺寸在网格里基本是一致的。这就让检测头回归的长宽高不再受到“透视变形”的干扰,模型更容易学到稳定的几何先验。
另一个关键点是和下游任务对齐。规划控制模块需要的不是“图像里有个框”,而是“自车坐标系下前方35米有个障碍物,宽2米,朝向偏左”。BEV输出的边界框天然就是这个世界坐标表示,不需要再做坐标变换和几何转换,整个链路短了一大截。再加上自车运动、多帧时序信息都可以直接在同一张BEV图上叠加,后续做目标跟踪、轨迹预测也顺理成章。
1.3 YOLOv8在这个方案里到底承担什么角色
可能有人觉得奇怪:YOLOv8不是二维图像检测器吗,怎么跑来做BEV 3D检测?实际上我把YOLOv8当作一套可迁移的检测范式:C2f轻量骨干、SPPF结构、PAN特征融合、anchor-free解耦头。这些设计不是为了某个特定输入而存在的,它们本质上是对“特征提取、特征融合、稠密预测”的组合。把输入从三通道图像换成BEV特征图,输出从二维框换成三维属性,这套骨架依然成立。
我选择YOLOv8系还有一个很现实的原因:工程化成本低。团队里已经有大量二维检测的部署经验,对解析网络结构、算子优化、后处理加速都很熟。在这个基础上做BEV改造,比引入一套完全陌生的专用三维检测框架要快得多,而且后续换平台、做量化也会省很多事。
2. 把YOLOv8改造成BEV空间3D检测器的具体改造点
这里要明确一点:并不是在图像上用YOLOv8先检测二维框,再把二维框投射到BEV。那样做的误差太大了,因为图像深度不准,二维框中心在BEV里会被投射到完全错误的位置。正确的做法是,让YOLOv8的骨干直接在BEV特征图上计算,检测头直接输出三维属性。
2.1 原来的2D检测头为什么直接搬过来会出问题
YOLOv8原本的检测头输出的是:目标类别、中心点坐标、宽高、以及objness(或分类置信度)。这套输出的“几何语义”是图像平面内的像素坐标,到了BEV空间就完全不够用了。BEV 3D检测头需要输出的几何量至少有:x、y、z位置,长l、宽w、高h,朝向角theta,乃至速度vx、vy。换句话说,回归头的输出维度从7个左右暴增到12个以上,分类分支和回归分支都要重做。
朝向角是其中最麻烦的。在图像平面里,目标方向可能表现为“左转”或“右转”,但在BEV网格里,角度是连续值,而且存在周期性——0度和360度是同一个方向。如果直接用回归头输出角度差,模型在周期边界上会学得很痛苦。常见的做法是把角度分成若干个bin,比如12个bin,每个bin内再回归一次微小残差。
2.2 网络主干的形状改动
YOLOv8的骨干通常吃进去的是 [B, 3, H, W] 的图像,输出多个尺度的特征图。现在输入换成 BEV 特征 [B, C, H, W],其中C是输入模态特征通道数,H和W是BEV网格的行列数。主干里的卷积、C2f、BN这些结构都不需要动,只需要把第一层卷积的输入通道从3改成C。
如果希望保留图像域信息,也可以做成双分支:一个分支吃图像特征做语义感知,一个分支吃点云BEV做几何感知,在某个特征层进行融合。这个方案更稳,但推理显存和算力会上去不少。我第一版用的是直接把BEV特征输入给YOLOv8的简化路径,效果也够用,关键是先把链路跑通。
2.3 3D属性回归头与损失函数设计
我设计的检测头依然是anchor-free,但每个BEV网格预测多个目标。具体到代码层面,思路大概是:
# 伪代码,只为展示输出张量形状 # bev_feature: [B, C, H, W],网络经过骨干和neck之后的特征 cls_logits = head_cls(bev_feature) # [B, num_classes, H, W] box_reg = head_reg(bev_feature) # [B, num_anchor * 8, H, W] dir_logits = head_dir(bev_feature) # [B, num_anchor * 2, H, W] vel_reg = head_vel(bev_feature) # [B, num_anchor * 2, H, W]其中 box_reg 的8个通道分别是中心x、中心y、z、长l、宽w、高h、以及角度残差的两个分量。角度可以表示成 sin(theta)、cos(theta),或者用角度bin分类加残差回归。损失函数我用了三层组合:
- 分类损失用focal loss,缓解前景目标数量远小于背景格子的类别不平衡;
- 位置回归用SmoothL1或L1 loss,对离群点更鲁棒;
- 角度的分类用交叉熵,残差回归用L1 loss;
- 速度用L1 loss,同时加一个简单的mask,只有靠近真值中心的格子才计算速度损失。
实际做下来,分类损失和回归损失的权重大约在1:1.5左右比较稳,如果某个数据集里目标很稀疏,回归权重还要再加大一些。
2.4 关于为什么保留“YOLO系”而非专用三维检测结构
专用三维检测框架大多基于点云做稀疏卷积或者体素编码,性能很好,但工程化难度也高。稀疏卷积在普通推理引擎上往往需要额外写算子,嵌入式平台的支持很差。YOLOv8系的骨干全是密集卷积和常见算子,到处都能跑,量化也比较友好。
还有一个容易被忽略的点:团队里同学对YOLO源码的熟悉程度非常高。改成BEV后,不管是调anchor、改neck还是改head,都能在已有代码框架内完成,不需要重新学一套命名规则和抽象逻辑。真到实车调试的时候,这一点能省下大量时间。
3. 三路传感器特征是如何被塞进同一张BEV图的
这是整个方案的核心,也是最脏最累的活。每个传感器都有自己的坐标系统和数据结构,要让它们进同一张BEV图,需要分别做空间变换和特征编码。
3.1 相机图像到BEV:视锥与深度分布
图像是透视投影,每个像素对应的是三维空间中的一条射线,所以要先把图像特征从像素平面投射到三维视锥里。经典做法是给每个像素估计一个离散深度分布:网络预测一组深度概率,比如将 2m 到 50m 分成64个深度bin,每个像素的特征按深度概率散布到对应深度的空间位置,再根据相机内外参投影到自车坐标系。
这一步最大的坑是深度分布太“平滑”。如果网络对某个像素深度很不确定,它会把特征平均撒到一大片空间里,BEV图上就会有一层很重的“重影”,目标边界全是模糊的。我的经验是,在深度估计分支上直接加监督信号,用激光雷达点云反投影到图像生成深度真值,哪怕只对少量像素做监督,也能显著提高深度分布的峰值置信度。
图像特征散布到三维后,需要在高度维上压缩。方式可以是直接取最大值、平均值,也可以用一个小卷积把高度维压缩到一列。压缩之后,图像分支就得到一张和激光雷达分支大小一样的BEV特征图。
3.2 激光雷达到BEV:柱体编码
激光雷达点云是稀疏的三维点集合,每个点有xyz坐标,还有反射强度等信息。常见做法是把空间在x、y方向划分成网格,每个网格对应一个柱体(pillar),柱体内的点计算一系列统计特征,比如x、y、z的均值,反射率的均值,柱体内点的数量,最大高度,等等。这些统计特征拼成一个特征向量,填进网格对应的位置,再经过几个卷积层,就能得到一张密实的BEV特征图。
这里要注意的点是网格分辨率。网格太大会丢失小目标的空间精度,网格太小则大部分格子是空的,计算和内存都很浪费。我第一版用了0.4米分辨率,后来在远距离场景发现效果不如0.5米稳定,因为太细的格子在高动态点云下很容易产生断裂的检测框。最终我倾向动态调整视场范围,近处用高分辨率,远处用低分辨率,相当于做了一个简单的多尺度融合。
3.3 毫米波雷达如何参与融合
毫米波雷达在公开道路上非常重要的一个属性是径向速度,也就是目标相对雷达沿视线方向的速度。激光雷达和相机都很难直接给出可靠的速度,而毫米波说明书里经常说“多普勒测速很准”,可真到了融合阶段,速度信息并不会自动变得有用。
雷达点数量少,稀疏程度比激光雷达还高,直接作为额外通道往BEV特征上叠,很容易被其他两个模态“淹没”。我采用的方案是给每个雷达点生成一个小范围的高斯热图特征,把速度、幅度等信息撒到周围几个格子里,相当于给雷达目标一个“存在性先验”。这样即使雷达点很少,融合层也能看到它的信号,不至于完全被忽略。
对于速度信息,需要把雷达测量到的径向速度分解到自车坐标系下的vx、vy。因为雷达角度分辨率低,同一个径向速度可能对应不同的横向速度,所以这个分解有误差。不要指望雷达速度能直接当监督真值用,它更适合作为模型速度预测的辅助约束。
3.4 特征融合机制的选型
多模态融合大致可以分成早期融合、中期融合、后期融合。早期融合就是把三路传感器的原始特征直接在输入阶段拼起来;后期融合是每个传感器各自检测完之后再做结果融合;中期融合是让三路特征先在BEV图上对齐,再通过卷积层混合。
我最终用的是中期融合,因为它比早期融合更鲁棒,能容忍某一路传感器的特征质量波动,又比后期融合更能利用底层特征信息。具体做法是:
- 三个分支分别生成相同分辨率的BEV特征;
- 对三个特征做一次标准化,避免某一路数值范围过大;
- 在通道维度上拼接,过几个卷积层和简单的通道注意力;
- 输出融合后的BEV特征给YOLO系骨干继续计算。
另外还加了一个可学习的调节门控,让网络在白天、夜晚、雨雾等条件下,自动调整相机和激光雷达特征的权重比例。这个门控的实现就是一个全局池化加两层MLP,最后输出每个模态的缩放系数,实测对夜间漏检有改善。
3.5 标定与时间同步:多模态融合的前提
所有模态在空间上对齐的基础是外参标定。相机外参把图像像素投影到自车坐标,激光雷达到自车坐标的变换,毫米波雷达到自车坐标的变换,缺一个整个BEV图就是花的。时间同步同样重要:如果相机帧和激光雷达帧之间相差100ms,车速60km/h时对应约1.7米的位移,检测框会直接偏出去。
我这边处理两件事:一是所有传感器都要统一到同一时间基准,二是对动态目标做运动补偿。采集时记录每个传感器的时间戳,训练和推理时先查一份最近时间戳映射表,如果时间差超过20ms,就用自车位姿预测的增量做插值,把点云或图像特征补偿到目标时间点。这个问题在静态测试里看不出来,一旦上了实车就会变成“框和真实位置永远差一截”的诡异现象,而且很难排查。
4. 数据、真值与训练:决定模型能不能收敛的隐藏环节
很多做2D检测的同学习惯把重点放在网络结构上,但3D多模态检测有一半的难度在数据准备和真值编码。结构是公开的,每个人都能搭,数据和真值才是拉开差距的地方。
4.1 从原始传感器数据到训练样本
训练一条多模态BEV检测链路,首先要有同步好的传感器数据。每次采集得到的原始数据里,至少包含相机图像、激光雷达点云、毫米波雷达点、以及对应时刻的自车速度、加速度、转向角。这些数据必须严格按照时间戳去匹配,才能生成一组有效的训练样本。
数据清洗是我建议放在第一优先级的事情。别急着开始训练,先把采集到的样本按场景挑一遍:去掉大量无目标的空场景,保留不同距离、不同天气、不同光照、不同遮挡程度的样本。只用干净晴天的数据训练出来的模型,到晚上或者雨天基本会直接崩掉。我在实际项目中专门留了20%的夜间和雨雾样本,专门用来验证融合门控是否真的在起作用。
4.2 BEV真值栅格编码与正负样本分配
拿到三维标注框之后,需要把标注框编码成和网络输出对齐的形式。每个三维框可以投影到BEV网格上,找出它覆盖了哪些格子;只要某个格子的中心落在这个框的内部,这个格子就作为“中心候选”,再根据它和框中心的距离分配正负样本。
正负样本分配在BEV里比2D里更敏感。因为大部分格子是空的,如果全部参与损失计算,模型会被“没有目标”的梯度淹没。我的处理方式是把损失计算限制在一个以真值中心为中心的正方形区域内,半径大约是目标长宽的一半加2个格子,区域内做focal loss,区域外全部忽略。这个策略参考了热力图检测的思路,收敛速度比全图负样本快很多。
朝向角的真值也不能直接用弧度制。我把角度分成若干bin,比如12个方向,每个方向之间相隔30度。这样网络先做方向分类,再在类别内做残差回归,既保证了角度的周期性,又简化了回归问题。
4.3 数据增强策略
3D检测的数据增强比2D多一个“坐标一致性”要求。对图像做翻转,对应BEV框的x坐标、y坐标、朝向角都要跟着翻转;对点云做全局旋转,物体角度也必须同步旋转。如果在增强过程中破坏了坐标系一致性,再好的网络也学不到正确的几何关系。
我常用的增强包括:
- 全局旋转:在一定角度范围内随机旋转所有传感器数据;
- 全局平移:随机小幅度平移,模拟车辆位置抖动;
- 点云dropout:随机丢弃部分点云,模拟遮挡和远距离稀疏;
- 模态随机失效:随机把某一模态的特征置零,训练模型在单模态失效时还能靠其他模态兜底;
- 图像颜色扰动:只作用于相机分支,用来模拟光照变化。
最有效的是“模态随机失效”。一开始我训练出来的融合模型,在图像关闭后会突然漏掉大量目标,说明模型本质上还是只依赖图像。加入这个增强后,就算相机完全黑掉,模型也能靠点云和雷达维持基本检测能力。
4.4 训练收敛经验
训练多模态模型时,我强烈建议分阶段进行,不要一股脑全量训练。先冻结融合层和检测头,只让骨干在单模态或弱融合条件下学基础特征;然后解冻检测头,训练一段时间;最后再解开所有层,用低学习率做微调。
这种做法的原因是,多模态学习一开始具有很强的“模态坍缩”倾向——网络发现只用图像特征就能大致完成检测,就会忽略点云和雷达的贡献,后续想纠正就很难。分阶段训练相当于每一步都强制网络关注所有模态。学习率上,骨干用较小的基础学习率,检测头用较大的学习率,差距在5到10倍之间,我用AdamW优化器,初始学习率大约0.001,配合余弦衰减和前3个epoch的线性预热。
4.5 预训练与迁移
我最初尝试直接用图像预训练权重初始化BEV骨干,效果不稳定,原因是BEV输入特征分布和图像三通道差异太大了,骨干第一层卷积虽然通道数改过了,但统计量完全对不上。后来改用单模态BEV预训练的方式:先用激光雷达BEV特征单独训练一个简陋版检测器,把骨干训出基本的几何感知能力,再把它作为多模态模型的初始化。这样多模态训练的起点不再是随机状态,融合层能更早地开始学习跨模态关系,整体收敛速度和最终精度都明显更好。
5. 指标、后处理与部署:从论文效果到实车表现的鸿沟
网上很多Demo视频看起来很震撼,但真正到了工程化阶段,要考虑的是指标之外的东西:推理帧率、内存占用、连续帧的稳定性、以及能不能塞进现有的计算平台。
5.1 该看什么指标:mAP、NDS和“实际可用性”
公开自动驾驶排行榜上常用 3D mAP 和 NDS 这两类指标。3D mAP 主要反映检测框的位置和类别准确度,NDS 是更综合的分数,把位置精度、大小精度、朝向精度、速度精度、属性精度都融合进去。对于感知团队来说,这两个指标都很重要,但我更看重的是连续帧上的误报率。
一个模型在3000帧离线数据上mAP很高,但实车跑起来可能每隔几十秒就冒出一个幽灵框,这种场景完全没法用。我会在离线评估之外,刻意挑一些连续帧数据,跑完检测之后接入一个简单的轨迹匹配逻辑,统计“只在单帧出现、没有连续轨迹支撑”的检测框比例,这个比例才更接近真实体验。
5.2 后处理NMS在BEV空间怎么实现
3D目标的后处理和2D不太一样。3D框之间做NMS时,需要计算两个三维立方体的交并比,而不能只用图片上的二维框面积。此外,BEV框的朝向具有周期性,两个框角度差接近360度和接近0度都要视为朝向一致,否则NMS会把同一辆车重复输出成两个目标。
我用了两层后处理:第一层用分类置信度阈值先过滤掉大部分低置信度的候选框;第二层对每个类别单独执行3D NMS,IoU阈值设在0.5左右。为了加快速度,可以先把框投影到BEV平面,用二维旋转矩形IoU做近似NMS,再用一次三维IoU精确筛选。实车上这样做能节省不少时间,精度损失也基本可以忽略。
5.3 部署优化点
计算平台上的第一个瓶颈往往不是模型本身,而是点云预处理和自定义算子。相机图像可以直接缩放归一化,点云柱体编码则需要遍历原始点、做网格划分、统计特征,这部分如果不用高效实现,可能比网络推理还慢。我建议提前把点云柱体编码做成一个独立的预处理模块,并针对批量数据测试耗时;如果耗时过高,就考虑用GPU并行或者把视场范围缩小。
模型本身的优化重点在算子融合和量化。YOLO系结构里卷积和BN可以融合,检测头的sigmoid、exp等激活函数可以和前面算子合并。量化时优先对融合层使用16位精度,对普通卷积用8位精度,因为融合层在不同模态之间混合特征,动态范围差异大,8位量化掉点比较明显。
内存方面,BEV特征图通常比图像特征图更占显存,因为网格是高度方向压缩后的稠密特征。如果原始BEV尺寸是 0.5米分辨率,视场120米乘60米,就有240乘120个格子,每个格子通道数几百,这个量级在任何GPU上都不算小。部署时要对BEV视场尺寸做裁剪,比如车辆周围90米乘60米,超过这个范围的目标本就不太重要。
5.4 可视化调试平台
模型效果好不好,光看指标不够,一定要做可视化。我在项目里搭了一个最简单的BEV调试工具:把激光雷达点云投影到俯视图上,叠上图像投射得到的语义特征,再叠加预测框和真值框。每一帧可以暂停、可以切换显示某个模态单独的特征图。
这套可视化帮我发现了大量问题。比如图像和点云的时间差,在一帧一帧拨的时候特别容易看出来:点和图像里的同一辆车在BEV图上是错位的,错位方向和大小差不多就是时间差造成的位移。做融合项目,我建议第一步就做可视化,不要等模型训练完了才看,否则你会花大量时间在错误数据上反复调参。
6. 复盘:这次多模态BEV改造踩过的几个坑
最后聊几个实际操作中让人印象深刻的坑,每一个都花了我不少时间排查,希望你看完能少走点弯路。
6.1 坑一:相机深度分布输出过于平滑,导致BEV特征重影
第一版模型训练到后期,图像分支的深度分布变得非常“圆滑”,每个像素都把特征分摊到很多深度bin上。结果就是BEV图上目标周围有一大团模糊的特征,检测框偏大,位置抖动。我一开始以为加大特征通道数能解决,结果没用。后来在深度估计分支上额外加了深度监督:从激光雷达点云投影出稀疏深度真值,训练时用一个很小的权重去约束深度分布必须在这几个深度bin上形成尖峰。加了之后,重影问题基本消失,框也一下子稳了。
6.2 坑二:点云柱体特征稀疏导致小目标漏检
我在远距离场景里漏检特别多,尤其是行人这种小目标。排查之后发现,柱体编码在远距离时,很多柱体里只有一两个点,统计特征非常不稳定,网络很难从这个特征里识别出目标。后来我把柱体编码的输入特征里增加了“点云高度范围”和“局部点密度”这两个手工特征,同时在特征融合时把图像分支的高分辨率特征上采样到BEV图再叠加,小目标召回率明显上升。不要小看这些手工特征,在数据量不够大的时候它们非常有用。
6.3 坑三:毫米波雷达的速度信息与目标框速度对不上
一开始我把毫米波雷达检测到的径向速度直接当作目标速度监督去使用,结果训练时损失一直震荡,模型输出的速度忽大忽小。后来仔细分析才发现,径向速度是目标相对于雷达的视线方向速度,和目标在世界坐标系下的真实速度完全是两码事。如果一个目标横向移动,径向速度会很小,但真实横穿速度可能很大。正确的做法是结合目标的角度和自车运动,先做坐标分解,再作为监督或辅助约束。把这一点想清楚之后,速度分支的收敛才算正常。
6.4 坑四:图像和点云时间戳不同步,造成检测框整体偏移
这个问题最隐蔽。数据采集时相机和激光雷达的触发时刻会有几十毫秒的差异,静态场景完全看不出问题,车一开起来就露馅了。我曾在高速路段看到预测框比真实目标位置整体往后拖了半米,一开始还以为是模型回归问题,后来把同一帧图像和点云单独叠在一起才发现,点云里的车已经越过图像里的车半个车位。修复方案是在数据进入网络前,根据帧间位姿做运动补偿,把所有模态对齐到同一时刻。多模态融合项目里,传感器同步优先级应该排在模型架构之前,不然你会在一个错位的数据集上浪费大量时间。
6.5 坑五:骨干迁移后BN统计量不收敛
最后一个是训练稳定性问题。用预训练骨干初始化之后,我发现前几个epoch的loss下降很快,但训练到一半又开始剧烈波动,验证集指标忽高忽低。查来查去,发现是因为BEV输入分布和图像预训练时期的分布差异太大,BN层统计量在不断重算,频繁震荡。解决办法是在预训练阶段先把骨干的所有BN层冻结,只训练后面的检测头和融合层,等模型稳定后再解冻BN,并给BN的动量参数设得大一些,让统计量快速适应BEV特征。这个操作虽然简单,但对收敛稳定性的帮助非常明显。
这轮改造下来,我最大的体会是:多模态BEV检测的难点从来不在某一个单独环节,而在于所有环节之间的自洽。传感器标定、时间同步、真值编码、增强策略、训练顺序、部署优化,任何一环对不上,都会以诡异故障的形式出现在最终结果里。如果现在让我重新做一个类似的感知项目,我会先把数据同步和可视化调试做好,再去碰网络结构。