4. 相机标定实战:使用OpenCV进行棋盘格标定
相机标定,说白了就是给相机做一次「视力矫正」。你想想看,镜头天生就有畸变,像素也不是完美的正方形,这些误差在测距时会被放大。我刚开始做双目测距时,就吃过这个亏——标定没做好,测出来的距离偏差了5厘米,后来才发现是标定板拍得太随意了。
这一章,咱们就手把手把标定流程走一遍。从拍棋盘格到算参数,再到评估精度,每一步我都会把坑点讲清楚。
4.1 准备工作:棋盘格与拍摄规范
先说说棋盘格。别小看这张黑白格子纸,它决定了标定的下限。
- 棋盘格尺寸:我建议用 9×6 的内角点(也就是 10×7 的格子),格子边长 20-30mm 比较合适。太大或太小都会影响角点检测的稳定性。
- 打印与粘贴:一定要贴在绝对平整的硬板上。我曾经偷懒贴在纸板上,结果标定出来的畸变参数全是错的。
- 拍摄要求:至少拍 15-20 张不同角度、不同距离的照片。注意:
- 棋盘格要占画面 1/3 以上
- 角度要覆盖俯仰、偏航、旋转
- 远近都要有,从 0.5 米到 3 米
- 避免反光和阴影
4.2 角点提取:findChessboardCorners 的细节
OpenCV 提供了 cv2.findChessboardCorners() 来提取角点。但直接调用往往不够,需要配合亚像素精细化。
import cv2
import numpy as np
# 棋盘格内角点数量 (宽, 高)
pattern_size = (9, 6)
# 准备世界坐标系中的点 (假设棋盘格在Z=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)
# 存储所有图片的点
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:
# 亚像素精细化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
objpoints.append(objp)
imgpoints.append(corners_refined)
# 可视化(可选)
cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
这里有个坑:findChessboardCorners 有时候会失败,尤其是光照不均匀或棋盘格太远。我建议加一个重试机制——如果检测失败,换一个自适应阈值再试一次。
4.3 计算内参和畸变系数
有了角点,就可以调用 cv2.calibrateCamera() 了。这个函数会返回相机矩阵、畸变系数、旋转向量和平移向量。
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("相机内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
输出结果长这样:
| 参数 | 含义 | 典型值 |
|---|---|---|
| fx, fy | 焦距(像素单位) | 约 600-1200 |
| cx, cy | 主点坐标 | 约图像中心 |
| k1, k2, p1, p2, k3 | 畸变系数 | 通常很小(±0.1) |
嗯,这里要注意:dist 是 5 个参数的数组,顺序是 [k1, k2, p1, p2, k3]。k1、k2 是径向畸变,p1、p2 是切向畸变,k3 是更高阶的径向畸变。如果标定结果中 k1 大于 0.5,说明你的镜头畸变很严重,或者标定板拍得不够好。
4.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
mean_error = total_error / len(objpoints)
print(f"平均重投影误差: {mean_error:.4f} 像素")
这个值多少算好?我的经验是:
- < 0.1 像素:非常优秀,可以用于高精度测距
- 0.1 - 0.3 像素:良好,大多数场景够用
- 0.3 - 0.5 像素:及格,需要检查标定过程
- > 0.5 像素:不合格,重新拍吧
4.5 知识体系总览
下面这张图把整个标定流程串起来了,你可以对照着看:
4.6 保存标定结果
标定完记得把参数存下来,后面测距要用。我习惯存成 NumPy 的 .npz 格式,方便加载:
np.savez('calibration_data.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print("标定结果已保存到 calibration_data.npz")
加载的时候也很简单:
data = np.load('calibration_data.npz')
mtx = data['mtx']
dist = data['dist']
嗯,到这里标定就完成了。你可能会问:「标定一次能用多久?」我的经验是,只要不碰镜头、不换焦距,一次标定能用很久。但如果你把相机拆了或者摔了,最好重新标定一次。
公众号:蓝海资料掘金营,微信deep3321