2、BEV感知基础:坐标系与空间变换、相机内外参、IPM逆透视变换、BEV网格定义与分辨率选择

好,咱们正式开始BEV感知的第一课。说实话,很多新手一上来就扎进网络结构里,结果连坐标系都没搞明白,最后标定数据对不上,整个项目白干。我见过太多这样的案例了。

这一章,咱们就把地基打牢。坐标系、空间变换、相机参数、IPM、BEV网格——这些概念你搞懂了,后面的路就好走了。

2.1 坐标系与空间变换

自动驾驶里,坐标系多到让人头疼。你想想看,一辆车上有激光雷达、摄像头、毫米波雷达,每个传感器都有自己的坐标系。怎么把它们统一起来?

我个人习惯把坐标系分成四层:

  • 像素坐标系:图像上的(u, v),单位是像素。原点在左上角。
  • 图像坐标系:物理成像平面上的(x, y),单位是毫米。原点在光轴与成像平面的交点。
  • 相机坐标系:以相机光心为原点,Z轴朝前,X轴朝右,Y轴朝下。
  • 车身坐标系:以车辆后轴中心为原点,Z轴朝上,X轴朝前,Y轴朝左。

嗯,这里要注意:不同车厂的车身坐标系定义可能不一样。有的用后轴中心,有的用车辆几何中心。我在项目中遇到过,某家供应商给的标定文件,坐标系原点差了10厘米,结果融合出来的目标位置全偏了。排查了整整两天才找到原因。

避坑指南: 拿到任何标定数据,第一件事就是确认坐标系定义。别想当然。

空间变换说白了就是旋转和平移。旋转用旋转矩阵R,平移用平移向量t。一个点从相机坐标系变换到车身坐标系,公式很简单:

P_body = R * P_camera + t

但实际工程中,我们通常用齐次坐标,把旋转和平移合并成一个4x4的变换矩阵。这样连续变换就变成了矩阵乘法,方便很多。

2.2 相机内外参

相机参数分内参和外参。内参描述相机本身的特性,外参描述相机在空间中的位置和朝向。

2.2.1 内参矩阵

内参矩阵K长这样:

K = [[fx,  0, cx],
     [ 0, fy, cy],
     [ 0,  0,  1]]

fx、fy是焦距(单位像素),cx、cy是主点坐标。说白了,内参就是告诉你:三维空间中的一个点,投影到图像上时,落在哪个像素位置。

我记得有一次做标定,发现fx和fy差了好几个像素。一开始以为是标定板没放平,后来查了半天,发现是相机模组装配时感光芯片歪了。这种硬件问题,软件再牛也救不了。

2.2.2 畸变参数

相机镜头不是完美的,会产生畸变。主要有两种:

  • 径向畸变:镜头形状导致的,图像边缘会扭曲。用k1, k2, k3三个参数描述。
  • 切向畸变:镜头和感光芯片不平行导致的。用p1, p2两个参数描述。

畸变矫正的公式我就不写了,OpenCV里都有现成的函数。但我要提醒你:畸变矫正一定要做在IPM之前。我曾经犯过这个错,先做了IPM再矫正畸变,结果地面上的车道线全是弯的,怎么调都调不直。

小技巧: 量产项目中,畸变参数可以预先计算好查找表,运行时直接查表映射,比实时计算快很多。

2.2.3 外参矩阵

外参就是相机相对于车身坐标系的旋转和平移。通常用一个4x4的齐次矩阵表示:

[R | t]
[0 | 1]

外参标定是量产中最容易出问题的环节。为什么?因为车辆行驶过程中,摄像头会震动,外参会慢慢漂移。我见过一个项目,新车下线时标定得好好的,跑了5000公里后,车道线检测就开始偏了。最后发现是摄像头支架松了。

所以量产方案里,一定要有在线外参自标定模块。这个后面我们会专门讲。

2.3 IPM逆透视变换

IPM,全称Inverse Perspective Mapping,逆透视变换。说白了,就是把相机拍到的图像,投影到地面上,得到鸟瞰图。

为什么要做IPM?因为BEV感知需要统一的空间表达。相机看到的是透视图像,近大远小;而BEV是俯视图,没有透视效应。IPM就是连接两者的桥梁。

