第四章:张正友标定法——棋盘格标定、角点检测、单应性矩阵、标定流程详解
说到相机标定,张正友标定法绝对是绕不开的经典。我入行那会儿,第一次做视觉测量项目,用的就是这套方法。说白了,它解决了我们最头疼的问题:怎么用一张打印的棋盘格,把相机的内参和外参算出来。
你可能会问:为什么非得用棋盘格?嗯,这里有个小秘密——棋盘格的角点检测特别稳定,而且它的几何结构简单,数学上容易处理。我个人习惯用7×9或8×10的棋盘格,太大太小都不好。
4.1 棋盘格标定:准备工作
先说说棋盘格。你打印一张棋盘格,贴在平板上,然后从不同角度拍十几张照片。注意,这里有个坑——棋盘格必须平整。我曾经用普通打印纸贴在纸板上,结果纸板有点弯,标定出来的参数偏差很大。
拍照时要注意几点:
- 棋盘格要占画面1/3以上
- 角度变化要丰富(俯仰、旋转、平移)
- 光照均匀,不要有反光
- 至少拍10-15张
4.2 角点检测:找到棋盘格的“锚点”
角点检测是标定的第一步。我们要找到棋盘格上每个黑白格子的交点。OpenCV里有个现成的函数——cv2.findChessboardCorners()。
它的原理其实不复杂:先对图像做二值化,然后找轮廓,再通过轮廓的拓扑关系定位角点。但实际用起来,你会发现有些照片就是检测不到角点。为什么?
我遇到过这种情况:光线太暗,或者棋盘格太远,角点就糊了。解决办法是:调整图像对比度,或者用cv2.cornerSubPix()做亚像素精度的角点定位。
import cv2
import numpy as np
# 棋盘格尺寸(内角点数量)
pattern_size = (7, 9) # 7列9行内角点
# 读取图像
img = cv2.imread('chessboard.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_sub = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
# 绘制角点
cv2.drawChessboardCorners(img, pattern_size, corners_sub, ret)
cv2.imshow('Corners', img)
cv2.waitKey(0)
4.3 单应性矩阵:连接世界坐标与图像坐标的桥梁
单应性矩阵(Homography)是张正友标定法的核心。它描述的是一个平面到另一个平面的投影变换。在标定中,就是棋盘格平面到像平面的变换。
数学上,单应性矩阵是一个3×3的矩阵,有8个自由度。为什么是8个?因为它是齐次坐标,可以归一化。你想想看,一个点从世界坐标(u, v)映射到图像坐标(x, y),关系是这样的:
[x] [h11 h12 h13] [u]
[y] = [h21 h22 h23] [v]
[1] [h31 h32 h33] [1]
每对匹配点可以提供两个方程,所以至少需要4对点才能解出H矩阵。但实际中,我们通常用更多点(比如棋盘格上的所有角点),然后用最小二乘法求解。
我记得有一次做项目,发现标定结果总是不对。排查了半天,原来是棋盘格上的角点坐标顺序搞反了。嗯,这里要提醒你:世界坐标系的定义必须一致,否则单应性矩阵算出来就是错的。
4.4 标定流程详解:一步步来
张正友标定法的完整流程,我画了一张图帮你理解:
流程看起来简单,但每一步都有细节。我重点说说第4步到第6步——这是标定的核心。
4.5 内参求解:从单应性矩阵到相机内参
张正友标定法的巧妙之处在于:利用单应性矩阵的约束条件,直接解出内参。具体来说,每个单应性矩阵H可以分解为:
H = K [r1 r2 t]
其中K是内参矩阵,r1、r2是旋转矩阵的前两列,t是平移向量。因为r1和r2是正交的,所以可以得到两个约束方程。多张照片就能解出K。
这里有个数学细节:内参矩阵K有5个参数(fx, fy, cx, cy, s),但实际中我们通常假设s=0(像素无倾斜)。所以至少需要3张照片才能解出全部内参。
内参矩阵 K = [fx, s, cx; 0, fy, cy; 0, 0, 1]
其中fx, fy是焦距,cx, cy是主点坐标,s是倾斜因子(通常为0)
4.6 非线性优化:让结果更精确
闭合解虽然能算出内参,但精度不够。为什么?因为闭合解没有考虑畸变。所以张正友标定法最后一步是非线性优化,用Levenberg-Marquardt算法最小化重投影误差。
重投影误差是什么?说白了,就是把世界坐标系中的角点,用标定结果投影到图像上,然后跟实际检测到的角点位置比较。误差越小,标定越准。
OpenCV里用cv2.calibrateCamera()一步到位:
# 准备世界坐标
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:
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)
4.7 标定结果评估:怎么看标得好不好
标定完了,怎么知道结果好不好?我一般看三个指标:
| 指标 | 理想值 | 说明 |
|---|---|---|
| 重投影误差 | < 0.5像素 | 所有角点的平均投影误差 |
| 主点坐标 | 接近图像中心 | cx ≈ 图像宽度/2, cy ≈ 图像高度/2 |
| 焦距 | 与镜头参数一致 | fx ≈ fy(对于方形像素) |
如果重投影误差大于1像素,那标定结果基本不能用。我遇到过这种情况:检查后发现是棋盘格不平整,或者照片有运动模糊。重新拍一组就好了。
4.8 实际应用中的注意事项
最后,分享几个我在项目中积累的经验:
- 棋盘格尺寸要准确:每个格子的实际物理尺寸(比如30mm)必须精确测量,否则标定出来的尺度是错的。
- 照片要覆盖整个视场:棋盘格出现在画面的各个位置,这样畸变校正才准确。
- 不要用鱼眼镜头:张正友标定法假设畸变较小,鱼眼镜头的畸变太大,需要专门的标定方法。
- 定期重新标定:相机如果受到震动或温度变化,内参可能会变。我一般每3个月重新标定一次。
嗯,张正友标定法就讲到这里。这套方法虽然经典,但用好了,能解决90%的视觉测量问题。下次你遇到标定不准的情况,不妨回头看看这几个步骤——多半是角点检测或者照片质量出了问题。
公众号:蓝海资料掘金营,微信deep3321