资讯详情

张正友相机标定实战:棋盘格、畸变矫正与避坑指南

📅 2026/10/14 8:41:10 | 华诺云谱 👁 阅读
张正友相机标定实战:棋盘格、畸变矫正与避坑指南
做视觉项目这些年来我觉得最容易被新手低估、又最影响后续所有环节精度的事情就是相机标定。很多同学花了大把时间调识别算法结果发现测距误差大得离谱、图像畸变矫正后边缘还是弯的回头查下来问题不在算法而在标定这第一步就没做扎实。张正友相机标定法可以说是计算机视觉入门绕不开的经典方案也是目前多数标定工具库默认使用的核心思路。这篇东西不打算复述教材推导而是想把我实际跑标定流程时踩过的坑、总结出的操作细节和判断标准一次性讲清楚给正在做视觉测量、三维重建、SLAM、机器人抓取或者单纯想做图像去畸变的同学一个可以直接照做的参考。1. 标定到底在算什么不是玄学是数学1.1 相机标定的本质把三维世界和二维像素对上号相机成像这件事说白了就是把三维空间里的物体通过镜头映射到二维传感器上。这个映射不是随意的它完全可以被一个数学模型描述。我们标定就是要搞清楚这个模型里的所有参数。最常用的基础模型是针孔模型。你可以把相机想象成一个小黑盒前面开了一个针孔外面的光线穿过针孔在盒子后面的平面上成像。针孔模型里最关键的两个东西是焦距和光心。焦距决定视角大小和放大倍数光心是成像平面中心对应的像素位置也就是主点。实际摄像头不会真的是针孔镜头是一组透镜所以除了焦距和光心还得考虑畸变。畸变就是镜头带来的成像变形最常见的是径向畸变表现为越靠近图像边缘直线越容易向内或者向外弯曲。此外还有切向畸变是镜头和传感器不平行导致的但量级通常比较小。我们最终要标出来的参数分成三类。第一类是内参包括焦距在x、y方向上的分量fx、fy和主点坐标cx、cy它们描述了相机内部的几何结构和镜头、传感器有关和相机怎么摆、摆在哪无关。第二类是外参描述了相机坐标系相对于世界坐标系的旋转和平移也就是相机在世界里的位置和朝向。第三类是畸变系数通常包括径向畸变参数k1、k2、k3和切向畸变参数p1、p2。搞明白了这三类参数分别代表什么后面看到标定结果输出的一大堆数字时才不会发懵。1.2 张氏标定法的核心思路凭什么用一张棋盘格就行在张正友标定法之前传统标定方法往往需要一个加工精度很高的三维标定靶比如两个垂直平面上排列的圆点阵列。那种设备贵还得精确知道每个特征点的三维坐标对普通实验室条件来说门槛太高。张氏标定法的核心贡献是证明了只用一块平面棋盘格标定板多拍几个不同角度和姿态的照片就能完整标出内参、外参和畸变系数。这一下把标定成本从几千上万块的设备拉低到了打印一张纸的程度。为什么棋盘格可以做到这件事关键在于棋盘格是一个平面它在数学上很容易描述。我们约定棋盘格所在平面的Z坐标为0那么平面上每个角点的三维坐标只用X和Y就能表示每个点对应图像上的一个像素坐标。从平面上的点到图像上的点这个变换可以用一个3x3的单应性矩阵H来表示。单应性矩阵里有8个自由度一对匹配点提供两个方程所以理论上4个点就能求解。棋盘格上有大量的角点计算一个可靠的H是完全够用的。更妙的是H里同时包含了内参和外参的信息。因为H K * [R的第一列R的第二列T]其中K是内参矩阵R的第一列和第二列是旋转矩阵的前两列T是平移向量。旋转矩阵有个数学性质它的各列之间是正交的而且长度相同。利用这两个约束条件每拍一张棋盘格照片就能给内参求解贡献两个方程。内参里往往有4个未知数fx、fy、cx、cy只要拍3张不同姿态的棋盘格就能得到6个方程把内参解出来。这就是张氏标定法最巧妙的地方不需要知道相机和棋盘格之间的距离也不需要知道棋盘格的三维姿态只需要几张不同角度的清晰照片。当然实际求解没有上面说的这么直接因为畸变的存在会让约束变复杂所以完整流程里还会加上畸变估计和整体优化。但核心思想就是这个平面棋盘格加不同姿态用单应性约束先求闭式解再用极大似然估计迭代精化。每一步都建立在简单的数学原理上可又非常工程友好。2. 从打印到拍摄标定板的准备和采集规范2.1 标定板选型纸质、亚克力还是液晶屏很多人觉得标定板随便打一张棋盘格就能用这话对一半。棋盘格的图案本身不重要重要的是角点位置精度和板面平整度。我最早在实验室就是拿A4纸打印了一张10x7的棋盘格网格边长20mm然后贴在一块厚实的硬纸板上。后来发现一个问题如果纸没有完全贴平或者硬纸板受潮弯曲靠近边缘的角点位置就会偏移标定出来的畸变系数也会跟着歪。在固定场景里长期使用我建议至少做到以下几点。一是用不易变形的平板做底比如亚克力板或者铝板把打印纸平整粘上去。二是网格边长不要太短太小会导致近距离拍摄时角点特别密集角点检测容易混边长太长又会让棋盘格在画面里的占比要么太大要么太小。我自己习惯用25mm到30mm的格子内角点数选择10x7或者9x6也就是整格是11x8或10x7。三是棋盘格底色要干净纯黑和纯白交替不要有反光覆膜。可以考虑哑光打印纸能有效减少反光干扰。也有朋友问能不能直接把棋盘格显示在屏幕上用显示器当标定板。屏幕标定板的好处是角点像素位置极端准确也不需要打印贴板坏处是屏幕本身有玻璃盖板和亮度不均匀近距离拍摄可能引入微小偏差。如果你是标定手机摄像头或者普通摄像头做实验用屏幕也是可行的但要注意将屏幕亮度调到均匀并且固定好后别动。如果是高精度工业测量还是踏踏实实用实体硬板。2.2 图像采集的要点和数量采集图像是整个标定流程里最看操作的部分因为这一步做得好坏直接决定了后面优化算法能拿到多大的信息量。我见过不少同学上来就对着棋盘格咔咔拍了20张结果全是正面的、板子占了屏幕一大半程序跑完重投影误差0.3像素还找不出原因。问题就出在姿态覆盖太单一。正确做法是让棋盘格在画面里以不同姿态出现。具体来说前排照片应该把棋盘格放在画面中央偏左、偏右、偏上、偏下这样的位置后排要把棋盘格倾斜倾斜角度控制在45度以内。除了左右倾斜也要有上下倾斜比如俯视和仰视角度。这样做的目的是让单应性约束条件在图像平面不同区域都能得到充分采样畸变系数才能被可靠地分离出来。数量上15到25张是比较稳妥的范围。少于10张内参约束不充分多余30张收益递减还容易掺入几张大角度烂图反而降低整体精度。我现在的习惯是采集的时候每换一个姿态就快速看一眼检测结果如果有几张角点检测失败的就直接在现场重拍绝不留到后面处理。拍摄时还要注意棋盘格不能移动手不能抖尽量用三脚架或者把相机放在稳定的台面上避免模糊。2.3 一个容易忽略的细节精度从图片拍摄开始就注定标定程序的算法再精巧输入图片的质量不行一切白搭。这个“质量”不仅仅是分辨率更包括焦距是否固定、光圈是否合适、光照是否均匀。相机如果是自动对焦模式必须在标定前锁定焦点。我发现一个很隐蔽的坑有些工业相机本身没有自动对焦但镜头是手动变焦环拍的过程中不小心碰一下焦距变了内参就完全失效。更麻烦的是自动曝光。在光线较强的位置拍几张在光线较暗的位置拍几张虽然棋盘格角点能检测出来但曝光变化会让某些边角的黑白对比度低于检测阈值或者图像噪声变大。最好使用固定曝光参数或者至少保持环境光照稳定。棋盘格在画面里的占比也很讲究。太大需要把相机凑很近图像边缘区域没有覆盖畸变参数不完整太小角点像素粒度不够亚像素细化也会不稳。我的经验是棋盘格在画面中大约占1/4到1/3比较合适。这样既保证角点之间有足够像素间隔又能让棋盘格移动到画面边缘。另外一个细节是拍摄时不要让棋盘格完全贴住图片边界因为角点检测算法在靠近边缘时容易出现矩形拟合偏差宁可留一点边距。3. 核心步骤逐段剖析角点、单应矩阵与参数优化3.1 角点检测怎么让计算机找到棋盘格张氏标定法里用到的特征点是棋盘格黑白方块的交点也就是内角点。OpenCV的cv2.findChessboardCorners函数就是干这个事的。函数输入棋盘格内角点数量比如图案是10x7的格子内角点就是9x6对应patternSize(9, 6)。不少新手在这里第一次翻车搞不清楚到底填是格子数还是角点数。记住填的是内角点的行数和列数不是格子数量。角点检测的几个细节值得多说几句。第一如果图像有轻微噪声或者明暗不均可以先转成灰度图再做一次直方图均衡化可以提高检测鲁棒性。第二检测到角点后可以用cv2.cornerSubPix做亚像素细化因为棋盘格角点在低分辨率下可能存在几个像素的定位偏差不细化会导致重投影误差偏大。第三图像次数多的时候最好把每个检测结果可视化出来看一眼不要只看是否返回True。我在实际项目里就发现过一组图片全部检测成功但其中一张的角点排序明显错位不观察根本发现不了。为了方便大家直接跑我贴一下常规的角点检测代码import cv2 import numpy as np pattern_size (9, 6) # 内角点数量 image cv2.imread(calib_001.jpg) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) found, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE ) if found: criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) cv2.drawChessboardCorners(image, pattern_size, corners_refined, found) cv2.imshow(corners, image)这段代码里的CALIB_CB_NORMALIZE_IMAGE会对图像进行归一化处理对光照不均特别有效。我在标定强反光板时发现加上这个flag能减少不少检测失败。3.2 单应矩阵与内外参求解方程是怎么立起来的角点检测完成之后我们得到了一组匹配棋盘格上每个角点在三维世界里的坐标因为Z0所以是二维坐标以及它们在图像里的像素坐标。对每个视角我们可以建立一个单应矩阵H。H是一个3x3矩阵它把棋盘格平面上的点映射到图像平面。对于第i个世界坐标点(X, Y, 1)和对应像素(u, v, 1)满足[u] [h11 h12 h13] [X] [v] [h21 h22 h23] [Y] [1] [h31 h32 h33] [1]这个式子展开后有8个独立未知量所以至少4对点就能求解。实际操作中我们会有几十对点用最小二乘法消除噪声影响。然后在数学上H还可以分解成内参矩阵K和旋转、平移向量H K * [r1, r2, t]其中r1和r2是旋转矩阵的前两列t是平移向量。因为r1和r2是正交单位向量所以可以得到两个经典约束r1转置乘r2等于0以及r1的模长等于r2的模长。这两个约束就是求解内参方程组的关键来源。每张照片拍下来H已知K中的未知内参就能通过这些约束逐步解出。实际代码里OpenCV使用的是先通过线性求解得到一个初始估计然后考虑畸变进行非线性优化。这里我不打算把方程一步步展开因为教材里写得很清楚。我更想强调的是“为什么需要多角度拍摄”。如果你只拍一张正面图那么r1和r2实际上就是图像平面坐标轴在三维空间里的方向正交性和模长约束退化了内参解不唯一。多角度拍摄相当于给方程组补充不同姿态下的约束把耦合在一起的参数分开。3.3 畸变模型与估计把误差从结果里剔出去棋盘格的角点坐标经过理想针孔模型投影到图像上应该和检测到的像素位置完全一致。但镜头畸变会让实际成像位置发生偏移偏移量用多项式来近似。径向畸变模型一般写成x_distorted x * (1 k1 * r^2 k2 * r^4 k3 * r^6) y_distorted y * (1 k1 * r^2 k2 * r^4 k3 * r^6)这里的x、y是归一化坐标r是点到光心的距离。也就是说越靠近图像边缘畸变位移越明显。切向畸变模型要复杂一点带p1和p2但一般量级较小。张氏标定法的处理流程是先假设没有畸变用线性方法求解内参和外参的初值再根据初值计算每个角点的理想投影位置对比实际检测位置估计出畸变系数最后把所有参数放在一起做一次全局优化。这个全局优化用的就是极大似然估计目标函数是所有角点的重投影误差平方和最小化。你可以把每个角点看成一个观测值我们希望找到一组内参、外参、畸变参数使得将棋盘格上每个点重新投影到图像时和实际检测位置的差距整体最小。重投影误差的计算本身不难理解用当前参数把世界坐标点投影成像素点这个投影点和角点检测得到的像素点之间的距离通常以像素为单位。误差为0.1像素意味着投影位置和检测位置平均差了0.1像素这已经是很高的精度了。我在标定完成后一定会把重投影误差打印出来因为它是一个直观的“标定总效果”衡量指标。3.4 OpenCV整套标定代码从图片列表到参数输出OpenCV的cv2.calibrateCamera把整个流程封装好了。在实际工程里我一般不直接一把梭而是先统一读取图片、检测角点、构建世界坐标再交给标定函数。下面是一个精简但完整的示例import cv2 import numpy as np import glob pattern_size (9, 6) square_size 0.025 # 格子边长单位米 objp np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32) objp[:, :2] np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2) objp * square_size obj_points [] img_points [] images sorted(glob.glob(calib_images/*.jpg)) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners cv2.findChessboardCorners( gray, pattern_size, flagscv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE ) if not found: print(fDetect failed: {fname}) continue criteria (cv2.TERM_CRITERIA_EPS cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners_refined cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) obj_points.append(objp) img_points.append(corners_refined) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None ) print(RMS error:, ret) print(Camera matrix:\n, mtx) print(Distortion coefficients:\n, dist)这段代码有几个地方要特别留意。square_size是棋格边长单位要和实际测量一致我建议统一用米。所有图片的分辨率必须一致否则gray.shape[::-1]会出错。rvecs和tvecs里保存的是每张图片对应的外参长度应该等于成功检测的图片数。标定完成后可以用cv2.undistort对图像做去畸变也可以用cv2.projectPoints把世界点投影回来手动算重投影误差。4. 常见问题与排查那些标定翻车的瞬间4.1 findChessboardCorners一直检测失败怎么回事这类问题我遇到得最多。最简单的检查项是pattern_size到底填对了没有。比如10x7的格子内角点是9x6如果填成(10, 7)程序会把每个格子的中心去找一遍大概率找不到。其次是图像清晰度画面模糊、运动模糊、对焦不准都会让角点定位失败。光照也很关键棋盘格被强光照射产生局部高光或者接近黑色区域反光发白都会导致阈值分割失败。还有一种情况是棋盘格太大或太小。如果你把整张棋盘格拍得很小每个格子在图像里只有三四个像素宽算法很难分辨角点。反过来格子占到画面80%以上镜头畸变大棋盘格的边缘格可能被压得很窄也检测不出来。我的经验是格子越长在画面中的每个格大概要有10像素以上才比较稳妥。如果实在检测不到可以先对图像做预处理gray cv2.equalizeHist(gray)或者用cv2.adaptiveThreshold增补对比度。还有一招是检查棋盘格图案本身是不是严格矩形。我之前用某个在线生成的棋盘格图片打印时发现它自带的白色边框会让检测区域偏移裁剪掉边框后立刻就好了。4.2 重投影误差很大到底多少算正常重投影误差的单位是像素理论上越小越好。常见的普通摄像头标定结果误差在0.1像素以内算是优秀0.1到0.2像素属于正常可用范围超过0.3像素就要警惕。工业相机配合硬质标定板0.05像素水平也不罕见。误差大通常有三个来源一是角点检测精度不够比如图像噪声大、棋盘格不平、反光导致亚像素定位偏二是参数模型和真实镜头不匹配比如用普通针孔模型去标定强鱼眼镜头三是数据集里混入了个别坏图比如有一张曝光过度或者棋盘格被手指挡住。对于最后一种情况我经常用perViewErrors来找出是哪张图拖后腿。cv2.calibrateCamera返回的ret是所有点的总RMS但如果用cv2.CALIB_RECOMPUTE_EXTRINSIC重新计算然后逐张算投影误差就能定位到问题图片。一个务实的做法是标定完先打印每张图的平均投影误差把这几个明显偏大的图删掉重新标定一次。但要注意不要为了追求漂亮数字反复删图删到只剩5张。数据太少的内外参是不可靠的。4.3 标定结果不稳定换一组图片内参就漂这个问题的根源通常是参数之间存在耦合或者某些数据没有提供足够约束。最典型的场景是只拍了大角度和近距离图像导致内参和畸变系数之间互相“抢”误差。举个例子如果所有棋盘格都拍在画面中心那么边缘畸变量基本没有被观察到畸变系数只能瞎猜如果所有棋盘格都处于同一姿态那么旋转约束不足内参解就容易不稳定。另外一个容易被忽略的原因是标定板不平。纸板弯曲、亚克力板变形都会让世界坐标里的角点平面假设失效。更隐蔽的是相机内部焦距变化尤其是变焦镜头如果拍摄过程中焦距被碰过内参必然漂。所以标定用的相机在整个流程中从硬件上锁死对焦和变焦软件上使用固定曝光是底线操作。我在实际项目里发现内参漂移还有一个常见来源图像分辨率不一致。同一批照片中有的被脚本缩放处理过有的没有部分图片的像元尺寸变了标定结果自然不对。建议统一用原始分辨率如果要缩放所有图片用完全相同的尺寸和插值方式。4.4 为什么矫正后的直线还是弯去畸变后直线依然弯通常说明畸变模型不够用或者畸变系数求错了。对于普通镜头k1、k2、p1、p2四个参数就够但如果镜头畸变很大比如廉价广角摄像头需要考虑k3也就是六阶径向畸变项。OpenCV默认会估计k3吗cv2.calibrateCamera默认使用全部畸变参数但如果用CALIB_FIX_K3之类的flag会把k3固定为0。检查一下你在标定时传的flags别因为固定了某些畸变参数导致模型不足。还有一种情况是畸变参数求出来了但你在cv2.undistort时没有使用新的相机矩阵newCameraMatrix。正确做法是先用cv2.getOptimalNewCameraMatrix得到缩放对齐后的内参矩阵再用这个矩阵做去畸变。否则矫正后的图像会损失一部分边缘像素甚至产生奇怪的拉伸。很多人以为只要调用undistort就完事了实际上这个函数的两个内参矩阵参数是有讲究的。4.5 换个场景标定为什么内参和之前不一样我把这种问题称为“同一台相机两种人格”。相机内参理论上应该固定但实际中如果镜头带可调光圈光圈大小改变会影响景深和畸变尤其是广角镜头如果相机有光学防抖或者自动裁剪开启防抖时画面的有效分辨率区域会变化。还有一点是温度工业相机长时间工作后感芯片受热膨胀内参会有轻微漂移。工程上我建议每次标定后把内参记录到文件如果发现漂移先检查相机设置是否被改动再检查是否温度过大或者镜头固定螺丝松动。如果一切正常但还是漂那就乖乖在当前场景重新标定一次毕竟标定是“此刻状态”模型不能指望一次标定吃一辈子。下面是一个常见的排查速查表问题现象可能原因解决办法角点检测失败光照不均、图案尺寸错误、模糊等化直方图、检查patternSize、重新拍摄重投影误差大个别坏图、板面不平、模型不足逐张查看误差、删掉坏图、检查flags内参漂移焦距变动、分辨率不一致锁死焦距、统一分辨率、重新标定边缘直线仍弯畸变参数不够、undistort参数错误启用k3、正确设置newCameraMatrix标定结果随机起伏数据集姿态太少、参数耦合增加不同角度姿态、剔除单一姿态图5. 实操心法让标定结果更稳的一些个人习惯5.1 我的标定流程小结整套流程走下来我现在习惯固定下来一套动作基本不会出大问题。第一步打印一块质量可靠的棋盘格贴在硬板上量好格子边长单位米。第二步固定相机光圈、焦距、曝光保证所有照片清晰。第三步采集20张左右的图片覆盖中心、边缘、倾斜、俯仰注意每张都要有棋盘格特征。第四步写一个脚本批量检测角点自动保存检测成功且角点排序正常的图片。第五步把所有合格图片交给calibrateCamera记录RMS。第六步重投影误差超过0.2像素就逐张定位并剔除问题图。第七步用同一套图片重新标定然后undistort一张包含明显直线的场景图目视验证直线是否变直。这套流程看起来很简单但每步都有值得抠的细节。比如采集图片时如果发现某张图检测失败我一般不会强行用而是直接重摆姿态再拍一张。又比如做双目视觉的话两张相机要分别标定内参再用stereoCalibrate计算双目标定参数这个后面可以单独写一篇。单目标定是一切的基础基础没打好后续三维重建、位姿估计的结果都会很不可靠。5.2 验证标定结果的两个硬指标很多人标定完成看到内参矩阵和畸变系数就以为完事了。我每次还会多走两步验证。第一步是在标定板上任意选两个距离很远的角点用cv2.solvePnP求出当前棋盘格在相机坐标系下的位姿再根据位姿将这两个角点的世界坐标投影回图像对比像素距离和实际检测距离。如果一个25mm格子边长的标定板反推出来的两个角点之间的距离和实际值误差在1%以内说明标定结果有实用价值。第二步更直接用cv2.undistort对一张原本有畸变的场景图做矫正找一条原本应该笔直的边缘比如墙面踢脚线、门框看看矫正后是否变直。如果边缘还有明显弯曲说明畸变参数还有问题。这一步不需要任何测量仪器肉眼就能判断特别适合在现场快速验收。我经常管它叫“门框测试”虽然不严谨但简单有效。5.3 扩展思考张氏标定法之外什么时候需要它张氏标定法的适用范围是普通针孔相机模型也就是视角不太广、畸变不是特别夸张的镜头。如果你用的是鱼眼镜头OpenCV里有单独的cv2.fisheye标定模块如果你标定的是工业面阵相机配合远心镜头那模型又不太一样如果是车载摄像头那种已经做过去畸变处理的传感器标定目标会变成外参标定。但不管是哪种场景张氏标定法里“用平面靶标多姿态采集、通过单应约束求内参、再优化畸变”的思想依然是底层的骨架。我后来做视觉定位和三维重建项目遇到一些奇怪的镜头也往往先用张氏流程把内参和畸变捞出来再针对特殊模型做修正。最后说一个我自己吃了不少亏才养成的习惯标定完成后的参数文件一定要包含相机型号、分辨率、镜头焦距、拍摄距离范围、标定板规格和日期。因为你会发现三个月后项目迭代需要重新标定如果不记录这些条件到时候连“当前参数是在什么条件下得到的”都说不清。把这个信息写进代码配置里能省掉很多重复劳动。标定不是跑一次就结束的事它是整个视觉系统的地基值得多花时间把它做扎实。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