☰
MATLAB单应性矩阵全流程:DLT手写、RANSAC与误差验证
2026/10/26 3:16:27 网站建设 项目流程

简介:面向计算机视觉、图像处理课程学习者与MATLAB初学者,这份资料围绕两幅图像平面间的点对应关系,演示单应性矩阵的计算流程,可用于图像配准、透视变换等基础实验。压缩包共12个文件,约2.13MB,包含6张双视角示例图片、4个MATLAB脚本、1个矩阵数据文件和1份程序说明,覆盖选点、求解与结果验证等环节。已有4392人学习下载,说明其在新手实践与课堂实验中具有一定参考价值。读者可借助示例图完成四点选取并生成中间数据,再运行后续脚本得到单应性矩阵,结合变换结果理解投影关系与参数含义;说明文档也有助于梳理运行顺序和排查常见问题,适合作为短周期课程作业或自学练手材料。

1. 单应性矩阵在 MATLAB 里就是 tform.T 那 9 个数,难的是算完怎么验

单应性矩阵这个词听着玄学,落到 MATLAB 里其实很具体:它就是estimateGeometricTransform2D返回的tform.T,一个 3×3、整体尺度任意的矩阵,描述同一个平面在两个视角之间的映射。文档拍歪了要拉平、航拍图要拼接、双目做平面校正,第一步都是把这 9 个数解出来。真正让人翻车的从来不是“算不出来”,而是算出来了、肉眼看还挺像、一量差几十个像素。这篇按能照着复现的粒度拆开:先手写一遍 DLT,弄明白 8 个自由度是怎么被 4 对点固定住的;再用工具箱函数跑通特征匹配到imwarp的整条链路;最后把误差怎么量、坑在哪一条条摆出来。适合手上已经有 MATLAB 和 Computer Vision Toolbox、正卡在某一步的从业者。

2. 从 4 对点到 8 个自由度:手写一遍 DLT 才知道 H 有多脆弱

不建议一上来就调estimateGeometricTransform2D。那个函数把检测、采样、剔除、重拟合全包了,参数一多,出问题时你根本不知道是哪一环坏了。先把最小二乘版本的 DLT 自己写一遍,只用几十行,后面看工具箱的报错信息才有判断力。这一章的四段代码可以直接存成四个.m文件放在同一个目录下互相调用。

2.1 为什么是 8 个自由度,不是 9 个

齐次坐标下点(x, y, 1)和(kx, ky, k)表示同一个位置,H 乘任意非零常数都不改变映射结果,所以 9 个元素里真正独立的只有 8 个。每对对应点在消去齐次因子之后给出 2 个线性方程,4 对点刚好 8 个方程,恰好定解——这就是“少于 4 对点不要谈单应性”的由来。

