4、相机标定:相机内参、外参、畸变系数、标定板与标定流程
相机标定,说白了就是给相机做一次「视力矫正」和「骨骼对齐」。
你想想看,我们做3D显示,所有立体感的源头都来自相机拍摄的左右眼图像。如果相机本身的参数不准,那后面的视差计算、深度估计全是空中楼阁。我入行那会儿就吃过这个亏——标定没做细,结果重建出来的模型整个是歪的,排查了三天才发现是内参矩阵里一个像素偏移。
嗯,这节课咱们就把相机标定这件事彻底讲透。
4.1 相机模型:小孔成像的数学表达
先回顾一下最基础的成像模型。理想情况下,相机就是一个针孔相机:
- 三维空间中的点 P(X, Y, Z)
- 通过光心投影到成像平面上的点 p(u, v)
- 这个映射关系,就是相机标定要解的核心方程
数学上,这个关系可以写成:
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中:
- K 是内参矩阵(相机自己的属性)
- [R | t] 是外参矩阵(相机在世界坐标系中的位姿)
- s 是尺度因子
核心理解:内参决定「怎么拍」,外参决定「在哪拍」。两者缺一不可。
4.2 内参矩阵:相机的「出厂设置」
内参矩阵 K 长这样:
K = [ fx 0 cx
0 fy cy
0 0 1 ]
每个参数都有物理含义:
| 参数 | 含义 | 典型值(参考) |
|---|---|---|
| fx, fy | 焦距(以像素为单位) | 500~2000 px |
| cx, cy | 主点偏移(光心在图像上的位置) | 图像中心附近 |
我在项目中遇到过一件事:某款工业相机标出来 cx 偏离中心 30 多个像素。一开始以为是标定出错了,后来查手册才发现——这款相机的传感器封装就是偏的。如果不标定,直接用图像中心当主点,那立体匹配的极线约束就全偏了。
我的习惯:每次拿到新相机,第一件事就是做一次内参标定,哪怕厂家给了出厂参数。因为运输震动、温度变化都会让内参漂移。
4.3 外参矩阵:相机在空间中的「站位」
外参矩阵 [R | t] 描述的是:
- R:旋转矩阵(3×3),表示相机朝向
- t:平移向量(3×1),表示相机位置
对于双目立体系统,我们需要标定两个相机之间的相对外参:
R_rel = R_right * R_left^T
t_rel = t_right - R_rel * t_left
这个相对关系,直接决定了左右图像的极线对齐质量。
我曾经调试过一套双目系统,左右图像总是有 2~3 个像素的垂直视差。折腾了两天,最后发现是外参标定的时候标定板放得太近,导致旋转矩阵的估计精度不够。后来把标定板放到 1.5 米以外重新标,垂直视差直接降到 0.3 像素以内。
注意:外参标定对距离非常敏感。标定板距离越远,旋转矩阵的精度越高。我建议标定距离至少是工作距离的 1.5 倍。
4.4 畸变系数:镜头的「真实面目」
没有镜头是完美的。实际镜头都会引入畸变,主要有两类:
| 畸变类型 | 表现 | 数学模型 |
|---|---|---|
| 径向畸变 | 直线变弯(桶形/枕形) | k1, k2, k3 |
| 切向畸变 | 镜头与传感器不平行 | p1, p2 |
畸变矫正公式(以径向畸变为例):
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)
其中 r 是像素到主点的距离。
嗯,这里要注意:畸变系数不是越大越好,也不是越小越好。关键是稳定。我见过一些廉价摄像头,畸变系数 k1 能到 -0.4,但只要标定准确,矫正后一样能用。反倒是那种标定一次一个样的镜头,才真正让人头疼。
避坑指南:我曾经用鱼眼镜头做室内定位,标定出来的 k3 特别大。一开始以为是标定错了,后来才发现——鱼眼镜头的畸变模型需要用到 5 阶甚至 6 阶多项式。标准针孔模型的 3 阶畸变模型根本不够用。
4.5 标定板:标定的「基准尺」
标定板是相机标定的物理基准。常见的标定板类型:
| 类型 | 优点 | 缺点 |
|---|---|---|
| 棋盘格 | 角点检测简单、稳定 | 需要覆盖整个视场 |
| 圆点阵列 | 亚像素精度高 | 光照敏感 |
| ChArUco板 | 自带编码、支持部分遮挡 | 实现复杂 |
我个人习惯用棋盘格,原因很简单:角点检测的算法最成熟,OpenCV 里直接调 findChessboardCorners 就行。但要注意棋盘格的尺寸——
我的经验:棋盘格每个格子的大小,建议在 20mm~40mm 之间。太小了角点检测容易受噪声影响,太大了又需要标定板本身很大,携带不方便。我常用的规格是 30mm 格子,9×7 内角点。
4.6 标定流程:从拍照到参数输出
完整的标定流程,我把它分成 5 步:
- 准备标定板:打印棋盘格,贴在平整的硬板上
- 采集图像:在不同角度、不同距离拍摄 15~30 张
- 角点检测:用算法提取每张图像中的棋盘格角点
- 参数求解:用最小二乘法解算内参、外参、畸变系数
- 重投影验证:用标定结果反算角点位置,计算重投影误差
下面这张图展示了整个标定流程的逻辑关系:
4.7 实战:用 OpenCV 做标定
下面是一段我常用的标定代码骨架:
import cv2
import numpy as np
# 1. 准备棋盘格角点坐标
pattern_size = (9, 6) # 内角点数
square_size = 30.0 # 格子大小(mm)
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1,2)
objp *= square_size
# 2. 遍历图像,检测角点
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
for fname in image_files:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 3. 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
# 4. 重投影误差
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} 像素")
我的建议:标定完成后,一定要做一次畸变矫正的视觉检查。把标定结果应用到一张有明显直线的图像上,看看矫正后的直线是不是真的直了。有时候重投影误差很小,但畸变矫正效果却不理想——这种情况我遇到过两次,都是因为标定图像没有覆盖到图像边缘。
4.8 标定质量评估
标定做完了,怎么知道好不好?我一般看三个指标:
| 指标 | 好 | 一般 | 差 |
|---|---|---|---|
| 重投影误差 | < 0.3 px | 0.3 ~ 0.8 px | > 0.8 px |
| 图像覆盖度 | 边缘+中心都有 | 中心为主 | 只有中心 |
| 角度覆盖度 | ±45° 以上 | ±30° | < ±20° |
嗯,这里要特别说一下图像覆盖度的问题。我见过不少新手,标定的时候把标定板放在画面正中间拍 20 张,结果边缘区域的畸变矫正效果一塌糊涂。你想想看,畸变在边缘最明显,如果标定板从来没到过边缘,那算法怎么知道边缘的畸变参数?
避坑指南:我曾经帮一个团队排查标定问题,他们的重投影误差只有 0.2 像素,但实际矫正效果很差。后来发现——他们用的标定板是 A4 纸打印的,贴在纸板上,纸板本身有轻微弯曲。标定板不平,角点位置就有系统误差,这个误差会直接传递到内参矩阵里。
所以我的习惯是:标定板一定要贴在刚性背板上,最好是亚克力板或者铝板。纸板?不行的。
好了,相机标定的核心内容就这些。内参、外参、畸变系数、标定板、标定流程——这五个要素构成了整个标定体系。下次你拿到一台新相机,按照这个流程走一遍,就能拿到可靠的相机参数,为后面的立体匹配和深度估计打好基础。