第二章:坐标系与投影基础

做BEV感知,说白了就是解决一个问题:怎么把相机看到的2D图像,映射到车顶俯瞰的3D空间里

这背后绕不开坐标系和投影变换。我刚开始接触这个领域时,觉得不就是几个坐标系来回倒腾嘛,结果第一次做实车标定,投影出来的点全飘到天上去了……嗯,从那以后我再也不敢小看这部分基础。

2.1 四个核心坐标系

BEV感知里,我们打交道的主要有四个坐标系。我习惯把它们分成两组来理解:

  • 传感器侧:像素坐标系、相机坐标系
  • 车体侧:车身坐标系、世界坐标系

2.1.1 像素坐标系 (u, v)

这就是你看到的图像本身。原点在左上角,u轴向右,v轴向下。单位是像素。

举个例子:一张1920×1080的图像,像素坐标(960, 540)就是正中心。

避坑提醒:我曾经在代码里直接把像素坐标当成物理坐标去算投影,结果全错了。记住,像素坐标只是离散的格子索引,不是物理尺寸。

2.1.2 相机坐标系 (Xc, Yc, Zc)

相机坐标系的原点在相机光心。Zc轴指向相机前方(也就是镜头朝向),Xc向右,Yc向下(跟图像坐标系对齐)。

这里有个关键点:相机坐标系是三维的,像素坐标系是二维的。从3D到2D,就是投影变换要干的事。

2.1.3 车身坐标系 (Xv, Yv, Zv)

车身坐标系的原点通常在后轴中心(或者车辆质心)。Xv指向车头,Yv指向左侧,Zv指向上方。

为什么用后轴中心?因为车辆运动学建模时,后轴中心是理想的参考点。我见过一些新手直接把GPS天线位置当车身原点,结果做轨迹规划时偏差很大。

2.1.4 世界坐标系 (Xw, Yw, Zw)

世界坐标系是全局参考系。在自动驾驶里,通常用UTM坐标系或者局部ENU坐标系。原点可以是任意选定的固定点,比如地图上的某个地标。

你想想看,车在跑,世界坐标系是不动的。车身坐标系跟着车动。所以从车身到世界的变换,本质上就是车辆的位姿(位置+朝向)。

2.2 内参与外参

搞清楚了坐标系,接下来就是怎么在它们之间来回切换。这里有两个核心概念:内参外参

2.2.1 内参矩阵 (Intrinsic Matrix)

内参描述的是相机本身的属性。说白了,就是光线穿过镜头后,怎么落到传感器上的。

内参矩阵K长这样:

K = [fx,  0, cx
      0, fy, cy
      0,  0,  1]
  • fx, fy:焦距(以像素为单位)。fx = 焦距(mm) × 像素密度(pixel/mm)
  • cx, cy:光心在像素坐标系中的位置。通常接近图像中心。

我记得第一次自己标定相机内参时,用OpenCV的棋盘格标定法,跑了20张图,结果fx和fy差了3个像素。后来发现是棋盘格打印时纸张有轻微变形。所以建议:标定板一定要贴平在硬板上

2.2.2 外参矩阵 (Extrinsic Matrix)

外参描述的是相机相对于车身(或世界)的位置和朝向。它是一个4×4的齐次变换矩阵:

T = [R, t
     0, 1]
  • R:3×3旋转矩阵,描述朝向
  • t:3×1平移向量,描述位置

举个例子:如果相机安装在车顶前方,t就表示相机光心相对于车身原点的偏移量。R则表示相机坐标系相对于车身坐标系的旋转。

注意:外参的坐标系定义一定要统一。我见过一个团队,A用"相机→车身",B用"车身→相机",结果融合时数据全乱了。建议团队内部统一用"从源坐标系到目标坐标系"的命名方式。

2.3 投影变换公式推导

好了,现在我们把所有零件凑起来。从世界坐标系的一个3D点,到像素坐标系的一个2D点,整个过程是这样的:

2.3.1 完整投影链

世界点 Pw → 车身点 Pv → 相机点 Pc → 像素点 p

每一步对应一个变换:

  1. 世界 → 车身:Pv = T_world_to_vehicle × Pw
  2. 车身 → 相机:Pc = T_vehicle_to_camera × Pv
  3. 相机 → 像素:p = K × Pc(归一化后取前两维)

合并起来就是:

p = K × T_vehicle_to_camera × T_world_to_vehicle × Pw

2.3.2 关键公式推导

从相机坐标系到像素坐标系这一步,是投影的核心。假设相机坐标系下有一个点 (Xc, Yc, Zc):

u = fx × (Xc / Zc) + cx
v = fy × (Yc / Zc) + cy

为什么除以Zc?因为透视投影——远处的物体看起来更小。你想想看,一个10米外的行人,在图像上可能只有几十个像素;而1米外的行人,能占满半个屏幕。

写成齐次坐标形式更简洁:

[u]   [fx,  0, cx, 0]   [Xc]
[v] = [ 0, fy, cy, 0] × [Yc]
[1]   [ 0,  0,  1, 0]   [Zc]
                         [ 1]