顺带记住 H 的几何结构:H = K2 * (R + t*n'/d) * inv(K1),其中n是平面法向、d是相机到平面的距离、K是内参。这条式子在 6 章还会用到,先说结论:单应性成立的前提是“场景是平面”或者“相机只做纯旋转”。楼梯、人像、近景远景混在一起的画面,怎么调参数都对不上,因为模型本身就不适用。

还有一个容易忽略的点:H 的 8 个自由度在整幅图上的约束强度并不均匀。4 对点如果挤在画面左上角,右下角完全靠外推,误差会被放大好几倍。这一点决定了后面 4 章要专门查内点覆盖度。

2.2 四点直接解:构造 A、SVD、取最小奇异向量

function H = dltRaw(p1, p2) % p1: N×2 源图坐标 (x,y);p2: N×2 目标图坐标 (x,y) % 返回 3×3 单应性矩阵,已做 H(3,3)=1 归一化 N = size(p1,1); assert(N >= 4 && size(p2,1) == N, '至少 4 对点,且两组点数必须一致'); A = zeros(2*N, 9); for i = 1:N x = p1(i,1); y = p1(i,2); u = p2(i,1); v = p2(i,2); % 每对点给出两个线性方程:p2 × (H*p1) = 0 展开后的前两行 A(2*i-1,:) = [-x, -y, -1, 0, 0, 0, u*x, u*y, u]; A(2*i ,:) = [ 0, 0, 0, -x, -y, -1, v*x, v*y, v]; end [~, ~, V] = svd(A, 'econ'); h = V(:, end); % 最小奇异值对应的右奇异向量,即齐次解 H = reshape(h, 3, 3).'; % h 按列展开,转置回来才是行主序的 H H = H / H(3,3); % 尺度归一化,方便比较和后续 imwarp end

逻辑上要说明三件事。第一,为什么不用A\zeros(2N,1):齐次方程组只有零解,常规求解没意义,正确做法是求A的零空间,也就是 SVD 里最小奇异值对应的右奇异向量。第二,reshape是列优先的,reshape(h,3,3)得到的是H',必须转置一次,这一步写错的表现是“结果有点像但完全对不上”,非常迷惑。第三,末尾除以H(3,3)只是为了尺度统一,它成立的前提是h33不接近 0;当变换把无穷远点映到有限位置时(比如极端斜视),h33可能非常小,除下去数值直接爆炸,这也是 5 章要排查的一类故障。

参数上,输入只要求N >= 4,给多于 4 对点它就是最小二乘解,对离群点完全没有抵抗力——这是dltRaw和后面 RANSAC 的根本差别,别拿它直接吃原始匹配点。

提示:调试阶段建议先手工挑 4 对肉眼确认无误的点喂进dltRaw,确认方向、正负号、转置都没写错,再接自动匹配。

2.3 Hartley 归一化:不写这一步,误差会差一个数量级

像素坐标动辄 1000~3000,A里u*x这类项能到 10^6 量级,几列之间的量级相差三四个数量级,矩阵条件数轻松破 10^12,double 也扛不住。表现就是:明明点是对得上的,解出来的 H 代回去重投影误差却在几个像素量级抖动。这不是玄学,是纯粹的数值问题。

function [T, pn] = hartleyNorm(p) % p: N×2。返回归一化矩阵 T(3×3)与归一化后的坐标 pn c = mean(p, 1); % 质心 d = mean(sqrt(sum((p - c).^2, 2))); % 到质心的平均距离 s = sqrt(2) / d; % 缩放,使平均距离落到 √2 T = [s, 0, -s*c(1); 0, s, -s*c(2); 0, 0, 1]; ph = (T * [p, ones(size(p,1),1)].').'; pn = ph(:,1:2) ./ ph(:,3); end function H = dltNormalized(p1, p2) % 归一化 → 求解 → 反归一化 [T1, n1] = hartleyNorm(p1); [T2, n2] = hartleyNorm(p2); Hn = dltRaw(n1, n2); H = T2 \ (Hn * T1); % p2 = inv(T2) * Hn * T1 * p1 H = H / H(3,3); end

反归一化的推导只有一行:归一化坐标满足n2 = Hn * n1,把n1 = T1*p1、n2 = T2*p2代进去,两边左乘inv(T2),就得到上面的式子。用左除\而不是inv(T2)*...,是为了避免显式求逆带来的额外误差。

参数上没什么可调的,√2是 Hartley 的经典取值,让归一化后点的平均欧氏距离等于√2,T的两个尺度都稳定在 10^-3 量级。这里有个容易疏忽的细节:dltRaw内部会做H(3,3)=1的归一化,在归一化坐标系下这一步几乎不会出问题,所以直接复用是安全的。

在 2000 像素量级的图上,不做归一化时 4 点解的重投影误差经常在几像素量级抖动,加上归一化通常能压到亚像素,这个差距在拼接、测量类任务里是能直接看出来的。

2.4 手写 RANSAC:4 点采样 + 自适应迭代次数

自动匹配出来的点里一般有 30%~70% 是错的,最小二乘会被这些外点直接带偏。RANSAC 的思路很朴素:每次随机抽 4 对点解一个候选 H,统计全体的重投影误差,谁的内点多留谁。

function [H, inl] = ransacH(p1, p2, thresh, conf, maxIter) % thresh: 内点阈值(像素);conf: 置信度;maxIter: 迭代上限 N = size(p1,1); bestCnt = 0; bestH = eye(3); bestInl = false(N,1); iter = 0; needIter = maxIter; while iter < min(needIter, maxIter) idx = randperm(N, 4); % 退化检查:任意三点共线时 4 点解不可靠,直接丢弃这组样本 if rank([p1(idx,:), ones(4,1)]) < 3 iter = iter + 1; continue; end Hc = dltNormalized(p1(idx,:), p2(idx,:)); q = (Hc * [p1, ones(N,1)].').'; q = q(:,1:2) ./ q(:,3); d = sqrt(sum((q - p2).^2, 2)); % 前向转移误差 m = d < thresh; if sum(m) > bestCnt bestCnt = sum(m); bestH = Hc; bestInl = m; w = bestCnt / N; pIn = w^4; % 抽到的 4 对点全是内点的概率 if pIn > 0 && pIn < 1 needIter = ceil(log(1-conf) / log(1-pIn)); % 自适应收敛 end end iter = iter + 1; end if bestCnt >= 4 H = dltNormalized(p1(bestInl,:), p2(bestInl,:)); % 全内点重拟合 else H = bestH; end inl = bestInl; end

参数怎么定:thresh不要写死 3,按图像尺度缩放,常见做法是max(2, 0.002*max(size(I))),4000 像素宽的航拍图给 8 像素并不算松;conf取 0.99 或 0.999;maxIter给 2000~5000 兜底,防止内点率极低时needIter被算成几千上万。自适应公式log(1-conf)/log(1-w^4)里,w^4是 4 点全内点概率,内点率 50% 时需要约 70 次迭代,内点率 20% 时需要近 6000 次——这就是为什么内点率低于两成时,光靠提高迭代次数是低效策略,该回头去改特征检测参数。

重拟合那一步用的是全部内点而不是最优的 4 点样本,能明显降低单次采样的随机性;代价是inl掩码对应的是重拟合前的 H,如果后面要严格对齐内点索引,记得再算一遍掩码。

3. 用 MATLAB 现成函数跑通全流程:从 SURF 匹配到 estimateGeometricTransform2D

自己写的 DLT 适合理解和小数据量验证,真实项目里一般还是走工具箱,因为特征检测、描述子、匹配、RANSAC 是一条高度耦合的流水线,手写容易在某个环节埋下不一致。这一章按“选型 → 匹配 → 估参 → 回代”四步走,最后给一个能直接跑的完整脚本。

3.1 特征检测与描述子的选型:SURF、SIFT、ORB 怎么挑

三种描述子的差别决定了后面参数怎么调:

描述子检测函数描述长度尺度/旋转不变适用场景
SURFdetectSURFFeatures64 维 float都是默认首选,速度快,匹配阈值好调
SIFTdetectSIFTFeatures128 维 float都是尺度差异大、光照变化强,慢但稳
ORBdetectORBFeatures256 位二进制旋转,尺度靠金字塔帧率高、图小、算力紧的场合

老版本里只有 SURF,近几年的版本才补齐 SIFT 和 ORB。如果detectSIFTFeatures报未定义,别急着改代码架构,把这块换成detectSURFFeatures加extractFeatures(..., 'Method','SURF')即可,下游的matchFeatures和estimateGeometricTransform2D完全不用动。

SURF 的两个关键参数是MetricThreshold和NumOctaves。前者是 Hessian 响应阈值,越小检出的点越多也越杂;后者是金字塔层数,相邻两图尺度差得多就调大,代价是耗时线性上涨。

3.2 匹配与比值检验:matchFeatures 的两个参数最关键

%% 1) 读图与预处理 I1 = imread('left.png'); I2 = imread('right.png'); G1 = im2single(im2gray(I1)); % im2gray 免去手写 rgb2gray 判断 G2 = im2single(im2gray(I2)); %% 2) 特征检测与描述 pts1 = detectSURFFeatures(G1, 'MetricThreshold', 800, ... 'NumOctaves', 4, 'NumScaleLevels', 6); pts2 = detectSURFFeatures(G2, 'MetricThreshold', 800, ... 'NumOctaves', 4, 'NumScaleLevels', 6); [feat1, valid1] = extractFeatures(G1, pts1, 'Method', 'SURF', 'SURFSize', 64); [feat2, valid2] = extractFeatures(G2, pts2, 'Method', 'SURF', 'SURFSize', 64); %% 3) 匹配 + 比值检验 idx = matchFeatures(feat1, feat2, 'MatchThreshold', 6, ... 'MaxRatio', 0.75, 'Unique', true); mPts1 = valid1(idx(:,1)); mPts2 = valid2(idx(:,2)); fprintf('原始匹配对:%d\n', size(idx,1));

