相机标定原理与实战:张正友标定算法从棋盘格到参数求解
做视觉项目这几年最绕不开的一个基础操作就是相机标定。无论是三维重建、机器人抓取还是增强现实里给真实场景叠模型第一步几乎都是先把相机参数求准。很多人直接用现成的标定工具跑一遍出结果就完事但真遇到“重投影误差总压不下来”“同一台相机标两次差很多”这类问题时就容易卡住。这篇我打算把张正友标定算法从原理到实战完整拆一遍并用一次完整的棋盘格标定流程把内参、外参、畸变系数是怎么一步步算出来的讲清楚。不管你是刚接触视觉的初学者还是已经在做落地项目的初级工程师都能在这里找到直接能用的经验。1. 相机标定到底在标什么1.1 相机成像模型小孔成像与镜头畸变先别急着碰代码把模型理清楚再说。理想情况下相机就是一个完美的小孔成像装置三维空间里的点通过光心投影到成像平面上形成一个二维像素点。这个过程涉及到四个坐标系世界坐标系、相机坐标系、图像物理坐标系和像素坐标系。世界坐标是客观三维点相机坐标是把它变换到以相机光心为原点的坐标图像物理坐标是投影到成像平面上的物理坐标毫米像素坐标是最后离散采样的整数坐标像素。从世界到像素链路其实就三段。第一段是刚体变换用旋转矩阵 R 和平移向量 t 把世界坐标系的点搬到相机坐标系。第二段是透视投影相机坐标系下的点按照焦距投影到成像平面得到一个归一化坐标。第三段是缩放和平移把物理坐标换算成像素坐标涉及 x 方向和 y 方向的像素尺寸倒数再加上主点偏移 cx、cy。把这三段合并成矩阵形式就是大家常见的 P K·[R|t]。其中 K 是内参矩阵包含四个关键量fx、fy 是等效焦距单位是像素cx、cy 是主点坐标理论上在图像中心实际装配误差会让它偏移一些。这四个量构成内参。但镜头不是理想小孔。受透镜形状和装配工艺影响实际成像会发生畸变主要分两类一类是径向畸变光线离透镜中心越远弯曲越厉害表现为图像边缘的直线向外弯或向内弯也就是桶形和枕形畸变另一类是切向畸变由透镜与成像平面不完全平行造成会让直线变成斜向弯曲。标定畸变系数就是在求这些畸变模型的参数。通常用五个参数表示k1、k2、k3 对应径向畸变p1、p2 对应切向畸变。1.2 内参、外参与畸变系数的含义回到实际需求。内参是相机自身的固有属性出厂后原则上不变。它决定了三维场景到二维图像的几何映射关系。外参则描述相机在世界坐标系中的位姿每次移动相机或物体外参都会跟着变。也就是说同一台相机内参不变但只要拍摄角度变了对应每张照片的外参就不同。畸变系数负责修正镜头带来的非线性变形。如果不做畸变校正一条直线在图像边缘会变成曲线测量的精度就是空中楼阁。标定结果里的 fx、fy 还会受到传感器像素尺寸和镜头实际焦距的影响。变焦镜头在不同焦段内参不同定焦镜头通常标一次就能用很久。理解这些之后你就会发现标定的本质是求解一组最优的内参、外参和畸变系数使得所有标定板角点投影到图像上的位置与检测到的真实位置尽可能接近。张正友算法的聪明之处就是用一块平面棋盘格把这个非线性问题的初值估计变得非常简单从而稳定收敛到全局最优。它不需要昂贵的三维标定块不需要精确测量靶标的深度只需要几张不同角度的平面照片就能把内参和畸变系数都求出来。2. 张正友标定算法的核心思路2.1 为什么选棋盘格标定靶有很多种棋盘格、圆点阵列、编码靶标。为什么张正友算法首选棋盘格首先是角点特征非常稳定。黑白方块交错角点是两种颜色邻域的公共顶点在图像里可以被定位到亚像素级别精度高这是后续重投影误差能压到 0.1 像素以下的基础。其次是成本低。白纸打印图案贴到硬质板上就能用不需要精密加工。第三是数学上好描述棋盘格所有角点都在同一个平面上相邻格子的间距已知角点的世界坐标可以直接设定不用额外测量三维坐标。用平面靶标是张氏标定法的另一个关键创新。三维标定物虽然单张照片就能提供更多约束但制作高精度三维靶标很难、很贵。平面棋盘格只需要一张照片就能提供一组单应关系多张照片就能提供多组约束足以解出全部内参。这就是性价比所在。2.2 从单应矩阵到内参约束一张棋盘格照片中所有角点的世界坐标都落在 z0 平面上。利用这一点可以把旋转矩阵 R 的第三列消掉得到一个 3×3 的单应性矩阵 H。它是内参矩阵、旋转矩阵前两列和平移向量的混合体。每张照片的角点像素坐标和对应的已知世界坐标构成了多组匹配点。单应矩阵 H 有 8 个有效自由度理论上至少需要 4 组匹配点就能线性求解。棋盘格上的角点远多于 4 个所以可以用最小二乘求超定解降低噪声影响。得到多张照片的 H 之后再利用旋转矩阵列向量之间的正交性和长度相等这两个天然约束可以构造出关于内参矩阵元素的线性方程。一般来说至少需要 3 张不同姿态的棋盘格照片才能解出全部内参初值。这个“初值”非常重要——后面所有非线性优化都需要一个靠近真实值的起点否则很容易掉进局部极小。2.3 极大似然优化与畸变迭代线性求解得到的解是在理想无畸变假设下的近似还没考虑镜头畸变也容易受角点检测噪声影响。张正友算法后续采用极大似然估计做非线性优化。假设所有角点检测噪声服从独立同分布的高斯分布那么最优参数应该让所有重投影误差的平方和最小。这就是我们常说的“最小化重投影误差”。畸变系数则采用交替求解的思路先假设零畸变用线性求出的初值估计内外参然后基于畸变模型用当前内外参反算出畸变参数接着把畸变参数代回去重新优化内外参。如此迭代几次畸变系数和内外参一起收敛。整个流程的本质就是先用几何约束拿到一个比较靠谱的初始值再扔给非线性优化器去精修。理解了这条主线你以后再看到任何标定工具箱都能明白它背后做了哪几步。3. 实战采集棋盘格照片与角点提取3.1 相机与棋盘格准备实战环节用普通工业相机或者手机摄像头都可以但条件控制必须到位。棋盘格要平整、图案清晰、尺寸已知。我习惯用内角点数 9×12 或 10×14 的棋盘格。这里说的“内角点数”不是棋盘格的总格子数而是黑白方块交叉顶点的行列数。比如 9×12 就是横向 9 个交叉点、纵向 12 个交叉点。打印后一定要确认一个格子的实际边长比如 25 mm。别小看边长测量误差——如果你尺子歪了 1 mm标定的焦距就会按比例偏移直接影响三维重建的尺度。棋盘格最好贴在硬质平板上不能有折痕、翘边或反光。自己打印的话尽量用激光打印喷墨容易洇墨导致黑白交界模糊角点定位精度会掉。相机设置也容易踩坑。标定过程中焦距必须固定自动对焦必须关掉。如果相机有自动曝光也建议固定曝光防止亮度变化影响角点提取。我用工业相机时通常手动调好焦距和曝光后把自动功能全部关闭然后才拍摄。3.2 拍摄采样的要点与技巧一个常见误区是觉得拍得多就行。真正重要的是多样性。让棋盘格在画面里依次出现在中心、左上、右上、左下、右下同时做出不同程度的倾斜前倾、后倾、左歪、右歪。还要注意让棋盘格整体在景深范围内不要出现局部模糊。如果照片边缘的角点模糊了那组数据基本就得废掉。拍多少张合适我的经验是 1520 张比较稳低于 10 张就容易欠约束。这里的“约束”说的是不同姿态给单应矩阵提供的信息量。如果所有照片都是正对相机、没有倾斜那得到的 H 约束高度同源内参求解会不稳定。拍摄时要像拿着棋盘格画画一样让它在画面里留下各种位置和角度覆盖均匀。光线也需要注意。避免强反光打到棋盘格上尤其白格子过曝之后角点附近的信息会被抹平。如果有条件用偏振片或者散射光源会更稳但一般室内灯光下也够用。3.3 角点提取的两种方式采集完成后下一关是提取角点。通常分两步粗提取和精定位。粗提取常用经典的角点检测算子在图像上计算响应然后结合棋盘格的拓扑结构把角点按行列排序。精定位是在粗检测得到的近似位置附近基于邻域梯度的方向信息做亚像素级优化。主流视觉库和标定工具箱都会提供现成接口但你要理解接口背后的假设它要求图像里检测到的角点数量与你指定的内角点数量完全一致。如果画面里有杂物恰好也形成一个角点就会干扰排序。所以标定前最好手动检查每一张图剔除不合格图片。还有一个细节角点检测时输入的图像分辨率要一致。如果你把高分辨率图缩放后再检测内参矩阵里的 fx、fy 也会跟着缩放最终结果需要换算回来否则会出现比例错误。我在初学时就因图省事缩放过图像导致标定焦距与相机真实焦距偏差一倍。4. 参数求解与结果验证4.1 计算单应矩阵与线性初值在实战里我们需要为每张棋盘格图像建立内角点的世界坐标。例如一个 9×12 内角点的棋盘角点世界坐标可以设为 (u·格边长, v·格边长, 0)其中 u、v 是角点行列索引。然后通过角点检测得到对应像素坐标整理成一个线性方程组。构造方程组的方式有很多核心是让每一对匹配点提供两行约束把单应矩阵的 9 个元素作为未知数组织成一个 2n×9 的矩阵。对这个矩阵做 SVD 分解最小奇异值对应的右奇异向量就是单应矩阵的 9 个元素。这样得到的 H 没有显式约束后续要把它分解为内参和外参。这个过程有明确的几何意义H 中包含了内参和该视角下的外参。根据旋转矩阵列向量正交和等长的性质可以从 H 中恢复出 fx、fy、cx、cy 的初值以及每一张图的旋转矩阵和平移向量。初值精度不需要很高只要不至于让优化发散就行。4.2 带畸变模型的非线性优化线性解出内参初值、外参初值后就要进入非线性优化阶段了。优化变量包括 fx、fy、cx、cy、畸变参数以及每张图片的旋转向量和平移向量。旋转向量是旋转矩阵的李代数表示3 个参数。所以总未知数大概就是 4 5 6×图片数。如果拍 15 张图未知数约 99 个对应的重投影误差方程则有 15×角点数×2 个。求解常用 Levenberg-Marquardt 算法它结合了梯度下降和高斯牛顿方法的优点对初值不敏感收敛稳定。损失函数是最小化所有角点的重投影误差平方和。重投影过程要严格对齐这条链路世界坐标点通过外参变换到相机坐标系按畸变模型修正坐标再通过内参投影到像素平面。这一步很容易在代码里把顺序写反。比如畸变模型是在归一化坐标上做修正还是在像素坐标上修正两种顺序完全不同。正确做法是先把相机坐标除以 z 得到归一化坐标然后在归一化坐标上施加径向和切向畸变最后再用内参把畸变后的归一化坐标换算到像素。4.3 标定结果怎么看重投影误差分析标定完成后第一件事是看平均重投影误差。它表示所有角点重投影后的像素位置与检测到的像素位置之间的平均距离。通常小于 0.1 像素就算不错小于 0.05 像素属于优秀。如果误差在 0.3 像素以上就说明有问题先别急着用结果。还要单独看每张图的误差分布。如果绝大多数图误差很小只有一两张图误差特别大那基本就是这几张图的角点定位有问题建议删掉重新标。可以把每张图的二维误差画成散点图观察是否远离整体分布。还有一种情况误差曲线收敛到了局部极小值表现为整体误差偏大且分布杂乱。这时可以换一组不同的初值重新优化比如去掉一两张图再标看结果是否稳定。另外标定结果一定要检查 fx 和 fy 的比值在合理范围内。对于像素为正方形的传感器二者非常接近只有长条形的传感器才会出现明显差异。如果你标出 fx 是 fy 的几倍多半是图像被缩放或者角点坐标系搞反了。5. 常见问题与排查技巧实录5.1 照片数量与角度选择不理想我见过不少朋友拿着 6 张照片就标结果误差很大。原因就是角度太少、约束不足。主点 cx、cy 和焦距 fx、fy 之间存在耦合如果棋盘格永远只出现在中心附近主点几乎不可观测结果自然忽大忽小。建议至少 10 张且要让棋盘格在图像里“活起来”有平移、有旋转、有倾斜。还可以用分块策略先用 5 张标一遍再加 5 张看内参是否稳定。如果内参变化幅度超过 1%说明图片集信息量不够继续补充角度。5.2 角点检测失败的排查角点检测失败的典型表现是检测到的点数不对或者角点排序混乱。先检查图片列表是否与棋盘格参数匹配比如内角点数是不是设反了。再检查画面里有没有遮挡、阴影、反光。有时镜头畸变太大边缘区域的黑白块会被严重拉变形粗检测找不到可以试试将棋盘格离镜头远一点减少极端透视。我还会做一步“可视化检查”把检测到的角点按行列画在图上看连线的走向是否自然。如果某些角点连成了斜线甚至交叉说明排序出了问题对应的图像要剔除。5.3 标定结果时好时坏的深层原因同一组照片重复标定两次结果不同多半是优化陷入了不同的局部极小值或者初值受检测噪声影响。实践中可以先用不同子集标定检查内参的方差。如果相差太大说明模型有问题常见原因是棋盘格不够平、边长量错或者相机标定时焦距变化了。还有一种隐蔽问题很多人会从图片的 EXIF 信息里读像素尺寸但传感器存在裁剪模式不同模式对应的有效像素区域不同。如果你用了裁剪后的分辨率却用全幅的物理尺寸去换算fx 就会与真实值不匹配。解决办法是始终以实际参与角点检测的图像分辨率作为标定基准最后再用统一尺度校验。5.4 如何验证标定质量最直接的验证是标定后用几张不同视角的图片做反投影看特征点对准情况。把标定结果保存下来写一个重投影函数把棋盘格角点的已知世界坐标投回图像和检测结果叠加显示。如果误差小于半像素肉眼看不出缝隙说明标定质量基本过关。更好的是做一个简单三角化用标定后的内外参去重建一个已知长度的线段查看相对误差。在机器人抓取流程里我常用标定参数把图像上的圆心反投影到世界平面再与机械臂示教点做比对精度一眼可见。这种“端到端”验证比单纯看标定误差更有说服力因为它把整条像素到物理空间的链路都串起来了。我个人实际操作中的体会是标定这件事三分算法七分采集。与其花时间调优化参数不如先把照片质量和棋盘格平整度做到位。多花十分钟拍好照片后面就能省下一小时调参。最后再分享一个小技巧标定完成后把内参矩阵和畸变系数存档到配置文件里并记录标定时的镜头焦距和分辨率。只要镜头没动过内参就可以长期复用但只要你碰过镜头、换过对焦环或者传感器模式变了就老实重新标一次别省这个时间。