☰
Matlab相机标定与图像去畸变:内参求解、畸变模型到undistortImage实践
2026/10/14 21:17:57 网站建设 项目流程

做视觉测量和图像拼接的老哥,应该都遇到过这种诡异情况:监视器里看画面挺正常,可一旦拿去算距离、拼图或拟合直线,边缘区域就莫名其妙地歪斜,直线变成弧线,测量结果漂移得很离谱。这个锅九成要扣在镜头畸变头上。要真正解决,就得老老实实做一次完整的相机标定,拿到内参和畸变系数,再借助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))是理想针孔模型下、在(z=1)平面上的投影坐标,它和镜头畸变直接发生关系。请记住这个点:畸变作用于归一化坐标,而不是像素坐标。这正是后面所有公式推导的起点。

最后一步,把归一化坐标通过内参矩阵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或3
EstimatedTangentialDistortion是否估计切向畸变逻辑值

字段里的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处理完图像后,直接拿OutputView='full'的输出图去做像素坐标测量,但忘了newOrigin的存在。

newOrigin是一个1×2向量,表示输出图像左上角相对原始图像坐标系的偏移。当OutputView='full'时,输出图像比原图大,真正的中内容区外扩了一圈,因此输出图像原点不再对应原图原点。如果测量系统是基于输出图像坐标的,实际物理坐标需要这样换算:

[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侧标定、验证、手写实现、跨平台导出,每个环节都跑通,项目里再遇到图像扭曲的场景,心里就有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询