1. 标定这件事到底在解决什么问题
1.1 相机为什么需要标定
先想一个特别基础的问题:你用手机拍了一张照片,照片上的某个像素点坐标为 (u, v),你能不能直接说出这个像素对应的物体在现实三维空间中的位置?答案是不能。因为从三维世界到二维图像,中间隔着一条完整的数学链路——针孔成像模型,而这条链路上每一个参数都在影响最终的映射结果。
焦距决定了成像的缩放比例,主点决定了图像坐标系的原点偏移,畸变系数则描述了镜头因为制造工艺和光学结构带来的非线性形变。这些参数统称为相机内参。此外,相机在拍摄每一张图片时都有自己的位置和朝向,这组参数叫做外参。所谓标定,核心工作就是把这些内参和外参一并求出来。
这个过程在工程里到底有多重要?我做视觉测量项目时体会很深。无论是做三维重建、双目测距、AR叠加,还是机械臂抓取定位,只要算法输出的是带物理单位的坐标,不是纯分类标签,相机标定就是绕不开的第一步。标定精度差个一两个像素,在近距离测量里可能只差几毫米,但一旦物距拉长到两三米,误差会被显著放大,整个系统的可靠性都会垮掉。所以标定不是可选项,而是视觉项目的基石。
1.2 张正友标定法为什么能成为工业标准
张正友标定法最早是1998年发表的论文《A Flexible New Technique for Camera Calibration》里提出的方法。它最大的贡献,是把以前需要高精度3D标定块的标定流程,简化成了打印一张棋盘格就能完成。
以前传统标定方法需要一个加工精度非常高的三维标定块,拍摄多组不同姿态的图片才能求解。这种设备在实验室里有,但普通开发者、学生根本搞不到,成本也高。张正友的方法只需要一个平面棋盘格,让相机在不同角度下拍摄多张照片,然后利用平面靶标上的已知角点坐标和对应像素坐标,先通过单应性矩阵估算初始内参,再通过非线性优化把所有参数放在一起精修。整个流程稳定、精度高、操作门槛低,所以后来OpenCV的calibrateCamera、MATLAB的Camera Calibrator都采用了这个思路,成了事实上的工业标准。
我把这个实验放在MATLAB里跑,主要是有两个考量:第一,MATLAB的Camera Calibrator图形化工具把角点检测、参数求解、误差展示全做成了可视化流程,非常适合先建立感性认识;第二,MATLAB底层提供了detectCheckerboardPoints、estimateCameraParameters这类封装好的函数,同时你也可以自己写核心求解代码来对照验证。两边结合,既能快速出结果,又能真正把原理吃透。
2. 实验前的准备工作
2.1 棋盘格打印与制作
棋盘格是张正友标定法的核心靶标,制作质量直接决定实验成败。第一次做实验的同学最容易忽视这步,随便找一个网页上的棋盘格图片用A4纸打印就开始了,结果角点检测成功率很低,标定结果也飘。
棋盘格有几个关键参数要提前定好:格子数量、每个格子的物理尺寸、打印尺寸。以我常用的12x9棋盘格为例,意思是内部角点是11x8个,每一格的实际边长设为30mm,整个棋盘打印出来大概占一张A4纸。打印之后一定要用直尺或者游标卡尺实测几个格子边长,确认实际尺寸和设定尺寸一致。不要完全信任打印机的缩放比例,有些打印软件默认“适应页面”会按比例缩放,导致实际尺寸偏离设定值。
另外两个容易被忽略的细节:棋盘格必须贴在一个绝对平整的硬板上,比如亚克力板、硬纸板,不能用软纸直接拿在手里;打印介质要尽量选择哑光纸,避免表面反光导致角点检测出现高光干扰。我在实验中曾经用过普通亮面喷墨纸,光线稍微偏一点,图像上就会出现一片白色光斑,那一块棋盘格根本无法检测。
2.2 图像拍摄的规范与技巧
图片的拍摄质量,是标定误差的天花板。这一点无论你用多好的算法都改变不了。
按张正友标定法的要求,拍摄时只需要一个平面靶标,但为了在数学上保证内参可解,棋盘格必须在相机视野内呈现多种不同的姿态。我一般遵循几个原则:第一,数量上拍15到20张,至少也要保证12张以上;第二,棋盘格与成像平面的夹角要有变化,倾斜角度从0度到45度都有覆盖,光拍正对相机的图是远远不够的;第三,棋盘格在画面中的占比保持在三分之一以上,太小了角点检测精度差;第四,拍摄距离要变化,让标定板在画面里有大有小,这样求解时不同距离的信息能更好地约束焦距和畸变参数。
实际操作时可以用手持棋盘格,边转动边拍。手机、工业相机、普通USB摄像头都可以作为拍摄设备。如果是手机,建议固定分辨率,关闭自动对焦锁定焦距,用后置主摄拍摄;如果镜头有光学防抖(OIS),拍摄时要让手机尽量稳定,防止运动模糊。另外每一张图片都要保证清晰,模糊的图宁愿删掉重拍,也不要用进标定集合里。
2.3 MATLAB环境准备
这个实验对MATLAB版本没有严格限制,R2020a之后都可以。我自己常年在R2021b和R2023b之间切换,操作逻辑基本一致。实验需要用到Computer Vision Toolbox,装这个工具箱的原因主要是它提供了棋盘格检测函数和标定参数估计函数。如果你的License只有基础模块,在命令行输入ver查看已安装工具箱列表,没有的话就需要先安装。
如果只是做实验验证原理,手写求解核心部分只依赖基础的矩阵运算,没有工具箱也能跑通,但检测棋盘格角点这一步还是建议用工具箱函数替代。手工做亚像素角点检测不是不行,但工程效率和鲁棒性远不如封装好的detectCheckerboardPoints。另外,如果是学生或者评测用户,可以用学校提供的校园License激活,或者使用MATLAB Online,不需要额外处理授权问题。
3. 基于MATLAB的标定实验全流程
3.1 用Camera Calibrator图形化完成标定
MATLAB的Camera Calibrator是入门最快的路径,我第一遍带学生做实验基本都走这个流程。启动方式很简单,在命令行输入:
cameraCalibrator或者在App选项卡里找到Camera Calibrator,点击打开。
第一步,导入图片。点击Add Images,选择你拍好的所有棋盘格图片。如果是视频中截取的帧,也可以直接加载视频文件。导入后MATLAB会自动尝试检测每张图里的棋盘格角点,并在图上用彩色圆圈标出。如果某张图显示“Not detected”,说明这张图质量不达标,可以直接从列表里移除。
第二步,设置棋盘格尺寸。在标定板参数栏里,有一个关键输入项——Checkerboard square size,单位是毫米。这里填的就是每个格子的物理边长,比如30。注意,系统默认认为相邻两个内角点之间的距离等于这个值,所以一定要填实际测量值。
第三步,选择畸变模型。MATLAB默认同时估计径向畸变和切向畸变,一般保持默认的3 Coefficients(3个径向畸变系数k1、k2、k3)加2个切向系数p1、p2。对于普通工业镜头和手机镜头,这个配置完全够用。只有当你使用鱼眼镜头或者广角畸变极大的镜头时,才需要考虑切换到Fisheye模型,否则默认选项即可。
第四步,点击Calibrate开始计算。计算过程通常几秒到几十秒不等,取决于图片数量和分辨率。计算完成后,界面会自动打开误差分析窗口,显示每张图片的重投影误差。这个误差衡量的是:利用当前估计的相机参数,把棋盘格的3D物理角点重新投影到图像平面,和实际检测到的像素坐标之间相差多少像素。误差越小说明参数拟合越好,经验上平均重投影误差小于0.5像素就是合格结果。
确认结果满意后,点击Export Camera Parameters,在工作区得到cameraParams对象。这个对象里包含了Camera Intrinsics(内参矩阵)、Radial Distortion(径向畸变系数)、Tangential Distortion(切向畸变系数)以及每张图片对应的Camera Extrinsics(外参)。后续不管是做畸变校正、还是做视觉测量,都可以直接使用它。
3.2 手写核心标定代码:从单应性矩阵到内外参分解
图形化工具能出结果,但如果你不知道背后的求解细节,换一个数据场景可能就不知道怎么调参数了。我更推荐在跑通App之后,再自己写一遍核心算法,这样才能真正理解张正友方法的本质。
手写代码的第一步是角点检测,仍然调用工具箱函数:
imageFiles = imageDatastore('calib_images/'); [imagePoints, boardSize] = detectCheckerboardPoints(imageFiles.Files); squareSize = 30; % 实际格边长,单位mm worldPoints = generateCheckerboardPoints(boardSize, squareSize);detectCheckerboardPoints返回的是每张图里所有内角点的像素坐标,顺序和generateCheckerboardPoints生成的物理坐标一一对应。这里的worldPoints默认z坐标为0,因为我们假设棋盘格位于世界坐标系的z=0平面上,这也是张正友标定法求解时的重要前提。
有了多组匹配点之后,核心求解可以分为几个步骤:
第一步,估计单应性矩阵H。每一张棋盘格图像,都可以建立起棋盘平面到图像平面的单应性变换。数学上,世界坐标点(X, Y, 0)映射到像素坐标(u, v)满足:
s * [u; v; 1] = K * [r1, r2, t] * [X; Y; 1]其中K是内参矩阵,r1和r2是旋转矩阵的前两列,t是平移向量。这个3x3的矩阵H = K * [r1, r2, t]就是单应性矩阵,每张图片可以估计一个H。估计H可以使用直接线性变换(DLT)方法,将每一对匹配点转化为两个线性方程,收集所有方程后利用SVD求解。
第二步,利用旋转向量的正交性约束求解内参。因为r1和r2是旋转矩阵的两列,它们必须满足正交且长度相等。这两个约束条件可以对内参矩阵K的元素构建出齐次线性方程组,形式是:
[ v12^T ] [ b ] = 0 [ (v11 - v22)^T ] [ b ]这里的vij是由单应性矩阵元素构造的向量,b是内参矩阵元素的二次组合。收集所有图片的约束后,用SVD求解b,再通过Cholesky分解恢复出内参矩阵K。这一步是整个算法的精华,也是初学者最容易卡住的地方——它不直接求解K,而是先求解一个由K的元素组合成的中间向量,再用数学分解把K还原出来。我当时第一次推导到这里也绕了一会儿,建议你拿纸笔把B = K^{-T} K^{-1}的对称矩阵形式展开,把6个未知数写出来,很快就能理清。
第三步,求解每张图片的外参。一旦内参K已知,对每一张图片的单应性矩阵H = [h1, h2, h3],旋转向量r1 = K^{-1} h1,r2 = K^{-1} h2,r3 = r1 x r2,平移向量t = K^{-1} h3。由于噪声影响,实际求出来的R = [r1, r2, r3]不一定满足旋转矩阵的严格正交性质,通常要通过SVD或者正交化处理(比如用[U,~,V] = svd(R); R = U*V')把R拉回旋转矩阵的空间。
第四步,求解畸变系数。张正友方法采用了一种很实用技巧:先用不含畸变的模型求出内外参初值,然后基于重投影误差建立关于畸变系数的线性方程组,最小二乘求解出k1、k2、p1、p2的初始估计。
第五步,非线性优化精修所有参数。最后把内外参、畸变系数全部作为优化变量,用Levenberg-Marquardt算法最小化所有角点的总重投影误差:
% 伪代码框架 options = optimoptions('lsqnonlin', 'Algorithm', 'levenberg-marquardt', 'Display', 'iter'); params0 = [fx, fy, cx, cy, k1, k2, p1, p2, ...]; % 初值拼接 [params_opt, resnorm] = lsqnonlin(@reprojectionError, params0, [], [], options);这一步虽然看起来只是数学上的精修,但在实际标定中非常关键。不做非线性优化的话,初值可能已经接近真值,但畸变较大时残差依然偏高,实验结果也不够理想。整个手写流程下来,你会发现其实核心代码并不长,关键是每一步的数学含义要清楚。
3.3 畸变校正的数学细节
标定完拿到畸变系数后,最直接的应用就是把畸变图像校正成理想针孔成像的图像。这里的模型需要理解,因为很多人在使用undistortImage时不清楚为什么有些图校正后边缘会被裁剪。
畸变的数学表达分为两部分。径向畸变由k1、k2、k3描述,校正公式为:
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)其中r是归一化坐标系下像素点到主点的距离。切向畸变由p1、p2描述,校正公式为:
x_corrected = x + 2*p1*x*y + p2*(r^2 + 2*x^2) y_corrected = y + p1*(r^2 + 2*y^2) + 2*p2*x*y在MATLAB中,可以直接调用:
[J, newOrigin] = undistortImage(I, cameraParams, 'OutputView', 'full');OutputView参数有两个常用取值:'full'表示输出完整校正后的图像,但边缘可能因为拉伸出现黑色区域;'same'表示保持原图尺寸,但边缘区域会被裁掉。实际项目中做特征匹配、目标检测,一般用'full',因为丢失信息更少;如果上游算法对图像尺寸有固定要求,再用'same'。
4. 从标定结果里读出有用信息
4.1 重投影误差怎么看
重投影误差是整个标定实验中最重要的质量指标。我用一个简单标准来判断:平均重投影误差小于0.1像素非常优秀,0.1到0.3像素是正常水平,0.3到0.5像素可以接受但需要检查拍摄数据,超过0.5像素则说明标定数据里混入了影响较大的坏点,建议排查一遍再重新标定。
注意,我这里说的是平均误差,单张图片的误差波动也要关注。如果某张图片的误差明显高于其他图片,比如其他图都在0.2像素左右,某张图突然到了1像素以上,这一般意味着标定板在该图片中出现了局部弯曲、运动模糊或者角点检测错位。处理方式很简单,把这张图从集合里删掉,重新标定。我在实验中还遇到过一种情况——所有图片的重投影误差都不高,但内参明显不合理,比如主点坐标偏离图像中心100多个像素,这就说明图片集覆盖的姿态不够丰富,特别是缺少足够的倾斜视角。
4.2 内外参的物理意义解读
标定结果里面的参数不是一堆干巴巴的数字,每一个都有实际物理含义,读懂它们能帮你快速判断标定是否成功。
内参矩阵K的形式是:
K = [fx, 0, cx; 0, fy, cy; 0, 0, 1]fx和fy是焦距在像素单位下的表示,物理上等于镜头焦距乘以传感器每毫米对应的像素数。一个常见的检验标准是,fx和fy的比值应该非常接近1。如果出现fx比fy大好几个百分点,可能意味着传感器像素不是正方形,或者标定板有透视角度导致的估计偏差。cx和cy是主点坐标,理论上应该接近图像分辨率的一半。对1920x1080的图像来说,主点应该在(960, 540)附近,偏差在20到30像素以内都是正常的。如果偏差过大,通常意味着还有部分畸变没有完全被模型拟合掉。
外参描述了每张图片拍摄时相机与棋盘格的相对位姿,也就是旋转矩阵R和平移向量t。在Camera Calibrator里可以用3D视图查看每个棋盘格在相机坐标系下的摆放位置,这个可视化能直观地验证你的拍摄过程是否覆盖了足够多的角度,也能检查外参估计有没有明显的跳变。
4.3 标定结果对后续视觉任务的影响
标定结果的最终价值,是服务于后续的视觉任务。以我做过的一个二维码定位项目为例:在畸变严重的广角镜头下,二维码边缘在图像中呈弯曲状,直接用角点检测算位姿,误差很大。标定后先用undistortImage校正图像,再跑透视变换,二维码的定位精度立刻提升了一个量级。这就是标定最直观的价值。
如果你是做双目视觉或者结构光测量,还需要做双目立体标定,即在单目标定基础上进一步求解两台相机之间的相对位姿。MATLAB里可以使用stereoCameraCalibrator完成。但无论单目还是双目,单目标定这一步的质量都会直接传递到后面的三维重建里,所以别急着赶进度,标定这一步值得多花时间把误差降下来。
5. 常见问题与排查技巧实录
5.1 角点检测失败与对策
实验中最常遇到的报错是detectCheckerboardPoints返回空值或者检测到的棋盘格数量不一致。
如果命令行提示检测不到棋盘格,优先检查三点:图片亮度是否均匀、棋盘格在画面中是否太小、图片是否模糊。我曾经遇到过一批图,灯光偏暖黄色,棋盘格黑色方块在暗光下和深色背景融为一体,检测失败率很高。解决办法是在拍摄时保证充足的白光照明,尽量让黑色方块和白色底板的对比度高。检不出来的时候,先用MATLAB的imshow看一下图片质量,缩小棋盘格在画面中的占比往往能立竿见影提升检测成功率。
另一个常见的问题是报错“检测到的棋盘格不是一致的尺寸”。这个报错通常出现在你用不同分辨率拍摄了图片,或者图片中棋盘格被部分遮挡导致检测到的角点数量不一致。解决办法是把不合格的图片移除,或者统一所有图片的分辨率再重新运行。
5.2 高畸变广角镜头的特殊处理
普通镜头标定很顺利,但当你换上一颗广角镜头时,问题就来了:边缘畸变特别大,棋盘格在画面边缘严重弯曲,角点检测依然能成功,但标定出来的重投影误差很高,怎么优化都降不下来。
针对这种情况,我总结了几条可落地的策略。第一,适当增加图片数量,广角镜头建议拍到25张以上,并且让棋盘格在画面中心和边缘均匀分布,特别是要让角点覆盖到畸变最严重的边缘区域;第二,拍摄时加入更多大倾斜角度的图像,让畸变参数有足够的约束条件;第三,检查畸变模型是否够用,普通镜头用3个径向系数,如果发现其中k3的置信区间非常大,说明模型自由度超出数据表达能力,可以改用2个径向系数试试。在MATLAB里可以通过修改Camera Calibrator的Coefficients设置来切换。如果畸变实在太严重,比如鱼眼镜头,MATLAB里建议直接选用Fisheye标定模式,普通针孔模型拟合不了这种级别的非线性畸变。
5.3 标定精度的实用建议
结合我做过的一系列标定实验,整理一个可以直接套用的自检清单:
| 检查项 | 合格标准 | 不合格时的处理 |
|---|---|---|
| 平均重投影误差 | <0.5像素 | 删除误差大的图片,检查棋盘格平整度 |
| fx与fy比例 | 0.95-1.05之间 | 检查图片分辨率是否统一,传感器像素是否为正方形 |
| 主点cx、cy | 接近图像中心,偏差<10% | 增加倾斜角度的图片,重新标定 |
| 畸变系数 | k1数量级在1e-2左右,k2在1e-3左右 | 系数异常大时检查畸变模型和图片质量 |
| 标定板物理尺寸 | 实测值与设定值误差<1mm | 用游标卡尺重新测量,修改squareSize参数 |
还有一个容易忽略的点:不要把所有图片一次性全部纳入标定集合,建议先选6到8张覆盖不同角度的图片做一轮快速标定,看单张误差分布,再把质量好的图逐步加入。这样做的好处是,当最终标定结果不理想时,你能很清楚地判断是哪张图拉高了误差,而不是面对20多张图一头雾水。
我在实际使用中发现,画面中棋盘格占比和拍摄姿态的多样性这两个因素,对最终标定精度的影响远大于算法选择的影响。很多人总想换更复杂的畸变模型来提升精度,其实更有效的方法是重新审视采集数据,把姿态覆盖做全,把图片拍清楚,把标定板贴平整。这三点做好了,哪怕用默认参数,结果也不会差到哪去。
这个实验做到最后,我最大的体会是:标定这件事,原理上虽然绕,但一旦亲手把代码流程跑通,再把图形化工具和手写结果对照一遍,整个相机模型就在脑子里扎根了,后面再做任何视觉相关的项目,心里都有底。