2. BEV感知基础
大家好,我是这门课的主讲工程师。今天我们来聊聊BEV感知里最基础、也最绕不开的一个话题——透视视角到鸟瞰视角的几何变换。
说实话,我刚入行做自动驾驶感知那会儿,第一次看到车载摄像头拍回来的画面,心里就一个想法:这画面歪歪扭扭的,怎么让车理解路在哪?后来才明白,我们需要把摄像头看到的“透视视角”,转换成从上往下看的“鸟瞰视角”。这个转换,就是BEV感知的基石。
核心思想:透视视角下,物体近大远小、车道线汇聚于消失点。鸟瞰视角下,所有物体保持真实比例,距离关系一目了然。
透视投影与逆透视映射(IPM)
先说说透视投影。摄像头成像,本质上是一个小孔成像模型。三维世界里的点,通过光心投影到二维成像平面上。这个过程会丢失深度信息,产生近大远小的效果。
那逆透视映射(IPM)是干嘛的?说白了,就是把这个过程反过来——从二维图像反推回三维世界的地平面。
我记得有一次做车道线检测项目,摄像头装在车顶,画面里车道线明显汇聚。直接用图像做检测,远端的车道线几乎挤在一起,根本分不清。后来用了IPM,把图像投影到地平面,车道线就变成平行线了,检测准确率直接提升了20%。
我的经验:IPM的前提是假设地面是平坦的。如果路面有坡度,投影就会失真。我在高速公路上跑过,效果还行;但在城市里遇到上坡下坡,就得额外处理了。
IPM的数学原理
IPM的核心公式其实不复杂。我们有一个3D点 (X, Y, Z),它在图像上的投影是 (u, v)。关系可以写成:
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中:
- K 是相机内参矩阵(焦距、主点坐标)
- [R | t] 是相机外参(旋转和平移)
- s 是尺度因子
做IPM时,我们假设 Z=0(地面高度),然后反解出 (X, Y)。这样就把图像上的每个像素,映射到了地平面上的一个点。
注意:IPM不是万能的。它假设地面是平面,且相机外参标定准确。我见过不少同学标定没做好,投影出来的BEV图歪七扭八,车道线都变成曲线了。标定这一步,千万别偷懒。
BEV网格坐标系定义
有了IPM,我们就能把图像投影到地平面了。但怎么组织这些数据呢?这就引出了BEV网格坐标系。
我个人习惯把BEV网格定义成这样的:
- 原点:通常放在自车后轴中心,或者前保险杠中心
- X轴:指向车辆前方
- Y轴:指向车辆左侧
- 网格大小:常见的有0.1m、0.2m、0.5m
- 范围:前向50m,左右各25m,这是比较典型的设置
你想想看,为什么这么定义?因为自车是感知系统的载体,以自车为中心做感知,最直观也最方便控制。
下面这张图展示了整个流程:
实际项目中的避坑指南
讲到这里,我得分享几个实际踩过的坑。
第一个坑:相机外参漂移
我曾经在一台测试车上跑IPM,刚开始效果很好。跑了几天后,发现BEV图越来越歪。排查了半天,发现是摄像头支架松动了,外参变了。从那以后,我每次上线前都会跑一遍外参校验。
第二个坑:地面不平
IPM假设地面是平的。但实际道路有坡度、有颠簸。我试过在过减速带时,BEV图里的车道线直接扭曲了。后来加了IMU信息做补偿,才稳住。
第三个坑:网格分辨率选择
网格太细(比如0.05m),计算量大,而且远处像素稀疏。网格太粗(比如0.5m),近处细节丢失。我一般折中选0.2m,兼顾精度和效率。
代码示例:IPM映射实现
下面给一段Python代码,展示如何把图像上的点映射到BEV网格:
import numpy as np
import cv2
def ipm_transform(image, K, R, t, grid_range, grid_resolution):
"""
将透视图像映射到BEV网格
:param image: 输入图像 (H, W, 3)
:param K: 相机内参 (3x3)
:param R: 旋转矩阵 (3x3)
:param t: 平移向量 (3,)
:param grid_range: [x_min, x_max, y_min, y_max] 单位米
:param grid_resolution: 网格分辨率 单位米
:return: BEV图像
"""
# 计算网格尺寸
x_min, x_max, y_min, y_max = grid_range
x_size = int((x_max - x_min) / grid_resolution)
y_size = int((y_max - y_min) / grid_resolution)
# 生成网格坐标
x_coords = np.linspace(x_min, x_max, x_size)
y_coords = np.linspace(y_min, y_max, y_size)
xx, yy = np.meshgrid(x_coords, y_coords)
# 地面点 (Z=0)
points_3d = np.stack([xx.ravel(), yy.ravel(), np.zeros_like(xx.ravel())], axis=1)
# 投影到图像平面
points_cam = R @ points_3d.T + t.reshape(3, 1)
points_pixel = K @ points_cam
points_pixel = points_pixel[:2] / points_pixel[2]
# 采样图像像素
bev_image = cv2.remap(image,
points_pixel[0].reshape(x_size, y_size).astype(np.float32),
points_pixel[1].reshape(x_size, y_size).astype(np.float32),
cv2.INTER_LINEAR)
return bev_image
小技巧:实际项目中,我一般会预计算一次映射表,存下来。每次推理时直接查表,能省掉大量矩阵运算时间。对于实时性要求高的系统,这个优化很关键。
总结一下
这一节我们聊了三个核心点:
- 透视到鸟瞰的几何变换:本质是坐标系的转换,把摄像头看到的透视画面,映射到地平面
- IPM逆透视映射原理:基于小孔成像模型,假设地面平坦,反解出地平面坐标
- BEV网格坐标系:以自车为中心,定义X轴向前、Y轴向左,用固定分辨率网格存储感知结果
嗯,这些概念听起来可能有点抽象。但相信我,当你亲手跑通一次IPM,看到歪歪扭扭的透视画面变成规整的BEV图时,那种成就感是很爽的。
下一节我们会深入BEV感知的另一个核心——多传感器融合。到时候见。
可可精品资料库