
做视觉测量和图像拼接的老哥应该都遇到过这种诡异情况监视器里看画面挺正常可一旦拿去算距离、拼图或拟合直线边缘区域就莫名其妙地歪斜直线变成弧线测量结果漂移得很离谱。这个锅九成要扣在镜头畸变头上。要真正解决就得老老实实做一次完整的相机标定拿到内参和畸变系数再借助Matlab相机标定例程里的去畸变流程把图像拉直。这篇博文我就把整条链路拆开揉碎——从畸变数学模型讲到标定实操再深入undistortImage和undistortPoints的调用方式最后手写一个源码级去畸变函数把Matlab源程序内部那点活儿全部摊开顺便把我在实际项目里踩过的坑一并交代清楚。1. 去畸变前必须搞懂的坐标系与畸变模型1.1 从三维世界到像素坐标的完整成像链路很多人看相机标定例程一上来就找API结果参数矩阵填得稀里糊涂。我建议先花十分钟把成像链路捋清楚因为畸变矫正的所有代码都建立在这条链路上。相机的成像本质是把三维世界坐标一步步投影到二维像素平面。完整过程涉及四个坐标系世界坐标系、相机坐标系、归一化平面坐标系、像素坐标系。先用世界坐标系下的一个三维点(P_w)出发。它经过外参矩阵——也就是旋转矩阵(R)和平移向量(t)——变换到相机坐标系(P_c)[ P_c R \cdot P_w t ]这一步描述的是“相机在世界里的位姿”标定结果里的RotationMatrices和TranslationVectors就是干这个的。到这里位置还没涉及镜头光学只是刚性的三维旋转平移。得到相机坐标系下的点((x_c, y_c, z_c))之后下一步是投影到归一化平面。所谓归一化就是除以(z_c)[ x_n \frac{x_c}{z_c}, \quad y_n \frac{y_c}{z_c} ]这个((x_n, y_n))是理想针孔模型下、在(z1)平面上的投影坐标它和镜头畸变直接发生关系。请记住这个点畸变作用于归一化坐标而不是像素坐标。这正是后面所有公式推导的起点。最后一步把归一化坐标通过内参矩阵K缩放到像素坐标系[ u f_x \cdot x_n c_x, \quad v f_y \cdot y_n c_y ]内参矩阵(K)的标准写法是[ K \begin{bmatrix} f_x 0 c_x \ 0 f_y c_y \ 0 0 1 \end{bmatrix} ]这里(f_x、f_y)分别是x、y方向的等效焦距单位是像素(c_x、c_y)是主点坐标。Matlab标定例程里返回的cameraParams.IntrinsicMatrix以及OpenCV里的cameraMatrix本质都是这个矩阵。1.2 径向、切向畸变的物理来源与数学表达为什么镜头会畸变因为现实世界的镜头不是数学上的理想针孔。两个主要来源径向畸变和切向畸变。径向畸变来自透镜形状。任何单透镜都存在球差光线经过透镜边缘时折射角度和近轴区域不一致导致图像边缘的放大率和中心不同。放大率变小是桶形畸变直线向中心弯放大率变大是枕形畸变直线向边缘弯。Matlab的相机模型里径向畸变用多项式近似。假设畸变前的归一化坐标为((x_n, y_n))畸变后的坐标为((x_d, y_d))径向畸变公式是[ x_d x_n \cdot (1 k_1 r^2 k_2 r^4 k_3 r^6) ] [ y_d y_n \cdot (1 k_1 r^2 k_2 r^4 k_3 r^6) ]其中(r^2 x_n^2 y_n^2)(k_1、k_2、k_3)是径向畸变系数。实际标定中(k_3)很少用到通常两个系数就足够。切向畸变来自镜头装配误差。镜头模组在安装时如果透镜光心和传感器平面不严格平行图像会呈现“楔形”偏移。切向畸变公式[ x_d x_n [2 p_1 x_n y_n p_2 (r^2 2 x_n^2)] ] [ y_d y_n [p_1 (r^2 2 y_n^2) 2 p_2 x_n y_n] ](p_1、p_2)是切向畸变系数。这个畸变量通常比径向畸变小一个数量级但广角镜头上不能忽略。综合模型是把径向和切向两部分叠加这也是Matlab标定工具箱和OpenCV默认使用的模型[ x_d x_n(1 k_1 r^2 k_2 r^4) 2p_1 x_n y_n p_2(r^2 2x_n^2) ] [ y_d y_n(1 k_1 r^2 k_2 r^4) p_1(r^2 2y_n^2) 2p_2 x_n y_n ]1.3 为什么Matlab畸变系数顺序是k1,k2,p1,p2,k3拿到cameraParams.RadialDistortion字段时你会发现它默认返回5个元素[k1, k2, p1, p2, k3]。这个顺序经常坑人因为里面混了两种畸变类型。我自己第一次用的时候也愣了一下。RadialDistortion这个名字听起来应该只放径向系数结果里面还塞着切向系数。用的时候一定要看清楚前两位是径向(k_1、k_2)中间两位是切向(p_1、p_2)最后一位才是(k_3)而且不标定三阶径向系数时它是0。Matlab的Camera Calibrator App里用户可以勾选“3 Coefficients”径向模型。如果画面中心区域畸变已经很小两个系数就够选三个反而会过拟合标定结果不稳定。切向畸变默认开启只有在确定镜头装配非常干净时才建议关闭否则保留它更保险。还有一点畸变模型在归一化坐标系中定义因此同一颗镜头在不同分辨率、不同内参的情况下标定出来的畸变系数理论上应该一致。换分辨率后只需要重新计算内参畸变系数可以沿用——前提是镜头物理结构没变。这个特性对多分辨率相机方案很有用。2. Matlab相机标定实操从棋盘格照片到cameraParams对象2.1 棋盘格拍摄的硬性要求去畸变代码写得再漂亮标定输入数据不行结果照样飘。我见过A同学用鼠标对着屏幕上的棋盘格图片拍了几张就开始标最后焦距偏差10%以上。拍棋盘格是有规矩的几条硬性要求标定板要打印在硬质平整材料上压克力板或硬纸板都行不能皱。单次标定建议拍15到25张图片。少于12张外参解算容易不稳定。视角不能全是正面。要有正视、左右倾斜、上下俯仰、旋转组合让棋盘格覆盖画面中心、四个角落和边缘。棋盘格在画面中的面积占比至少1/3格子太小角点检测精度上不去。拍摄时焦距锁死标定后拍摄的相机焦距也必须和标定时一致否则标定参数全部作废。一句话总结让棋盘格遍布画面的每一个区域且至少有三分之一数量的图带明显倾斜角度。2.2 Camera Calibrator App操作流程在Matlab命令窗口输入cameraCalibrator即可打开Camera Calibrator App。操作流程不复杂但每一步都有讲究。第一步点Add Images载入拍摄的棋盘格图片。载入后App会自动运行角点检测如果某张图检测失败图上会标记出错直接检查那张图是不是过曝或太模糊。第二步输入棋盘格边长SquareSize。这一步决定了标定的物理尺度单位统一成mm。注意这里的边长是单个格子的边长不是整个棋盘格的边长。我见过把整板尺寸填进去的标定结果全错还找不到原因。第三步在Options区域确认畸变模型设置。径向畸变默认2个系数切向畸变默认开启。设置无误后点Calibrate。第四步看Mean Reprojection Error。这个值代表标定板角点在图像上的重投影误差平均值通常小于0.5像素算正常。如果某张图明显拉高误差选中那张图右键选择删除或取消勾选重新标定。第五步确认标定结果稳定后点Export Camera Parameters把cameraParams对象导出到工作区用save(myCameraParams.mat, cameraParams)保存后面所有去畸变代码都从这份文件里读取参数。2.3 cameraParams对象的关键字段标定完cameraParams对象里装的东西很多但日常去畸变只需要关注下面这几个字段字段名含义备注IntrinsicMatrix内参矩阵注意是(K^T)x方向焦距、y方向焦距、主点都在里面RadialDistortion畸变系数向量 ([k_1, k_2, p_1, p_2, k_3])顺序易踩坑TangentialDistortion切向畸变系数 ([p_1, p_2])已单独给出也和RadialDistortion重叠WorldPoints标定板的物理坐标用于计算外参和重投影误差NumRadialDistortionCoefficients径向畸变系数个数2或3EstimatedTangentialDistortion是否估计切向畸变逻辑值字段里的IntrinsicMatrix有个隐藏坑——它是转置的。常用的K矩阵是上三角结构而Matlab保存的是它的转置也就是cameraParams.IntrinsicMatrix返回的矩阵第一列才是(f_x、0、0)第一行是(f_x、0、c_x)转过来的位置。要得到标准K必须K cameraParams.IntrinsicMatrix;这一步不处理后续手写投影代码时坐标全乱。2.4 标定质量的快速判定标定App显示的Mean Reprojection Error只是最基础的指标。我把经验分成三个层次第一层看误差绝对值。小于0.3像素很优秀0.3到0.6可用超过1像素建议补拍重标定。第二层看误差分布。打开App里的Show Undistorted或查看每张图的重投影误差柱状图如果某张图误差是其他图的2倍以上大概率是运动模糊或局部反光优先删除它。第三层看内参合理性。普通工业镜头标出来的(f_x、f_y)应该接近传感器分辨率水平比如500万像素相机在1400万像素模式下焦距不会只有几百像素。主点(c_x、c_y)应该接近图像宽高的一半。如果主点偏移超过图像宽度10%先怀疑标定板覆盖率不够补拍边缘角度。3. 去畸变API调用undistortImage、undistortPoints和官方例程的改造3.1 undistortImage语法与参数说明标定完成正式进入去畸变调用环节。undistortImage是最直观的函数——输入一张畸变图输出一张矫正图。完整语法[J, newOrigin] undistortImage(I, cameraParams); [J, newOrigin] undistortImage(I, cameraParams, OutputView, outputView); [J, newOrigin] undistortImage(I, cameraParams, Interpolation, cubic); [J, newOrigin] undistortImage(I, cameraParams, FillValues, 0);参数名写法上有个细节老版本用的是单引号语法如OutputView,full新版本同样支持名值对方式没有本质差异。OutputView有三个选项每个都直接影响输出图像尺寸和坐标体系选项输出尺寸适用场景same与原图相同视频流逐帧处理、坐标测量full包含全部原始像素标定校验、图像质量评估valid裁剪到有效区域去除黑边、拼接全景默认值是same但很多人不知道以为不传参数就和full一样实际不是。OutputView选same时图像尺寸不变但去畸变后边缘信息会被裁掉四个角可能出现黑色填充。选full时输出因为要容纳畸变投影区尺寸会比原图大一圈此时返回的newOrigin表示输出图像左上角对应原图的坐标偏移后面做物理坐标换算时要用。Interpolation控制重映射插值算法可选linear、cubic、nearest。质量优先用cubic速度优先用linear。FillValues设置越界填充值默认0就是黑色。最简单的调用示例是这样load(myCameraParams.mat, cameraParams); I imread(distorted_chessboard_02.jpg); [J, newOrigin] undistortImage(I, cameraParams, OutputView, full); figure; imshowpair(I, J, montage); title(上/左原始图下/右去畸变图);3.2 undistortPoints的适用场景很多项目不需要整图去畸变只需要把检测到的特征点坐标变成无畸变坐标比如二维码定位、标定板角点、图像拼接时的特征匹配。这时候用undistortPoints更合适不用白做一次全图插值。[undistortedPts, reprojectionErrors] undistortPoints(imagePoints, cameraParams);imagePoints是一个(N \times 2)的矩阵每行是一个像素坐标。返回的undistortedPts是对应的无畸变像素坐标reprojectionErrors是每个点的重投影误差。这里要注意undistortPoints内部同样是先把像素坐标反投影到归一化平面、去畸变后再投影回像素平面。因此它输出的坐标和undistortImage处理后的同一点坐标是一致的。做视觉测量时我通常用undistortPoints处理角点再配合undistortImage出可视化图像两者互补不做重复计算。3.3 官方例程在哪里找、怎么改Matlab的相机标定例程其实很好找但新手经常卡在“怎么打开源程序”这一步。实际上在命令窗口执行doc undistortImage帮助文档页面底部会有Examples区域官方例程带有可直接运行的脚本比如标定后测量平面物体尺寸、摄像头标定与图像去畸变等场景的完整示例。打开后直接Open Live Script就能看到完整的源程序。建议的学习路径是先把官方脚本完整跑通用自带的示例数据验证环境。然后把脚本里的示例图片路径替换成自己的图片示例cameraParams替换成自己标定的cameraParams对象。跑通后删除所有可视化辅助代码只保留去畸变核心调用封装成自己的工具函数。我自己的工具函数就是从官方例程剥下来的核心只有几行function [J, newOrigin] undistortMyImage(I, cameraParams, outputView) if nargin 3 outputView same; end [J, newOrigin] undistortImage(I, cameraParams, ... OutputView, outputView, ... Interpolation, cubic, ... FillValues, 255); end把FillValues设为255是因为我处理的图像背景多为白色黑色填充边看久了总以为是拍摄暗角。4. 绝不玄学手写源代码级去畸变实现与交叉验证4.1 去畸变的本质是重映射官方undistortImage源码在封装层之上但从数学模型上看整个去畸变过程不复杂本质上就是一次图像重映射。先明确一件事畸变模型给出的是“理想坐标→畸变坐标”的正向映射。而去畸变要做的是拿到畸变图像上的像素找到它在理想图像上对应位置。最直接的做法有两种正向映射对原图每个像素计算它去畸变后应该落在目标图哪个位置。问题是目标图像上会有空洞需要额外插值填补。反向映射对目标图像每个像素反向计算它在原图中应该采样哪个位置的像素。Matlab和OpenCV的remap都采用这种方法因为目标图像每个像素都能确定地拿到原图像素值天然无空洞。undistortImage内部就是反向映射先根据输出图像大小建立一个像素网格把每个网格点当作理想无畸变坐标输入畸变模型算出对应的畸变坐标再从原图采样。我手写的函数完全复刻这个思路。4.2 Matlab实现反向映射去畸变下面这个函数不依赖任何工具箱只用基础的矩阵运算和interp2完全可以替代undistortImage的部分功能function J undistortByHand(I, cameraParams) % 根据cameraParams手写反向映射去畸变 % I: 输入畸变图像 % J: 输出去畸变图像尺寸与原图相同 K cameraParams.IntrinsicMatrix; % 注意转置 fx K(1,1); fy K(2,2); cx K(1,3); cy K(2,3); k cameraParams.RadialDistortion(:); p cameraParams.TangentialDistortion(:); if numel(k) 2 k(2) 0; end if numel(k) 3 k(3) 0; end if numel(p) 2 p [0, 0]; end [h, w, chs] size(I); [u, v] meshgrid(1:w, 1:h); [X, Y] meshgrid(1:w, 1:h); % 插值用原始网格 % 像素坐标 - 归一化坐标 xn (u - cx) / fx; yn (v - cy) / fy; r2 xn.^2 yn.^2; radial 1 k(1)*r2 k(2)*r2.^2 k(3)*r2.^3; dx 2*p(1)*xn.*yn p(2)*(r2 2*xn.^2); dy p(1)*(r2 2*yn.^2) 2*p(2)*xn.*yn; % 畸变后的归一化坐标 xd xn .* radial dx; yd yn .* radial dy; % 再投影回像素坐标得到原图中的采样位置 ud xd * fx cx; vd yd * fy cy; % 对每个通道插值采样 J zeros(size(I), like, I); for ch 1:chs J(:,:,ch) interp2(X, Y, double(I(:,:,ch)), ud, vd, linear, 0); end end有几处细节值得说interp2的网格坐标必须完整传入省略X,Y时Matlab会默认用1:size虽然人眼看不出来但坐标对不齐会导致输出轻微偏移统一传入更稳妥。fillval设为0和undistortImage的默认行为一致。越界采样位置会被填0。这段代码没有做迭代求解理想无畸变坐标的步骤因为我们的目标网格本身就定义在理想坐标系里直接正向套用畸变公式求得原图像素位置省去了反解非线性方程的过程。这也是undistortImage的内核思路。4.3 如何验证自己写的函数精度手写函数能不能放心用不能靠感觉要跑交叉验证。我习惯做两件事。第一随机点坐标一致性检查。在图像范围内生成10000个随机像素点分别用undistortPoints和手写逻辑处理比较坐标差。注意undistortPoints内部执行了和我们几乎相同的计算因此差异应该接近浮点误差。rng(0); N 10000; pts rand(N, 2) .* [size(I,2), size(I,1)]; [ptsUndist, ~] undistortPoints(pts, cameraParams); % 调用手写逻辑 K cameraParams.IntrinsicMatrix; fx K(1,1); fy K(2,2); cx K(1,3); cy K(2,3); k cameraParams.RadialDistortion(:); p cameraParams.TangentialDistortion(:); xn (pts(:,1) - cx) / fx; yn (pts(:,2) - cy) / fy; r2 xn.^2 yn.^2; radial 1 k(1)*r2 k(2)*r2.^2 k(3)*r2.^3; dx 2*p(1)*xn.*yn p(2)*(r2 2*xn.^2); dy p(1)*(r2 2*yn.^2) 2*p(2)*xn.*yn; myPts [(xn .* radial dx) * fx cx, ... (yn .* radial dy) * fy cy]; diffPts sqrt(sum((ptsUndist - myPts).^2, 2)); fprintf(最大坐标差: %.6f 像素\n, max(diffPts));第二全图对比。用自己的函数处理一张棋盘格再与undistortImage的结果对比用一个简单的结构相似度或均方根误差做指标。理论上两幅图应该几乎一样因为底层原理相同插值算法差异只会造成轻微边缘差别。J1 undistortByHand(I, cameraParams); J2 undistortImage(I, cameraParams, OutputView, same, Interpolation, linear); diffImg double(rgb2gray(J1)) - double(rgb2gray(J2)); rmsDiff sqrt(mean(diffImg(:).^2)); fprintf(全图RMS差异: %.4f\n, rmsDiff);我实际跑过RMS差异通常小于0.3灰度值属于插值方式导致的正常噪声。跑完这两步验证手写函数就可以放心用了。5. 去畸变开发中容易踩的坑以及我的排查思路5.1 IntrinsicMatrix的转置陷阱这可能是Matlab标定里最阴的一个设计。OpenCV里cameraMatrix是教科书式布局[f_x, 0, c_x; 0, f_y, c_y; 0, 0, 1]但Matlab返回的cameraParams.IntrinsicMatrix是转置[f_x, 0, 0; 0, f_y, 0; c_x, c_y, 1]如果不知道这一点直接用cameraParams.IntrinsicMatrix去乘归一化坐标结果完全错位。我当时在实验里做坐标投影画面里的点在图上对不上排查了半天最后发现就是矩阵方向搞反了。验证方法很简单打印一下矩阵就一目了然K_T cameraParams.IntrinsicMatrix K cameraParams.IntrinsicMatrix输出结果里K_T第一行包含的是(f_x, 0, 0)而标准K的第一行是(f_x, 0, c_x)。所有手写投影代码统一用转置后的K就不会出问题。5.2 OutputView和newOrigin导致坐标错位第二个高频坑发生在测量场景。很多人用undistortImage处理完图像后直接拿OutputViewfull的输出图去做像素坐标测量但忘了newOrigin的存在。newOrigin是一个1×2向量表示输出图像左上角相对原始图像坐标系的偏移。当OutputViewfull时输出图像比原图大真正的中内容区外扩了一圈因此输出图像原点不再对应原图原点。如果测量系统是基于输出图像坐标的实际物理坐标需要这样换算[J, newOrigin] undistortImage(I, cameraParams, OutputView, full); % 假设在输出图像上检测到特征点坐标为(uJ, vJ) uOriginal uJ - newOrigin(1); vOriginal vJ - newOrigin(2);这个偏移量通常在几十到上百像素足够让圆形物体圆心偏移好几毫米。图像拼接场景更是如此拼接前所有特征点坐标都要先减去newOrigin再送入变换矩阵。5.3 只标了正面图导致边缘校正失败有些项目赶进度用正对棋盘格的20张图标定完看着误差挺小结果拿到广角图像边缘畸变依然严重。原因很简单所有标定图都是正面视角棋盘格集中在画面中央边缘区域的畸变完全没被观测到畸变系数自然估计不准。正确的做法是确保图像序列中有大量带倾斜角度的图让棋盘格出现在画面四个角落。如果现场条件允许把标定板放在画面边缘把畸变最大的区域充分暴露给算法。这里有个判断技巧标定后检查畸变系数向量如果k1绝对值小于0.001而实际镜头是明显广角基本可以断定标定数据没有覆盖边缘需要补拍。5.4 全图去畸变的性能优化预计算映射表undistortImage最大的问题是慢。因为它每次都要重新算一遍畸变公式和插值网格。视频流逐帧去畸变时这个开销很浪费因为相机标定一次后畸变参数固定每个像素的映射关系也固定。优化思路很简单预先计算整张图每个像素对应的采样坐标存成映射表后续每一帧只做纯插值。% 初始化阶段只计算一次映射表 function [mapU, mapV] initUndistortMap(cameraParams, h, w) K cameraParams.IntrinsicMatrix; fx K(1,1); fy K(2,2); cx K(1,3); cy K(2,3); k cameraParams.RadialDistortion(:); p cameraParams.TangentialDistortion(:); [u, v] meshgrid(1:w, 1:h); xn (u - cx) / fx; yn (v - cy) / fy; r2 xn.^2 yn.^2; radial 1 k(1)*r2 k(2)*r2.^2 k(3)*r2.^3; dx 2*p(1)*xn.*yn p(2)*(r2 2*xn.^2); dy p(1)*(r2 2*yn.^2) 2*p(2)*xn.*yn; mapU (xn .* radial dx) * fx cx; mapV (yn .* radial dy) * fy cy; end % 初始化 h size(I,1); w size(I,2); [mapU, mapV] initUndistortMap(cameraParams, h, w); [mapX, mapY] meshgrid(1:w, 1:h); % 视频循环里 for f 1:numFrames frame read(videoObj, f); undistFrame zeros(size(frame), like, frame); for ch 1:3 undistFrame(:,:,ch) interp2(mapX, mapY, ... double(frame(:,:,ch)), mapU, mapV, linear, 0); end % 后续处理 undistFrame end这样每帧只执行插值畸变公式计算被压缩到初始化阶段完成一次实测在500万像素图像上速度能提升5到10倍视频流场景绝对够用。5.5 去畸变效果的双重验证标定误差好看不代表去畸变效果一定好。我习惯做两个实打实的验证棋盘格直线验证。放一张边缘畸变明显的棋盘格图像去畸变前后对比。无畸变图像里棋盘格的行列边缘应该呈直线如果还有弧度说明畸变模型没有完全描述实际镜头大概率是径向系数阶数不够或标定板不完整。角点重投影验证。用undistortImage处理图像后重新跑一遍detectCheckerboardPoints把检测到的角点与undistortPoints计算出的理论位置比较看平均偏差。如果偏差在0.2像素以内说明整个流程的数学内核是闭合的。[ptsDetected] detectCheckerboardPoints(J); [ptsUndistorted, ~] undistortPoints(ptsOriginal, cameraParams); errs sqrt(sum((ptsDetected - ptsUndistorted).^2, 2)); fprintf(平均角点偏差: %.3f 像素\n, mean(errs));6. 从Matlab走向其他平台的参数迁移6.1 导出内参与畸变系数为通用格式Matlab项目往往只是原型验证量产阶段大家都在C或Python环境最常见的目标是OpenCV。OpenCV使用的畸变模型和Matlab基本一致系数顺序和含义都对应因此可以直接把参数导出来用。最稳妥的导出方式是生成OpenCV的YAML格式直接喂给FileStorage读取function exportToOpenCV(cameraParams, filename) K cameraParams.IntrinsicMatrix; k cameraParams.RadialDistortion(:); p cameraParams.TangentialDistortion(:); if numel(k) 3, k(3) 0; end if numel(p) 2, p [0, 0]; end distCoeffs [k(1), k(2), p(1), p(2), k(3)]; fid fopen(filename, w); fprintf(fid, %%YAML:1.0\n); fprintf(fid, ---\n); fprintf(fid, camera_matrix: !!opencv-matrix\n); fprintf(fid, rows: 3\n cols: 3\n dt: d\n); fprintf(fid, data: [ %f, 0., %f, 0., %f, %f, 0., 0., 1. ]\n, ... K(1,1), K(1,3), K(2,2), K(2,3)); fprintf(fid, distortion_coefficients: !!opencv-matrix\n); fprintf(fid, rows: 1\n cols: 5\n dt: d\n); fprintf(fid, data: [ %f, %f, %f, %f, %f ]\n, distCoeffs); fclose(fid); end导出后用OpenCV的initUndistortRectifyMap和remap即可得到相同效果的去畸变图像。6.2 Matlab与OpenCV的坐标约定差异跨界迁移最大的坑不只是矩阵转置还有坐标系原点约定。Matlab的像素坐标约定从1开始图像左上角像素是(1,1)。OpenCV的像素坐标从0开始左上角是(0,0)。这意味着从Matlab导出角点坐标时每列都要减1再传入OpenCV的标定和去畸变接口。做反方向迁移时则要加回去。别看只差1个像素在亚像素测量场景里这个偏移足以让毫米级定位精度劣化到厘米级。我见过一个项目标定参数一模一样两侧结果对不上最后发现就是坐标原点的1像素差异。最省事的做法是把Matlab标定得到的内参和畸变系数导成文件后在OpenCV侧不做任何坐标变换只用它们做remap因为remap的映射表和坐标原点无关插值逻辑自然会处理好。只有在直接用相机矩阵做投影计算时才需要小心原点约定。6.3 一个小技巧从k1符号快速判断畸变类型最后分享个实操里很常用的小技巧。拿到Matlab导出的畸变系数不用跑图也能快速判断镜头畸变方向。看k1的符号(k_1 0)桶形畸变图像边缘向外膨胀直线向远离画面中心的方向弯曲。(k_1 0)枕形畸变图像边缘向中心收缩直线向画面中心方向弯曲。这个判断对镜头选型很有用。做机器视觉测量时桶形畸变比枕形畸变更常见但两者在标定模型里都能被正确处理所以更重要的是畸变量级。如果(k_1)的绝对值超过0.3说明镜头畸变很严重即便标定矫正后插值重采样的边缘区域信息损失也不可忽视这种情况下就该考虑换低畸变镜头而不是单纯靠算法硬扛。到参数迁移这一步整个去畸变流程就已经完全闭环了Matlab侧标定、验证、手写实现、跨平台导出每个环节都跑通项目里再遇到图像扭曲的场景心里就有底了。