4. 内参标定原理:张正友标定法、角点检测与单应性矩阵
内参标定,说白了就是搞清楚你的相机“怎么看世界”的。焦距、主点、畸变参数——这些玩意儿决定了图像里每个像素对应真实世界的什么方向。我入行那会儿,师傅扔给我一块棋盘格,说“去,把相机标了”。当时觉得挺玄乎,后来才发现,核心就三件事:拍棋盘、找角点、解方程。
今天咱们就聊聊这三件事背后的原理。张正友标定法,是目前工业界最主流的做法。它不需要你知道相机在哪儿,也不需要精密的3D靶标,一块打印的棋盘格就够了。
4.1 张正友标定法的核心思想
张正友标定法,本质上是一个“从2D到2D”的映射问题。你想想看,棋盘格上的角点,在物理世界里是共面的(都在一张纸上)。相机拍下来,这些点就到了图像平面上。
我们已知的是:
- 棋盘格上角点的3D坐标(假设Z=0,以棋盘格左上角为原点)
- 图像上检测到的2D像素坐标
未知的是:
- 相机的内参矩阵K
- 每张图片对应的外参(旋转R和平移t)
- 畸变系数
张正友的巧妙之处在于:先忽略畸变,用“单应性矩阵”把内外参解耦,再迭代优化。 这样就把一个复杂的非线性问题,拆成了两步走的线性+非线性问题。
核心公式(简化版):
s * [u, v, 1]^T = K * [R | t] * [X, Y, 0, 1]^T
因为Z=0,所以旋转矩阵R的第三列被“吃掉”了,剩下的部分就变成了一个3x3的单应性矩阵H。
我个人习惯把这一步叫做“降维打击”。把3D到2D的投影,先简化成平面到平面的透视变换。解出H,再从中拆出K和R、t。
4.2 棋盘格角点检测
角点检测是标定的第一步,也是决定成败的一步。角点检不准,后面算得再漂亮也是白搭。
OpenCV里最常用的是 cv::findChessboardCorners()。它的原理大致分三步:
- 二值化与轮廓提取:把图像转成灰度,用自适应阈值找到棋盘格的黑色方块轮廓。
- 四边形筛选:找到所有四边形,按棋盘格的拓扑结构(行、列数)进行匹配。
- 亚像素精化:在整数像素位置附近,用梯度信息把角点坐标精确到亚像素级别。
这里有个坑,我踩过好几次:
我曾经遇到过的问题:
棋盘格反光太强,或者光照不均匀,导致二值化后黑色方块连成一片。OpenCV死活找不到角点。
解决办法:
- 用哑光纸打印棋盘格,别用光面纸。
- 拍摄时保证光照均匀,避免强光直射。
- 如果实在不行,可以试试
cv::findChessboardCornersSB(),这个函数对光照变化更鲁棒。
亚像素精化这一步,我建议一定要做。直接用整数像素坐标去标定,误差会大一个数量级。代码很简单:
// 找到角点(整数像素)
bool found = cv::findChessboardCorners(gray, patternSize, corners, flags);
// 亚像素精化
cv::cornerSubPix(gray, corners, cv::Size(11,11), cv::Size(-1,-1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.001));
4.3 单应性矩阵求解
单应性矩阵H,描述的是两个平面之间的映射关系。在张正友标定法里,它连接了“棋盘格平面”和“图像平面”。
H是一个3x3的矩阵,有8个自由度(因为尺度归一化)。求解H,至少需要4对匹配点。但为了抗噪声,我们通常会用更多的点,然后用最小二乘法求解。
常用的方法是直接线性变换(DLT)。原理不复杂:
- 每一对匹配点,可以写出两个线性方程。
- 把所有的方程堆起来,形成一个超定方程组 Ah = 0。
- 用SVD分解,最小特征值对应的特征向量,就是H的9个元素。
嗯,这里要注意:DLT对噪声比较敏感。所以解出H之后,我通常会再用Levenberg-Marquardt算法做一次非线性优化,最小化重投影误差。
我的个人习惯:
在求解H之前,先把坐标做归一化处理。把像素坐标和世界坐标都缩放到[-1, 1]之间。这样可以提高数值稳定性,避免因为坐标值差异太大导致矩阵病态。
OpenCV的 cv::findHomography() 内部就做了这个归一化,所以直接用就好。
4.4 从单应性矩阵到内参
当我们有了一张图片的单应性矩阵H,就可以从中提取内参K和外参R、t。具体做法是:
- 把H分解成三个列向量:H = [h1, h2, h3]。
- 根据旋转矩阵的性质(R的列向量正交且模长为1),可以列出关于内参的约束方程。
- 多张图片的约束方程联立,就可以解出内参K。
这个过程在数学上比较繁琐,但OpenCV的 cv::calibrateCamera() 已经帮我们封装好了。你只需要提供角点坐标和棋盘格尺寸,它内部会自动完成H求解、内参提取和全局优化。
不过,理解原理还是有用的。有一次我在现场调试,发现标定结果总是收敛不到一个稳定的值。排查了半天,发现是棋盘格尺寸写错了——我告诉程序格子是30mm,实际打印出来是28.5mm。这种错误,如果你不懂原理,可能永远找不到原因。
4.5 本章知识结构
下面这张图,概括了内参标定的完整流程和核心逻辑。你可以把它当作一个“思维导图”来用。
4.6 实践中的几个建议
| 环节 | 常见问题 | 我的建议 |
|---|---|---|
| 棋盘格制作 | 反光、尺寸不准 | 用哑光纸,量好实际尺寸(精确到0.1mm) |
| 图像采集 | 模糊、过曝 | 保证快门速度够快,避免运动模糊;控制曝光 |
| 角点检测 | 漏检、误检 | 用 findChessboardCornersSB 试试 |
| 标定结果 | 重投影误差大 | 检查是否有畸变未校正,或者图像数量不够 |
内参标定这件事,说难不难,说简单也不简单。我见过有人用手机拍了几张照片就标定了,也见过有人折腾了一整天结果还是不对。差别在哪?就在对原理的理解和对细节的把控上。
记住:角点检准了,H算对了,内参自然就出来了。 别急着跑代码,先把每一步的原理搞清楚。