3、相机标定实战:张正友标定法、内参外参求解、畸变校正与重投影误差评估

好,到了真正动手的环节了。前面我们聊了标定板怎么设计、角点怎么检测,但那些都是准备工作。这一章,咱们要把相机标定的核心流程走一遍——从数学原理到代码实现,从参数求解到效果评估。

说实话,我第一次完整跑通张正友标定法的时候,看着重投影误差从几个像素降到零点几,那种感觉还是挺爽的。但后来踩的坑也不少,比如棋盘格图片拍得不够多、标定板放得太近……这些我都会在下面一一提到。

3.1 张正友标定法:核心思想与流程

张正友标定法,说白了就是“用平面标定板来解相机参数”。你不需要一个三维的标定物,只需要在不同角度拍几张棋盘格照片就行。这个方法最大的好处就是——操作简单,精度够用。

它的核心流程可以概括为四步:

  1. 拍摄多张标定板图像(至少10-20张,角度要丰富)
  2. 提取每张图像中的角点坐标(用我们上一章讲的方法)
  3. 利用单应性矩阵求解内参初值(闭式解)
  4. 非线性优化所有参数(包括畸变系数)

嗯,这里要注意:第三步和第四步是分开的。先求一个粗略的解,再用优化方法精调。为什么?因为直接做非线性优化容易掉进局部最优,有个好的初值就稳多了。

关键理解:张正友标定法假设标定板是平面(Z=0),这样三维到二维的映射就简化为一个单应性矩阵。每张图片提供一个单应性矩阵,多个单应性矩阵联合求解,就能解出内参。

张正友标定法核心流程 张正友标定法核心流程 拍摄多张棋盘格图像 提取亚像素角点坐标 计算单应性矩阵 H 利用多组 H 求解内参初值 非线性优化(含畸变参数) 输出:内参 + 外参 + 畸变系数 💡 关键点: 每张图像提供一个单应性矩阵 H,多个 H 联合约束内参。至少需要 3 张不同角度的图像才能解出全部内参。

3.2 内参与外参的求解过程

咱们来拆解一下具体的数学过程。别怕,我会尽量用大白话讲清楚。

3.2.1 单应性矩阵与内参的关系

每张标定板图像,我们都能建立一个从“棋盘格平面”到“图像平面”的映射,这就是单应性矩阵 H。H 是 3x3 的矩阵,它包含了内参和外参的信息:

H = K * [r1  r2  t]

其中 K 是内参矩阵,r1、r2 是旋转矩阵的前两列,t 是平移向量。因为棋盘格平面 Z=0,所以第三列旋转向量用不上。

这里有个重要的约束:r1 和 r2 是正交的单位向量。利用这个约束,我们可以从 H 中解出 K。

我的经验:在实际项目中,我习惯先检查 H 矩阵的质量。如果某张图像的 H 矩阵条件数很大(比如 > 100),说明这张图像质量不好,我会直接剔除,不参与后续求解。

3.2.2 内参矩阵的闭式解

张正友的方法给出了一个优雅的闭式解。简单说,就是构造一个关于内参的线性方程组,然后用最小二乘法求解。具体公式我就不展开了,但核心思想是:

  • 利用旋转向量的正交约束,得到关于内参的线性方程
  • 至少需要 3 张图像才能解出全部 5 个内参(fx, fy, cx, cy, skew)
  • 实际中建议用 10-20 张图像,用最小二乘法求最优解

3.2.3 外参的求解

内参求出来后,每张图像的外参(旋转矩阵 R 和平移向量 t)就可以单独求解了。方法也很直接:

  1. 从 H 和 K 反算出 r1, r2, t
  2. r3 = r1 × r2(叉积)
  3. 对旋转矩阵做 SVD 分解,强制满足正交性

嗯,这里有个细节:因为噪声的存在,直接算出来的 R 可能不满足正交性,所以需要用 SVD 做一次“正交化”处理。

3.3 畸变校正:径向畸变与切向畸变

镜头畸变是绕不开的问题。我见过不少项目,标定完内参就以为完事了,结果畸变校正没做好,测量精度直接崩了。

畸变主要分两类:

畸变类型 产生原因 数学模型 典型表现
径向畸变 镜头曲率不完美 k1, k2, k3 三个参数 画面边缘的直线变弯(桶形/枕形)
切向畸变 镜头与传感器不平行 p1, p2 两个参数 画面倾斜、梯形失真