IPM的原理其实不复杂。假设地面是平的,我们知道相机的高度h和俯仰角θ,那么图像上的每个像素(u, v)都可以映射到地面上的一个点(X, Y, 0)。

公式推导我就不展开了,直接给核心代码:

def ipm_transform(u, v, K, R, t, ground_height=0):
    # 1. 像素坐标转归一化坐标
    x_norm = (u - K[0, 2]) / K[0, 0]
    y_norm = (v - K[1, 2]) / K[1, 1]
    
    # 2. 构建相机坐标系下的射线
    ray_cam = np.array([x_norm, y_norm, 1.0])
    
    # 3. 转到车身坐标系
    ray_body = R @ ray_cam
    
    # 4. 计算与地面的交点
    # 地面方程: Z = ground_height
    t = (ground_height - t[2]) / ray_body[2]
    X = t[0] + t * ray_body[0]
    Y = t[1] + t * ray_body[1]
    
    return X, Y
注意: IPM假设地面是平的。如果路面有坡度,IPM会失效。这时候就需要用Occupancy Network来弥补了。

实际工程中,IPM有个大问题:远处的地面分辨率极低。你想想看,图像上的一行像素,近处对应几十厘米,远处可能对应好几米。所以IPM的有效范围通常只有30-50米。再远就不靠谱了。

2.4 BEV网格定义与分辨率选择

BEV网格,就是把地面划分成一个个小格子。每个格子代表一个空间位置。这是BEV感知的基础数据结构。

2.4.1 网格定义

一个典型的BEV网格定义包含:

  • 范围:X方向[-50m, 50m],Y方向[-25m, 25m](以车为中心)
  • 分辨率:每个格子的大小,比如0.1m x 0.1m
  • 尺寸:网格的行数和列数,由范围和分辨率计算得到

举个例子:

BEV_CONFIG = {
    'x_range': [-50.0, 50.0],   # 米
    'y_range': [-25.0, 25.0],   # 米
    'resolution': 0.1,          # 米/格
    'x_size': 1000,             # (50 - (-50)) / 0.1
    'y_size': 500               # (25 - (-25)) / 0.1
}

2.4.2 分辨率怎么选?

这是个大问题。分辨率高了,细节丰富但计算量大;分辨率低了,计算快但精度不够。怎么平衡?

我个人建议分场景考虑:

应用场景 推荐分辨率 说明
车道线检测 0.05m - 0.1m 需要精细的线条信息
障碍物检测 0.1m - 0.2m 车辆行人不需要太精细
可行驶区域 0.2m - 0.5m 粗粒度语义分割即可
Occupancy 0.1m - 0.2m 兼顾精度和计算量

我在量产项目里,通常用0.1m分辨率。为什么?因为0.1m对应10cm,这个精度对大多数感知任务都够用了。而且1000x500的网格,在现在的芯片上跑起来也不吃力。

经验之谈: 如果你用Transformer做BEV特征提取,分辨率可以适当降低。Transformer对位置编码很敏感,低分辨率反而更容易学到全局特征。我试过0.2m分辨率,效果并不比0.1m差多少,但计算量减了一半。

2.4.3 网格坐标与物理坐标的转换

这个一定要写对,不然所有数据都对不上。核心就两个函数:

def grid_to_world(gx, gy, config):
    wx = gx * config['resolution'] + config['x_range'][0]
    wy = gy * config['resolution'] + config['y_range'][0]
    return wx, wy

def world_to_grid(wx, wy, config):
    gx = int((wx - config['x_range'][0]) / config['resolution'])
    gy = int((wy - config['y_range'][0]) / config['resolution'])
    return gx, gy

嗯,这里要注意:网格坐标的(0,0)对应物理坐标的(-50, -25),不是(0,0)。很多新手会搞混,导致画出来的BEV图是反的。

2.5 本章小结

这一章的内容,说白了就是BEV感知的"基建工程"。坐标系搞清楚了,IPM做对了,网格定义好了,后面的模型训练和部署才能顺利进行。

我建议你动手做个小练习:拿一张车载摄像头拍的图片,手动实现IPM变换,把结果投影到BEV网格上。这个过程走一遍,你对这些概念的理解会深很多。

下一章,咱们聊聊BEV感知的核心——如何把多视角图像的特征融合到BEV空间里。到时候会讲到LSS、Transformer这些主流方案。准备好了吗?