4. 外参矩阵

好,我们继续往下走。前面几章我们把内参、畸变聊透了,说白了就是搞清楚了光线怎么穿过镜头、怎么落在传感器上。但有个关键问题一直悬着:相机到底放在世界坐标系里的哪个位置?

你想想看,一个机械臂抓取工件,视觉系统告诉它“目标在像素坐标 (500, 300)”,这完全没用。机械臂需要的是目标在世界坐标系下的 X、Y、Z 值。从像素坐标到世界坐标,中间必须经过一道桥梁——外参矩阵。

我个人习惯把外参理解为“相机的身份证”。它记录了相机在世界坐标系里的位置和朝向。有了它,我们才能把世界坐标系里的一个点,映射到相机坐标系里,然后再用内参去投影到像素平面。

核心公式:

Pcamera = R · Pworld + t

其中 R 是旋转矩阵,t 是平移向量。这个变换就是刚体变换——物体不会变形,只是被挪了个位置、转了个方向。

4.1 刚体变换的本质:不拉伸、不扭曲、只旋转和平移

刚体变换,英文叫 Rigid Body Transformation。名字听着唬人,其实意思很简单:你把一个杯子从桌上拿到柜子里,杯子本身没变形,只是位置和朝向变了。

在三维空间里,任何刚体运动都可以分解为两部分:

  • 旋转:绕 X、Y、Z 三个轴转角度
  • 平移:沿 X、Y、Z 三个方向移动

为什么不能先平移再旋转?嗯,这里要注意:矩阵乘法不满足交换律。先转再挪和先挪再转,结果完全不同。我早期做手眼标定时就踩过这个坑,顺序搞反了,标定出来的结果偏差了十几毫米。

标准做法是:先旋转,后平移。也就是公式里 R 乘在 Pworld 前面,t 加在后面。

4.2 旋转矩阵 R:3×3 的正交矩阵

旋转矩阵 R 是一个 3×3 的矩阵,它有三个重要性质:

  • 正交性:RT · R = I,即矩阵的逆等于它的转置
  • 行列式为 +1:保证是纯旋转,不包含镜像
  • 自由度为 3:虽然矩阵有 9 个元素,但受正交约束限制,实际只有 3 个独立参数

绕 X、Y、Z 轴旋转的基矩阵如下:

旋转轴 旋转矩阵(绕 θ 角) 说明
绕 X 轴 [[1, 0, 0], [0, cosθ, -sinθ], [0, sinθ, cosθ]] X 坐标不变,Y、Z 旋转
绕 Y 轴 [[cosθ, 0, sinθ], [0, 1, 0], [-sinθ, 0, cosθ]] Y 坐标不变,X、Z 旋转
绕 Z 轴 [[cosθ, -sinθ, 0], [sinθ, cosθ, 0], [0, 0, 1]] Z 坐标不变,X、Y 旋转

实际应用中,我们很少直接写这些矩阵。OpenCV 提供了 cv2.Rodrigues() 函数,可以把旋转向量转成旋转矩阵,反过来也行。

import cv2
import numpy as np

# 假设我们有一个旋转向量(3x1)
rvec = np.array([[0.1], [0.2], [0.3]], dtype=np.float64)

# 转成旋转矩阵
R, _ = cv2.Rodrigues(rvec)

print("旋转矩阵 R:")
print(R)

我的经验:标定板上的角点坐标,我习惯用 棋盘格左上角第一个内角点 作为世界坐标系原点。这样平移向量 t 的物理意义最直观——就是相机光心相对于棋盘格原点的偏移量。

4.3 平移向量 t:3×1 的偏移量

平移向量 t 就是相机坐标系原点在世界坐标系下的坐标。说白了,就是相机光心在世界坐标系里的位置。

举个例子:如果 t = [100, 50, 200]T(单位毫米),那就意味着相机光心在世界坐标系的 X 方向偏了 100mm,Y 方向偏了 50mm,Z 方向偏了 200mm。

注意:t 的单位和世界坐标系一致。如果你标定板上的格子是 30mm,那 t 的单位就是毫米。如果你用的是 1m 的标定板,那 t 的单位就是米。千万别混用,我见过有人把毫米和米混在一起,结果算出来的距离差了 1000 倍。

4.4 外参矩阵的齐次形式

为了计算方便,我们通常把旋转和平移合并成一个 4×4 的齐次矩阵:

| R   t |
| 0   1 |

这样,一个世界坐标点 [X, Y, Z, 1]T 乘以这个 4×4 矩阵,就直接得到相机坐标系下的坐标了。一步到位,不用先乘 R 再加 t。

齐次坐标的好处是:可以把多个变换串联起来。比如手眼标定中,机械臂基座到相机、相机到标定板、标定板到机械臂末端,这些变换矩阵可以连乘,最终得到机械臂基座到末端的变换关系。

4.5 外参矩阵的物理意义:从世界到相机