然后归一化:u = u / Zc, v = v / Zc。

2.3.3 逆投影:从像素到3D射线

BEV感知里,我们经常需要做逆操作——给定一个像素点,反推它在3D空间中的可能位置。

但注意:单个像素点无法唯一确定3D位置,只能确定一条射线。

Xc = (u - cx) × Zc / fx
Yc = (v - cy) × Zc / fy

这里Zc是未知的。所以我们需要额外的信息来约束Zc,比如:

  • 深度估计(单目深度网络)
  • 地面假设(假设点在路面上)
  • 多目立体匹配

我的经验:在BEV感知中,最常用的做法是"地面假设+逆投影"。假设所有感兴趣的点都在地面上(Z=0),然后通过逆投影计算出对应的3D坐标。虽然不完美,但足够高效。我曾经在一个项目中用这种方法做车道线检测,效果出奇的好。

2.4 实操:坐标系转换代码

下面给出一段Python代码,演示完整的投影流程。我习惯用NumPy做矩阵运算,清晰又高效。

import numpy as np

def project_world_to_pixel(Pw, K, T_vehicle_to_camera, T_world_to_vehicle):
    """
    将世界坐标系下的3D点投影到像素坐标系
    
    参数:
        Pw: 世界坐标系下的点,形状 (3,) 或 (N, 3)
        K: 内参矩阵,形状 (3, 3)
        T_vehicle_to_camera: 车身→相机变换矩阵,形状 (4, 4)
        T_world_to_vehicle: 世界→车身变换矩阵,形状 (4, 4)
    
    返回:
        p: 像素坐标,形状 (N, 2)
    """
    # 确保输入是齐次坐标
    if Pw.ndim == 1:
        Pw = Pw.reshape(1, -1)
    
    N = Pw.shape[0]
    Pw_h = np.hstack([Pw, np.ones((N, 1))])  # (N, 4)
    
    # 世界 → 车身 → 相机
    Pc_h = (T_vehicle_to_camera @ T_world_to_vehicle @ Pw_h.T).T  # (N, 4)
    
    # 归一化
    Pc = Pc_h[:, :3] / Pc_h[:, 2:3]  # (N, 3)
    
    # 相机 → 像素
    p_h = (K @ Pc.T).T  # (N, 3)
    p = p_h[:, :2] / p_h[:, 2:3]  # (N, 2)
    
    return p

# 示例:内参矩阵(假设640×480图像,焦距约500像素)
K = np.array([[500,   0, 320],
              [  0, 500, 240],
              [  0,   0,   1]])

# 外参:相机安装在车顶,向前看
# 假设相机在车身坐标系下位于 (0, 0, 1.5) 米,无旋转
T_vehicle_to_camera = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 1.5],
    [0, 0, 0, 1]
])

# 世界到车身:假设车辆位于世界原点,朝向正X
T_world_to_vehicle = np.eye(4)

# 测试:世界坐标系下 (10, 0, 0) 的点(前方10米)
Pw = np.array([10, 0, 0])
p = project_world_to_pixel(Pw, K, T_vehicle_to_camera, T_world_to_vehicle)
print(f"像素坐标: ({p[0,0]:.1f}, {p[0,1]:.1f})")
# 输出应该接近 (320, 240),因为点在正前方,投影到图像中心

2.5 常见坑与避坑指南

这部分我踩过的坑比较多,挑几个典型的说说:

坑1:坐标系方向搞反

我曾经在写外参时,把旋转矩阵的符号弄反了。结果投影出来的点全跑到车后面去了。检查了三天才发现是R矩阵的yaw角符号错了。

建议:每次写完变换矩阵,先用一个已知点做验证。比如把(1,0,0)投影一下,看是不是落在预期位置。

坑2:齐次坐标忘记归一化

很多新手在写投影时,直接拿齐次坐标的前三维当3D坐标用。但齐次坐标的最后一维不一定是1,需要先归一化。

建议:养成习惯,每次做完矩阵乘法后,显式地除以最后一维。

坑3:内参标定不准确

fx和fy差几个像素,在近距离可能看不出来。但到了50米外,投影误差能到1-2米。这在BEV感知里是致命的。

建议:标定内参时,至少拍20-30张不同角度的棋盘格照片。覆盖图像的四个角落和中心区域。

2.6 小结

这一章我们聊了:

  • 四个核心坐标系:像素、相机、车身、世界
  • 内参和外参的含义与矩阵形式
  • 从3D世界到2D图像的完整投影链
  • 逆投影的思路与局限性
  • 实际代码实现和常见坑

说实话,坐标系和投影这部分,看起来简单,但真正做项目时最容易出问题。我建议你学完这章后,自己动手写一遍投影代码,然后用真实数据验证一下。只有亲手踩过坑,才能真正理解。

下一章,我们会进入BEV感知的核心——视角转换与特征映射。到时候会用到这章的所有知识,所以务必把基础打牢。


公众号:蓝海资料掘金营,微信deep3321