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 多摄像头融合中的坐标系对齐
多摄像头融合时,每个摄像头都有自己的相机坐标系。我们需要把它们统一到同一个世界坐标系下。
我常用的做法是:
- 选择一个主摄像头(比如前视),以其相机坐标系为基准。
- 标定其他摄像头相对于主摄像头的外参(即旋转和平移)。
- 所有摄像头的数据都转换到主摄像头坐标系下,再统一转到世界坐标系。
举个例子,假设有左、右两个摄像头:
# 左摄像头到主摄像头的变换
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,就会刻在骨子里了。我记得有一次在车上调试,发现目标检测框总是偏左半个车身。查了半天,原来是外参标定文件里平移向量的单位搞错了——标定给的是毫米,我代码里当米用了。嗯,这种低级错误,犯过一次就不会再犯了。
下一章我们聊聊图像畸变校正,那是另一个容易翻车的地方。