MatchThreshold在 float 描述子下是“相对最大可能距离的百分比”,SURF 一般给 5~10,给到 20 以上基本等于不做筛选。MaxRatio是 Lowe 的最近邻/次近邻比值,0.7~0.8 之间是常用区间,画面里有大量重复纹理时压到 0.6,会牺牲数量换正确率。Unique设为 true 表示一对一匹配,能挡住一部分“一个点匹配到一片点”的情况。

extractFeatures返回的第二项valid1才是真正被算出描述子的点,可能比pts1少——边界附近的点因为 patch 越界会被丢掉。所以索引一定要走valid1,拿pts1去对idx是典型的错位 bug,表现是匹配图看上去乱得一塌糊涂。

3.3 估计 H 并回代:inlierIdx、tform.T 与 imwarp 的衔接

%% 4) RANSAC 估计单应性 try [tform, inlierIdx] = estimateGeometricTransform2D( ... mPts1, mPts2, 'projective', ... 'MaxDistance', 3, 'MaxNumTrials', 3000, 'Confidence', 99.9); catch ME error('估计失败,检查匹配点数量与分布:%s', ME.message); end H = tform.T; % 3×3,H(3,3) 已被归一化为 1 fprintf('匹配 %d 对,内点 %d 对,内点率 %.1f%%\n', ... size(idx,1), sum(inlierIdx), 100*mean(inlierIdx)); %% 5) 回代显示 J = imwarp(I1, tform, 'OutputView', imref2d(size(I2))); figure; subplot(1,2,1); showMatchedFeatures(I1, I2, mPts1(inlierIdx), mPts2(inlierIdx), 'montage'); title('内点匹配'); subplot(1,2,2); imshowpair(I2, J, 'blend'); title('对齐结果,重影越小越好');

