3、单目标定原理:内参矩阵与外参矩阵、单应性矩阵、张正友标定法、棋盘格角点检测

聊到单目标定,很多新手第一反应就是「跑个OpenCV的calibrateCamera就完事了」。嗯,确实能出结果,但一旦结果不对,你根本不知道问题出在哪。我当年第一次标定双目时,重投影误差0.3像素,自我感觉良好,结果三维重建出来全是扭曲的——后来才发现是内参矩阵里的主点偏移量算错了。

所以这一章,咱们把单目标定的核心原理掰开揉碎。说白了,就是搞清楚相机怎么把三维世界映射到二维像素的,以及我们怎么反过来推算这个映射关系。

3.1 内参矩阵:相机自己的「基因」

内参矩阵描述的是相机内部的几何特性。它只跟相机本身有关,跟你把相机放在哪儿、对着哪儿拍,都没关系。

标准形式长这样:

K = [fx  0  cx]
    [ 0  fy  cy]
    [ 0   0   1]

其中:

  • fx, fy:焦距在x、y方向上的像素尺度。注意,不是物理焦距,是「物理焦距 × 像素密度」。
  • cx, cy:主点坐标,也就是光轴与成像平面的交点。理想情况下在图像正中心,但实际总有偏移。

我在项目中遇到过一台工业相机,标出来的cx偏离中心整整15个像素。一开始以为是标定板没放正,反复标了五次,结果都一样。后来查手册才发现,那款相机的传感器封装本身就有偏移——嗯,这就是内参矩阵存在的意义。

我的习惯:拿到新相机,第一件事就是标定内参。哪怕厂家给了标定文件,我也会自己标一遍。因为运输震动、镜头拧紧力度,都会让内参微变。

3.2 外参矩阵:相机在空间中的「位置和姿态」

外参矩阵描述的是相机坐标系相对于世界坐标系的旋转和平移。它由两部分组成:

  • 旋转矩阵 R:3×3,描述三个轴的旋转
  • 平移向量 t:3×1,描述相机原点在世界坐标系中的位置

外参矩阵通常写成齐次形式:

[R | t] = [r11 r12 r13 | tx]
         [r21 r22 r23 | ty]
         [r31 r32 r33 | tz]

你想想看,每次移动相机,外参就变了。所以标定的时候,我们拍多张不同角度的棋盘格,本质上就是在求解不同的外参矩阵。

我曾经踩过的坑:外参矩阵的旋转部分,OpenCV用的是「旋转向量」表示法(Rodrigues),不是直接给矩阵。如果你手动拼接,一定要用cv2.Rodrigues()做转换,否则结果全乱套。

3.3 单应性矩阵:连接两个平面的「桥梁」

单应性矩阵H,是张正友标定法的核心。它描述的是两个平面之间的透视变换关系。

在标定场景中,棋盘格是一个平面(世界坐标系的Z=0平面),成像平面是另一个平面。所以:

s * [u, v, 1]^T = K * [R | t] * [X, Y, 0, 1]^T

因为Z=0,可以简化成:

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

这里的K * [r1, r2, t]就是单应性矩阵H,3×3的矩阵。

说白了,H把棋盘格上的每个角点,映射到图像上的像素坐标。我们通过检测到的角点对,就能解出H。

关键点:一张棋盘格照片,只能解出一个H。而一个H,能提供两个约束方程(因为r1和r2是正交的)。所以理论上,拍3张不同角度的照片,就能解出内参矩阵的5个未知数。但实际中,我建议至少拍15-20张,覆盖不同角度和距离。

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

张正友标定法,说白了就是「用平面棋盘格代替三维标定块」。它最大的贡献是:

  1. 不需要精密的3D标定块——打印一张棋盘格就行,成本几乎为零
  2. 算法稳定——先闭式求解,再非线性优化,不容易陷入局部最优
  3. 考虑了畸变——在优化阶段同时估计径向畸变和切向畸变

它的流程大致是:

  1. 检测棋盘格角点
  2. 计算每张图的单应性矩阵H
  3. 利用H的约束,求解内参矩阵的闭式解
  4. 利用内参,反算每张图的外参
  5. 加入畸变参数,做全局非线性优化(Levenberg-Marquardt)

我记得第一次手动实现这个流程时,卡在「从H提取内参」那一步。公式推导了整整两天,最后发现OpenCV一行代码就搞定了——但自己推导一遍,对理解帮助极大。

3.5 棋盘格角点检测:成败在此一举

角点检测是标定的第一步,也是最容易出问题的一步。如果角点坐标不准,后面所有计算都是白搭。

OpenCV提供了两个主要函数:

  • cv2.findChessboardCorners():检测棋盘格角点
  • cv2.cornerSubPix():亚像素精炼,把角点精度从像素级提升到亚像素级

使用示例:

import cv2
import numpy as np

# 棋盘格内角点数量(内角点,不是格子数)
pattern_size = (9, 6)

# 读取图像
img = cv2.imread('chessboard.jpg')
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_refined = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
    
    # 绘制并显示
    cv2.drawChessboardCorners(img, pattern_size, corners_refined, ret)
    cv2.imshow('Corners', img)
    cv2.waitKey(0)
避坑指南:我曾经因为棋盘格打印时纸张褶皱,导致角点检测总是失败。后来发现,把棋盘格贴在平整的硬纸板或亚克力板上,成功率从60%提升到95%以上。另外,光照要均匀,避免反光。

3.6 本章知识体系

下面这张图,把单目标定的核心逻辑串起来了:

单目标定核心逻辑 ① 输入:棋盘格图像 多张不同角度照片(≥15张) 已知世界坐标(Z=0平面) ② 角点检测 findChessboardCorners cornerSubPix(亚像素精炼) ③ 单应性矩阵 H 每张图解一个H 3×3透视变换矩阵 ④ 求解内参矩阵 K 利用H的约束方程 闭式解 → 初始值 ⑤ 求解外参 [R|t] 每张图独立求解 旋转向量 + 平移向量 ⑥ 畸变参数 + 全局优化 径向畸变 k1,k2,k3 切向畸变 p1,p2 ⑦ 输出:标定结果 张正友标定法流程

从图上你能看到,整个流程是串行的。角点检测的质量,直接影响H的精度;H的精度,又决定了内参和外参的初始值。最后一步全局优化,把所有参数(内参、外参、畸变)一起做非线性最小二乘,得到最终结果。

我的建议:标定完成后,一定要检查重投影误差。一般低于0.3像素就算不错,低于0.1像素就是优秀。如果超过0.5像素,建议检查角点检测是否有误,或者棋盘格是否平整。

好了,单目标定的原理就聊到这儿。下一章我们会把这些原理落地,手把手教你用OpenCV完成一次完整的单目标定。


专注资料整理