3. 相机标定基础:张正友标定法原理、棋盘格角点检测、单应性矩阵求解

说到双目立体匹配,第一步就是让两个相机“认识”这个世界。说白了,相机标定就是给相机做一次精准的“视力检查”。我这些年做立体视觉项目,踩过最大的坑就是标定没做好,结果后面重建出来的三维点云全是歪的。嗯,咱们今天就把这块硬骨头啃下来。

3.1 为什么需要相机标定?

你想想看,相机镜头不是完美的。光线穿过镜头会发生畸变,传感器也不是绝对水平。这些误差如果不校正,左右图像的特征点就永远对不准。

相机标定要解决三个核心问题:

  • 内参:焦距、主点坐标、畸变系数——这些是相机“出厂自带”的属性
  • 外参:两个相机之间的旋转和平移关系——这是双目系统的“骨架”
  • 畸变校正:把桶形畸变、枕形畸变这些“近视散光”给矫正过来

核心观点:标定精度直接决定了立体匹配的上限。我见过有人花两周调匹配算法,结果发现是标定板歪了0.5度——所有努力白费。

3.2 张正友标定法:为什么它成了行业标准?

张正友教授在1998年提出的这个方法,可以说是改变了整个计算机视觉的工程实践。以前标定需要精密的3D标定块,又贵又难用。张正友法只需要一个打印的棋盘格,拍几张照片就行。

它的核心思路是这样的:

  1. 假设棋盘格在Z=0平面上(简化问题)
  2. 通过角点检测找到图像坐标和世界坐标的对应关系
  3. 求解单应性矩阵,再分解出内参和外参
  4. 用最大似然估计做非线性优化,得到精确结果

我个人习惯用至少15-20张不同角度的棋盘格照片。太少的话,标定结果会不稳定。我记得有一次赶项目只拍了8张,结果重投影误差0.8个像素,后来补拍到20张,误差降到了0.15像素——差距就是这么大。

3.3 棋盘格角点检测:从图像到坐标

角点检测是标定的第一步,也是最容易出问题的一步。OpenCV的findChessboardCorners函数大家应该都用过,但你知道它背后是怎么工作的吗?

大致流程是这样的:

  • 图像预处理:转灰度、自适应阈值化
  • 四边形检测:找到所有可能的棋盘格四边形
  • 角点亚像素精化:用梯度法把角点定位到亚像素精度

实战技巧:拍摄棋盘格时,确保光照均匀。我曾在强光下拍摄,棋盘格反光导致角点检测失败。后来用哑光纸打印,问题就解决了。

这里给出一个典型的角点检测代码片段:

import cv2
import numpy as np

# 棋盘格尺寸(内角点数量)
pattern_size = (9, 6)

# 准备世界坐标系中的点
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)
        # 亚像素精化
        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)
        
        # 可视化
        cv2.drawChessboardCorners(img, pattern_size, corners2, ret)

避坑指南:我曾经遇到一个情况,棋盘格照片里角点检测总是少几个。排查了半天,发现是棋盘格打印时边缘被裁切了。记住:棋盘格最外圈的方格必须完整可见,否则检测会失败。

3.4 单应性矩阵求解:从2D到2D的映射

单应性矩阵(Homography)是张正友标定法的数学核心。它描述了两个平面之间的投影变换关系。在标定场景中,就是棋盘格平面(世界坐标)到图像平面(像素坐标)的映射。

数学上,单应性矩阵H是一个3×3的矩阵,有8个自由度(最后一个元素归一化为1)。对于每一对匹配点,我们可以建立两个方程:

// 对于点 (x, y) 在世界坐标, (u, v) 在图像坐标
// 齐次坐标下: [u, v, 1]^T = H * [x, y, 1]^T

// 展开后得到两个方程:
// u = (h11*x + h12*y + h13) / (h31*x + h32*y + 1)
// v = (h21*x + h22*y + h23) / (h31*x + h32*y + 1)

求解H至少需要4对匹配点。但实际中我们会用更多的点(棋盘格有54个内角点),然后用最小二乘法求解超定方程组。

常用的求解方法是DLT(Direct Linear Transform,直接线性变换),配合SVD分解。OpenCV的findHomography函数封装了这一切。

关键点:单应性矩阵求解的质量直接影响内参分解的精度。我建议每次求解后都检查一下重投影误差,如果超过0.5像素,说明这组数据有问题,需要剔除。

3.5 从单应性矩阵到相机内参

当我们有了多个视角的单应性矩阵H₁, H₂, ..., Hₙ,就可以求解相机内参了。张正友法利用了一个重要性质:旋转矩阵的列向量是正交的。

具体来说,对于每个H矩阵,我们可以写出:

H = [h1, h2, h3] = λ * K * [r1, r2, t]

其中:
- K 是内参矩阵
- r1, r2 是旋转矩阵的前两列
- t 是平移向量
- λ 是尺度因子

利用 r1^T * r2 = 0 和 ||r1|| = ||r2|| 这两个约束,
可以得到关于内参的线性方程。

通过收集多个视角的约束,我们可以构建一个线性方程组,用SVD求解出内参矩阵K的各个元素。这个过程在OpenCV的calibrateCamera函数中自动完成。

参数 含义 典型值(640×480相机)
fx, fy 焦距(像素单位) 约500-800
cx, cy 主点坐标 约320, 240
k1, k2, k3 径向畸变系数 通常在±0.5以内
p1, p2 切向畸变系数 通常在±0.01以内

3.6 标定流程总结

整个标定流程,我建议按这个步骤来:

  1. 准备棋盘格:打印9×6或12×9的棋盘格,贴在硬板上
  2. 拍摄图像:从不同角度拍摄20-30张,覆盖视场各个区域
  3. 角点检测:用findChessboardCorners检测所有图像
  4. 求解内参:调用calibrateCamera得到内参和畸变系数
  5. 评估结果:检查重投影误差,一般应小于0.3像素
  6. 双目标定:用stereoCalibrate求解两个相机的外参

个人经验:标定完成后,我习惯拍一张棋盘格放在视场中央,用标定结果去畸变,看看棋盘格的直线是不是真的变直了。这是最直观的验证方法。

相机标定知识体系 相机标定 张正友标定法原理 棋盘格角点检测 单应性矩阵求解 Z=0平面假设 最大似然优化 亚像素精化 自适应阈值 DLT+SVD求解 重投影误差检查 输出:内参 + 外参 + 畸变系数

这张图把整个标定的知识体系串起来了。从张正友法的核心假设,到角点检测的工程细节,再到单应性矩阵的数学求解,最后输出我们需要的所有参数。

最后提醒:标定不是一次性工作。相机如果受到震动、温度变化,或者镜头被拧动过,都需要重新标定。我习惯在每个项目开始前都做一次标定,确保数据可靠。


专注资料整理