第三章 张正友标定法:棋盘格角点检测、单应性矩阵求解、内参外参估计、畸变参数计算

张正友标定法,说白了就是用一个棋盘格照片,把相机内参、外参、畸变参数全算出来。这个方法在工业界用得特别多,我这些年做仿生眼系统,几乎每个项目都离不开它。为什么这么流行?因为它操作简单,精度又高,你只需要打印一张棋盘格,拍几张不同角度的照片就行。

嗯,咱们一步步来看,这个方法到底是怎么工作的。

3.1 棋盘格角点检测

第一步,得从图像里找到棋盘格的角点。你想想看,棋盘格上那些黑白交界的点,就是我们要找的角点。这些点位置精确,容易识别,是后续计算的基础。

我个人习惯用 OpenCV 的 findChessboardCorners 函数。它内部用的是 Harris 角点检测加上亚像素精化。我在项目中遇到过一个问题:如果棋盘格反光太厉害,角点就找不准。后来我加了个自适应阈值处理,效果好了很多。

核心步骤:

  1. 将图像转为灰度图
  2. 使用 Harris 角点检测找到候选角点
  3. 通过亚像素精化提高角点精度
  4. 按棋盘格的行列顺序排列角点
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 知识体系总览

下面这张图展示了张正友标定法的完整流程。从图像输入到参数输出,每一步都有明确的输入输出关系。

张正友标定法知识体系 棋盘格图像 角点检测 单应性矩阵求解 内参外参估计 畸变参数计算 输入:棋盘格照片 输出:角点坐标 输出:H矩阵 输出:fx,fy,cx,cy,R,t 输出:k1,k2,k3,p1,p2

这张图把整个流程串起来了。你从棋盘格图像开始,经过角点检测、单应性矩阵求解、内参外参估计,最后得到畸变参数。每一步的输出都是下一步的输入,环环相扣。

总结:张正友标定法是一个成熟、稳定的标定方法。你只要按照流程走,多拍几张照片,注意光线和棋盘格平整度,就能得到不错的标定结果。我在仿生眼项目中用了无数次,效果一直很稳定。

专注资料整理