第三章 张正友标定法:棋盘格角点检测、单应性矩阵求解、内参外参估计、畸变参数计算
张正友标定法,说白了就是用一个棋盘格照片,把相机内参、外参、畸变参数全算出来。这个方法在工业界用得特别多,我这些年做仿生眼系统,几乎每个项目都离不开它。为什么这么流行?因为它操作简单,精度又高,你只需要打印一张棋盘格,拍几张不同角度的照片就行。
嗯,咱们一步步来看,这个方法到底是怎么工作的。
3.1 棋盘格角点检测
第一步,得从图像里找到棋盘格的角点。你想想看,棋盘格上那些黑白交界的点,就是我们要找的角点。这些点位置精确,容易识别,是后续计算的基础。
我个人习惯用 OpenCV 的 findChessboardCorners 函数。它内部用的是 Harris 角点检测加上亚像素精化。我在项目中遇到过一个问题:如果棋盘格反光太厉害,角点就找不准。后来我加了个自适应阈值处理,效果好了很多。
核心步骤:
- 将图像转为灰度图
- 使用 Harris 角点检测找到候选角点
- 通过亚像素精化提高角点精度
- 按棋盘格的行列顺序排列角点
import cv2
import numpy as np
# 读取图像
img = cv2.imread('chessboard.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 棋盘格尺寸(内角点数量)
pattern_size = (9, 6)
# 查找角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
# 亚像素精化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
# 绘制角点
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
小技巧:我曾经在光线不均匀的环境下标定,角点检测老是失败。后来我改用 cv2.adaptiveThreshold 做预处理,问题就解决了。你如果遇到类似情况,可以试试。
3.2 单应性矩阵求解
找到角点之后,我们要建立图像坐标和世界坐标之间的关系。这个关系用一个 3x3 的矩阵表示,叫单应性矩阵 H。
为什么会用到单应性矩阵?因为棋盘格是平面的,所以图像点和世界点之间可以用一个投影变换来描述。说白了,H 矩阵就是把三维空间中的平面点映射到二维图像上。
求解 H 矩阵需要至少 4 对匹配点。实际中我们会用所有角点,通过最小二乘法来求解。我记得有一次做双目仿生眼标定,左右眼的 H 矩阵算出来差异很大,后来发现是棋盘格没放平。嗯,这里要注意,棋盘格一定要平整。
# 构建世界坐标系中的点
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)
# 求解单应性矩阵
H, mask = cv2.findHomography(objp[:, :2], corners.reshape(-1, 2))
注意:单应性矩阵求解时,如果角点分布不均匀,或者图像有畸变,结果会不准确。我建议你多拍几张不同角度的照片,取平均值。
3.3 内参外参估计
有了单应性矩阵 H,我们就可以估计相机的内参和外参了。内参包括焦距、主点坐标,外参包括旋转矩阵和平移向量。
这个过程分两步:
- 第一步:从 H 矩阵中分解出内参。利用旋转矩阵的正交性,可以列出约束方程,然后通过 SVD 分解求解。
- 第二步:用内参和 H 矩阵反推外参。旋转矩阵 R 和平移向量 t 都可以直接算出来。
我个人习惯用 OpenCV 的 calibrateCamera 函数,它把这些步骤都封装好了。你只需要提供角点坐标和世界坐标就行。
# 准备数据
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
# 假设有多个棋盘格图像
for img_path in image_paths:
img = cv2.imread(img_path)
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 包含 fx, fy, cx, cy。fx 和 fy 是焦距,cx 和 cy 是主点坐标。这些参数决定了相机的成像模型。
3.4 畸变参数计算
镜头畸变是不可避免的。常见的畸变有径向畸变和切向畸变。径向畸变让直线变弯,切向畸变让图像倾斜。
张正友标定法用 5 个参数来描述畸变:k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。这些参数通过最小化重投影误差来求解。
我在项目中遇到过一个问题:畸变参数算出来之后,校正图像时发现边缘区域变形严重。后来我检查发现,是因为标定照片数量不够,而且角度变化太小。我建议你至少拍 15-20 张不同角度的照片,覆盖整个视场。
# 畸变校正
img = cv2.imread('distorted.jpg')
h, w = img.shape[:2]
# 获取新的相机矩阵
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
# 校正图像
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪图像
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
避坑指南:我曾经用一张畸变严重的广角镜头做标定,校正后图像边缘还是有点扭曲。后来我改用 cv2.undistortPoints 逐点校正,精度提高了不少。你如果对精度要求高,可以试试这个方法。
3.5 知识体系总览
下面这张图展示了张正友标定法的完整流程。从图像输入到参数输出,每一步都有明确的输入输出关系。
这张图把整个流程串起来了。你从棋盘格图像开始,经过角点检测、单应性矩阵求解、内参外参估计,最后得到畸变参数。每一步的输出都是下一步的输入,环环相扣。
总结:张正友标定法是一个成熟、稳定的标定方法。你只要按照流程走,多拍几张照片,注意光线和棋盘格平整度,就能得到不错的标定结果。我在仿生眼项目中用了无数次,效果一直很稳定。