1. 为什么偏要在相机 BEV 体系里加一路毫米波雷达
做 BEV 3D 目标检测这几年,视觉方案几乎成了默认答案:环视相机便宜、图像语义丰富、bev 特征图一铺开,检测头接上去就能出框。我刚接触 RCBEVDet 这个方向时也犯嘀咕——雷达点云一帧就那么几百上千个点,角分辨率稀得可怜,看着像撒了一把胡椒在自车周围,凭什么能帮上忙。真正在车上跑过一段时间、经历过几次逆光和暴雨之后,我的看法彻底变了:不是雷达能帮上忙,而是在某些场景里,缺了它就没有保底方案。
RCBEVDet 是围绕毫米波雷达与环视相机的前融合 BEV 检测展开的一套方案,核心目标是把雷达的测距准、抗恶劣天气、带径向速度这几项能力,塞进原本纯视觉的 BEV 检测框架里,而且不是简单在检测框层面做后融合,而是在 BEV 特征层面就把两路信息揉到一起。它适合的人群很明确:已经跑通过一套纯视觉 BEV 检测(不管是 LSS 系还是 query 系),想要在雨雾、夜间、远距离这几个薄弱环节上补一刀的算法工程师;也适合刚入门 BEV 感知、想搞清楚“多模态到底怎么融”的同学当做拆解样本。如果你连 BEV 特征图怎么来的都还没搞明白,建议先把单模态跑通再来,不然踩坑时会分不清是融合的问题还是底座的问题。
1.1 纯视觉 BEV 绕不过去的三个物理限制
相机本质上是个被动的光子接收器,这句话说起来轻描淡写,落到实车上就是三条硬约束。
第一条是光照。逆光时前视相机对着太阳,画面里前车只剩一个剪影;夜间无路灯路段,曝光拉长之后运动模糊直接把行人糊成一团。这类场景下图像特征的质量是断崖式下跌的,不是靠数据增强能补回来的,因为你喂进去的像素里本来就没有信息。
第二条是散射介质。雨、雾、扬尘、前车卷起的水雾,都会让图像对比度整体塌陷。我印象最深的一次是傍晚小雨,雨刮刚刮过的那一帧还能看清车道线,下一帧又被水膜糊住,检测结果在两帧之间反复跳。这种抖动对下游规控来说是灾难。
第三条是深度估计的几何衰减。基于环视几何的 BEV 方案,深度精度随距离增长而下降,越远越糊。而 BEV 网格往往铺到 50 米甚至更远,远端的检测框位置误差会被放大。雷达不一样,它靠飞行时间直接测距,测距误差跟距离基本无关。
1.2 雷达的“糙”里藏着两样相机给不了的东西
很多人嫌弃雷达点云稀疏,这个判断只对了一半。稀疏是事实,但雷达点里有两个通道是图像根本产生不了的。
| 信息维度 | 相机 | 毫米波雷达 | 对检测的价值 |
|---|---|---|---|
| 稠密语义/纹理 | 强 | 无 | 分类、区分车型与行人 |
| 直接径向测距 | 间接(三角化) | 直接(飞行时间) | 远距离定位精度 |
| 径向速度(多普勒) | 需多帧估计 | 单帧直接输出 | 区分静止物与运动目标 |
| RCS 反射强度 | 无 | 有 | 材质/尺寸的粗线索 |
| 雨雾鲁棒性 | 差 | 好 | 恶劣天气保底 |
| 高度信息 | 有 | 多数量产型号缺失 | 需靠图像补 |
径向速度这一项的价值经常被低估。一个在高速上静止的故障车,视觉上看它和慢速行驶的车在单帧里几乎没有区别,必须靠多帧关联才能分辨;雷达单帧就能告诉你“这个目标的多普勒是 0”,直接把它标成静止障碍物。RCS则是个粗糙但有效的材质探头,金属车身的反射截面和行人的反射截面量级差得很远,在图像过曝或欠曝时能当个兜底判据。
代价也要说清楚:量产毫米波雷达多数没有可靠的高度输出,点云基本压在一个水平面上;角度分辨率差,相邻两个目标容易糊成一个;多径反射会造出“幽灵目标”,隧道壁、护栏、金属井盖都是重灾区。RCBEVDet 这类方案的设计难点,一大半都花在怎么把这种“高置信度但低分辨率”的信号,和“高分辨率但受环境影响大”的图像信号对齐到同一张 BEV 图上。
1.3 RCBEVDet 的整体数据流长什么样
把整条链路摊开看,它其实分三条支路再汇合:
- 图像支路:多路环视图像进 2D backbone 提特征,再经视角转换模块(LSS 式的深度分布加权,或者 query 式的 BEV 采样)压到统一的 BEV 网格上,得到图像 BEV 特征。
- 雷达支路:原始点云先做逐点特征提取,这一步会拿图像特征做引导;然后把散点 scatter 到 BEV 网格,过一个 BEV 编码器,得到雷达 BEV 特征。
- 融合支路:两路 BEV 特征在 backbone 的多个层级上做跨注意力融合,融合后的特征送进检测头出 3D 框。
注意这里有个关键设计:融合不是只在最后一层做一次。浅层特征分辨率高、位置信息准,深层特征语义强、感受野大,只在深层融合会丢掉小目标的定位精度,只在浅层融合又拿不到足够的语义来做分类。多层融合是这套方案能同时兼顾远近目标的原因。
2. 把稀疏点云变成 BEV 特征图:雷达编码器的取舍
这一块是整套方案里我认为最值得细看的部分。直接把点云 voxel 化再套一个 3D 卷积,在小规模雷达数据上是跑不出东西的——点太少了,卷出来的特征图大片空白,卷积核扫过去全是零。
2.1 为什么非要用图像特征去“指导”点云编码
单看一个雷达点,它携带的信息是:x、y、z(多数情况 z 不可靠)、径向速度、RCS、时间戳。满打满算五六个有效维度。光靠几层 MLP 从这几维里学出“这是轿车还是卡车”的判别特征,理论上可行、实际上很难收敛,因为同一种车型的 RCS 分布方差很大,不同车型的分布又会重叠。
RCBEVDet 的做法是把点投影到图像平面上,在对应的像素位置双线性采样图像特征,拼到点特征后面。打个比方,这相当于给每个雷达点贴一张“它落在画面哪个位置、那里的纹理长什么样”的小纸条。贴完之后,点特征的维度从个位数涨到几百维,语义信息一下就丰富了。
这个操作看起来简单,实操里三个地方最容易翻车:
- 投影越界。BEV 范围铺到 50 米,但相机 FOV 覆盖不了那么宽,大量雷达点会投到图像边界外。我的处理是把越界点的图像特征置零,同时加一个 valid mask 通道告诉网络“这个点的图像信息不可信”。不加 mask 的话网络会把零向量当成一种真实特征去学,反而污染。
- 采样位置精度。双线性采样默认像素坐标系是连续的,但标定参数里如果内参有半个像素的偏差,采到的特征就会整体偏移。这个误差在近处不明显,远处会放大。
- 通道维度对齐。图像 backbone 不同 stage 的输出通道数不同,选哪一层去采样直接影响效果。用太浅的层,语义不够;用太深的层,分辨率又太低,一个雷达点采到的可能是一整块区域的均值。我的经验是用中间层,分辨率降采样 8 倍那一档。
2.2 RCS 和径向速度这两个通道的预处理
这两列数据的处理细节,文档里一般不会写,但处理不好会直接拖垮收敛。
RCS 的动态范围极大。金属车身的 RCS 可能到几十 dBsm,行人只有个位数甚至负值。直接喂原值,网络会被少数大值主导。标准做法是先截断到某个区间(比如 -20 到 40 dBsm),再做线性归一化或者 log 压缩。截断阈值要根据你手上雷达型号的实际统计分布来定,别照抄别人的数字——不同厂家的增益标定差别很大。我一般会先跑一遍全量数据的直方图,看 1% 和 99% 分位数落在哪,据此定截断边界。
径向速度的坐标系转换是最容易出错的地方。雷达输出的是沿视线方向的相对速度,转到自车坐标系需要按方位角做投影分解,得到 vx 和 vy。这里有两个陷阱:一是符号约定,多普勒的正负在不同厂家定义相反;二是自车运动补偿,如果你的雷达输出没有扣掉自车速度,那么所有静止物体都会带一个和车速等大的反向速度,模型会把它学成“所有背景都在动”。我在这上面浪费过整整两天,最后发现是自车信号没接进去。
2.3 scatter 到 BEV 网格时那几个不显眼的选择
散点转规则网格这一步,代码量不大,但参数选择影响很大。
网格分辨率的选择是个平衡:0.4 米一格,点云细节保留得多,但每个格子里平均不到一个点,特征图极其稀疏;0.8 米一格,稀疏性缓解了,但两个相邻目标容易落进同一格。RCBEVDet 这类方案通常选在和图像 BEV 网格一致的尺度上,因为后面要融合,分辨率不一致就得插值,插值本身又会引入位置误差。
多点落入同一格子怎么聚合也有讲究。max pooling 能保留最强的反射,但会丢掉速度信息——同一个格子里一辆车和一个护栏,max 之后只剩护栏的强反射,车就消失了。我倾向于分通道做不同的聚合:RCS 用 max,速度用 mean,同时额外保留“格内点数”作为一个通道,让网络自己判断这个格子可不可信。
提示:BEV 网格的原点偏移和 y 轴朝向,雷达支路和图像支路必须完全一致。这个错位是最隐蔽的 bug,因为两路特征各自看起来都正常,只有融合之后才会表现出“框整体偏移一个固定距离”。验证方法很简单:把两路 BEV 特征各自降维到单通道可视化出来,叠在一起看车道线之类的静态结构是否重合。
# 散点聚合伪代码,重点在分通道聚合与有效点计数 import torch def scatter_to_bev(points, feats, bev_range=(-51.2, 51.2), res=0.4): # points: [N, 3] (x, y, z) 自车坐标系 # feats: [N, C] 其中 C 包含 rcs, vx, vy 等 grid = int((bev_range[1] - bev_range[0]) / res) ix = ((points[:, 0] - bev_range[0]) / res).long().clamp(0, grid - 1) iy = ((points[:, 1] - bev_range[0]) / res).long().clamp(0, grid - 1) linear = ix * grid + iy # 展平索引 bev = torch.zeros(1, feats.shape[1], grid, grid, device=points.device) # RCS 通道用 scatter_reduce 的 amax,其余用 mean bev[..., 0] = torch.zeros(grid * grid, device=points.device).scatter_reduce( 0, linear, feats[:, 0], reduce="amax", include_self=False ).view(grid, grid) # 有效点计数通道,帮助网络判断该格子置信度 cnt = torch.zeros(grid * grid, device=points.device).scatter_add( 0, linear, torch.ones_like(linear, dtype=torch.float) ).view(grid, grid).clamp(max=8.0) / 8.0 bev[..., -1] = cnt return bev3. 两路特征的对齐与跨注意力融合
融合模块是整个方案里最“看起来高级”的部分,但我个人的经验是:八成的融合效果由对齐质量决定,只有两成来自注意力模块本身的设计。对齐没做好,再花哨的注意力机制也只是在错误的空间位置上做加权。
3.1 空间对齐:融合效果下降的首要原因
对齐包含三件事,任何一件出问题都会让融合变成负收益。
外参标定是第一件。相机和雷达的安装位置不同,从雷达到相机的变换矩阵哪怕差 0.5 度俯仰角,在 50 米处就会造成近 0.5 米的纵向偏差。这个偏差在近处感知不到,远处足以让两个相邻车道的目标混淆。我的做法是先用静态标定给出初值,再用一段直线行驶数据做在线微调:找路边的连续护栏或长直车道线,雷达点应该在图像上贴合其边缘,用这个残差反向优化外参。
时间同步是第二件。雷达通常 10 到 20 Hz,相机 10 到 30 Hz,两者的采样时刻往往差几十毫秒。车速 30 米每秒时,50 毫秒就是 1.5 米的位置差——这个量级已经超过一个车宽了。必须用硬件的同步信号做触发,或者至少用时间戳做运动补偿,把雷达点按自车运动外推到图像曝光时刻。软件层面忽略这一点的项目我见过不止一个,表现就是“融合后高速场景变差、低速场景变好”。
运动畸变是第三件。雷达在旋转扫描时,一帧内的点其实是在不同时刻采到的,车在动,点云就会歪。帧率低、车速高的时候尤其明显。补偿方法是按每个点的相对采集时刻做外推,需要雷达驱动把逐点时间戳透传上来——很多驱动默认不给,得去翻手册。
注意:标定自检有个土办法但特别有效——把雷达点按标定参数投到图像上,用不同颜色标出距离远近(比如近处红色、远处蓝色),然后找一段有清晰车辆轮廓的场景看叠加图。如果近处贴合、远处发散,多半是外参角度有问题;如果整体平移,那是平移分量错了;如果远近都贴但横向偏,检查是不是坐标系手性搞反了。
3.2 跨注意力里 query 和 key 该给谁
RCBEVDet 用的是跨注意力形式的多层融合。这里有个设计选择值得展开:谁是 query,谁是 key/value。
我的理解是,让雷达 BEV 特征做 query、图像 BEV 特征做 key/value更符合两者的信息特性。雷达点虽然稀疏,但每一个都是“这里确实有东西”的高置信度证据;图像特征稠密但可能被光照欺骗。让雷达去提问、图像来回答,等于用雷达的几何可靠性去约束图像特征的采样位置,避免了图像里大片背景区域对融合特征的稀释。
反过来做也能work,但你会发现在雨雾天图像整体退化时,以图像为 query 的融合会把噪声放大。双向交叉注意力当然更强,代价是计算量翻倍。
计算量这块必须有清醒认识。BEV 特征图假设是 128×128,通道 128,标准注意力的复杂度是 O((HW)²),直接算根本跑不动。所以实际实现要么用可变形注意力(每个 query 只采样固定数量的参考点),要么做窗口化局部注意力,要么在融合前先降采样。可变形注意力是我比较推荐的,它天然适合这种“稀疏查询、稠密键值”的场景,还能顺带学出每个雷达位置应该往哪个图像区域看。
3.3 融合层放在哪几层,不是拍脑袋决定的
| 融合位置 | 特征分辨率 | 语义强度 | 适合弥补的问题 | 代价 |
|---|---|---|---|---|
| 仅深层 | 低(如 16×16) | 强 | 分类混淆、大目标 | 小目标定位精度损失明显 |
| 仅浅层 | 高(如 128×128) | 弱 | 小目标、远距离定位 | 分类头拿不到足够语义 |
| 多层(本方案) | 多尺度 | 兼顾 | 远近目标共存 | 显存与算力开销上升 |
多层融合的实操要点是融合权重的初始化。如果融合模块一开始就输出很大的值,相当于在训练初期用噪声覆盖掉预训练好的图像特征,收敛会非常慢甚至崩掉。我的做法是把融合分支的输出投影层初始化为接近零,等于让模型一开始退化成纯图像检测器,随着训练推进再逐步“打开”雷达信息。这个技巧在多个项目里都救过我。
4. 训练策略:雷达和图像不能共用一套增强
多模态训练最容易犯的错,是把单模态那套训练配方直接搬过来。雷达数据的统计特性和图像差得太远,增强策略、学习率、损失权重都得单独考虑。
4.1 数据增强必须成对设计
几何类增强(翻转、旋转、缩放)两路必须严格同步,而且坐标系变换要一一对应。图像水平翻转对应的是自车坐标系 y 轴取反,雷达点的 y 坐标也要取反、方位角要取补角、径向速度的横向分量也要变号。这串操作漏掉任何一个,就会出现“图像里的车在左边、雷达点跑到右边”的错位,而这种错误在 loss 曲线上完全看不出来,只会在最终指标上表现为莫名其妙的掉点。
雷达特有的增强里,随机丢点很有效。按 10% 到 20% 的比例随机丢弃点,模拟雨雾衰减和远距离回波丢失,能显著提升模型在恶劣天气下的稳健性。目标级复制粘贴(把一个目标的点云抠出来贴到另一个位置)对小目标召回提升明显,但要注意粘贴位置的多普勒速度要重新计算,否则会造出“以 80 迈速度静止”的物理上不可能的目标,模型可能学到错误关联。
图像侧的增强照常做,但颜色抖动要克制。雷达分支的逐点特征是从图像特征采样来的,如果图像被调得面目全非,雷达点采到的引导特征也会跟着失真,等于两路一起被污染。
4.2 类别不平衡在雷达模态上被放大
图像里一个远处的行人好歹还有几十个像素,雷达里可能就是两三个点。这种“双重稀疏”让远距离小目标的召回率成为整个系统的短板。
我一般会按距离分桶(0-20 米、20-35 米、35-50 米、50 米以上)分别统计各类别的召回,而不是只看总体 mAP。总体指标好看但远距离行人召回只有 30% 的情况非常常见,而这个短板在实车上恰恰最致命。针对性的处理包括:给远距离目标的回归损失加权、在采样时对含远距离正样本的帧做超额采样、以及适当提高 BEV 网格在远处的分辨率(如果算力允许)。
4.3 收敛节奏上的几个观察
图像分支用纯视觉预训练权重初始化,雷达分支和融合模块随机初始化,这个组合的直接后果是训练前期两路特征分布差异巨大,融合模块的梯度噪声很重。我的处理是给雷达分支和融合模块单独设一个更小的初始学习率,配 1 到 2 个 epoch 的 warmup,等两路特征尺度接近了再统一学习率。
另一个观察是融合模块真正开始起作用的时间点。可以定期把融合分支关掉(输出置零)跑一次验证,对比开与关的指标差。我实测下来,前几个 epoch 关闭融合的指标反而更高,大概在总训练进度的三分之一处才反超,之后差距持续拉大。如果你发现训练到一半融合仍然是负收益,别急着调结构,先回去查对齐。
5. 实测中绕不开的几个问题
5.1 融合之后指标不升反降,排查顺序怎么定
这是最高频的问题。我的排查链路是固定顺序的,因为它按“可能性从高到低”排列:
- 先确认单模态基线是干净的。把融合模块旁路掉,只留雷达支路单独出检测结果,看它能不能在自己模态上跑出一个合理的数。如果纯雷达基线本身就不行,融合不可能救回来。
- 查对齐。按前面说的方法把雷达点投到图像上可视化,重点看 30 米以外的区域。这一条能解决我遇到问题的六成以上。
- 查融合权重的分布。把跨注意力的注意力图可视化出来,如果注意力均匀铺满整个 BEV 图,说明它根本没学到“该看哪”;如果全集中在某一小块,可能是位置编码出了问题。
- 查增强是否引入错位。把增强关掉重训一个短周期,对比指标。这一步能排除掉大部分“莫名其妙”的退化。
- 最后才怀疑结构设计。说实话,在我处理过的情况里,真正需要改网络结构的不到两成。
5.2 幽灵目标:雷达特有的误检来源
金属井盖、护栏、隧道壁、大型车辆的侧裙,都会产生多径反射,在 BEV 上表现为一串位置固定但速度异常的虚假目标。这类误检对纯视觉方案来说几乎不存在,是引入雷达之后新增的负担。
压制手段有三层:RCS 与速度的一致性检验——静止的金属结构 RCS 很高但速度必须是零(扣掉自车运动后),如果一个高 RCS 目标带着非零速度,多半是反射假象;高度过滤——如果你的雷达有哪怕粗略的高度输出,把明显高于或低于地面一定范围的点剔掉;时序一致性——真实目标在多帧之间的位置变化符合运动学,幽灵目标往往是跳变的。第三层最有效但会引入延迟,实时系统里要权衡。
5.3 恶劣天气下到底能赚多少
这块我不喜欢给漂亮的数字,因为收益高度依赖具体雷达型号和场景分布。定性的结论是:中雨、扬尘、逆光这几种场景下,融合方案相对纯视觉的召回提升是明显的,尤其是 30 米以外的目标和静止障碍物;大暴雨时增益会收窄,因为雨滴本身会产生杂波,雷达点云里多出一层弥漫的低 RCS 噪声,需要额外做滤波。雾天反而是雷达的主场,能见度降到几十米时视觉基本失效,雷达还能稳定输出测距。
代价是有的:暴雨场景下如果不做杂波抑制,融合方案的误检率会比纯视觉高。我的经验是加一个基于 RCS 和点密度的前置滤波,雨滴的回波强度普遍偏低且空间分布弥散,用一个简单的阈值加邻域密度判断就能滤掉大半。
6. 部署侧的现实约束
6.1 算力账要先算清楚
跨注意力融合的开销不能想当然。以 128×128 的 BEV 特征、128 通道为例,可变形注意力在每个 query 采样 4 个点的设定下,单层融合的浮点运算量大致在几 GFLOPs 量级,多层叠加再加上图像 backbone 和 BEV 转换模块,整体很容易超出车端芯片的预算。
降本的手段按性价比排序:融合层只保留两到三层而不是每层都融、在融合前对 BEV 特征做通道压缩、把雷达支路的 BEV 编码器换成深度可分离卷积、可变形注意力的采样点数从 4 降到 2。最后一项降得最狠但精度损失也最明显,我一般放在最后考虑。
6.2 量化时那些不报错但掉点的算子
雷达支路里 scatter 类的操作在部分推理框架上不支持量化,会退回到浮点执行,形成一个隐性的性能瓶颈。跨注意力里的 softmax 和位置编码对量化也比较敏感,int8 之后注意力分布容易塌缩成近似均匀分布。
我的做法是分层策略:图像 backbone 和检测头正常量化,融合模块和雷达编码器先保持 fp16,验证精度损失在可接受范围后再逐步下探。另外一定要做逐层输出比对,把量化模型和浮点模型在每一层的输出余弦相似度都算一遍,找出第一个明显掉下来的层。只看最终指标的话,你很难定位是哪一层出的问题。
6.3 标定漂移是个长期问题
车开久了,轻微碰撞、悬挂老化、温度变化都会让外参慢慢偏掉。离线标定再准,跑三个月也会漂。实际项目里必须有一套在线监控:用静止目标(护栏、路面反射)或者长直车道线作为参照,持续统计雷达点投影到图像上的残差,超过阈值就报警或者触发在线重标定。
这套机制我在项目后期才加上,代价是前期有好几例“用户反馈某些场景检测框整体偏左”的问题,查到最后都是标定漂移,而不是算法本身。
我个人在这个方向上折腾下来最深的体会是:多模态融合的收益上限由传感器本身的互补性决定,但你能拿到多少,几乎完全取决于工程细节——标定、同步、坐标系、预处理归一化这些“不性感”的部分。网络结构改来改去带来的提升,往往还不如把投影对齐误差从 0.5 米压到 0.1 米来得实在。如果让我给刚上手的人一句建议,那就是先把纯雷达单模态跑到一个能看的水平,再谈融合;这一步跳过去,后面所有的调参都会变成盲猜。