3. 内参与外参

聊到相机标定,绕不开的就是内参和外参。很多初学者一看到矩阵就头大,觉得这是数学家在折腾人。其实没那么玄乎——说白了,内参是相机自己的“性格”,外参是相机在空间里的“站位”。

我刚开始做视觉项目时,也在这上面栽过跟头。有一次在产线上装了一台相机,怎么标定都对不上,折腾了两天才发现是外参算错了方向。嗯,从那以后我对这两个概念就特别较真。

3.1 相机内参矩阵:相机的“基因密码”

内参矩阵描述的是相机内部的几何特性。它告诉你:三维空间里的一个点,是怎么投影到二维图像上的。

标准形式长这样:

K = [fx  0  cx]
    [ 0  fy cy]
    [ 0   0  1]

每个符号都有物理含义:

符号 含义 单位 典型值范围
fx, fy x、y方向的等效焦距(像素单位) pixel 500 ~ 5000
cx, cy 主点坐标(光轴与像平面交点) pixel 图像中心附近
我的经验:fx 和 fy 理论上应该相等,但实际生产中传感器像素不是完美的正方形,所以会有微小差异。我见过一些廉价工业相机,fx 和 fy 能差出 2~3 个像素——这种相机做高精度测量基本没戏。

内参矩阵的作用,就是把相机坐标系下的点 (Xc, Yc, Zc) 映射到像素坐标 (u, v):

s * [u]   =   K * [Xc]
    [v]           [Yc]
    [1]           [Zc]

这里的 s 是尺度因子,说白了就是归一化用的。你想想看,三维点有深度 Zc,但像素坐标只有二维,这个 s 就是用来消掉深度的。

3.2 畸变系数:镜头不是完美的

理想情况下,镜头应该遵循小孔成像模型。但现实中的镜头都有畸变——尤其是广角和鱼眼镜头,畸变特别明显。

畸变主要分两类:

  • 径向畸变:光线通过透镜边缘时弯曲更厉害,造成图像“桶形”或“枕形”变形
  • 切向畸变:镜头与成像平面不平行,造成图像倾斜

数学上用多项式来建模:

径向畸变:
x_distorted = x * (1 + k1*r² + k2*r⁴ + k3*r⁶)
y_distorted = y * (1 + k1*r² + k2*r⁴ + k3*r⁶)

切向畸变:
x_distorted = x + [2*p1*x*y + p2*(r² + 2*x²)]
y_distorted = y + [p1*(r² + 2*y²) + 2*p2*x*y]

畸变系数向量通常写成 5 个参数:[k1, k2, p1, p2, k3]。k1、k2、k3 是径向畸变系数,p1、p2 是切向畸变系数。

我曾经踩过的坑:有一次标定完发现校正后的图像边缘有奇怪的波纹,查了半天才发现是 k3 参数过拟合了。对于普通工业镜头,只用 k1、k2 就够了,k3 只在高畸变镜头(比如鱼眼)时才需要。别盲目加参数!

3.3 外参:相机在世界中的位置

外参描述的是相机坐标系与世界坐标系之间的变换关系。它由两部分组成:

  • 旋转矩阵 R:3×3 的正交矩阵,描述两个坐标系之间的旋转关系
  • 平移向量 t:3×1 的向量,描述两个坐标系原点之间的位移

外参的数学表达:

[Xc]   =   R * [Xw] + t
[Yc]         [Yw]
[Zc]         [Zw]

或者写成齐次形式:

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

旋转矩阵 R 有 3 个自由度(绕 x、y、z 轴的旋转角),平移向量 t 也有 3 个自由度。所以外参一共 6 个自由度。

关键理解:内参是固定的(相机出厂后不变),外参是变化的(每次移动相机都要重新标定)。我在做机器人手眼标定时,经常需要反复调整外参——因为机器人末端一动,相机的位置就变了。

3.4 完整投影模型:从世界到像素

把内参和外参串起来,就得到了完整的投影公式:

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

这个公式就是相机标定的核心。你想想看,标定板上的角点坐标 (Xw, Yw, Zw) 是已知的,图像上的像素坐标 (u, v) 是检测出来的,未知的就是 K、R、t 和畸变系数。标定的过程,就是解这个方程组。

下面这张图展示了整个流程:

相机成像完整流程 世界坐标系 (Xw, Yw, Zw) 外参 相机坐标系 (Xc, Yc, Zc) 投影 归一化平面 (x, y) 内参+畸变 像素坐标系 (u, v) 关键说明: 1. 世界→相机:通过外参 [R|t] 变换,将世界点转换到相机坐标系 2. 相机→归一化:透视投影,除以 Zc 得到归一化坐标 (x, y) 3. 归一化→像素:通过内参矩阵 K 和畸变模型,得到最终像素坐标 (u, v)

3.5 标定板与角点检测

标定需要已知三维坐标的参考物。最常用的是棋盘格标定板:

  • 棋盘格:黑白相间的方格,角点检测简单可靠
  • 圆点阵:圆形标记,适合高精度标定
  • Charuco板:结合棋盘格和 ArUco 码,支持部分遮挡

我个人习惯用棋盘格,因为 OpenCV 的 findChessboardCorners() 函数非常成熟,鲁棒性很好。但要注意:

我的建议:标定板要打印在平整的硬板上,不要用普通纸。我见过有人用 A4 纸打印标定板,结果纸一弯,角点坐标全偏了,标定出来的内参误差很大。花几十块钱做个亚克力板,省心很多。

3.6 标定流程与参数求解

完整的标定流程如下:

  1. 采集图像:从不同角度拍摄 15~20 张标定板图像
  2. 角点检测:提取每张图像中的棋盘格角点
  3. 初始化内参:用线性方法估算内参矩阵
  4. 初始化外参:对每张图像估算旋转矩阵和平移向量
  5. 非线性优化:用 Levenberg-Marquardt 算法最小化重投影误差
  6. 输出结果:得到内参矩阵、畸变系数、每张图像的外参

OpenCV 中一行代码就能搞定:

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

返回的参数:

变量 含义 形状
mtx 内参矩阵 K 3×3
dist 畸变系数 [k1, k2, p1, p2, k3] 1×5
rvecs 每张图像的旋转向量(可用 Rodrigues 转矩阵) N×3
tvecs 每张图像的平移向量 N×3
注意:旋转向量 rvecs 不是旋转矩阵!需要用 cv2.Rodrigues() 转换成 3×3 的旋转矩阵。我刚开始学的时候直接拿 rvecs 当矩阵用,结果算出来的投影坐标全错了——这个坑我替你们踩过了。

3.7 重投影误差:标定质量的标尺

标定完怎么知道结果好不好?看重投影误差。

简单说,就是把标定板上的三维角点,用标定出来的内参和外参投影回图像,算投影点和实际检测点之间的距离。误差越小,标定越准。

# 计算重投影误差
total_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    total_error += error

print(f"平均重投影误差: {total_error/len(objpoints):.4f} 像素")

一般工业场景要求重投影误差小于 0.5 像素。如果误差超过 1 个像素,说明标定有问题——可能是标定板不平、图像质量差、或者角点检测不准。

我的经验值:用 12mm 镜头配 500 万像素相机,标定 20 张图,重投影误差通常在 0.1~0.3 像素之间。如果超过 0.5,我会重新采集数据。别想着用算法去弥补数据质量的问题——数据不行,算法再牛也白搭。

好了,内参和外参就讲到这里。记住一句话:内参是相机的“基因”,外参是相机的“位置”。搞清楚了这两个概念,后面的畸变校正和三维重建就顺理成章了。