简介:这份资源聚焦计算机视觉中的平面单应性求解,面向正在学习图像配准、透视变换或做相关课程实验的高校学生与工程技术人员。它解决的是两幅图像平面之间点对应关系的计算问题:先在两幅图中各选取四个对应点,得到匹配数据,再据此求解单应性矩阵,为后续的视角变换与图像拼接打基础。资源包共12个文件,约2.13MB,以4个m脚本为核心,承担选点交互、矩阵计算与透视变换等任务;6个jpg是成对视角的示例图像,可直接用于跑通流程;另有1个mat保存选点结果,1个txt说明运行顺序,便于按步骤验证。已有4392人学习下载。整体流程清晰、数据现成,读者既能直接复现完整的单应性估计与变换效果,也可在脚本基础上修改选点方式、替换自有图像,理解四点求解矩阵的原理与验证方法,适合作为入门实践与小规模实验的参考材料。
1. 两张平面图对不上时,先别急着加特征点:单应性矩阵 MATLAB 的适用边界
两张图拍的是同一块平面,角点却总是差几个像素,很多人第一反应是特征点不够多,真正的问题往往出在单应性矩阵 MATLAB 的坐标约定和点对顺序上。这套脚本围绕自动特征匹配、手动选点、H 求解和误差检查串成一条线,输入两张图,输出 3×3 单应性矩阵、匹配点、内点比例和重投影误差。它适合做图像拼接、文档矫正、平面测量、AR 标定前置,也适合拿来做课程设计里的图像配准 Demo。不适合深度变化大、视差明显或镜头畸变很重的场景。新手可以按自动流程跑通,熟手重点看方向、阈值和验证部分。
2. 单应性矩阵的几何前提与 MATLAB 坐标约定:为什么 4 对点只是下限
2.1 平面假设与 8 个自由度:4 对点为什么只是下限
单应性矩阵描述的是两个平面之间的投影映射。把一张图上的点写成齐次坐标[x y 1],另一张图上的对应点写成[x' y' 1],关系可以写成[x' y' 1]^T ~ H [x y 1]^T,其中H是 3×3 矩阵。由于齐次坐标尺度不影响最终像素位置,H通常固定h33=1,真正需要求解的是 8 个未知量。每对匹配点能提供两个独立方程,所以理论下限是 4 对不共线点。注意“不共线”这三个字,四点里有三点落在同一条直线上,方程会退化,MATLAB 可能给你一个数值上能算、几何上不能用的 H。
常见做法是先把点坐标归一化,再做 DLT。归一化的目的不是提高精度这么简单,而是把坐标从几百上千像素压到相近量级,避免方程矩阵条件数过大。fitgeotrans和estimateGeometricTransform2D内部会做归一化,但自己写四点法时不做归一化,很容易得到一个“看起来能变换、实际一推就飞”的矩阵。平面假设也有边界:相机纯旋转、拍摄同一块平面、平面距离远且深度变化小,这几种情况单应性比较稳;如果前景和背景深度差很大,单应性只能对齐其中一个平面,另一个平面会出现明显视差。这类场景不要硬调阈值,换模型或者分块处理更实际。
2.2 MATLAB 图像坐标是 (x,y) 还是 (row,col):fitgeotrans 与 estimateGeometricTransform2D 的方向
MATLAB 里最容易翻车的地方不是算法,而是坐标和方向。图像矩阵用I(row,col)索引,size(I)返回的是[高 宽 通道],但特征点对象里的Location是[x y],也就是[列 行]。detectSURFFeatures、detectSIFTFeatures、cpselect导出的点都是[x y]。如果你把Location直接当成[row col]去索引图像,像素会偏到另一个位置,后面匹配得再漂亮也没有意义。
方向同样要统一。fitgeotrans(movingPoints,fixedPoints,'projective')得到的是从movingPoints到fixedPoints的变换,imwarp(I1,tform,'OutputView',imref2d(size(I2)))是把I1往I2对齐。estimateGeometricTransform2D(matchedPoints1,matchedPoints2,'projective')得到的是从matchedPoints1到matchedPoints2的变换。matchFeatures输出的indexPairs第一列对应第一组特征,第二列对应第二组特征,构造matchedPoints1和matchedPoints2时不要写反。下面这段代码只做一件事:确认图像尺寸顺序和点坐标顺序。
I1 = imread('scene_left.png'); I2 = imread('scene_right.png'); % size 返回 [高 宽 通道],不是 [宽 高 通道] sz1 = size(I1); fprintf('I1 高=%d 宽=%d 通道=%d\n', sz1(1), sz1(2), sz1(3)); % 取一个点 (x=120, y=80),正确索引是 I1(y,x) x = 120; y = 80; if x >= 1 && x <= size(I1,2) && y >= 1 && y <= size(I1,1) pixel = I1(y,x,:); fprintf('像素值: %s\n', mat2str(squeeze(pixel)')); end代码里的sz1(1)是高,sz1(2)是宽;点坐标x对应列,y对应行。参数没有玄学,关键是不要把Location的列直接塞进I1(:,x)。我一般会在脚本开头打印一次尺寸和一对点,后面所有变换都按这个约定走,省得中途换图时方向错乱。
2.3 选自动还是手动:特征匹配、RANSAC 与人工选点的边界
自动流程适合纹理丰富、角点明显的场景,比如桌面、书页、瓷砖、标定板。检测器给出关键点,matchFeatures做描述子匹配,estimateGeometricTransform2D用 RANSAC 剔除外点。RANSAC 的价值在于它不要求所有匹配点都正确,只要内点足够多、分布不共线,就能估出一个可用的 H。手动流程适合低纹理、重复纹理、文档扫描件或者自动匹配总是被外点带偏的图。cpselect让人工点四角或控制点,fitgeotrans直接求解,优点是可控,缺点是对单点误差敏感,因为fitgeotrans默认不做 RANSAC。
| 维度 | 自动特征匹配 | 手动选点 |
|---|---|---|
| 适用场景 | 纹理丰富、角点明显 | 低纹理、文档、控制点可辨认 |
| 最少点对 | 4 对,实际建议 20 对以上 | 4 对,实际建议 8 到 12 对 |
| 外点处理 | RANSAC 自动剔除 | 依赖人工检查 |
| 主要风险 | 重复纹理导致误匹配 | 单点误差直接进 H |
| 结果可解释性 | 中等 | 高 |
自动流程不是点越多越好。MetricThreshold调低会引入大量弱角点,匹配数量上去了,内点比例可能反而下降。手动流程也不是点越密越好,控制点集中在图像一角,H 在另一角的误差会被放大。常见做法是自动先跑一遍,拿到内点和 H,再用cpselect叠加检查,把明显外点替换掉,最后用fitgeotrans或重跑 RANSAC 收口。这样既有自动的覆盖度,又有人工的确定性。
3. 自动特征匹配求 H:SURF/SIFT 检测、matchFeatures 与 RANSAC 的完整脚本
3.1 读图、灰度化与特征检测器的选择(SURF/SIFT/ORB)
自动流程第一步不是直接调estimateGeometricTransform2D,而是把输入图和检测器选对。彩色图先转灰度,减少通道间冗余;如果图像对比度很低,可以先用imadjust或histeq做轻度增强,但不要过度,过度增强会把噪声变成“特征点”。检测器方面,SURF 在不少旧脚本里常见,速度快、尺度不变;SIFT 在较新版本里可用,稳定性好;ORB 更快,适合实时或大图,但描述子区分度弱一些。下面这段代码用 SURF 作为入口,注释里保留 SIFT 和 ORB 的替换位置,方便在不同环境里跑通。
I1 = imread('scene_left.png'); I2 = imread('scene_right.png'); % 统一转灰度,避免通道数不一致 if size(I1,3) == 3, gray1 = rgb2gray(I1); else, gray1 = I1; end if size(I2,3) == 3, gray2 = rgb2gray(I2); else, gray2 = I2; end % SURF 检测:MetricThreshold 越低点越多,500 是常用起点 points1 = detectSURFFeatures(gray1, 'MetricThreshold', 500); points2 = detectSURFFeatures(gray2, 'MetricThreshold', 500); % 如果当前环境没有 SURF,可换成: % points1 = detectSIFTFeatures(gray1); % points2 = detectSIFTFeatures(gray2); % 或: % points1 = detectORBFeatures(gray1); % points2 = detectORBFeatures(gray2); % 提取描述子,validPoints 与 features 行一一对应 [features1, validPoints1] = extractFeatures(gray1, points1); [features2, validPoints2] = extractFeatures(gray2, points2); fprintf('I1 特征点 %d 个,I2 特征点 %d 个\n', ... size(validPoints1,1), size(validPoints2,1));这段代码的关键参数是MetricThreshold。它控制检测器保留多少响应较强的点,值越小点越多,但弱角点和噪声也会进来。extractFeatures返回的validPoints1和features1必须成对使用,后面构造匹配点时只能索引进validPoints1,不能索引进原始points1,否则描述子和坐标会错位。NumOctaves和NumScaleLevels一般用默认值,除非图像尺度变化特别大;先把检测器跑通,再谈调参。
3.2 特征匹配与几何变换估计:matchFeatures、estimateGeometricTransform2D
匹配阶段有两个参数最影响结果:MatchThreshold和MaxRatio。前者控制描述子距离的绝对上限,后者做最近邻比值检验,常见取值 0.7 到 0.8。Unique设为true可以避免一个特征被重复匹配。匹配完不要急着相信所有点对,RANSAC 才是剔除几何外点的关键。estimateGeometricTransform2D的MaxDistance是像素阈值,表示一个点经过变换后离对应点多远仍算内点;它和图像分辨率、噪声水平直接相关,不要一张 4000 像素宽的图还沿用 1 像素阈值。
% 描述子匹配,MaxRatio 做比值检验,Unique 避免重复匹配 indexPairs = matchFeatures(features1, features2, ... 'MatchThreshold', 10, 'MaxRatio', 0.7, 'Unique', true); % indexPairs 第一列对应 features1/validPoints1,第二列对应 features2/validPoints2 matchedPoints1 = validPoints1(indexPairs(:,1)); matchedPoints2 = validPoints2(indexPairs(:,2)); % 估计从 matchedPoints1 到 matchedPoints2 的投影变换 tform = estimateGeometricTransform2D(matchedPoints1, matchedPoints2, ... 'projective', 'MaxDistance', 3, 'Confidence', 99.9, 'MaxNumTrials', 2000); H = tform.T; H = H / H(3,3); % 归一化,方便比较和保存 % 用重投影误差统计内点,阈值 5 像素只用于检查,不参与 RANSAC P1 = matchedPoints1.Location; P2 = matchedPoints2.Location; p1t = transformPointsForward(tform, P1); err = sqrt(sum((p1t - P2).^2, 2)); inlierIdx = err < 5; fprintf('初始匹配 %d 对,内点 %d 对,内点比例 %.1f%%\n', ... size(P1,1), sum(inlierIdx), 100*mean(inlierIdx)); fprintf('中位重投影误差 %.3f 像素\n', median(err(inlierIdx))); figure; showMatchedFeatures(I1, I2, matchedPoints1(inlierIdx), ... matchedPoints2(inlierIdx), 'montage'); title('RANSAC 内点匹配');MaxDistance设 3 像素适合中等分辨率、噪声不大的图;如果图像很大,可以按短边的 0.1% 到 0.3% 估算。Confidence越高,RANSAC 越愿意多迭代,MaxNumTrials是上限。H归一化到H(3,3)=1只是统一尺度,不改变变换结果;如果H(3,3)接近 0,说明这个矩阵的尺度不适合这样归一化,先检查点对和方向。inlierIdx这里用 5 像素重算,是为了给后续人工检查一个直观比例,和 RANSAC 内部阈值不是一回事。
3.3 把 H 用起来:imwarp 对齐、内点统计与结果保存
求出 H 只是中间结果,真正要验证的是把它作用到图像上能不能对齐。imwarp的OutputView用参考图的尺寸,保证输出和I2同一坐标系。imshowpair的blend模式能把两张图叠在一起看边缘是否重影。保存时不要只存H,把tform、err、inlierIdx一起存下来,后面换图或复现实验时不用重新跑匹配。下面这段代码把对齐、叠加和保存串起来。
% 以 I2 的尺寸作为输出视窗,把 I1 变换到 I2 坐标系 outputView = imref2d(size(I2)); I1w = imwarp(I1, tform, 'OutputView', outputView); figure; imshowpair(I1w, I2, 'blend'); title('I1 变换后与 I2 叠加'); % 保存结果,后续验证脚本直接加载 save('homography_result.mat', 'H', 'tform', 'err', 'inlierIdx', ... 'matchedPoints1', 'matchedPoints2'); % 检查 H 的数值范围,过大或过小都值得回头看 fprintf('H = \n'); disp(H); fprintf('H 条件数 %.3e\n', cond(H));OutputView不能省,省了以后imwarp可能按变换后的包围盒输出,图像尺寸和I2不一致,叠加时会对不齐。cond(H)是辅助指标,不是绝对判据;归一化后条件数仍然很大,通常说明点分布太集中或者存在近共线点。保存matchedPoints1和matchedPoints2是为了后面做重投影和逆变换验证,不要只留一个H就结束。
4. 手动选点与精化:cpselect、fitgeotrans 和四点法的可控流程
4.1 用 cpselect 选控制点:导出 movingPoints/fixedPoints 的变量约定
自动匹配在低纹理图上经常给不出足够内点,这时候手动选点更稳。cpselect(I1,I2)会打开控制点选择工具,左边是I1,右边是I2,点选对应位置后导出到工作区。变量约定要记牢:movingPoints对应待变换图I1,fixedPoints对应参考图I2。点选顺序也要一致,先点I1的左上角,再点I2的左上角,不要一边从左上开始、另一边从右下开始。控制点建议覆盖图像四角和中间区域,至少 4 对,实际用 8 到 12 对更稳。
I1 = imread('doc_left.png'); I2 = imread('doc_right.png'); % 打开控制点选择工具,手工点选对应位置 cpselect(I1, I2); % 在工具里点选完成后,将 movingPoints 和 fixedPoints 导出到工作区 % movingPoints: I1 上的点,N×2,列顺序 [x y] % fixedPoints : I2 上的点,N×2,列顺序 [x y] % 如果变量名不是这两个,导出后手动改名,避免后面 fitgeotrans 用错cpselect的交互结果不是越密越好。控制点集中在文档中央,四角没有约束,透视校正后边缘会翘;控制点全挤在一条直线上,H 直接退化。点选时放大图像,尽量点到同一个物理角点,差两三个像素看起来不多,经过透视外推后可能放大到十几个像素。导出后先size(movingPoints)和size(fixedPoints)对一下,行数必须一致,列数必须是 2。
4.2 用 fitgeotrans 求 H 并检查条件数:归一化与 h33
fitgeotrans的调用很直接,'projective'表示投影变换。它要求至少 4 对点,内部会做归一化,但不会像 RANSAC 那样剔除粗差,所以手动点里有一个明显错误,H 就会被带偏。求出tform.T后统一除以H(3,3),再算重投影误差和条件数。下面这段代码把求解和检查放在一起。
% movingPoints -> fixedPoints 的投影变换 tform = fitgeotrans(movingPoints, fixedPoints, 'projective'); H = tform.T; H = H / H(3,3); % 正变换回投,检查手动点对的残差 p1t = transformPointsForward(tform, movingPoints); err = sqrt(sum((p1t - fixedPoints).^2, 2)); fprintf('控制点 %d 对,中位误差 %.3f 像素,最大误差 %.3f 像素\n', ... size(movingPoints,1), median(err), max(err)); fprintf('H 条件数 %.3e\n', cond(H)); % 把误差大的点标出来,回到 cpselect 里重点检查 badIdx = find(err > 3); if ~isempty(badIdx) fprintf('误差较大的点索引: %s\n', mat2str(badIdx')); endfitgeotrans的点顺序是movingPoints在前、fixedPoints在后,反了就是求逆变换。H(3,3)归一化后,H的数值尺度统一,但条件数仍然受点分布影响。中位误差小于 1.5 像素、最大误差没有离群点,通常可以接受;如果某个点误差明显大于其他点,先怀疑点选位置,不要急着改模型。手动点少的时候,单个坏点的杠杆很大,这也是我一般会留 8 对以上控制点的原因。
4.3 局部精化与半自动方案:先自动再人工替换外点
纯手动稳但费时,纯自动快但低纹理图容易翻车,半自动更实用:先跑第 3 章的自动流程,得到matchedPoints1、matchedPoints2和tform,再用cpselect带着初始点打开,人工删掉明显外点、补几个角点,最后用fitgeotrans重新求解。这样既保留了自动匹配的覆盖度,又把关键位置的控制权拿回来。下面这段代码展示初始点带入和重新求解的写法。
% 先用自动匹配结果作为初始控制点打开 cpselect % 如果当前版本不支持带初始点,先 cpselect(I1,I2),再手工补点 cpselect(I1, I2, matchedPoints1.Location, matchedPoints2.Location); % 在工具里删除明显外点,补 2 到 4 个四角点,再导出 movingPoints/fixedPoints tform2 = fitgeotrans(movingPoints, fixedPoints, 'projective'); H2 = tform2.T; H2 = H2 / H2(3,3); % 对比自动和精化后的重投影误差 pAuto = transformPointsForward(tform, matchedPoints1.Location); errAuto = sqrt(sum((pAuto - matchedPoints2.Location).^2, 2)); pRef = transformPointsForward(tform2, movingPoints); errRef = sqrt(sum((pRef - fixedPoints).^2, 2)); fprintf('自动中位误差 %.3f,精化中位误差 %.3f\n', ... median(errAuto), median(errRef));半自动流程的关键是“替换”而不是“叠加”。自动外点不删掉,补再多角点也会被拉偏;手动补点要覆盖四角,不要只补纹理最丰富的区域。cpselect里看到的点坐标是[x y],导出后直接给fitgeotrans,不要再转置。精化后的tform2用imwarp重新叠加一次,边缘重影明显变小才算收口。
5. 避坑与排查:单应性矩阵 MATLAB 最常翻车的 5 个点
单应性矩阵在 MATLAB 里翻车,大多数不是公式不会,而是方向、坐标和条件没对齐。下面 5 条是血泪经验里出现频率最高的,每条按“现象、原因、解决”拆开,遇到问题时直接对号入座。
5.1 现象:imwarp 后图像往反方向跑;原因:变换方向搞反;解决:统一 moving 到 fixed 并打印一对点
现象是imwarp(I1,tform)以后,I1不但没对齐I2,反而跑到画面外,或者叠加后完全错位。原因通常是把fitgeotrans的movingPoints和fixedPoints写反了,或者把estimateGeometricTransform2D(matchedPoints2,matchedPoints1,...)的参数顺序颠倒了。fitgeotrans返回的是从第一个参数到第二个参数的变换,imwarp默认把输入图按这个变换正向映射。解决方法是固定一个方向:待变换图I1上的点永远放前面,参考图I2上的点永远放后面,然后用transformPointsForward打印一对点的变换结果,确认它落在I2的对应位置附近。不要靠“看起来差不多”判断方向,数值验证一次比反复imwarp快。
5.2 现象:匹配点看着对,H 却把图拉飞;原因:x/y 与 row/col 混用;解决:所有点统一用 Location 的 [x y]
现象是showMatchedFeatures里连线看着挺整齐,求出的 H 却把图像拉成奇怪形状。原因常见于手动构造点坐标时把[row col]当成[x y],或者用I1(y,x)取像素后直接把(y,x)塞进fitgeotrans。SURFPoints.Location、SIFTPoints.Location、cpselect导出变量都是[x y],图像矩阵索引才是(row,col)。解决方法是所有控制点、匹配点、手动点统一用[x y],需要索引像素时显式写I1(y,x),并在脚本开头用一对已知点验证:transformPointsForward(tform,[x y])的结果应该落在参考图的[x' y']附近。坐标混用是黑匣子式错误,越早打印越好。
5.3 现象:匹配点只有五六对,H 不稳定或直接报错;原因:低纹理、重复纹理或共线点;解决:换检测器、补手动点、检查秩
现象是estimateGeometricTransform2D报“not enough points”或者求出的 H 对微小改动极其敏感。原因可能是图像纹理太少,SURF/SIFT 只找到少量点;也可能是重复纹理导致匹配点集中在一小块区域,甚至近似共线。解决方法是先看匹配点数量和内点分布,不要只看总数。可以把检测器换成 ORB 或 SIFT,调低MetricThreshold增加点,再用cpselect补四角控制点。检查共线可以用中心化后的点矩阵秩:rank([P1(:,1)-mean(P1(:,1)), P1(:,2)-mean(P1(:,2)), ones(size(P1,1),1)]),如果小于 3,说明点分布退化。点数不够时硬调 RANSAC 参数没有意义,补点或换图更直接。
5.4 现象:MaxDistance 调小内点多但误差大;原因:RANSAC 阈值和分辨率不匹配;解决:按短边比例设阈值并用重投影复核
现象是把MaxDistance从 5 调到 1,内点数量看起来不少,但 H 对齐后边缘重影反而更严重。原因是阈值不是“越小越准”,它表示 RANSAC 容忍的像素偏差,图像分辨率高、噪声大时,1 像素会把正确匹配也判成外点,剩下少数巧合内点把 H 带偏。解决方法是按图像短边的 0.1% 到 0.3% 设初始阈值,比如短边 1000 像素先用 2 到 3 像素;再用独立的重投影误差中位数和内点比例复核,不要只盯 RANSAC 输出的内点数。阈值调整属于玄学区,固定一套“先比例、后误差”的流程比反复试数靠谱。
5.5 现象:文档能对齐,建筑立面怎么都不对;原因:非平面、视差或镜头畸变;解决:分块单应或先做畸变校正
现象是同一套代码在文档图上效果很好,换到建筑立面、室内场景就到处重影。原因是单应性只适用于平面场景,建筑立面如果有阳台、凸出物,或者相机有俯仰和位移,视差会让不同深度的区域无法用一个 H 对齐;镜头畸变明显时,直线在图像里本来就不是直线,单应性也救不回来。解决方法是先判断场景是否近似平面,非平面可以分块做多个单应,或者先做镜头畸变校正再匹配;如果只是文档校正,尽量正对拍摄、减少透视外推。不要在一个 H 上死磕,模型前提不满足时,调参只是把错误摊平。
6. 验证 H 是否可信:重投影误差、逆变换与叠加检查的实操
拿到 H 之后,我一般不会直接交给下游,而是固定跑三个验证动作:正变换重投影、逆变换回投、图像叠加。正变换看I1的点落到I2后偏多少,逆变换看I2的点回到I1是否一致,叠加看边缘重影。下面这段代码把三个动作串起来,阈值只作为参考,实际按图像分辨率和任务精度调整。
% 正变换重投影误差 P1 = matchedPoints1.Location; P2 = matchedPoints2.Location; p1t = transformPointsForward(tform, P1); errFwd = sqrt(sum((p1t - P2).^2, 2)); % 逆变换回投误差 tformInv = invert(tform); p2b = transformPointsForward(tformInv, P2); errBack = sqrt(sum((p2b - P1).^2, 2)); errSorted = sort(errFwd); q90 = errSorted(max(1, round(0.9 * numel(errSorted)))); fprintf('正变换中位 %.3f px,90 分位 %.3f px\n', median(errFwd), q90); fprintf('逆变换中位 %.3f px\n', median(errBack)); % 叠加检查 outputView = imref2d(size(I2)); I1w = imwarp(I1, tform, 'OutputView', outputView); figure; imshowpair(I1w, I2, 'blend'); title('H 验证叠加图');| 指标 | 可信区间 | 需要复查 | 不建议直接使用 |
|---|---|---|---|
| 正变换中位误差 | 小于 1.5 像素 | 1.5 到 3 像素 | 大于 3 像素 |
| 内点比例 | 大于 60% | 30% 到 60% | 小于 30% |
| 逆变换中位误差 | 与正变换接近 | 明显偏大 | 大于正变换两倍 |
| 叠加重影 | 边缘基本重合 | 局部轻微重影 | 大面积错位 |
我吃过一次亏:自动匹配内点比例 80%,看着很漂亮,结果控制点全挤在图像左上角,右下角文档边缘重影十几像素。从那以后我每次拿到 H 都强制走一遍重投影、逆变换和四角叠加,再决定要不要交给下游。下载包里带了示例图和验证脚本,拿到后先跑第 3 章的自动流程,再用这一章的代码过一遍,别省这一步。希望帮到你。
本文还有配套的精品资源,点击获取