相机内参标定原理:张正友标定法、棋盘格与单应性矩阵
相机内参标定,说白了就是搞清楚你的相机到底是怎么「看」世界的。每个镜头都有脾气——焦距多少、光心偏不偏、镜头有没有畸变。这些参数不弄清楚,后面做手眼标定、三维重建全是白搭。
我入行那会儿,第一次标定用的是最传统的三维标定块,又大又沉,还得精确加工。后来张正友博士在1998年提出了用平面棋盘格做标定的方法,这简直是解放了咱们工程师。你只需要打印一张棋盘格,从不同角度拍几张照片,就能把内参算出来。
嗯,这一章我们就来拆解这个方法的核心:棋盘格角点检测、单应性矩阵求解,以及整个标定流程的数学逻辑。
张正友标定法的核心思想
张正友标定法为什么能成为工业界的事实标准?因为它巧妙地绕过了三维信息。传统方法需要知道标定物上每个点的三维坐标,而张正友只需要一个平面棋盘格,棋盘格上每个角点的物理坐标是已知的(比如角点间距30mm),但相机看到的只是二维图像坐标。
关键来了:平面棋盘格上的点都在Z=0的平面上。这个约束让三维到二维的投影关系简化成了一个单应性矩阵(Homography)。每拍一张不同角度的棋盘格照片,就能得到一个单应性矩阵。多张照片的单应性矩阵联合求解,就能把内参和外参解耦出来。
核心流程速览:
- 打印棋盘格,贴在硬板上
- 从不同角度拍摄10-20张照片
- 检测每张照片中的棋盘格角点(亚像素精度)
- 计算每张照片的单应性矩阵
- 利用单应性矩阵的约束条件求解内参初值
- 考虑畸变参数,做非线性优化
棋盘格角点检测:从像素到亚像素
角点检测是标定的第一步,也是最容易翻车的一步。我记得刚带团队时,有个同事拍的照片反光严重,角点检测死活过不去。后来发现是棋盘格打印在光面纸上,灯光一打全是高光。
OpenCV里用的cv::findChessboardCorners(),底层逻辑是这样的:
- 图像预处理:转灰度图,自适应阈值二值化
- 四边形检测:找图像中所有四边形轮廓,筛选出符合棋盘格拓扑结构的四边形
- 角点排序:按棋盘格的行列顺序排列角点
- 亚像素精化:用
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,就可以解内参了。具体做法是:
- 根据正交约束,从每个H推导出两个关于内参的线性方程
- 把所有方程堆叠成\(V \cdot b = 0\)的形式,其中b是内参的向量化表示
- 解出b后,反推出fx, fy, cx, cy
这个推导过程比较数学化,但OpenCV已经帮我们封装好了。你只需要调用cv::calibrateCamera(),传入角点坐标和棋盘格物理尺寸,就能得到内参矩阵和畸变系数。
不过,我建议你理解一下背后的逻辑——当标定结果异常时,你能快速定位问题。比如重投影误差大于0.5像素,说明标定质量有问题,可能是照片数量不够、棋盘格不平整、或者角点检测有偏差。
标定流程的SVG结构图
下面这张图展示了从拍摄到输出内参的完整流程:
畸变模型:别让你的图像变形
实际镜头都有畸变,主要是径向畸变和切向畸变。径向畸变是镜头形状引起的,表现为「桶形」或「枕形」变形。切向畸变是镜头和成像平面不平行导致的。
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张)、角度变化太小(都在同一个视角拍的)。