3. 单相机内参标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数

单相机内参标定,说白了就是搞清楚你的相机到底是怎么「看」世界的。每个镜头都有脾气——焦距多少、光心偏不偏、画面边缘有没有变形。这些参数不标定清楚,后面四个相机联合起来肯定乱套。

我刚开始做多相机系统时,觉得内参标定就是个例行公事,随便拍几张棋盘格跑一下就行。结果有一次,四个相机各自标定完,拼接出来的全景图在接缝处总是对不齐。折腾了两天,最后发现是其中一个相机的畸变系数没收敛好。嗯,从那以后我再也不敢小看这一步了。

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

张正友标定法,核心思路其实很巧妙——你不需要知道棋盘格在空间中的精确位置,只需要它是个平面。你拿着棋盘格在相机前变换姿态,算法就能从多张图像中解出内参。

为什么这招管用?因为棋盘格上的角点坐标是已知的(比如每个格子30mm),它们在图像上的投影位置又可以被检测到。每张图像提供一个单应性矩阵,多个单应性矩阵联立,就能把内参约束出来。

核心优势:

  • 只需要一个平面棋盘格,不需要精密的三维标定块
  • 操作简单,手持棋盘格变换姿态即可
  • 鲁棒性好,对噪声和畸变都有不错的容忍度
  • 数学上封闭解+非线性优化,收敛稳定

我个人习惯用至少15-20张不同姿态的棋盘格图像。太少的话,内参容易过拟合,尤其是畸变系数。

3.2 棋盘格角点检测:从图像到亚像素坐标

角点检测是标定的第一步,也是最容易出问题的一步。你想想看,如果角点坐标本身就有偏差,后面算出来的内参能准吗?

OpenCV里用的是cv::findChessboardCorners(),它的大致流程是这样的:

  1. 图像预处理:转灰度、自适应阈值化,增强棋盘格对比度
  2. 四边形检测:找到图像中所有可能的四边形区域
  3. 棋盘格拓扑验证:检查四边形是否按棋盘格规律排列(黑白交替)
  4. 角点精化:用cv::cornerSubPix()把角点坐标推到亚像素精度

这里有个坑——我曾经遇到过棋盘格反光太强,导致部分角点被高光淹没。检测出来的角点数量不对,程序直接报错。后来我加了一条经验规则:

避坑指南:

我曾经在强光环境下拍棋盘格,角点检测总是漏掉边缘的几个点。后来发现,把棋盘格贴在哑光板上,或者用偏振片滤掉反光,检测成功率能提升到95%以上。另外,棋盘格不要太小——在图像中至少占1/4画面,否则角点间距太近,亚像素精化效果会变差。

亚像素精化这一步很多人会忽略。实际上,cornerSubPix()用的是灰度梯度法,在角点附近拟合一个鞍点模型,精度能从像素级提升到0.1像素以内。我建议迭代次数设到30,终止精度设到0.001。

3.3 内参矩阵:相机是如何投影的?

内参矩阵K,就是描述相机内部几何结构的3x3矩阵。它长这样:

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

其中:

  • fx, fy:焦距,单位是像素。注意,它不是物理焦距(毫米),而是物理焦距除以像元尺寸。比如焦距12mm,像元大小3.45μm,那fx ≈ 12 / 0.00345 ≈ 3478像素
  • cx, cy:光心坐标,理论上在图像正中心,但实际会有偏移

为什么fx和fy可能不一样?因为CMOS的像元不一定是正方形。我遇到过一些工业相机,像元是矩形,fx和fy能差出1-2%。如果不标定出来,后续的三维重建会引入系统性误差。

内参矩阵的作用,就是把相机坐标系下的三维点投影到像素平面。公式很简单:

s * [u, v, 1]^T = K * [X_c, Y_c, Z_c]^T

s是尺度因子,因为深度信息在投影过程中丢失了。

3.4 畸变系数:镜头不是完美的

没有镜头是完美的。畸变主要分两类:

畸变类型 产生原因 数学模型 典型表现
径向畸变 透镜曲面不完美 k1, k2, k3 画面边缘直线变弯(桶形/枕形)
切向畸变 镜头与CMOS不平行 p1, p2 画面倾斜,像被拧了一下

OpenCV里常用的畸变模型是5参数模型:(k1, k2, p1, p2, k3)。k3一般只在鱼眼镜头或畸变很大的广角镜头里才需要。普通镜头用4参数(k1, k2, p1, p2)就够了。

注意:

畸变系数不是越大越好,也不是越小越好。我曾经标定过一个镜头,k1跑到了-0.45,看起来很大,但实际就是这个镜头的真实畸变。不要为了追求「好看」而限制畸变系数的范围。另外,如果标定出来的畸变系数符号和预期相反(比如桶形畸变应该是负k1,结果跑出正数),说明标定图像质量有问题,需要重新采集。

3.5 标定流程与质量评估

完整的单相机内参标定流程,我一般这么走:

  1. 采集图像:15-20张,覆盖画面各个区域,姿态包括俯仰、旋转、平移
  2. 角点检测:检查每张图的角点是否全部正确检出
  3. 初始估计:用封闭解计算内参和畸变的初值
  4. 非线性优化:用Levenberg-Marquardt算法最小化重投影误差
  5. 质量评估:检查重投影误差,一般应小于0.5像素

重投影误差是衡量标定质量的核心指标。它的含义是:把棋盘格上的三维角点,用标定出的内参和畸变系数投影回图像,和实际检测到的角点位置差多少。平均误差小于0.3像素算优秀,0.5像素以内可接受,超过1像素就要重新标定了。

我个人的经验阈值:

  • 重投影误差 < 0.3 px:标定质量很好,可以用于高精度测量
  • 重投影误差 0.3 - 0.5 px:标定质量合格,适合一般视觉应用
  • 重投影误差 > 0.5 px:建议检查图像质量或增加标定图像
  • 重投影误差 > 1.0 px:标定失败,需要重新采集数据

3.6 本章知识结构

下面这张图把单相机内参标定的核心逻辑串起来了。从棋盘格图像出发,经过角点检测、单应性矩阵求解、内参估计、畸变优化,最后得到完整的内参和畸变系数。每一步都有对应的OpenCV函数和注意事项。

单相机内参标定流程 单相机内参标定核心流程 棋盘格图像采集 角点检测与精化 单应性矩阵求解 内参初始估计 畸变系数优化 全局非线性优化 输出:内参矩阵 + 畸变系数 质量评估:重投影误差 误差过大,重新优化 关键函数: findChessboardCorners() calibrateCamera()

3.7 实战中的几个关键细节

最后分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  • 棋盘格不要太小:在图像中至少占1/4画面,否则角点间距太近,亚像素精化效果差
  • 姿态要丰富:不要只拍正对着相机的姿态,要多拍倾斜、旋转、靠近边缘的。我一般会拍一组「绕X轴转」、「绕Y轴转」、「前后移动」的组合
  • 剔除坏图:如果某张图的角点检测明显不准(比如边缘角点偏移),果断删掉。10张好图比20张烂图强
  • 检查重投影误差分布:不要只看平均值,要看每张图的误差。如果某张图误差特别大,说明那张图的姿态或角点有问题

一个小技巧:

标定完成后,我习惯用cv::undistort()把一张棋盘格图像去畸变,然后目视检查画面边缘的直线是否变直了。如果原来弯曲的棋盘格线变直了,说明畸变标定是有效的。这个方法虽然粗糙,但很直观,能快速发现明显的问题。