资讯动态

相机标定结果怎么用?Matlab去畸变原理与手写实现详解

发布时间:2026/10/14 19:19:53 来源:尧图企业网站定制
做视觉测量、三维重建或者SLAM这类项目绕不开相机标定。把Matlab自带标定例程跑通之后很多人盯着导出的cameraParams对象不知道下一步该怎么用更常见的情况是手头明明有一份现成的标定源程序却不知道怎么把它接到自己的去畸变流程里。这篇文章就把这块彻底讲透从畸变模型的数学原理到去畸变的核心逻辑再到内建函数和手写代码两种实现方式最后把源程序的调用路径完整梳理一遍。内容不深但每一步都会给到可以直接抄的代码和参数经验适合刚接触视觉标定的人也适合那些想脱离App、自己掌握校正流程的工程师。1. 先把标定结果看明白内参、畸变系数和对象结构1.1 针孔模型下的四个内参平时我们说的相机标定本质上是估计一个映射关系三维空间里的点是怎么投影到图像像素坐标上的。最理想化的模型是针孔相机模型它把投影过程压缩成一个3×3的内参矩阵K [fx, 0, cx; 0, fy, cy; 0, 0, 1]这里的fx和fy分别是x、y方向上的等效焦距单位是像素cx和cy是主点坐标也就是光轴与成像平面的交点。对于同一台相机在分辨率不变的情况下这四个数基本是固定的。Matlab标定后你可以在cameraParams对象里直接读出这四个值字段名分别是FocalLength和PrincipalPoint。很多刚接触标定的人有一个误区以为内参只是用来做三维重建的跟图像校正没关系。实际上去畸变的第一步就是把像素坐标换算到归一化相机坐标而这一步就必须用到fx、fy、cx、cy。所以后面无论是调用undistortImage还是自己写校正函数第一步永远是先把内参提出来。1.2 畸变模型径向畸变和切向畸变是怎么进到公式里的真实镜头不是完美针孔光线经过镜片时会发生弯曲这就带来了畸变。畸变分两大类径向畸变和切向畸变。径向畸变是沿着半径方向产生的形变表现为画面中心正常、越靠近边缘弯曲越明显。它来源于镜片形状常见的就是桶形畸变和枕形畸变。Matlab里用k1、k2、k3三个系数来描述x_dist x * (1 k1*r^2 k2*r^4 k3*r^6) y_dist y * (1 k1*r^2 k2*r^4 k3*r^6)其中x、y是归一化相机坐标r^2 x^2 y^2。对于普通工业镜头k1、k2通常就够了k3只有在视场角很大的广角镜头上才明显。如果Matlab返回的RadialDistortion只有两个元素那说明标定时只估计了前两阶这种情况很常见不用觉得不完整。切向畸变则是镜头与成像平面不完全平行造成的用p1、p2两个系数描述x_dist x_dist 2*p1*x*y p2*(r^2 2*x^2) y_dist y_dist p1*(r^2 2*y^2) 2*p2*x*y切向畸变的量级通常比径向小很多但如果你要做的项目对像素精度要求高比如用单目视觉测尺寸这一项就不能省。1.3 标定结果对象里到底存了什么Matlab标定之后所有的标定结果都会打包到一个cameraParameters对象里。这里面的字段值得逐个认一遍IntrinsicMatrix内参矩阵注意Matlab里存的格式和上面写的K是转置关系读取时不要搞反。FocalLength等效焦距2元素的向量[fx, fy]。PrincipalPoint主点坐标2元素向量[cx, cy]。RadialDistortion径向畸变系数2或3个元素。TangentialDistortion切向畸变系数2个元素。ReprojectionErrors重投影误差用来判断标定质量。WorldPoints和ImagePoints标定时用的世界坐标和检测到的图像角点。这些字段就是后续一切操作的入口。尤其是做去畸变的时候实际用到的就是FocalLength、PrincipalPoint、RadialDistortion、TangentialDistortion这四个。所以我建议写完标定后第一时间用一行代码把参数打印出来确认数值的量级是不是合理比如fx、fy在几百像素到几千像素之间畸变系数一般不会超过1要是冒出个几十的畸变系数那标定过程十有八九出问题了。2. 去畸变到底做了什么模型、内建函数与手写实现2.1 去畸变的核心逻辑不是把像素挪个位置那么简单去畸变的本质是对无畸变图像里的每一个像素找到它在原始畸变图像中对应的像素位置然后把这个位置的灰度值取过来。理解这一点特别重要因为很多初学者会以为去畸变是把畸变图像里的像素往周围推结果写出来的代码总是出现网格状空洞。正确思路是反向映射。先确定目标图像即校正后的图像的尺寸和像素网格然后把每个像素坐标通过内参转成归一化相机坐标再套用畸变公式算出它在原始图中对应的位置最后用插值方式采样。这样做既不会有空洞也符合图像处理里输出图像逐个像素求值的常规做法。这个逻辑也解释了为什么去畸变是相对耗时的操作每个像素都要做一次坐标变换和插值。不过实际项目中畸变参数和内参在一次标定后就是固定的所以你可以预先算好一张映射表把原图像素和目标像素的对应关系存下来之后对每一帧图像直接查表采样速度会提升很多。2.2 用Matlab内建函数一行搞定如果你的需求是把图像校正到视觉上满意Matlab自带的undistortImage是最稳的选择。它的调用方式极其简单% 加载标定结果 load(cameraParams.mat); % 读取待校正图像 I imread(distorted_image.jpg); % 去畸变输出尺寸与原图相同 I_undistorted undistortImage(I, cameraParams);这里有个参数值得专门说明OutputView。它的默认值是same含义是输出图像尺寸跟输入一致校正后图像边缘会被裁掉一部分。如果你想保留全视场把它设成full输出图像会比原图大四周会出现黑色区域这些黑边是没有图像内容的区域。在视觉测量项目里我一般用same因为黑边区域在后续处理中容易带来伪边缘。另外undistortImage默认使用双线性插值。如果你的图像是特征点密集的类型比如条纹图案可以考虑用OutputView,same配合Interpolation,cubic参数画面会更平滑但耗时也更高。2.3 手写去畸变代码把公式变成像素变换内建函数虽好但你不能靠它走遍天下。有时候项目要求把算法移植到别的平台有时候你需要精确控制每个采样点的行为这时候就必须自己实现。这个代码不复杂核心就是把2.1说的反向映射翻译成Matlab语言function J undistortManual(I, cameraParams) % 获取图像尺寸 [h, w, nCh] size(I); % 提取内参 fx cameraParams.FocalLength(1); fy cameraParams.FocalLength(2); cx cameraParams.PrincipalPoint(1); cy cameraParams.PrincipalPoint(2); % 提取畸变系数 k cameraParams.RadialDistortion; p cameraParams.TangentialDistortion; if numel(k) 2 k(3) 0; % 只有两阶径向系数补零即可 end % 构造目标图像的像素网格 [U, V] meshgrid(1:w, 1:h); % 像素坐标转到归一化相机坐标 x (U - cx) / fx; y (V - cy) / fy; % 径向畸变量 r2 x.^2 y.^2; r4 r2.^2; r6 r4 .* r2; radial 1 k(1)*r2 k(2)*r4 k(3)*r6; % 完整畸变模型得到原始畸变图中的采样坐标 xd x .* radial 2*p(1)*x.*y p(2)*(r2 2*x.^2); yd y .* radial p(1)*(r2 2*y.^2) 2*p(2)*x.*y; % 归一化坐标转回像素坐标 Ud xd * fx cx; Vd yd * fy cy; % 在原始图像上插值采样 J zeros(h, w, nCh, like, I); for c 1:nCh J(:,:,c) interp2(double(I(:,:,c)), Ud, Vd, linear, 0); end J cast(J, class(I)); end这个函数最需要注意的是interp2的用法。第一个参数是二维灰度矩阵第二个参数是目标采样点的x坐标第三个是y坐标。Matlab的interp2默认网格是x从1到列数、y从1到行数所以我们传入的Ud、Vd必须和这个网格对应。边界外的点会返回NaN我传了最后一个参数0让边界外的像素直接取黑这样不会在后续处理里引入NaN。你可以用一行代码验证手写版本和内置版本的结果差异I_builtin undistortImage(I, cameraParams); I_manual undistortManual(I, cameraParams); imshowpair(I_builtin, I_manual, diff);正常情况下两张图在像素值上的差异应该是0或者个位数偏差这个偏差主要来自插值边界处理方式不同。如果差异很大优先检查畸变系数是不是取对了顺序。3. 源程序怎么调交互式标定、脚本调用与批量出图3.1 先理清源程序的两种形态调用源程序这个话题得先分清楚你手里的源程序长什么样。我在实际项目里遇到的标定例程基本是两种形态。第一种是Matlab App形式。你在命令行输入cameraCalibrator打开交互式标定工具通过界面添加标定板图像、设定方格尺寸、点击Calibrate最后Export出cameraParams对象。这种形式适合快速验证标定效果它的好处是可视化强能实时看到重投影误差但坏处是不方便自动化。如果你的项目要处理几十组标定图像或者标定参数要随着产线数据定期更新每回都去点App是不现实的。第二种是脚本或函数形态的源码。这类源程序通常由一个主脚本和几个辅助函数组成主脚本负责读图、检测角点、估计参数、保存结果辅助函数负责画误差分布图、可视化角点等。调用这种源程序的方式最直接把整个文件夹加入Matlab路径然后在命令行执行主脚本的函数名。但很多人在这里就会卡壳因为根本不知道主脚本的入口参数是什么。这里分享一个我自己的识别技巧拿到源程序后先别急着运行花两分钟用Matlab编辑器打开所有.m文件找到没有function定义的那个文件它就是主脚本有function且文件名和函数名一致的文件就是辅助函数。主脚本里的变量和注释基本就是你需要准备的输入。3.2 用脚本方式跑通标定流程无论源程序包装得多复杂底层标定流程一定是固定的几步。我之前在一个模拟测量项目里从头写过一份核心流程大概是这样的% 指定标定板图像文件夹 imageFolder calib_images; images imageDatastore(fullfile(imageFolder, *.jpg)); % 标定板方格边长单位毫米 squareSize 20; % 自动检测棋盘格角点 [imagePoints, boardSize] detectCheckerboardPoints(images.Files); % 生成对应的世界坐标系角点坐标 worldPoints generateCheckerboardPoints(boardSize, squareSize); % 估计相机参数 cameraParams estimateCameraParameters(imagePoints, worldPoints, ... ImageSize, size(readimage(images, 1))); % 查看平均重投影误差 meanError mean(cameraParams.ReprojectionErrors(:)); fprintf(平均重投影误差: %.3f 像素\n, meanError); % 保存参数给去畸变流程用 save(cameraParams.mat, cameraParams);这段代码里detectCheckerboardPoints会自动在每张图里找棋盘格角点找不到的图会被跳过所以输入图像数量可以多于实际成功检测的数量。如果是调用别人的源程序大概率也是这几个函数的重排和封装。你只要关注三点标定板图像的路径传对了没有、squareSize和真实方格尺寸是否一致、保存出来的cameraParams变量名是否跟后续代码对得上。很多源程序里喜欢用变量名cameraParams但有的会用camParams或者params。这时候不要硬套模板看一眼源程序里的变量名再往下写。我自己就吃过这个亏拿着standard的变量名去调别人代码结果导出对象叫别的名字脚本直接报错。3.3 把标定结果接入去畸变批量出图和视频流源程序跑完之后最实用的一个场景是把标定结果批量用到整批图像或者视频帧上。这里的关键是利用畸变参数固定的特性预生成映射表。以视频流为例假设你已经有了cameraParams现在要实时对一个视频做去畸变。如果每一帧都调用undistortImage性能会很难看。更好的做法是像2.3那样先根据图像尺寸生成一张采样坐标映射表之后每一帧只需要做一次interp2% 预生成采样坐标 [h, w, ~] size(firstFrame); [U, V] meshgrid(1:w, 1:h); % 计算Ud、Vd这里略去公式直接调用2.3的中间步骤 % ... 得到 Ud, Vd % 对每一帧做查表插值 for i 1:numFrames frame readFrame(videoObj); frameUndist zeros(h, w, 3, uint8); for c 1:3 frameUndist(:,:,c) interp2(double(frame(:,:,c)), Ud, Vd, linear, 0); end % 继续你的处理流程 end这个技巧在批量处理几百张离线图片时也适用。表只算一次后面的插值操作其实就是几次矩阵运算速度能比逐帧调用undistortImage快几倍。有一点需要注意如果输入图像尺寸变了映射表必须重新生成因为Ud、Vd是和图像分辨率强相关的。4. 一个可复现的实例从标定图像到去畸变出图4.1 场景描述与标定板拍摄规范为了让上面的内容落进实际场景这里用一个模拟项目来完整走一遍。场景是某视觉测量项目需要给传送带上的零件拍照然后测量零件的长宽。相机是普通工业相机镜头畸变明显在画面边缘尤其严重。如果不去畸变测量结果在不同位置差异会很大最大误差能到几个毫米。拍摄标定板时我遵循了几条原则标定板要覆盖画面中心到边缘的所有区域尤其不能只在中间拍要变换标定板的倾斜角度和距离让算法能从不同视角估计内外参光线要均匀避免反光造成角点检测失败每次拍摄保证标定板静止且清晰。我采集了15张不同姿态的棋盘格图片棋盘格方格边长20mm这个尺寸是根据实际测量视场选的视场大方格就要大否则角点间距太小会影响亚像素精度。4.2 完整标定加去畸变代码串联整个流程用代码串起来就是这样% 1. 标定 squareSize 20; images imageDatastore(calib_images/*.jpg); [imagePoints, boardSize] detectCheckerboardPoints(images.Files); worldPoints generateCheckerboardPoints(boardSize, squareSize); cameraParams estimateCameraParameters(imagePoints, worldPoints, ... ImageSize, size(readimage(images, 1))); % 2. 检查重投影误差 meanError mean(cameraParams.ReprojectionErrors(:)); disp([平均重投影误差: , num2str(meanError), 像素]); % 3. 读取真实待测图像 I imread(part_image.jpg); % 4. 去畸变保留原图尺寸 I_ud undistortImage(I, cameraParams, OutputView, same); % 5. 显示对比 figure; imshowpair(I, I_ud, montage); title(左原始图像 右去畸变图像);在这个模拟项目里平均重投影误差大约是0.4像素属于比较好的水平。从对比图里可以明显看到画面边缘原本弯曲的直线在校正后变直了。如果你看到校正后图像边缘有轻微拉伸或轻微锯齿那是插值带来的正常现象不是标定失败。4.3 校正效果验证直线变直了才叫成功验证去畸变效果有一个很直观的方法在原始图像里找一条明显的直线边缘比如传送带边缘或者零件的一条长边用Matlab的drawline在原始图上沿着边缘画一条线再看看去畸变后的图上这条线是否仍然贴合边缘。如果边缘在校正后变成了一条光滑直线说明去畸变做对了。如果做的是测量项目还需要再做一层坐标换算。去畸变后的图像像素坐标已经满足了针孔模型的投影关系此时可以通过内参把像素坐标换算到相机坐标系下的无畸变坐标% 假设图像上两个点的像素坐标是 (u1,v1) 和 (u2,v2) fx cameraParams.FocalLength(1); fy cameraParams.FocalLength(2); cx cameraParams.PrincipalPoint(1); cy cameraParams.PrincipalPoint(2); p1 [(u1 - cx) / fx, (v1 - cy) / fy]; p2 [(u2 - cx) / fx, (v2 - cy) / fy]; % 在已知物体平面深度的前提下像素距离转毫米距离 Z 500; % 相机到平面的距离单位mm mmPerPixel_x Z / fx; % 实际上是每个像素对应的物理尺寸要注意这里的Z必须由实际测量场景给出比如通过标定平面到相机的距离确定或者用另一个已知尺寸的对象标定像素到毫米的比例。去畸变解决的是镜头形变问题但它不会自动解决尺度问题。5. 去畸变实战避坑常见异常与排查清单5.1 标定结果里畸变系数出现0或NaN很多人在标定后发现RadialDistortion里有0或NaN第一时间就觉得标定失败了。我遇到的情况里最常见的原因是标定图像数量太少或者标定板姿态太单一。标定算法在估计畸变系数时需要看到画面各个区域的形变信息如果15张图全是近似正面拍摄中心区域的图占了绝大多数算法就无法可靠估计边缘畸变这时可能干脆给一个0。另一个产生NaN的原因是个别图像角点检测异常特别是标定板图片里有反光或者遮挡检测出的角点位置跟真实位置偏差很大。解算时这些野值会污染整个估计结果。处理办法很简单先看每张图的角点检测结果把明显不合格的图删掉重新标定。建议的标准是平均重投影误差小于0.5像素算优秀小于1像素算可用。超过1像素就要回头检查标定板的平整度、图像清晰度以及角点检测质量了。5.2 校正后图像边缘拉伸或出现黑边去畸变后边缘拉伸是使用undistortImage时最容易遇到的现象。这个现象本身是物理上的必然畸变最严重的区域在画面边缘校正后的边缘像素会从原始图像更靠近中心的位置采样导致边缘区域被放大。如果边缘拉伸得过分首先要检查畸变系数是不是偏大。黑边则是OutputView参数选择的问题。用full会保留全视场代价是四周出现无像素区域用same会裁掉黑边代价是损失部分视场。工业场景下我用same较多因为黑边区域在后续阈值分割中会被误判成目标边缘。不过如果你要做的应用是全景拼接或者相机联动那还是要保留全视场这时要记得在算法里屏蔽黑边区域。5.3 相机分辨率改变后参数是否还能用这是一个特别容易踩的坑。相机标定的内参和畸变系数是像素单位它跟你用的图像分辨率绑定。你把1920×1080的图像缩放成1280×720fx、fy、cx、cy不会自动缩放直接套用原参数做去畸变结果一定不对。严谨的做法是重新标定。如果没办法重新标定也可以做一个近似换算fx、fy、cx、cy都按分辨率缩放比例乘一下畸变系数k1、k2、p1、p2保持不变但这个近似只对缩放有效对裁剪无效而且精度不如重新标定。我吃过一次亏后现在的习惯是标定结果文件里存一份图像分辨率字段每次加载参数时先对比当前图像分辨率不一致就弹警告。if size(I,1) ~ cameraParams.ImageSize(1) || size(I,2) ~ cameraParams.ImageSize(2) warning(当前图像分辨率与标定分辨率不一致去畸变结果可能不准确); end这套流程在我后来的几个项目里反复用已经变成我的标准操作了。如果你也经常纠结标定完怎么用建议先把手写去畸变代码跑一遍把每个字段、每个公式的对应关系摸清楚。这样无论以后是接别人的源程序还是换一套标定工具心里都有底。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