4. 相机标定基础:张正友标定法原理、标定板设计、OpenCV标定流程

说到视觉定位,有个坎儿你绕不过去——相机标定。我刚开始做SLAM那会儿,总觉得标定是件小事,随便拍几张棋盘格就跑。结果呢?定位误差大得离谱,回环检测也老是抽风。后来才明白,相机标定是整个视觉系统的基石。你想想看,如果相机本身的内参都不准,后面的位姿估计、三角测量、三维重建,那全是空中楼阁。

这一节,我们就来聊聊最经典的张正友标定法。为什么选它?因为它在精度和操作便利性之间取得了很好的平衡。你不需要昂贵的三维标定块,只需要打印一张棋盘格,拍几张不同角度的照片,就能把相机的内参、外参、畸变系数全算出来。说白了,这就是工业界和学术界最通用的标定方案。

核心要点:张正友标定法的本质,是利用二维平面靶标(棋盘格)上的已知角点,建立从世界坐标系到像素坐标系的投影关系,然后通过最小化重投影误差来求解相机参数。

4.1 张正友标定法原理

张正友标定法的数学基础,其实就两个核心步骤:

  1. 计算单应性矩阵——建立棋盘格平面与图像平面之间的映射关系
  2. 分解内参和外参——从单应性矩阵中提取出相机的内部参数和外部参数

具体来说,我们假设棋盘格平面位于世界坐标系的 Z=0 平面上。那么,一个三维点 (X, Y, 0) 到像素坐标 (u, v) 的投影关系可以写成:

s * [u, v, 1]^T = K * [r1, r2, t] * [X, Y, 1]^T

这里的 K 是内参矩阵,r1、r2 是旋转矩阵的前两列,t 是平移向量。这个 3×3 的矩阵 H = K * [r1, r2, t] 就是我们说的单应性矩阵。

嗯,这里要注意:单应性矩阵有 8 个自由度,而一个角点可以提供两个方程。所以理论上,你至少需要 4 个角点才能解出 H。但实际中,棋盘格上通常有几十个角点,我们用最小二乘法来求解,这样更鲁棒。

我的经验:我在项目中遇到过标定板反光严重的情况,角点检测总是跳来跳去。后来我改用哑光纸打印棋盘格,并且在拍摄时避免强光直射,角点检测的稳定性明显提升。另外,棋盘格的黑白格子数量最好是奇数×奇数,这样中心点更容易定位。

得到单应性矩阵后,怎么分解出内参呢?这里利用了旋转矩阵的正交性约束。因为 r1 和 r2 是单位正交的,所以:

r1^T * r2 = 0
||r1|| = ||r2|| = 1

把 H = K * [r1, r2, t] 代入这两个约束,就能得到关于内参的线性方程。多张图片提供多个约束,最后用最小二乘法解出内参矩阵 K。

内参解出来后,外参也就顺理成章了:

r1 = λ * K^(-1) * h1
r2 = λ * K^(-1) * h2
t = λ * K^(-1) * h3

其中 λ 是归一化因子。最后再用 SVD 把旋转矩阵投影到 SO(3) 流形上,保证它是真正的旋转矩阵。

4.2 标定板设计

标定板的设计,看似简单,其实有不少门道。我见过有人随便在A4纸上画个网格就去标定,结果精度一塌糊涂。标定板的设计直接影响角点检测的精度和标定结果的稳定性。

常见的标定板类型有两种:

类型 特点 适用场景
棋盘格(Checkerboard) 角点检测简单,亚像素精度高 大多数通用场景
圆点阵列(Circle Grid) 圆心检测精度高,对畸变不敏感 大畸变镜头、鱼眼相机

我个人习惯用棋盘格,因为 OpenCV 的 findChessboardCorners 函数非常成熟,几乎不需要调参。但如果你用的是鱼眼镜头或者广角镜头,圆点阵列可能更合适——圆心的检测不受径向畸变影响,精度更稳定。

避坑指南:我曾经用一张褶皱的棋盘格去标定,结果内参算出来焦距偏差了 5%。后来才发现,标定板必须贴在绝对平整的硬板上,任何弯曲都会引入系统误差。另外,棋盘格的格子尺寸要精确测量,误差控制在 0.1mm 以内。你想想看,如果格子尺寸标错了,那整个标定结果就全歪了。

