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 变换到相机坐标系下。嗯,这里要注意:外参矩阵是刚体变换,不改变尺度,不产生畸变。

你想想看,整个相机标定的流程其实就是:

  1. 世界坐标系下的 3D 点(棋盘格角点)
  2. 通过外参矩阵变换到相机坐标系
  3. 通过内参矩阵投影到像素坐标系
  4. 加上畸变修正

外参矩阵就是连接「真实世界」和「相机视野」的桥梁。

4.4 外参矩阵与位姿估计

实际标定中,每张棋盘格图片都会对应一组外参。因为相机没动,棋盘格动了——或者说,棋盘格没动,相机动了。这取决于你以哪个坐标系为参考。

我记得有一次做多相机标定,三个相机从不同角度拍同一个棋盘格。每个相机都有自己的外参矩阵,我需要把它们统一到同一个世界坐标系下。这时候就涉及到外参矩阵的链式变换:

P_cam2 = R2 · (R1^T · P_cam1 - t1) + t2

说白了,就是先把相机1坐标系下的点变回世界坐标系,再变到相机2坐标系下。这个操作在双目视觉、多视图几何里非常常见。

实际项目经验:

我在做机器人抓取项目时,需要把相机坐标系下的物体位姿转换到机械臂基坐标系。这时候外参矩阵就是相机到机械臂的变换。标定这个变换的过程叫「手眼标定」,本质上就是求解 AX = XB 方程。嗯,这个我们后面章节会详细讲。

4.5 外参矩阵的几何可视化

光讲公式太抽象了。我画了一张图,帮你直观理解外参矩阵在做什么:

外参矩阵:世界坐标系 → 相机坐标系 世界坐标系 (棋盘格) Xw Yw Zw 相机坐标系 (光心) Xc Yc Zc R, t 公式: Pcam = R · Pworld + t 齐次形式:Pcam = [R | t] · Pworld

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 是平移向量,表示世界原点在相机坐标系下的位置
  • 每张标定图片对应一组外参,因为棋盘格相对于相机的位置不同
  • 单位一致性是工程中最容易出错的点,务必检查