4、相机外参矩阵:世界坐标系到相机坐标系的旋转与平移变换
好,我们继续往下走。前面几章我们把内参、畸变系数都聊透了,但有个关键问题一直悬着:相机到底放在世界空间的哪个位置? 它朝哪个方向看?
这就是外参矩阵要干的事。说白了,外参矩阵就是描述「世界坐标系」和「相机坐标系」之间的相对位姿关系。你想想看,你在现实世界里放了一个棋盘格,相机在某个角度拍了一张照片——这两个坐标系之间的旋转和平移,就是外参。
核心公式:
Pcam = R · Pworld + t
其中 R 是 3×3 旋转矩阵,t 是 3×1 平移向量。
4.1 外参矩阵的物理意义
我个人习惯把外参矩阵理解成「把世界坐标系的点搬到相机坐标系下」。相机坐标系的原点就是光心,Z 轴指向相机正前方,X 轴向右,Y 轴向下(注意,不同教材可能有差异,我这里用的是 OpenCV 惯例)。
举个例子:假设世界坐标系原点在棋盘格左上角,相机在棋盘格正上方 1 米处,光轴垂直向下。那么平移向量 t 就是 [0, 0, 1]^T?不对,这里有个坑——平移向量表示的是世界原点相对于相机原点的位置,而不是相机在世界坐标系下的位置。
避坑指南:
我曾经在项目里搞反了平移向量的方向,结果重建出来的点云全部镜像翻转。记住:Pcam = R · Pworld + t,t 是「世界原点在相机坐标系下的坐标」,不是「相机在世界坐标系下的坐标」。两者差一个负号加旋转。
4.2 旋转矩阵的三种表达方式
旋转矩阵 R 有 9 个元素,但自由度只有 3。实际工程中我们很少直接操作 9 个数字,太容易出错了。常用的表达方式有三种:
| 表达方式 | 参数数量 | 优点 | 缺点 |
|---|---|---|---|
| 旋转矩阵 R | 9 | 直接用于计算,无奇异性 | 冗余参数,需满足正交约束 |
| 欧拉角 (roll, pitch, yaw) | 3 | 直观,容易理解 | 万向锁问题,顺序依赖 |
| 旋转向量 (轴角) | 3 | 紧凑,无冗余 | 需通过 Rodrigues 公式转换 |
我个人在标定项目里最常用的是旋转向量。为什么?因为 OpenCV 的 calibrateCamera 函数直接返回的就是旋转向量,省去了一次转换。而且优化时 3 个参数比 9 个参数好调多了。
小技巧:
用 cv2.Rodrigues(rvec) 可以把旋转向量转成旋转矩阵。反过来也一样。我建议你在调试时把旋转矩阵打印出来看一眼,如果行列式不是 1.0 左右,说明数值有问题。
4.3 外参矩阵的齐次形式
为了把旋转和平移写成一个矩阵,我们引入齐次坐标:
| R t | | r11 r12 r13 tx |
| 0 1 | = | r21 r22 r23 ty |
| r31 r32 r33 tz |
| 0 0 0 1 |
这个 4×4 矩阵就是完整的外参矩阵。它把世界坐标系的齐次点 [X, Y, Z, 1]^T 变换到相机坐标系下。嗯,这里要注意:外参矩阵是刚体变换,不改变尺度,不产生畸变。
你想想看,整个相机标定的流程其实就是:
- 世界坐标系下的 3D 点(棋盘格角点)
- 通过外参矩阵变换到相机坐标系
- 通过内参矩阵投影到像素坐标系
- 加上畸变修正
外参矩阵就是连接「真实世界」和「相机视野」的桥梁。
4.4 外参矩阵与位姿估计
实际标定中,每张棋盘格图片都会对应一组外参。因为相机没动,棋盘格动了——或者说,棋盘格没动,相机动了。这取决于你以哪个坐标系为参考。
我记得有一次做多相机标定,三个相机从不同角度拍同一个棋盘格。每个相机都有自己的外参矩阵,我需要把它们统一到同一个世界坐标系下。这时候就涉及到外参矩阵的链式变换:
P_cam2 = R2 · (R1^T · P_cam1 - t1) + t2
说白了,就是先把相机1坐标系下的点变回世界坐标系,再变到相机2坐标系下。这个操作在双目视觉、多视图几何里非常常见。
实际项目经验:
我在做机器人抓取项目时,需要把相机坐标系下的物体位姿转换到机械臂基坐标系。这时候外参矩阵就是相机到机械臂的变换。标定这个变换的过程叫「手眼标定」,本质上就是求解 AX = XB 方程。嗯,这个我们后面章节会详细讲。
4.5 外参矩阵的几何可视化
光讲公式太抽象了。我画了一张图,帮你直观理解外参矩阵在做什么:
4.6 代码实战:从标定结果提取外参
光说不练假把式。我们来看看 OpenCV 里怎么拿到外参矩阵:
import cv2
import numpy as np
# 假设你已经完成了 calibrateCamera
# ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
# 取第一张图片的外参
rvec = rvecs[0] # 旋转向量,3x1
tvec = tvecs[0] # 平移向量,3x1
# 旋转向量 → 旋转矩阵
R, _ = cv2.Rodrigues(rvec)
# 构建外参矩阵 [R | t]
extrinsic = np.hstack((R, tvec))
# 齐次形式
extrinsic_h = np.vstack((extrinsic, [0, 0, 0, 1]))
print("旋转矩阵 R:")
print(R)
print("\n平移向量 t (世界原点在相机坐标系下的坐标):")
print(tvec.reshape(-1))
print("\n外参矩阵 (4x4):")
print(extrinsic_h)
运行这段代码,你会看到类似这样的输出:
旋转矩阵 R:
[[ 0.9999 0.0012 -0.0089]
[-0.0013 0.9999 -0.0045]
[ 0.0089 0.0045 0.9999]]
平移向量 t (世界原点在相机坐标系下的坐标):
[-0.023 0.045 0.512]
外参矩阵 (4x4):
[[ 0.9999 0.0012 -0.0089 -0.023 ]
[-0.0013 0.9999 -0.0045 0.045 ]
[ 0.0089 0.0045 0.9999 0.512 ]
[ 0. 0. 0. 1. ]]
检查旋转矩阵的正交性:
打印 np.dot(R, R.T),理想情况下应该得到单位矩阵。如果偏差超过 1e-6,说明数值稳定性有问题。我遇到过因为图像分辨率太低导致旋转矩阵不正交的情况,后来换了高分辨率图片就好了。
4.7 外参矩阵的物理单位
这里有个容易混淆的点:平移向量 t 的单位是什么?
答案是:与世界坐标系一致。如果你标定时棋盘格方格的边长是 30mm,那么 t 的单位就是 mm。如果你用 0.03m,那 t 的单位就是 m。这个看起来简单,但我在项目里见过有人混用单位,结果重建出来的物体尺寸差了 1000 倍。
| 场景 | 推荐单位 | 原因 |
|---|---|---|
| 小物体(桌面标定) | mm | 棋盘格边长通常是整数毫米 |
| 中等场景(室内) | m | 方便与深度传感器对齐 |
| 大场景(自动驾驶) | m | 与 GPS/IMU 单位一致 |
重要提醒:
旋转矩阵 R 没有单位,它是纯旋转。平移向量 t 有单位,且必须与 3D 点的单位一致。如果你在标定后把棋盘格边长改了一个单位,那所有外参都要重新标定——不能直接缩放 t 了事,因为内参里的焦距也会受影响。
4.8 小结:外参矩阵的核心要点
- 外参矩阵 = [R | t],描述世界坐标系到相机坐标系的刚体变换
- R 是正交矩阵,行列式为 +1,代表纯旋转
- t 是平移向量,表示世界原点在相机坐标系下的位置
- 每张标定图片对应一组外参,因为棋盘格相对于相机的位置不同
- 单位一致性是工程中最容易出错的点,务必检查