我们来看一个完整的变换流程:

  1. 世界坐标系:标定板上的一个角点,坐标 Pw = [Xw, Yw, Zw]T
  2. 乘以旋转矩阵 R:把世界坐标旋转到与相机坐标系同向
  3. 加上平移向量 t:把原点从世界坐标系原点移到相机光心
  4. 得到相机坐标:Pc = [Xc, Yc, Zc]T

说白了,外参矩阵就是把世界坐标系“搬”到相机坐标系。搬的过程中,物体不能变形,只能转和挪。

我曾经踩过的坑:在做双目立体视觉标定时,左右相机的 R 和 t 是相对于同一个世界坐标系的。但如果你分别标定左右相机,得到的外参是各自独立的,不能直接拿来用。必须做立体标定,计算出左右相机之间的相对旋转和平移。否则,重建出来的三维点会严重扭曲。

4.6 知识结构图:外参矩阵的核心逻辑

下面这张图帮你理清外参矩阵在整个相机标定中的位置和作用:

外参矩阵知识结构图 外参矩阵:从世界坐标到相机坐标 世界坐标系 标定板上的点 外参矩阵 R (旋转) + t (平移) 4×4 齐次矩阵 相机坐标系 光心为原点 Pw Pc 旋转矩阵 R (3×3) 正交矩阵:RT·R = I 行列式 = +1 自由度 = 3(欧拉角/旋转向量) 平移向量 t (3×1) 相机光心在世界系的位置 单位:mm / m(与标定板一致) 三个分量:tx, ty, tz 刚体变换核心公式 Pc = R · Pw + t 先旋转,后平移。顺序不可颠倒!

4.7 外参矩阵的获取方式

实际工程中,外参矩阵通常通过以下方式获得:

方法 适用场景 精度 操作复杂度
张正友标定法 单目相机标定 高 低(拍棋盘格即可)
手眼标定 机械臂+相机系统 中高 高(需要机械臂运动)
PnP 求解 已知 3D-2D 对应点 中 低(OpenCV 直接调用)
直接测量 固定安装、精度要求低 低 低(用尺子量)

我个人最常用的是张正友标定法。你只需要打印一张棋盘格,拍十几张不同角度的照片,OpenCV 的 cv2.calibrateCamera() 就能同时算出内参和外参。每张照片对应一个外参矩阵,因为相机和标定板的相对位置在变。

import cv2
import numpy as np

# 假设你已经提取了所有角点
# objpoints: 世界坐标系下的角点坐标(3D)
# imgpoints: 图像坐标系下的角点坐标(2D)

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

# rvecs 和 tvecs 是列表,每张图片对应一个
# 第 i 张图片的外参
R, _ = cv2.Rodrigues(rvecs[i])  # 旋转向量 -> 旋转矩阵
t = tvecs[i]                     # 平移向量

print(f"第 {i} 张图片的旋转矩阵:\n{R}")
print(f"第 {i} 张图片的平移向量:\n{t}")

我的习惯:标定完成后,我会把外参矩阵保存下来。然后在实际部署时,用 PnP 求解 实时计算当前帧的外参。因为标定板一旦移动,外参就变了。PnP 只需要 4 个以上的 3D-2D 对应点,就能算出新的 R 和 t,非常灵活。

4.8 避坑指南:外参矩阵的常见问题

  • 旋转矩阵不正交:数值计算误差可能导致 R 不满足正交性。可以用 SVD 分解强制正交化:U, _, Vt = np.linalg.svd(R); R_orth = U @ Vt
  • 平移向量单位不一致:标定板格子尺寸用毫米,t 就是毫米;用米,t 就是米。混用会导致灾难性后果。
  • 坐标系定义混乱:OpenCV 的相机坐标系是 X 向右、Y 向下、Z 向前(沿光轴方向)。如果你用其他库,一定要确认坐标系定义是否一致。
  • 外参随标定板移动而改变:每次重新放置标定板,外参都会变。这是正常的,因为世界坐标系是固定在标定板上的。

我曾经踩过的坑:有一次做机器人抓取项目,标定结果总是偏 2-3mm。查了两天,最后发现是旋转矩阵的 欧拉角顺序 搞错了。OpenCV 默认的旋转顺序是 Z-Y-X,而我用的机器人库默认是 X-Y-Z。同样的角度,顺序不同,结果天差地别。从那以后,我每次做手眼标定都会先打印出旋转矩阵看一眼,确认数值合理再往下走。

4.9 小结

外参矩阵是连接世界坐标系和相机坐标系的桥梁。它由旋转矩阵 R 和平移向量 t 组成,本质是一个刚体变换。记住三点:

  • 先旋转,后平移,顺序不能乱
  • R 是正交矩阵,行列式为 +1
  • t 的单位和世界坐标系一致

掌握了外参,你就能把任意一个世界坐标点映射到相机坐标系下。再结合内参,就能算出它在图像上的像素位置。反过来,有了像素坐标和深度信息,也能反推出世界坐标。这就是视觉定位和测量的基础。