3. 内参与外参
聊到相机标定,绕不开的就是内参和外参。很多初学者一看到矩阵就头大,觉得这是数学家在折腾人。其实没那么玄乎——说白了,内参是相机自己的“性格”,外参是相机在空间里的“站位”。
我刚开始做视觉项目时,也在这上面栽过跟头。有一次在产线上装了一台相机,怎么标定都对不上,折腾了两天才发现是外参算错了方向。嗯,从那以后我对这两个概念就特别较真。
3.1 相机内参矩阵:相机的“基因密码”
内参矩阵描述的是相机内部的几何特性。它告诉你:三维空间里的一个点,是怎么投影到二维图像上的。
标准形式长这样:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
每个符号都有物理含义:
| 符号 | 含义 | 单位 | 典型值范围 |
|---|---|---|---|
| fx, fy | x、y方向的等效焦距(像素单位) | pixel | 500 ~ 5000 |
| cx, cy | 主点坐标(光轴与像平面交点) | pixel | 图像中心附近 |
内参矩阵的作用,就是把相机坐标系下的点 (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 是切向畸变系数。
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 和畸变系数。标定的过程,就是解这个方程组。
下面这张图展示了整个流程:
3.5 标定板与角点检测
标定需要已知三维坐标的参考物。最常用的是棋盘格标定板:
- 棋盘格:黑白相间的方格,角点检测简单可靠
- 圆点阵:圆形标记,适合高精度标定
- Charuco板:结合棋盘格和 ArUco 码,支持部分遮挡
我个人习惯用棋盘格,因为 OpenCV 的 findChessboardCorners() 函数非常成熟,鲁棒性很好。但要注意:
3.6 标定流程与参数求解
完整的标定流程如下:
- 采集图像:从不同角度拍摄 15~20 张标定板图像
- 角点检测:提取每张图像中的棋盘格角点
- 初始化内参:用线性方法估算内参矩阵
- 初始化外参:对每张图像估算旋转矩阵和平移向量
- 非线性优化:用 Levenberg-Marquardt 算法最小化重投影误差
- 输出结果:得到内参矩阵、畸变系数、每张图像的外参
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 |
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 个像素,说明标定有问题——可能是标定板不平、图像质量差、或者角点检测不准。
好了,内参和外参就讲到这里。记住一句话:内参是相机的“基因”,外参是相机的“位置”。搞清楚了这两个概念,后面的畸变校正和三维重建就顺理成章了。