标定板的设计参数,我建议这样选:

  • 格子数量: 9×7 或 11×8(内角点数量),太少约束不够,太多容易过拟合
  • 格子尺寸: 20mm~30mm,太小了角点检测困难,太大了需要更大的标定板
  • 标定板尺寸: 覆盖相机视场的 1/3 到 2/3,太小了标定精度差
  • 材质: 哑光纸 + 硬质底板,避免反光和弯曲

4.3 OpenCV标定流程

好了,理论讲完了,咱们直接上代码。OpenCV 的标定流程其实就几个步骤,但每一步都有坑。我带你走一遍。

4.3.1 采集标定图片

首先,你需要拍摄 15~20 张不同角度的标定板图片。注意,不是随便拍,要保证:

  • 标定板出现在图像的各个区域(左上、右下、中心、边缘)
  • 标定板与相机平面的夹角在 0°~45° 之间变化
  • 标定板在图像中的大小占 1/4 到 3/4
  • 避免运动模糊和过曝

我记得有一次,一个学生拍了 30 张图片,结果标定误差还是很大。我一看,他所有图片的标定板都在图像正中央,角度也几乎没变。这相当于只提供了一个约束,内参当然解不出来。说白了,标定图片的多样性比数量更重要。

4.3.2 角点检测

OpenCV 提供了现成的角点检测函数:

import cv2
import numpy as np

# 棋盘格内角点数量(列数-1,行数-1)
pattern_size = (9, 6)
# 格子尺寸(单位:mm)
square_size = 25.0

# 准备世界坐标系中的角点坐标
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

# 存储所有图片的角点
obj_points = []  # 世界坐标系中的点
img_points = []  # 图像坐标系中的点

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:
        # 亚像素精细化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        
        obj_points.append(objp)
        img_points.append(corners)
        
        # 可视化角点
        cv2.drawChessboardCorners(img, pattern_size, corners, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

我的经验:亚像素精细化这一步千万别省。我刚开始做的时候,直接用 findChessboardCorners 的原始结果去标定,重投影误差一直在 0.5 像素以上。加上 cornerSubPix 之后,误差直接降到 0.1 像素。这 0.4 像素的差距,在视觉定位中可能就是几厘米的误差。

4.3.3 执行标定

角点检测完成后,调用 calibrateCamera 函数即可:

# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    obj_points, img_points, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

输出结果中:

  • mtx:内参矩阵,包含 fx, fy, cx, cy
  • dist:畸变系数,包含 k1, k2, p1, p2, k3
  • ret:重投影误差,通常应小于 0.3 像素

如果重投影误差大于 0.5 像素,说明标定质量有问题。我建议你检查一下:角点检测是否准确?标定板是否平整?图片是否足够多样?

4.3.4 畸变校正

标定完成后,可以用内参和畸变系数对图像进行校正:

# 畸变校正
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
new_mtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

# 方法一:直接校正
dst = cv2.undistort(img, mtx, dist, None, new_mtx)

# 方法二:映射校正(更高效)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, new_mtx, (w, h), 5)
dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

# 裁剪掉黑色边框
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]

避坑指南:我曾经直接用 undistort 校正图像,结果边缘区域出现了严重的拉伸变形。后来才发现,getOptimalNewCameraMatrix 可以自动计算一个合适的新内参矩阵,在保留更多图像内容和减少畸变之间取得平衡。另外,如果你做的是视觉定位,建议用映射校正(方法二),因为 remap 可以预先计算好映射表,实时运行时效率更高。

4.4 标定质量评估

标定完了,怎么知道结果好不好?我一般看三个指标:

  1. 重投影误差: 小于 0.3 像素算合格,小于 0.1 像素算优秀
  2. 内参稳定性: 用不同的图片子集标定,看内参的波动范围
  3. 畸变校正效果: 看校正后的图像中,直线是否真的变直了

我个人习惯在标定完成后,把标定板放在场景中,用标定结果去反投影角点,看看误差分布。如果误差在图像中心区域很小,边缘区域很大,说明畸变模型可能不够用(比如需要更多的畸变参数)。

好了,相机标定的基础就讲到这里。张正友标定法虽然经典,但实际应用中还有很多细节需要注意。下一节我们会聊聊标定误差的来源和补偿方法,到时候再深入展开。


专注资料整理