相机内参标定原理:张正友标定法、棋盘格与单应性矩阵

相机内参标定,说白了就是搞清楚你的相机到底是怎么「看」世界的。每个镜头都有脾气——焦距多少、光心偏不偏、镜头有没有畸变。这些参数不弄清楚,后面做手眼标定、三维重建全是白搭。

我入行那会儿,第一次标定用的是最传统的三维标定块,又大又沉,还得精确加工。后来张正友博士在1998年提出了用平面棋盘格做标定的方法,这简直是解放了咱们工程师。你只需要打印一张棋盘格,从不同角度拍几张照片,就能把内参算出来。

嗯,这一章我们就来拆解这个方法的核心:棋盘格角点检测、单应性矩阵求解,以及整个标定流程的数学逻辑。

张正友标定法的核心思想

张正友标定法为什么能成为工业界的事实标准?因为它巧妙地绕过了三维信息。传统方法需要知道标定物上每个点的三维坐标,而张正友只需要一个平面棋盘格,棋盘格上每个角点的物理坐标是已知的(比如角点间距30mm),但相机看到的只是二维图像坐标。

关键来了:平面棋盘格上的点都在Z=0的平面上。这个约束让三维到二维的投影关系简化成了一个单应性矩阵(Homography)。每拍一张不同角度的棋盘格照片,就能得到一个单应性矩阵。多张照片的单应性矩阵联合求解,就能把内参和外参解耦出来。

核心流程速览:

  1. 打印棋盘格,贴在硬板上
  2. 从不同角度拍摄10-20张照片
  3. 检测每张照片中的棋盘格角点(亚像素精度)
  4. 计算每张照片的单应性矩阵
  5. 利用单应性矩阵的约束条件求解内参初值
  6. 考虑畸变参数,做非线性优化

棋盘格角点检测:从像素到亚像素

角点检测是标定的第一步,也是最容易翻车的一步。我记得刚带团队时,有个同事拍的照片反光严重,角点检测死活过不去。后来发现是棋盘格打印在光面纸上,灯光一打全是高光。

OpenCV里用的cv::findChessboardCorners(),底层逻辑是这样的:

  1. 图像预处理:转灰度图,自适应阈值二值化
  2. 四边形检测:找图像中所有四边形轮廓,筛选出符合棋盘格拓扑结构的四边形
  3. 角点排序:按棋盘格的行列顺序排列角点
  4. 亚像素精化:用cv::cornerSubPix()把角点精度推到亚像素级别

这里有个坑——棋盘格的内角点数量。比如你打印的是9×6的棋盘格,实际内角点是8×5。我见过有人数错格子,标定出来的焦距直接偏了10%。

我的经验:

  • 棋盘格尺寸:内角点建议7×7到9×6之间,太少约束不够,太多容易漏检
  • 打印后贴在平整的铝板或亚克力板上,不要贴纸板(会弯曲)
  • 拍摄时保证棋盘格占画面1/4到1/2,太小了角点检测不稳定
  • 避免纯白色背景,棋盘格边缘要有对比度

单应性矩阵:连接世界坐标与图像坐标的桥梁

单应性矩阵是一个3×3的矩阵,描述了同一平面在两个不同视角之间的投影变换。在张正友标定法中,它连接了棋盘格平面(Z=0)和图像平面。

数学上,一个三维点\(P=[X, Y, Z, 1]^T\)投影到图像点\(p=[u, v, 1]^T\)的关系是:

s * [u]   =   [fx  0   cx]   [r11  r12  r13  t1]   [X]
    [v]       [0   fy  cy] * [r21  r22  r23  t2] * [Y]
    [1]       [0    0   1]   [r31  r32  r33  t3]   [Z]
                                                      [1]

因为棋盘格上所有点的Z=0,所以第三列旋转向量\([r13, r23, r33]^T\)被消掉了。简化后:

s * [u]   =   [fx  0   cx]   [r11  r12  t1]   [X]
    [v]       [0   fy  cy] * [r21  r22  t2] * [Y]
    [1]       [0    0   1]   [r31  r32  t3]   [1]

中间的3×3矩阵就是单应性矩阵H。它包含了内参矩阵K和外参矩阵的前两列旋转向量以及平移向量。

每张棋盘格照片可以建立一个方程:

H = K * [r1  r2  t]