在张正友标定法中,畸变参数是和非线性优化一起求解的。具体做法是:

  • 先用闭式解求出内参和外参的初值
  • 然后把这些参数作为初始值,加上畸变参数,一起做非线性最小二乘优化
  • 优化的目标函数就是——所有角点的重投影误差之和最小

我曾经踩过的坑:有一次我用广角镜头做标定,只用了 k1 和 k2 两个径向畸变参数,结果边缘区域的校正效果很差。后来加上 k3 参数,效果才明显改善。所以,如果你的镜头视场角比较大(比如 > 90°),建议用三个径向畸变参数。

3.4 重投影误差:标定质量的试金石

标定做得好不好,不能光看参数有没有求出来。我们需要一个量化的指标——重投影误差。

什么是重投影误差?简单说就是:

  1. 用标定出来的内参和外参,把三维空间中的角点投影到图像上
  2. 计算投影点与实际检测到的角点之间的像素距离
  3. 对所有角点取平均,就是重投影误差

这个值越小,说明标定越准。一般来说:

  • < 0.5 像素:非常优秀
  • 0.5 - 1.0 像素:良好,大多数应用可接受
  • > 1.0 像素:需要检查标定过程是否有问题

我的判断标准:我个人习惯把重投影误差控制在 0.3 像素以内。如果超过 0.5 像素,我会重新检查标定板图片的质量——是不是有模糊的?是不是标定板放得太偏了?是不是角点检测精度不够?

3.5 实战代码:OpenCV 标定流程

理论讲完了,咱们直接上代码。OpenCV 提供了完整的标定工具,核心函数就几个:

import cv2
import numpy as np
import glob

# 1. 准备棋盘格角点的三维坐标
checkerboard_size = (9, 6)  # 内角点数
square_size = 25.0  # 棋盘格边长,单位 mm

objp = np.zeros((checkerboard_size[0] * checkerboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:checkerboard_size[0], 
                        0:checkerboard_size[1]].T.reshape(-1, 2)
objp *= square_size

# 2. 存储所有图像的三维点和二维点
objpoints = []  # 三维点
imgpoints = []  # 二维点

images = glob.glob('calibration_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # 提取角点
    ret, corners = cv2.findChessboardCorners(gray, checkerboard_size, None)
    
    if ret:
        objpoints.append(objp)
        
        # 亚像素精化
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        imgpoints.append(corners2)

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

# 4. 计算重投影误差
total_error = 0
for i in range(len(objpoints)):
    imgpoints2, _ = cv2.projectPoints(
        objpoints[i], rvecs[i], tvecs[i], mtx, dist
    )
    error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    total_error += error

print("平均重投影误差: {:.3f} 像素".format(total_error / len(objpoints)))

代码说明:

  • cv2.calibrateCamera 是核心函数,它内部实现了张正友标定法的完整流程
  • 亚像素精化步骤(cornerSubPix)非常关键,能显著提升标定精度
  • 重投影误差的计算用 cv2.projectPoints 把三维点投影回图像平面

3.6 畸变校正与图像去畸变

标定完成后,我们就可以对任意图像做畸变校正了。OpenCV 提供了两个函数:

# 方法一:直接去畸变
undistorted = cv2.undistort(img, mtx, dist, None, mtx)

# 方法二:先计算映射表,再重映射(适合视频流)
mapx, mapy = cv2.initUndistortRectifyMap(
    mtx, dist, None, mtx, (w, h), cv2.CV_32FC1
)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

方法二的好处是——映射表可以提前算好,后面每帧图像只需要做一次 remap,速度更快。我在做实时视频标定系统时,用的就是方法二。

注意:去畸变后,图像边缘区域会被拉伸或裁剪。如果你需要保留完整的视场角,可以考虑用 cv2.getOptimalNewCameraMatrix 来调整内参矩阵,平衡畸变校正效果和视场角损失。

3.7 标定质量检查清单

最后,我整理了一份标定质量的检查清单。每次做完标定,我都会对照着检查一遍:

检查项 合格标准 常见问题
重投影误差 < 0.5 像素 角点检测不准、图像模糊
图像数量 ≥ 15 张 太少导致参数不稳定
角度覆盖 俯仰、偏航各 ±30° 角度单一导致外参退化
标定板占比 占图像 20%-60% 太小角点精度差,太大超出景深
畸变校正效果 直线变直,边缘无异常 畸变模型不够(如缺 k3)