4. OpenCV实战:使用cv2.findChessboardCorners与cv2.calibrateCamera进行相机标定
相机标定,说白了就是给镜头做「体检」。
我刚开始做视觉项目时,总觉得标定是件很玄乎的事。后来踩过几次坑才明白——标定做不好,后面的三维重建、测距、拼接全是白搭。今天咱们就手把手走一遍OpenCV的标定流程,把棋盘格找角点、算内参、去畸变这些事彻底搞明白。
4.1 标定的核心逻辑
先理清一个概念:相机标定到底在干什么?
简单说,就是建立「真实世界坐标」到「图像像素坐标」的映射关系。这个映射由两部分组成:
- 内参矩阵:焦距、主点位置、像素倾斜因子
- 畸变系数:径向畸变(k1,k2,k3)、切向畸变(p1,p2)
你想想看,一个理想的小孔成像模型,图像应该是完美的矩形。但实际镜头有曲率,边缘会弯曲——这就是畸变。标定就是算出这些畸变参数,然后反向矫正。
关键公式(理解即可,不用背):
投影方程:s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中K是内参矩阵,R和t是外参(旋转和平移)。
4.2 棋盘格:标定的「标准答案」
为什么用棋盘格?因为它有天然的角点——黑白交界处,亚像素精度高,而且图案简单稳定。我个人习惯用7×10的棋盘格(内角点6×9),格子大小20mm或30mm都行。
我的经验:棋盘格一定要打印平整,贴在硬纸板或亚克力板上。我曾经用普通A4纸贴在墙上,结果标定出来的焦距偏差了3%——因为纸面有褶皱。
采集图像时,注意几点:
- 至少拍15-20张不同角度
- 棋盘格要出现在画面各个区域(四角、中心、边缘)
- 角度变化要大:俯仰、偏航、旋转都要有
- 避免纯正面拍摄(那样解算不稳定)
4.3 cv2.findChessboardCorners:找到角点
这个函数是标定的第一步。它会在图像里搜索棋盘格的内角点,返回它们的像素坐标。
import cv2
import numpy as np
# 棋盘格内角点数量(列数-1, 行数-1)
pattern_size = (6, 9) # 7×10的棋盘格
# 准备标定图像
img = cv2.imread('calib_01.jpg')
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)
# 画出来看看
cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
cv2.imshow('Corners', img)
cv2.waitKey(0)
注意:findChessboardCorners对光照敏感。如果棋盘格反光或者阴影太重,它会找不到角点。我建议在均匀光照下拍摄,或者用偏光片消除反光。
为什么会找不到角点?常见原因:
- 棋盘格太小或太大(占画面比例不合适)
- 图像模糊(运动模糊或对焦不准)
- 角度太极端(比如几乎平行于光轴)
4.4 cv2.calibrateCamera:算内参和畸变
找到角点后,就可以交给calibrateCamera了。它会根据多张图像的角点坐标,用最小二乘法解算出内参和畸变系数。
# 准备世界坐标系中的点(假设棋盘格在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)
# 如果格子是20mm,就乘以20
objp *= 20.0 # 单位:mm
# 存储所有图像的角点
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)
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
输出解读:
mtx是3×3矩阵,形如:
[[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
fx、fy是焦距(像素单位),cx、cy是主点坐标。理想情况下cx≈图像宽度/2,cy≈图像高度/2。
dist是5个系数:[k1, k2, p1, p2, k3]。
4.5 去畸变:矫正图像
标定完,就该用结果去矫正图像了。OpenCV提供了两个函数:
# 方法一:直接矫正
dst = cv2.undistort(img, mtx, dist, None, mtx)
# 方法二:先算映射表,再重映射(适合批量处理)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx,
(w, h), cv2.CV_32FC1)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
方法二效率更高,因为映射表可以复用。我一般在批量处理视频帧时用remap,单张图片用undistort就够了。
避坑指南:我曾经在矫正后图像边缘出现黑色区域——这是因为畸变矫正后,边缘像素被拉伸了。解决办法是用cv2.getOptimalNewCameraMatrix调整内参,保留更多有效区域。
# 调整内参,保留全部有效像素
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)
# 裁剪掉黑色边框
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
4.6 评估标定质量
标定完怎么知道好不好?看重投影误差。
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("平均重投影误差: ", total_error / len(objpoints))
误差小于0.5像素算不错,小于0.3像素算优秀。如果误差大于1像素,说明标定图像质量有问题——要么角点检测不准,要么图像数量不够。
我的教训:有一次标定误差0.8像素,我以为是算法问题。折腾了两天,最后发现是棋盘格打印时尺寸不对——格子实际是19.5mm,我代码里写的是20mm。所以,物理尺寸一定要量准。
4.7 完整流程总结
嗯,到这里标定的核心流程就走完了。我画了个流程图,帮你理清思路:
标定这件事,说白了就是「用已知去推算未知」。棋盘格提供了已知的世界坐标,角点检测提供了像素坐标,calibrateCamera负责解方程。只要数据质量好,结果就靠谱。
我个人建议:标定完一定要做一次实际验证——拍一张已知尺寸的物体,用矫正后的图像去测量,看看误差在不在容忍范围内。理论再漂亮,不如实测一锤定音。