其中r1和r2是旋转矩阵的前两列,t是平移向量。因为旋转矩阵是正交的,所以r1和r2满足:

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

这两个约束条件,就是求解内参的关键。

单应性矩阵求解:DLT算法

求解单应性矩阵最常用的方法是直接线性变换(DLT)。给定4对以上的匹配点(棋盘格角点的世界坐标和图像坐标),就可以解出H。

对于每一对匹配点\((X, Y)\)和\((u, v)\),可以写出两个方程:

u = (h11*X + h12*Y + h13) / (h31*X + h32*Y + h33)
v = (h21*X + h22*Y + h23) / (h31*X + h32*Y + h33)

整理成线性形式:

h11*X + h12*Y + h13 - u*h31*X - u*h32*Y - u*h33 = 0
h21*X + h22*Y + h23 - v*h31*X - v*h32*Y - v*h33 = 0

把所有的点对堆叠起来,就得到一个\(2n \times 9\)的矩阵A。解\(A \cdot h = 0\),用SVD分解,最小奇异值对应的右奇异向量就是H的9个元素。

注意:DLT算法对噪声敏感。我建议至少用10对以上的角点(棋盘格通常有几十个角点,足够了),并且用RANSAC或最小中值法剔除误匹配。另外,数据归一化很重要——把坐标缩放到[-1, 1]区间,能显著提高数值稳定性。

从单应性矩阵到内参

有了多个视角的单应性矩阵H,就可以解内参了。具体做法是:

  1. 根据正交约束,从每个H推导出两个关于内参的线性方程
  2. 把所有方程堆叠成\(V \cdot b = 0\)的形式,其中b是内参的向量化表示
  3. 解出b后,反推出fx, fy, cx, cy

这个推导过程比较数学化,但OpenCV已经帮我们封装好了。你只需要调用cv::calibrateCamera(),传入角点坐标和棋盘格物理尺寸,就能得到内参矩阵和畸变系数。

不过,我建议你理解一下背后的逻辑——当标定结果异常时,你能快速定位问题。比如重投影误差大于0.5像素,说明标定质量有问题,可能是照片数量不够、棋盘格不平整、或者角点检测有偏差。

标定流程的SVG结构图

下面这张图展示了从拍摄到输出内参的完整流程:

张正友标定法流程图 张正友标定法完整流程 拍摄多角度棋盘格照片 棋盘格角点检测(亚像素) 计算每张照片的单应性矩阵H 利用正交约束求解内参初值 非线性优化(含畸变参数) 输出:内参矩阵 + 畸变系数 关键参数 • 照片数量:10-20张 • 棋盘格尺寸:30-50mm • 内角点:7×7 或 9×6 • 重投影误差 < 0.5px 常见问题 • 照片角度变化要足够大 • 避免纯旋转(无平移) • 棋盘格不能太靠近图像边缘 • 光照均匀,无反光

畸变模型:别让你的图像变形

实际镜头都有畸变,主要是径向畸变和切向畸变。径向畸变是镜头形状引起的,表现为「桶形」或「枕形」变形。切向畸变是镜头和成像平面不平行导致的。

OpenCV使用的畸变模型是:

x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y

其中k1, k2, k3是径向畸变系数,p1, p2是切向畸变系数。一般标定5个畸变参数就够了,k3只在鱼眼镜头或大畸变镜头中需要。

避坑指南:我曾经标定一个广角镜头,发现重投影误差始终在1.5像素左右。检查后发现是畸变模型选错了——用了3个径向参数,但镜头只需要2个。把k3去掉后,误差降到了0.3像素。所以,不要盲目用全参数模型,先试试k1, k2, p1, p2四个参数。

标定质量评估:怎么看结果靠不靠谱

标定完不是就完事了,你得验证结果。我一般看三个指标:

指标 合格标准 说明
重投影误差 < 0.5 像素 角点检测精度和标定质量的综合反映
焦距一致性 fx ≈ fy(对非鱼眼镜头) fx和fy差异超过5%说明标定有问题
光心位置 接近图像中心 cx ≈ 图像宽度/2,cy ≈ 图像高度/2

如果重投影误差大于1个像素,我建议你重新拍一组照片。常见原因包括:棋盘格不平、照片数量不够(少于10张)、角度变化太小(都在同一个视角拍的)。