4、棋盘格标定法:角点检测、亚像素优化与 calibrateCamera() 参数详解
棋盘格标定,说白了就是给摄像头拍一张「标准答案」的照片,然后反推它的内部参数。我做了这么多年图像质量,这步是绕不开的基本功。今天咱们就把它拆开揉碎了讲清楚。
4.1 棋盘格角点检测:从图像到坐标
角点检测是整个标定的第一步。OpenCV 提供了 findChessboardCorners() 这个函数,它的核心逻辑其实很简单——在图像里找到那些黑白格子相交的点。
核心函数原型:
ret, corners = cv2.findChessboardCorners(image, patternSize, flags=None)
参数说明:
- image:输入图像,最好是灰度图。我习惯先转成灰度再处理,能省不少计算量。
- patternSize:棋盘格的内角点数,格式是 (cols, rows)。注意是内角点,不是格子数。比如 9×6 的棋盘,内角点是 8×5。
- flags:可选标志位,常用的有
CALIB_CB_ADAPTIVE_THRESH和CALIB_CB_NORMALIZE_IMAGE。
我的经验:如果检测失败,先检查 patternSize 对不对。我曾经有个项目,客户给的棋盘是 10×7 的格子,内角点应该是 9×6,结果我写成了 10×7,折腾了半天才发现。
检测流程大致是这样的:
- 对图像做自适应二值化,把棋盘格变成黑白分明的区域。
- 用轮廓检测找到所有四边形区域。
- 筛选出符合棋盘格拓扑结构的四边形集合。
- 在四边形顶点处提取亚像素精度的角点坐标。
嗯,这里要注意:光照不均匀的时候,自适应阈值比固定阈值靠谱得多。我一般都会加上 CALIB_CB_ADAPTIVE_THRESH 这个标志。
4.2 亚像素角点优化:把精度推到小数点后
检测到的角点坐标是整数像素级别的,但实际标定需要更高的精度。这时候就要用 cornerSubPix() 来做亚像素优化。
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (5,5), (-1,-1), criteria)
这个函数的原理是利用角点附近像素的梯度信息,通过迭代优化找到真正的角点位置。说白了,就是在整数像素周围「微调」,找到梯度变化最剧烈的地方。
| 参数 | 含义 | 我的建议值 |
|---|---|---|
| winSize | 搜索窗口大小 | (5,5) 或 (11,11) |
| zeroZone | 死区大小,避免中心点干扰 | (-1,-1) 表示不设死区 |
| criteria | 迭代终止条件 | 30次迭代或0.001像素精度 |
避坑指南:我曾经遇到过亚像素优化后角点反而偏移的情况。后来发现是棋盘格本身有畸变,或者图像噪声太大。这时候先做高斯模糊再优化,效果会好很多。
你想想看,如果角点精度只有 0.5 个像素,标定出来的焦距可能偏差 2-3%。对于高精度测量来说,这误差就太大了。所以亚像素优化这步,千万别省。
4.3 calibrateCamera() 参数详解:核心标定函数
有了角点坐标,接下来就是重头戏——calibrateCamera()。这个函数负责解算摄像头的内参、畸变系数和外参。
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objectPoints, imagePoints, imageSize,
cameraMatrix=None, distCoeffs=None,
flags=0, criteria=None
)
咱们一个一个参数来看:
4.3.1 objectPoints 和 imagePoints
- objectPoints:世界坐标系中的三维点。棋盘格标定中,我们假设棋盘在 Z=0 平面上,所以这些点的 Z 坐标都是 0。
- imagePoints:对应的图像坐标系中的二维点,就是咱们刚才检测到的角点。
这里有个细节:objectPoints 的单位是「格子边长」的倍数。比如格子边长 30mm,那第一个点就是 (0,0,0),第二个是 (30,0,0),以此类推。单位不重要,因为标定解算的是比例关系。
4.3.2 imageSize
图像的宽高,单位是像素。这个参数用来初始化内参矩阵的猜测值。我习惯用 (width, height) 的元组形式传入。
4.3.3 cameraMatrix 和 distCoeffs
这两个参数既是输入也是输出。传入时可以是 None,函数会自动初始化并返回标定结果。
| 输出参数 | 含义 | 典型值范围 |
|---|---|---|
| mtx | 内参矩阵 (3×3) | fx, fy 约等于图像宽高 |
| dist | 畸变系数 (1×5) | k1, k2, p1, p2, k3 |
| rvecs | 旋转向量列表 | 每张图一个 |
| tvecs | 平移向量列表 | 每张图一个 |
4.3.4 flags 参数
这个参数控制标定算法的行为,我常用的几个:
cv2.CALIB_USE_INTRINSIC_GUESS:使用传入的内参矩阵作为初始值。如果你已经知道大概的焦距,用这个能加快收敛。cv2.CALIB_FIX_ASPECT_RATIO:固定 fx/fy 的比例。对于方形像素的传感器,这个很有用。cv2.CALIB_ZERO_TANGENT_DIST:假设切向畸变为零。对于大多数镜头,这个假设是合理的。cv2.CALIB_RATIONAL_MODEL:使用有理模型,可以校正更复杂的畸变。但需要更多图像。
我的习惯:第一次标定用默认 flags=0,看看重投影误差。如果误差大于 0.5 像素,再考虑加 CALIB_RATIONAL_MODEL。别一上来就开全特效,容易过拟合。
4.3.5 criteria 参数
迭代终止条件,和亚像素优化类似。我一般用 (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6)。
4.4 标定流程全景图
说了这么多,咱们用一张图把整个流程串起来:
4.5 完整代码示例
最后,给一个完整的标定代码片段。这是我项目里常用的模板:
import cv2
import numpy as np
import glob
# 棋盘格参数
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
# 存储所有图像的点
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, pattern_size,
flags=cv2.CALIB_CB_ADAPTIVE_THRESH + cv2.CALIB_CB_NORMALIZE_IMAGE)
if ret:
objpoints.append(objp)
# 亚像素优化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (5,5), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化
cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
重要提醒:标定用的图像一定要覆盖整个视场。我见过有人只拍了中心区域的棋盘,结果标定出来的畸变系数完全不靠谱。记住,边缘区域的图像和中心区域的图像一样重要。
好了,棋盘格标定的核心内容就这些。从角点检测到亚像素优化,再到 calibrateCamera() 的参数细节,每一步都有它的道理。实际项目中,多拍几张图、多试几组参数,总能找到最适合你镜头的标定方案。