3、图像坐标系与变换:像素坐标系、图像坐标系、相机坐标系、世界坐标系及其转换关系

各位同学,今天我们来聊聊多摄像头融合里最基础、也最容易搞混的一块内容——坐标系变换。

说实话,我刚入行那会儿,被这四个坐标系绕得晕头转向。有一次做环视拼接,像素坐标转世界坐标时少乘了一个内参,结果车停在那,鸟瞰图里的车却歪了45度。嗯,从那以后,我对坐标系变换就格外上心。

说白了,摄像头看到的是一张二维图片,但我们要用它来理解三维世界。这中间就需要一套「翻译规则」。这套规则,就是坐标系之间的转换关系。

3.1 四个坐标系,一张图说清楚

我们先捋一遍这四个坐标系分别是什么:

  • 像素坐标系 (Pixel Coordinate System):就是图像上每个像素的位置,单位是像素。原点通常在左上角,u轴向右,v轴向下。
  • 图像坐标系 (Image Coordinate System):把像素坐标转换成物理尺寸,单位是毫米。原点在图像中心(主点),x轴向右,y轴向下。
  • 相机坐标系 (Camera Coordinate System):以相机光心为原点,Z轴指向镜头前方,X轴向右,Y轴向下。这是连接2D和3D的桥梁。
  • 世界坐标系 (World Coordinate System):我们真实世界的绝对坐标系。你可以任意定义它的原点,比如车辆后轴中心、某个路标等等。

核心思路: 从像素坐标 → 图像坐标 → 相机坐标 → 世界坐标,每一步都有对应的变换矩阵。反过来也一样。

3.2 像素坐标系 → 图像坐标系

这一步其实很简单。像素坐标 (u, v) 是离散的整数,图像坐标 (x, y) 是连续的物理值。转换公式如下:

x = (u - cx) * dx
y = (v - cy) * dy

其中 (cx, cy) 是图像主点坐标(单位像素),dx、dy 是每个像素在x、y方向上的物理尺寸(单位mm/像素)。

我习惯写成矩阵形式,这样后面做批量计算更方便:

[ x ]   [ dx   0  -cx*dx ] [ u ]
[ y ] = [  0  dy  -cy*dy ] [ v ]
[ 1 ]   [  0   0     1    ] [ 1 ]

小技巧: 实际项目中,cx、cy、dx、dy 都从相机标定结果里直接拿。别自己量,我试过,误差大得离谱。

3.3 图像坐标系 → 相机坐标系

这一步用到了相机内参。说白了,就是把二维图像点投影到三维相机坐标系下的归一化平面上。

公式长这样:

Xc = x * Zc / f
Yc = y * Zc / f
Zc = Zc  (深度值,未知)

写成齐次坐标形式:

[ Xc ]   [ f  0  cx  0 ] [ x ]
[ Yc ] = [ 0  f  cy  0 ] [ y ]
[ Zc ]   [ 0  0   1  0 ] [ 1 ]
[ 1  ]   [ 0  0   0  1 ]

这里 f 是焦距。注意,单目相机下,Zc 是未知的。所以从单张图无法直接得到三维坐标,必须结合深度估计或多目视觉。

我曾经踩过的坑: 做双目融合时,左右相机的内参矩阵必须分别标定,不能共用一套。否则视差计算出来全是错的,重建出来的点云像哈哈镜。

3.4 相机坐标系 → 世界坐标系

这一步就是刚体变换,包含旋转和平移。公式如下:

[ Xw ]   [ R  t ] [ Xc ]
[ Yw ] = [      ] [ Yc ]
[ Zw ]   [ 0  1 ] [ Zc ]
[ 1  ]            [ 1  ]

其中 R 是 3x3 旋转矩阵,t 是 3x1 平移向量。这个 (R, t) 就是相机的外参。

我个人的习惯是,先把旋转矩阵用欧拉角或四元数表示,调试时更直观。比如:

# 假设绕Z轴旋转30度,再平移 (1, 2, 0) 米
import numpy as np
theta = np.radians(30)
R = np.array([[np.cos(theta), -np.sin(theta), 0],
              [np.sin(theta),  np.cos(theta), 0],
              [0,              0,             1]])
t = np.array([[1], [2], [0]])

3.5 完整转换流程(从像素到世界)

把上面三步串起来,就是完整的投影公式:

[ Xw ]                [ fx  0  cx  0 ] [ R  t ] [ u ]
[ Yw ] = inv(Zc) * K * [ 0  fy  cy  0 ] * [     ] * [ v ]
[ Zw ]                [ 0   0   1  0 ] [ 0  1 ] [ 1 ]
[ 1  ]                [ 0   0   0  1 ]

其中 K 是内参矩阵,[R|t] 是外参矩阵。Zc 是深度值。

反过来,从世界坐标到像素坐标,就是投影变换:

s * [u, v, 1]^T = K * [R|t] * [Xw, Yw, Zw, 1]^T

这里的 s 就是 Zc,是个缩放因子。

避坑指南: 我曾经在写环视拼接代码时,把内参矩阵里的 fx 和 fy 搞反了。结果图像拉伸得像哈哈镜。后来我每次标定完,都会用一张棋盘格验证一下投影误差——把世界坐标点投影回图像,看看和实际像素差多少。误差超过1个像素,我就重新标定。

3.6 多摄像头融合中的坐标系对齐

多摄像头融合时,每个摄像头都有自己的相机坐标系。我们需要把它们统一到同一个世界坐标系下。

我常用的做法是:

  1. 选择一个主摄像头(比如前视),以其相机坐标系为基准。
  2. 标定其他摄像头相对于主摄像头的外参(即旋转和平移)。
  3. 所有摄像头的数据都转换到主摄像头坐标系下,再统一转到世界坐标系。

举个例子,假设有左、右两个摄像头:

# 左摄像头到主摄像头的变换
R_left_to_main, t_left_to_main

# 右摄像头到主摄像头的变换
R_right_to_main, t_right_to_main

# 某个点在左摄像头坐标系下的坐标 P_left
# 转换到主摄像头坐标系:
P_main = R_left_to_main * P_left + t_left_to_main

我的经验: 标定多摄像头时,最好用同一个标定板,同时出现在所有摄像头视野中。这样外参标定的一致性会好很多。如果视野不重叠,那就得用间接标定法,误差会大一些。

3.7 总结一下

好了,我们来快速回顾一下:

转换方向 所需参数 关键点
像素 → 图像 主点 (cx, cy)、像素尺寸 (dx, dy) 离散到连续
图像 → 相机 焦距 f、深度 Zc 单目缺深度
相机 → 世界 旋转 R、平移 t(外参) 刚体变换
像素 → 世界(完整) 内参 K + 外参 [R|t] + 深度 Zc 投影公式

说实话,这些公式看起来有点枯燥,但你在实际项目中多调几次bug,就会刻在骨子里了。我记得有一次在车上调试,发现目标检测框总是偏左半个车身。查了半天,原来是外参标定文件里平移向量的单位搞错了——标定给的是毫米,我代码里当米用了。嗯,这种低级错误,犯过一次就不会再犯了。

下一章我们聊聊图像畸变校正,那是另一个容易翻车的地方。