3、BEV网格与特征表示:BEV网格定义、分辨率选择、特征图映射、多尺度特征融合
好,咱们进入正题。BEV感知里最基础也最绕不开的一个概念,就是BEV网格。说白了,就是把相机看到的透视图像,硬生生掰成从天上往下看的俯视图。这个过程中,网格怎么定义、分辨率选多大、特征怎么映射进去,每一步都有坑。我踩过不少,今天一并讲清楚。
3.1 BEV网格定义:坐标系与空间范围
先说说网格是什么。BEV网格就是一个二维的栅格,覆盖在自车周围的地面上。每个格子代表一个物理区域,比如50厘米×50厘米。整个网格的大小,决定了你能感知多远、多宽。
我个人习惯把网格定义成三个参数:X方向范围、Y方向范围、网格分辨率。举个例子,常见的设定是:
- X方向:-50米到50米(车头方向)
- Y方向:-25米到25米(车宽方向)
- 分辨率:0.5米/格
这样网格的尺寸就是200×100。嗯,这里要注意,X和Y的范围不一定对称。城市道路场景,我更关注前方和侧方,后方可以稍微砍一点。
核心要点:网格的原点通常放在自车后轴中心,或者车辆几何中心。我个人推荐后轴中心,因为车辆运动学模型都是以它为基准的。
坐标系方向也得统一。我见过团队用不同的坐标系,结果联调时数据全乱套。建议统一成:X向前、Y向左、Z向上,这是自动驾驶行业的主流约定。
3.2 分辨率选择:精度与计算量的博弈
分辨率怎么选?这问题我当年纠结了很久。你想想看,分辨率越高,小目标检测越准,但计算量也越大。反过来,分辨率低了,远处一辆自行车可能就糊成一个点。
我给大家一个经验值:
| 场景 | 推荐分辨率 | 网格尺寸(50m×50m) | 备注 |
|---|---|---|---|
| 高速场景 | 0.5m - 1.0m | 100×100 - 50×50 | 目标大、速度高,粗粒度够用 |
| 城区场景 | 0.25m - 0.5m | 200×200 - 100×100 | 行人、自行车多,需要细粒度 |
| 泊车场景 | 0.1m - 0.2m | 500×500 - 250×250 | 近距离、高精度要求 |
我曾经在一个项目中,为了追求极致精度,把分辨率设成了0.1米。结果模型跑一次推理要200毫秒,根本没法实时。后来妥协到0.25米,精度只掉了2%,速度却快了4倍。所以,别盲目追求高分辨率,够用就好。
我的小技巧:如果你不确定分辨率,先跑一个0.5米的基线。然后看模型在远处小目标上的召回率。如果召回率低,再考虑提升分辨率。别一上来就搞0.1米,那是给自己找麻烦。
3.3 特征图映射:从图像到BEV的投影
网格定义好了,接下来就是怎么把图像特征填进去。这一步叫特征图映射,也叫BEV投影。核心思路是:对于BEV网格中的每个格子,找到它在图像中对应的像素区域,然后把图像特征搬过来。
具体怎么做?我给大家拆解一下:
- 生成BEV网格坐标:每个格子有一个中心点坐标 (x, y, 0),z=0表示地面。
- 投影到图像平面:利用相机内外参,把 (x, y, 0) 投影到图像上,得到像素坐标 (u, v)。
- 采样图像特征:在图像特征图上,用双线性插值采样 (u, v) 位置的特征。
- 填充BEV特征图:把采样到的特征填回对应的BEV网格位置。
代码实现大致长这样:
def project_bev_to_image(bev_grid, cam_intrinsic, cam_extrinsic):
"""
bev_grid: (H_bev, W_bev, 3) 每个格子的世界坐标 (x, y, 0)
cam_intrinsic: (3, 3) 相机内参
cam_extrinsic: (4, 4) 相机外参 (世界到相机)
return: (H_bev, W_bev, 2) 对应的像素坐标
"""
# 1. 将世界坐标转为齐次坐标
ones = torch.ones_like(bev_grid[..., :1])
world_coords = torch.cat([bev_grid, ones], dim=-1) # (H, W, 4)
# 2. 世界 -> 相机 -> 图像
cam_coords = world_coords @ cam_extrinsic.T # (H, W, 4)
img_coords = cam_coords[..., :3] @ cam_intrinsic.T # (H, W, 3)
# 3. 归一化得到像素坐标
u = img_coords[..., 0] / (img_coords[..., 2] + 1e-6)
v = img_coords[..., 1] / (img_coords[..., 2] + 1e-6)
return torch.stack([u, v], dim=-1)
注意!投影时一定要处理遮挡问题。如果一个BEV格子对应到图像上的多个像素(比如有物体遮挡),你需要决定取哪个。我常用的做法是取最近的那个,或者用深度信息做加权平均。
为什么会这样?因为地面假设不总是成立。比如一个高处的路牌,它的投影点可能和地面上的某个点重合。这时候如果不做处理,特征就会混叠。我曾经因为这个原因,模型把路牌误检成了行人,后来加了深度过滤才解决。
3.4 多尺度特征融合:让模型看得更远更细
单尺度的BEV特征图有个问题:大目标和小目标不能兼顾。你想想看,0.5米的分辨率,一辆大卡车占了十几个格子,特征很丰富;但一个行人可能只占1-2个格子,特征稀疏得很。
解决方案就是多尺度特征融合。我常用的做法有两种:
- 图像侧多尺度:从图像 backbone 提取不同分辨率的特征图(比如1/8、1/16、1/32),分别投影到BEV空间,然后融合。
- BEV侧多尺度:先投影一个基础分辨率的BEV特征,然后用类似FPN的结构,在BEV空间内做上采样和下采样。
我个人更推荐第一种。为什么呢?因为图像侧的多尺度特征已经包含了丰富的语义信息,直接投影到BEV,能保留更多细节。第二种方法虽然计算量小,但信息损失比较大。
融合方式也有讲究:
| 融合方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 直接相加 | 简单、计算快 | 特征可能被淹没 | 特征尺度相近时 |
| 通道拼接+卷积 | 可学习、效果好 | 参数量大 | 大多数场景 |
| 注意力加权 | 动态调整权重 | 计算复杂 | 需要精细控制时 |
我一般先用通道拼接+1×1卷积做融合,简单有效。如果效果不够好,再上注意力机制。别一上来就搞复杂的,先跑通再说。
避坑指南:多尺度融合时,不同尺度的特征图分辨率可能不一样。比如1/8的特征图投影到BEV是200×100,1/16的只有100×50。这时候需要把小的上采样到大的尺寸,或者把大的下采样到小的。我建议统一上采样到最大尺寸,因为上采样不会丢失信息,下采样会。
嗯,说到这,我想起一个项目。当时我们做多尺度融合,发现小目标的检测精度一直上不去。排查了很久,最后发现是1/32的特征图投影到BEV后,分辨率太低,行人只有半个格子。后来我们把1/8和1/16的特征图作为主力,1/32的只用来检测大目标,问题就解决了。所以,不是所有尺度都要用,要根据目标大小来选。
好了,这一章的内容就这些。BEV网格和特征表示是整个BEV感知的基石,搞懂了这些,后面的模型结构、训练技巧才能落地。下一章我们聊聊BEV特征编码器,看看怎么从多视角图像中提取出高质量的BEV特征。