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的有效范围通常只有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的网格,在现在的芯片上跑起来也不吃力。
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这些主流方案。准备好了吗?