4、单目摄像头标定实战:棋盘格角点检测与calibrateCamera详解
好,咱们进入真正的动手环节了。前面几章我们把相机模型、畸变原理、标定板设计都捋了一遍,现在终于要拿OpenCV干活了。这一章我会带着你走完单目标定的完整流程——从棋盘格角点检测,到调用calibrateCamera,再到评估标定结果好不好。
说实话,标定这件事,理论看十遍不如亲手跑一遍。我当年第一次标定车载环视摄像头时,以为调个API就完事了,结果重投影误差大得离谱,排查了两天才发现是角点检测时棋盘格图像没拍好。嗯,这些坑我会在下面一一给你指出来。
4.1 棋盘格角点检测:findChessboardCorners
角点检测是标定的第一步,也是最容易出问题的一步。说白了,就是让OpenCV在图像里找到棋盘格上那些黑白方块的交点。
核心API就两个:
ret, corners = cv2.findChessboardCorners(image, patternSize, flags)
corners2 = cv2.cornerSubPix(gray, corners, winSize, zeroZone, criteria)
参数说明:
| 参数 | 含义 | 我的建议 |
|---|---|---|
| image | 输入图像(灰度或彩色) | 直接传灰度图,省去内部转换开销 |
| patternSize | 棋盘格内角点数,如(9,6) | 注意是内角点,不是格子数! |
| flags | 检测算法标志 | 一般用CALIB_CB_ADAPTIVE_THRESH就够了 |
| winSize | 亚像素搜索窗口半尺寸 | 我习惯用(5,5),太小不稳定 |
| criteria | 迭代终止条件 | (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) |
我曾经踩过的坑: patternSize传成了棋盘格的行列数(10,7),而不是内角点数(9,6)。OpenCV不会报错,但角点位置全偏了,标定出来的内参完全不能用。记住:内角点数 = 格子数 - 1。
检测到角点后,我强烈建议你用cv2.drawChessboardCorners把角点画出来看看。肉眼确认一下,比什么都靠谱。
cv2.drawChessboardCorners(image, patternSize, corners, ret)
cv2.imshow('Corners', image)
4.2 calibrateCamera函数详解
角点检测完了,就该进入核心环节了。calibrateCamera这个函数,说白了就是求解相机内参矩阵和畸变系数的最小二乘优化问题。
函数签名长这样:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objectPoints, imagePoints, imageSize,
cameraMatrix, distCoeffs,
flags=None, criteria=None
)
输出参数解读:
- ret:重投影误差的RMS值,越小越好。我一般要求<0.5像素才算合格。
- mtx:相机内参矩阵,3x3,包含fx, fy, cx, cy。
- dist:畸变系数向量,默认5个参数(k1,k2,p1,p2,k3)。
- rvecs, tvecs:每张图片的旋转向量和平移向量,标定板相对于相机的外参。
个人经验: 我习惯在调用calibrateCamera之前,先把cameraMatrix初始化为单位矩阵,distCoeffs初始化为全零。这样优化起点比较中性,收敛更快。
flags参数我常用的几个:
| 标志 | 作用 | 什么时候用 |
|---|---|---|
| cv2.CALIB_FIX_K3 | 固定k3为0 | 图片数量少时,防止过拟合 |
| cv2.CALIB_ZERO_TANGENT_DIST | 固定p1,p2为0 | 确认镜头没有切向畸变时 |
| cv2.CALIB_FIX_PRINCIPAL_POINT | 固定主点(cx,cy) | 已知主点在图像中心时 |
4.3 标定结果评估:重投影误差
标定完了,怎么知道结果好不好?最直观的指标就是重投影误差。
原理很简单:用标定得到的内参和外参,把世界坐标系中的棋盘格角点投影回图像平面,算一下投影点和实际检测到的角点之间的距离。这个距离的平均值就是重投影误差。
OpenCV提供了现成的函数:
total_error = 0
for i in range(len(objectPoints)):
imgpoints2, _ = cv2.projectPoints(
objectPoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imagePoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
total_error += error
print(f"平均重投影误差: {total_error/len(objectPoints):.4f} 像素")
我的判断标准:
- < 0.3像素:优秀,可以用于高精度环视拼接
- 0.3 - 0.5像素:良好,满足大部分车载应用
- 0.5 - 1.0像素:及格,需要检查标定图片质量
- > 1.0像素:不合格,重新采集数据吧
为什么会重投影误差大?我遇到过的情况有:
- 标定图片模糊,角点检测不准
- 棋盘格没有覆盖整个视野
- 图片数量太少(我建议至少15-20张)
- 标定板平面度不够(车载环视常用亚克力板,容易变形)
4.4 标定流程全景图
下面这张图把整个单目标定流程串起来了,你可以对照着走一遍:
4.5 完整代码示例
最后,给你一个可以直接跑的完整脚本。这是我个人习惯的写法,注释都写清楚了:
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,2)
objpoints = [] # 世界坐标系点
imgpoints = [] # 图像坐标系点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (5,5), (-1,-1), criteria)
imgpoints.append(corners2)
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print(f"内参矩阵:\n{mtx}")
print(f"畸变系数:\n{dist}")
print(f"重投影误差: {ret:.4f} 像素")
注意: 标定图片不要用JPEG压缩太狠的,压缩伪影会影响角点检测精度。我习惯用PNG或者低压缩率的JPEG。
好了,这一章的内容就到这儿。你拿着上面的代码和流程,找一组标定图片跑一遍,很快就能上手。标定这件事,动手做一次比看十遍书都管用。