4. 相机标定基础:张正友标定法原理、标定板设计、OpenCV标定流程
说到视觉定位,有个坎儿你绕不过去——相机标定。我刚开始做SLAM那会儿,总觉得标定是件小事,随便拍几张棋盘格就跑。结果呢?定位误差大得离谱,回环检测也老是抽风。后来才明白,相机标定是整个视觉系统的基石。你想想看,如果相机本身的内参都不准,后面的位姿估计、三角测量、三维重建,那全是空中楼阁。
这一节,我们就来聊聊最经典的张正友标定法。为什么选它?因为它在精度和操作便利性之间取得了很好的平衡。你不需要昂贵的三维标定块,只需要打印一张棋盘格,拍几张不同角度的照片,就能把相机的内参、外参、畸变系数全算出来。说白了,这就是工业界和学术界最通用的标定方案。
核心要点:张正友标定法的本质,是利用二维平面靶标(棋盘格)上的已知角点,建立从世界坐标系到像素坐标系的投影关系,然后通过最小化重投影误差来求解相机参数。
4.1 张正友标定法原理
张正友标定法的数学基础,其实就两个核心步骤:
- 计算单应性矩阵——建立棋盘格平面与图像平面之间的映射关系
- 分解内参和外参——从单应性矩阵中提取出相机的内部参数和外部参数
具体来说,我们假设棋盘格平面位于世界坐标系的 Z=0 平面上。那么,一个三维点 (X, Y, 0) 到像素坐标 (u, v) 的投影关系可以写成:
s * [u, v, 1]^T = K * [r1, r2, t] * [X, Y, 1]^T
这里的 K 是内参矩阵,r1、r2 是旋转矩阵的前两列,t 是平移向量。这个 3×3 的矩阵 H = K * [r1, r2, t] 就是我们说的单应性矩阵。
嗯,这里要注意:单应性矩阵有 8 个自由度,而一个角点可以提供两个方程。所以理论上,你至少需要 4 个角点才能解出 H。但实际中,棋盘格上通常有几十个角点,我们用最小二乘法来求解,这样更鲁棒。
我的经验:我在项目中遇到过标定板反光严重的情况,角点检测总是跳来跳去。后来我改用哑光纸打印棋盘格,并且在拍摄时避免强光直射,角点检测的稳定性明显提升。另外,棋盘格的黑白格子数量最好是奇数×奇数,这样中心点更容易定位。
得到单应性矩阵后,怎么分解出内参呢?这里利用了旋转矩阵的正交性约束。因为 r1 和 r2 是单位正交的,所以:
r1^T * r2 = 0
||r1|| = ||r2|| = 1
把 H = K * [r1, r2, t] 代入这两个约束,就能得到关于内参的线性方程。多张图片提供多个约束,最后用最小二乘法解出内参矩阵 K。
内参解出来后,外参也就顺理成章了:
r1 = λ * K^(-1) * h1
r2 = λ * K^(-1) * h2
t = λ * K^(-1) * h3
其中 λ 是归一化因子。最后再用 SVD 把旋转矩阵投影到 SO(3) 流形上,保证它是真正的旋转矩阵。
4.2 标定板设计
标定板的设计,看似简单,其实有不少门道。我见过有人随便在A4纸上画个网格就去标定,结果精度一塌糊涂。标定板的设计直接影响角点检测的精度和标定结果的稳定性。
常见的标定板类型有两种:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 棋盘格(Checkerboard) | 角点检测简单,亚像素精度高 | 大多数通用场景 |
| 圆点阵列(Circle Grid) | 圆心检测精度高,对畸变不敏感 | 大畸变镜头、鱼眼相机 |
我个人习惯用棋盘格,因为 OpenCV 的 findChessboardCorners 函数非常成熟,几乎不需要调参。但如果你用的是鱼眼镜头或者广角镜头,圆点阵列可能更合适——圆心的检测不受径向畸变影响,精度更稳定。
避坑指南:我曾经用一张褶皱的棋盘格去标定,结果内参算出来焦距偏差了 5%。后来才发现,标定板必须贴在绝对平整的硬板上,任何弯曲都会引入系统误差。另外,棋盘格的格子尺寸要精确测量,误差控制在 0.1mm 以内。你想想看,如果格子尺寸标错了,那整个标定结果就全歪了。
标定板的设计参数,我建议这样选:
- 格子数量: 9×7 或 11×8(内角点数量),太少约束不够,太多容易过拟合
- 格子尺寸: 20mm~30mm,太小了角点检测困难,太大了需要更大的标定板
- 标定板尺寸: 覆盖相机视场的 1/3 到 2/3,太小了标定精度差
- 材质: 哑光纸 + 硬质底板,避免反光和弯曲
4.3 OpenCV标定流程
好了,理论讲完了,咱们直接上代码。OpenCV 的标定流程其实就几个步骤,但每一步都有坑。我带你走一遍。
4.3.1 采集标定图片
首先,你需要拍摄 15~20 张不同角度的标定板图片。注意,不是随便拍,要保证:
- 标定板出现在图像的各个区域(左上、右下、中心、边缘)
- 标定板与相机平面的夹角在 0°~45° 之间变化
- 标定板在图像中的大小占 1/4 到 3/4
- 避免运动模糊和过曝
我记得有一次,一个学生拍了 30 张图片,结果标定误差还是很大。我一看,他所有图片的标定板都在图像正中央,角度也几乎没变。这相当于只提供了一个约束,内参当然解不出来。说白了,标定图片的多样性比数量更重要。
4.3.2 角点检测
OpenCV 提供了现成的角点检测函数:
import cv2
import numpy as np
# 棋盘格内角点数量(列数-1,行数-1)
pattern_size = (9, 6)
# 格子尺寸(单位:mm)
square_size = 25.0
# 准备世界坐标系中的角点坐标
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
# 存储所有图片的角点
obj_points = [] # 世界坐标系中的点
img_points = [] # 图像坐标系中的点
for img_path in image_paths:
img = cv2.imread(img_path)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
# 亚像素精细化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
obj_points.append(objp)
img_points.append(corners)
# 可视化角点
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
我的经验:亚像素精细化这一步千万别省。我刚开始做的时候,直接用 findChessboardCorners 的原始结果去标定,重投影误差一直在 0.5 像素以上。加上 cornerSubPix 之后,误差直接降到 0.1 像素。这 0.4 像素的差距,在视觉定位中可能就是几厘米的误差。
4.3.3 执行标定
角点检测完成后,调用 calibrateCamera 函数即可:
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
obj_points, img_points, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
输出结果中:
mtx:内参矩阵,包含 fx, fy, cx, cydist:畸变系数,包含 k1, k2, p1, p2, k3ret:重投影误差,通常应小于 0.3 像素
如果重投影误差大于 0.5 像素,说明标定质量有问题。我建议你检查一下:角点检测是否准确?标定板是否平整?图片是否足够多样?
4.3.4 畸变校正
标定完成后,可以用内参和畸变系数对图像进行校正:
# 畸变校正
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
# 方法一:直接校正
dst = cv2.undistort(img, mtx, dist, None, new_mtx)
# 方法二:映射校正(更高效)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, new_mtx, (w, h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 裁剪掉黑色边框
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
避坑指南:我曾经直接用 undistort 校正图像,结果边缘区域出现了严重的拉伸变形。后来才发现,getOptimalNewCameraMatrix 可以自动计算一个合适的新内参矩阵,在保留更多图像内容和减少畸变之间取得平衡。另外,如果你做的是视觉定位,建议用映射校正(方法二),因为 remap 可以预先计算好映射表,实时运行时效率更高。
4.4 标定质量评估
标定完了,怎么知道结果好不好?我一般看三个指标:
- 重投影误差: 小于 0.3 像素算合格,小于 0.1 像素算优秀
- 内参稳定性: 用不同的图片子集标定,看内参的波动范围
- 畸变校正效果: 看校正后的图像中,直线是否真的变直了
我个人习惯在标定完成后,把标定板放在场景中,用标定结果去反投影角点,看看误差分布。如果误差在图像中心区域很小,边缘区域很大,说明畸变模型可能不够用(比如需要更多的畸变参数)。
好了,相机标定的基础就讲到这里。张正友标定法虽然经典,但实际应用中还有很多细节需要注意。下一节我们会聊聊标定误差的来源和补偿方法,到时候再深入展开。