MaxDistance是内点的像素阈值,跟 2.4 里的thresh一个含义,图大就放大。MaxNumTrials在匹配对数上千时建议给到 3000 以上,因为estimateGeometricTransform2D内部也做自适应收敛,但样本分布差的时候迭代次数会顶到上限。Confidence通常 99 以上,代价是迭代次数。

tform的方向是“第一组点 → 第二组点”,也就是mPts1所在的I1变换到I2的坐标系。imwarp(I1, tform, 'OutputView', imref2d(size(I2)))这句的方向必须和它一致,写成imwarp(I2, tform)就会出现整幅图飞出去的画面。tform.T已经在内部做过尺度归一化,可以放心和手写的H直接比较,这是交叉验证两条链路的好办法。

3.4 一个可以直接跑的完整脚本

把上面的片段拼起来就是可执行版本,用try/catch包住估计步骤是必要的——点数不足时estimateGeometricTransform2D会直接抛错,而不是返回一个空结果。

function [H, stats] = homographyFromImages(I1, I2, metricThresh, maxDist) % 输入两幅图与两个关键参数,输出 3×3 单应性与统计信息 G1 = im2single(im2gray(I1)); G2 = im2single(im2gray(I2)); p1 = detectSURFFeatures(G1, 'MetricThreshold', metricThresh); p2 = detectSURFFeatures(G2, 'MetricThreshold', metricThresh); [f1, v1] = extractFeatures(G1, p1, 'Method', 'SURF', 'SURFSize', 64); [f2, v2] = extractFeatures(G2, p2, 'Method', 'SURF', 'SURFSize', 64); idx = matchFeatures(f1, f2, 'MatchThreshold', 6, 'MaxRatio', 0.75, 'Unique', true); if size(idx,1) < 8 error('有效匹配只有 %d 对,先降 MetricThreshold 或换 SIFT', size(idx,1)); end [tform, inl] = estimateGeometricTransform2D(v1(idx(:,1)), v2(idx(:,2)), ... 'projective', 'MaxDistance', maxDist, 'MaxNumTrials', 3000, 'Confidence', 99.9); H = tform.T; stats = struct('nMatch', size(idx,1), 'nInlier', sum(inl), ... 'ratio', mean(inl), 'tform', tform); end

参数入口就两个:metricThresh控制特征密度(800 起步,点太少降到 300,噪声多提到 1500),maxDist控制内点口径(跟图幅走)。返回的stats里带上tform,是因为后续imwarp需要它而不是裸矩阵;H单独拎出来是为了做误差计算和跟手写版本对拍。

4. 把 H 验一遍:重投影误差、对称误差与退化判定

能跑出tform不等于结果可信。工程上真正花时间的是验证:这个 H 到底准到什么程度、在哪些区域不可信、什么时候应该判定为失败而不是硬用。这一章给三种误差口径、一次重拟合、三个退化判据。

4.1 三种误差口径:前向、后向、对称转移

只算前向误差会漏掉一类问题:H 在前向映射上把点都压到很接近的位置,误差看起来很小,但反变换回去发散。对称转移误差同时约束两个方向,是验收时更靠谱的指标。

function [ef, eb, es, samp] = homogErr(H, p1, p2) % ef/eb/es:前向、后向、对称转移误差(像素);samp:Sampson 距离 n = size(p1,1); q = (H * [p1, ones(n,1)].').'; q = q(:,1:2) ./ q(:,3); qi = (H \ [p2, ones(n,1)].').'; qi = qi(:,1:2) ./ qi(:,3); ef = sqrt(sum((q - p2).^2, 2)); eb = sqrt(sum((qi - p1).^2, 2)); es = sqrt(ef.^2 + eb.^2); % Sampson 距离:代数误差的一阶几何修正,做内点判定比欧氏误差省事 x1 = [p1, ones(n,1)]; x2 = [p2, ones(n,1)]; num = sum((x2 * H) .* x1, 2).^2; % (x2' * H * x1)^2 Hx1 = (H * x1.').'; Htx2 = ( <p> <a href="https://download.csdn.net/download/u011624019/10483800" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

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

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

立即咨询