简介:这是一份面向计算机视觉初学者、课程实验学习者与图像配准实践者的 MATLAB 单应性矩阵计算资料,重点解决两幅平面图像之间点对应关系的求解与验证问题。压缩包共 12 个文件,约 2.13MB,包含 4 个 m 脚本、6 张 jpg 示例图、1 个 mat 参数文件和 1 个 txt 说明文档,分别承担选点、矩阵计算、图像变换验证、示例数据与运行提示等用途,结构紧凑,便于按流程逐步操作。资源已有 4392 人学习下载,在图像拼接、透视校正和基础视觉实验等入门场景中具有一定参考价值。读者可借助示例图完成四个对应点的选取,生成并载入单应性矩阵,再运行验证脚本观察变换效果,从而理解从点对应关系到矩阵求解、再到图像映射的完整流程;配套说明也有助于降低环境配置与运行门槛。
1. 单应性矩阵:四个点为什么就能把两张图缝在一起
两张照片,同一面墙,一个左斜一个右斜。想把它俩对齐,直觉上要估一堆参数,结果透视收缩怎么都对不上。单应性矩阵(Homography)解决的就是这件事:一个 3×3 矩阵、8 个自由参数,把“平面到平面”的投影关系完整描述下来,透视、缩放、旋转、平移全在里面。在 MATLAB 里算出它,最短只要三行;但要让这三行的结果可信,得先搞明白两件事——为什么四个点就够,以及九个元素里那一个为什么可以被一个比值吃掉。搞清楚这两点,后面调特征点、调 RANSAC、解释结果为什么会飘,才有落脚的地方,否则全是玄学调试。
2. 八自由度从哪来:把投影变换拆成可解的线性方程组
2.1 齐次坐标:把除法藏进第三维
单应性的定义式本身带着除法。把点写成齐次向量 $\mathbf{x} = (x, y, 1)^\top$,映射关系就是 $\mathbf{x}' \sim H\mathbf{x}$,展开成分量:
$$ u = \frac{h_{11}x + h_{12}y + h_{13}}{h_{31}x + h_{32}y + h_{33}}, \qquad v = \frac{h_{21}x + h_{22}y + h_{23}}{h_{31}x + h_{32}y + h_{33}} $$
分母那一项是透视的关键。它随位置线性变化,所以越远的地方被压缩得越厉害,这正是仿射变换(分母恒为 1)做不出来的效果。齐次表示的好处是把“除以第三维”这个动作推迟到最后一步,中间所有运算都是线性的。
接着是尺度的自由度。$H$ 和 $\lambda H$ 代入上式,分子分母同时乘 $\lambda$,结果一模一样。也就是说九个元素里有一个是冗余的,真正独立的只有 8 个。常见的做法是强行令 $h_{33}=1$,但这一步有代价:当真实的 $h_{33}$ 接近 0(比如相机光轴几乎平行于成像平面)时,强行归一化会数值爆炸。所以在求解阶段一般不固定 $h_{33}$,而是靠范数约束 $|h|=1$ 来消掉尺度,最后再视情况归一化。
8 个自由度对应 8 个独立约束,一组点对应给两个方程,所以 4 组点刚好够。这是最朴素的计数,也是后面所有实现的最小输入。
2.2 DLT 的两行系数:一组对应点给出两个方程
把定义式交叉相乘,去掉分母,得到两个关于 $h$ 的线性方程。把 $h$ 按行展开成 9 维列向量 $[h_{11}, h_{12}, h_{13}, h_{21}, h_{22}, h_{23}, h_{31}, h_{32}, h_{33}]^\top$,一组对应点 $(x,y)\to(u,v)$ 就贡献两行:
% 第 k 组对应点 (x,y) -> (u,v),h 按行展开为 9 维列向量 % 第一行来自 u 的方程,第二行来自 v 的方程 A(2*k-1, :) = [-x, -y, -1, 0, 0, 0, u*x, u*y, u]; A(2*k , :) = [ 0, 0, 0, -x, -y, -1, v*x, v*y, v];这两行的结构值得盯一眼:前三个系数是源点坐标取负,中间三个恒为 0 或取负,后三个是目标坐标与源坐标的乘积。乘积项 $ux, uy, vx, vy$ 就是透视信息的来源——没有它们,矩阵退化成仿射。
N 组点堆出 $2N \times 9$ 的矩阵 $A$,求解目标变成 $A h = 0$。N=4 时 $A$ 是 8×9,秩一般等于 8,零空间恰好是一维,解唯一(差一个尺度)。N>4 时方程超定,一般没有精确解,退而求其次最小化 $|Ah|^2$,在 $|h|=1$ 约束下,解就是 $A^\top A$ 最小特征值对应的特征向量,等价于 $A$ 的最小奇异值对应的右奇异向量。这就是 DLT(Direct Linear Transform)的全部内容——没有迭代,没有初值,一次 SVD 出结果。
注意:DLT 最小化的是代数误差 $|Ah|$,不是几何上的重投影距离。两者的量级依赖坐标数值,坐标越大,代数误差对远端点的偏袒越严重。这正是 2.3 要处理的坑。
N=4 这种“恰好”的情况有个隐患:只要四个点里有三个近似共线,$A$ 的秩就掉到 7 以下,零空间变成二维,SVD 返回的那个向量只是众多解里随便挑的一个,结果毫无意义。所以工程上没人真的只用四个点,实际是用几十上百组匹配点喂进去,让超定方程把噪声平均掉。
2.3 归一化:不写这三步,A 的条件数会翻车
像素坐标动辄几百上千,$A$ 的后三列含 $u \cdot x$ 这样的乘积,量级到 $10^6$。前两列却是 $10^0$ 量级。同一个矩阵里列的量级差六个数量级,条件数自然难看,常见能到 $10^8 \sim 10^{12}$。这时候 SVD 的数值精度会被吃掉一大截,解出来的 $h$ 在小数点后几位全是噪声,重投影误差看着还行,四个角却飞了。
Hartley 归一化的思路很朴素:把两组点各自搬到“平均位置在原点、平均距离为 $\sqrt{2}$”的坐标系里再解。具体三步——算质心、平移到原点;算各点到质心的平均距离 $d$;整体缩放 $\sqrt{2}/d$。这构成一个相似变换矩阵 $T$,对源点和目标点各做一个。
function [pn, T] = normalizePoints(p) c = mean(p, 1); % 质心 d = mean(sqrt(sum((p - c).^2, 2))); % 到质心的平均距离 s = sqrt(2) / d; % 缩放到平均距离为 sqrt(2) T = [s, 0, -s*c(1); 0, s, -s*c(2); 0, 0, 1]; pn = (T * [p, ones(size(p,1),1)].').'; pn = pn(:, 1:2) ./ pn(:, 3); % 齐次除法,还原成二维 end三个参数各司其职:c消掉平移,s消掉尺度,$\sqrt{2}$ 只是一个约定值(让归一化后的坐标量级落在 1 附近)。用pn1、pn2解出 $H_n$ 之后,还要反归一化回原坐标系:$H = T_2^{-1} H_n T_1$。这一步不能省,否则算出来的矩阵只能作用于归一化坐标。
实测中,同样一组点,归一化前后cond(A)通常从 $10^9$ 量级掉到 $10^1 \sim 10^2$ 量级,重投影误差能差一个数量级。这也解释了一个现象:手写 DLT 和工具箱函数结果对不上时,八成不是公式错了,而是少做了归一化。
3. MATLAB 实现:从 estimateGeometricTransform2D 到手写 DLT
3.1 三行调用背后的流水线
工具箱的入口很直接,但它内部做的事情比表面多。完整链路是:检测特征点 → 提描述子 → 匹配 → 几何估计(带 RANSAC)→ 返回变换对象和内点索引。
I1 = imread('view_left.png'); % 待变换的图(moving) I2 = imread('view_right.png'); % 参考图(fixed) g1 = rgb2gray(I1); g2 = rgb2gray(I2); pts1 = detectSURFFeatures(g1, 'MetricThreshold', 800); pts2 = detectSURFFeatures(g2, 'MetricThreshold', 800); [F1, V1] = extractFeatures(g1, pts1); [F2, V2] = extractFeatures(g2, pts2); % 匹配:MaxRatio 是比率测试阈值,Unique 保证一对一 idx = matchFeatures(F1, V2, 'MatchThreshold', 10, 'MaxRatio', 0.6, 'Unique', true); m1 = pts1(idx(:,1)); m2 = pts2(idx(:,2)); % 几何估计:RANSAC 采样 + DLT + 内点重估 [tform, inlierIdx] = estimateGeometricTransform2D(m1, m2, 'projective', ... 'MaxDistance', 3, 'Confidence', 99.9, 'MaxNumTrials', 5000); H_row = tform.T; % 注意:这是行向量约定下的矩阵 fprintf('内点数 %d / %d\n', nnz(inlierIdx), numel(inlierIdx));参数含义逐个说清:'projective'指定 8 自由度模型(仿射是'affine',6 自由度)。MaxDistance是内点判定的重投影距离阈值,单位就是像素,取 2~5 对中等分辨率图像比较稳。Confidence是“至少采到一组全内点样本”的概率,MaxNumTrials是采样次数上限,这两个共同决定 RANSAC 什么时候停。inlierIdx是逻辑向量,长度等于输入匹配数,后面做重估或者剔除误匹配都要用它。
新一些的版本把这个函数整合成了estgeotform2d,参数语义基本一致,返回的也是变换对象。老名字还能用,脚本迁移时不用急着改。
imwarp把变换落到实处:
Rfixed = imref2d(size(g2)); % 以参考图的尺寸为输出画布 Iwarp = imwarp(I1, tform, 'OutputView', Rfixed); % 把 I1 变换到 I2 的视角imwarp的语义是“把 moving 图按变换投到 fixed 图的空间”,所以tform必须是从I1到I2的方向。方向搞反的话,图像会被投到画面外,只剩一片黑——这是最常见的翻车现场之一。
3.2 手写 DLT:SVD 取零空间
工具箱好用,但要理解机制、或者需要自己控制归一化和内点重估,就得手写。下面这段是完整的归一化 DLT:
function H = dltHomography(p1, p2) % p1, p2: N×2 的对应点坐标,N >= 4 assert(size(p1,1) >= 4 && size(p1,1) == size(p2,1), ... '至少需要 4 组对应点,且两组点数必须相同'); [pn1, T1] = normalizePoints(p1); [pn2, T2] = normalizePoints(p2); N = size(pn1, 1); A = zeros(2*N, 9); for k = 1:N x = pn1(k,1); y = pn1(k,2); u = pn2(k,1); v = pn2(k,2); A(2*k-1, :) = [-x, -y, -1, 0, 0, 0, u*x, u*y, u]; A(2*k , :) = [ 0, 0, 0, -x, -y, -1, v*x, v*y, v]; end [~, ~, V] = svd(A); h = V(:, end); % 最小奇异值对应的右奇异向量 Hn = reshape(h, [3, 3]).'; % 按行展开,MATLAB 列主序需要转置 H = (T2 \ Hn) * T1; % 反归一化 H = H / H(3,3); % 尺度归一到 h33 = 1,便于比较 end几个细节值得单独拎出来。reshape(h, [3,3])的坑在于 MATLAB 是列主序:$h$ 的前三个元素被填进第一列,而我们的展开顺序是按行的,所以必须转置一次。写成reshape(h,3,3).'是等价且更紧凑的写法,用.'而不是',因为后者是共轭转置,虽然实数矩阵下没差别,但养成习惯能避免以后处理复数数据时莫名其妙出错。
反归一化那一行(T2 \ Hn) * T1里,\是左除,等价于inv(T2) * Hn,但数值上更稳。顺序不能反:源点先乘 $T_1$ 进归一化空间,目标点出归一化空间要乘 $T_2^{-1}$,一进一出必须夹住 $H_n$。
最后H / H(3,3)只是为了让矩阵可读、可比较。如果 $H(3,3)$ 的绝对值小于 1e-12,说明这个归一化不该做,直接跳过。
3.3 T 还是 T 转置:MATLAB 的行向量约定
这是最容易踩、又最难自查的坑。手写的 DLT 用的是列向量约定:$\mathbf{x}' \sim H\mathbf{x}$,即 $H$ 左乘。而 MATLAB 的projective2d/tform.T用的是行向量约定:$[x'\ y'\ w'] = [x\ y\ 1] \cdot T$。两者互为转置。
所以当你把手写的H和tform.T打印出来对比时,看到的是两个互为转置的矩阵,看起来“完全不一样”,但它们是同一个变换。构造对象时要显式转:
H = dltHomography(inlier1, inlier2); % 列向量约定 tform2 = projective2d(H.'); % 转成行向量约定再构造 err = mean(sqrt(sum((transformPointsForward(tform2, p1) - p2).^2, 2)));反过来,从tform.T拿去做数学推导,也要记得转回来。我的习惯是:所有自己写的函数一律用列向量约定,只在和工具箱交互的那一行做转置,并且在那行写注释说明。混着用必然出错,而且错得很隐蔽——矩阵数值都是对的,只是作用方向不对,结果图像看起来“好像变了又好像没变”。
4. 特征点与 RANSAC:H 的精度在拟合之前就被决定了
4.1 检测子与描述子的取舍:SURF、ORB、SIFT 在同一对图上的差别
DLT 只是求解器,它拿到的点质量决定了结果上限。工具箱里常用三种检测子,特性差异明显:
| 检测子 | 尺度不变 | 旋转不变 | 计算开销 | 适用场景 |
|---|---|---|---|---|
| SURF | 是 | 是 | 中 | 纹理中等、有尺度变化的平面 |
| ORB | 部分 | 是 | 低 | 实时、纹理丰富、无大幅尺度变化 |
| SIFT | 是 | 是 | 高 | 大尺度差异、模糊、光照变化 |
对单应性估计来说,“尺度不变”比“快”重要得多。两张图的拍摄距离不同,同一个纹理斑块在两张图上的尺度差可能到 2~3 倍,不具尺度不变性的检测子会在其中一张图上根本检不到这个点,匹配数看着不少,但分布极度不均匀——全挤在图像一角。点分布不均匀时,DLT 对小区域的拟合很好,对远处的外推完全失控,这就是 5.1 那个现象的成因之一。
MetricThreshold是 SURF 的检测阈值,调大检测点变少但更显著,调小则点多而杂。平面场景纹理一般时,800~1500 是个可以起步的范围;纹理极弱(比如纯色墙面)时,任何检测子都救不了,得换思路,比如手动标点或者用模板匹配。
4.2 比率测试与互查:把外点挡在 DLT 之前
matchFeatures的核心是最近邻距离比:对每个描述子找最近和次近两个候选,若最近距离 / 次近距离 <MaxRatio,才认为这个匹配可信。原理很直白——正确匹配的最近邻应该显著优于次近邻,如果两者差不多近,说明这个描述子没有区分度,多半是重复纹理(地砖、窗户、文字)导致的误匹配。
MaxRatio取 0.6~0.8,越小越严格。0.6 通常能砍掉大量重复纹理误匹配,代价是匹配总数下降。如果一开始匹配数就只有二三十个,把阈值压到 0.6 可能只剩个位数,反而不够 RANSAC 用,这时候宁可放宽到 0.75,让 RANSAC 去筛。
'Unique', true保证一对一:一个特征点只能被匹配一次。不开这个选项时,可能出现多个源点映到同一个目标点,这种多对一的退化样本对 DLT 没有增益,还会拉偏结果。
还有一种更严格的做法是互查(mutual check):正向匹配一次,反向再匹配一次,只保留双向一致的。matchFeatures本身不直接提供这个开关,需要手动做两次再取交集。代价是多一次匹配的开销,收益是在重复纹理场景下误匹配率显著下降。纹理规整的平面(建筑立面、棋盘格地板)值得加这一步。
4.3 RANSAC 的三个参数:内点阈值、置信度、采样次数
RANSAC 的逻辑是:每次随机抽 4 组点算一个 $H$,统计有多少点在阈值内,迭代到足够可信为止,最后用全部内点重算一次。三个参数互相牵制。
内点阈值MaxDistance直接对应重投影误差的容忍度。设太小,正确的点因为亚像素误差被判成外点,内点数不够;设太大,错误匹配被放进来,$H$ 被拉偏。经验上取 2~4 像素,图像分辨率高时相应放大。
采样次数和置信度的关系是算出来的,不是拍脑袋。若内点比例是 $w$,每次采样需要 4 个点,那么一次采样全为内点的概率是 $w^4$,$N$ 次采样至少成功一次的概率是 $1-(1-w^4)^N$。令其等于设定的置信度 $p$,解出:
$$ N = \frac{\log(1-p)}{\log(1-w^4)} $$
代入 $w=0.3$、$p=0.99$:$w^4 \approx 0.0081$,$N \approx 4.605 / 0.00813 \approx 566$。也就是说三成内点的情况下,要跑五百多次采样才有 99% 的把握拿到一组干净样本。这就是MaxNumTrials默认值往往偏小、需要手动调大的原因——外点比例高时,默认的几百次不够,RANSAC 会提前退出,返回一个“自认为最优”的错解。
实践中的做法是:先打印内点比例nnz(inlierIdx)/numel(inlierIdx),如果低于 0.2,说明匹配环节就已经崩了,调 RANSAC 参数是治标;上了 0.5 之后,用上面的公式算一下 $N$,设成算出来值的 1.5~2 倍留余量。
5. 避坑与排查:H 算歪的五个现场
5.1 现象:整体重投影误差很小,但图像四角飞了
原因几乎总是点分布退化。所有内点集中在画面的一小块区域,或者近似落在一条线上。前者导致外推区域完全没约束,后者让 $A$ 的秩掉到 7,零空间不唯一,SVD 返回的解是任意的。判断方法是算内点坐标的协方差矩阵,看最小特征值;或者直接把内点画在图上,一眼就能看出是不是挤成一坨。
解决思路有三层:一是提高MetricThreshold让检测子去找更分散的显著点;二是在匹配之后按网格做空间均匀采样,强制每个格子里的点数不超过上限;三是对解出来的 $H$ 做合理性检查,把图像四角用 $H$ 变换一遍,看是否落在参考图范围的一到两倍内,超出就判为失败。第三层是后悔药,前两层才是根治。
5.2 现象:两张图分辨率差一倍,H 的平移项大得离谱
原因不在算法,在坐标尺度。$H$ 的平移分量 $h_{13}, h_{23}$ 的量级和像素坐标直接相关,图放大一倍,这两个值也翻倍,本身不说明任何问题。真正的问题是有人忘了做归一化,或者把imref2d的坐标系搞混了。
排查方法是拿一个已知点代进去验算:取源图中心点,transformPointsForward,看落点是否在肉眼判断的对应位置上。如果落点偏差是“成比例地偏”,基本就是尺度问题;如果偏得毫无规律,那是求解本身出了错。另外,imref2d的默认空间坐标是像素中心为整数,和某些外部工具(把左上角像素中心当 0.5)差半个像素,做亚像素级对齐时要显式设置XWorldLimits/YWorldLimits。
5.3 现象:自己算的 H 和 tform.T 数值完全对不上
先别怀疑公式,看是不是转置。如 3.3 所述,工具箱用行向量约定,手写代码用列向量约定,两者互为转置。验证方式很简单:对一个随机点做变换,两种方式结果一致就说明只是约定差异。不一致才需要往下查。
如果真的不一致,按这个顺序排查:reshape是不是漏了转置;反归一化是不是写成了 $T_1^{-1} H_n T_2$(顺序反了);normalizePoints里齐次除法有没有做。这三个是手写 DLT 出错的高频位置,基本能覆盖九成问题。
5.4 现象:同一对图,跑两次结果不一样
RANSAC 是随机采样,结果天然带随机性,尤其在内点比例不高、MaxNumTrials又设得小的时候,两次运行可能选出不同的内点集,$H$ 就差出来了。这不是 bug。
但如果差异大到肉眼可见,说明内点集本身不稳定,问题回到 4.3:调大MaxNumTrials和Confidence,或者先降外点比例再跑。还有个办法是固定随机种子(rng(0))做可复现的调试,定位问题后再放开。生产代码里不要固定种子——固定种子只是掩盖了采样不足的事实。
5.5 现象:点在图上对得很准,warp 之后大片黑边或被裁掉
这是输出视图没设对,不是 $H$ 错了。imwarp默认按变换后的实际范围输出,图像可能整体漂到画布外。做法是显式传'OutputView', imref2d(size(refImage)),以参考图为基准画布;或者先用imwarp变换四角算出新范围,再构造imref2d覆盖全部内容。
黑边的另一半原因是重采样方式。imwarp默认线性插值,变换后落在原图范围外的像素填 0,看起来是黑的。想避免,可以指定'FillValues', NaN再自行合成,或者干脆用vision.AlphaBlender做两张图的重叠融合,把黑边区域按权重过渡掉。
6. 代数误差到几何误差:精修与验证的取舍
DLT 最小化的是 $|Ah|$,它和真正的重投影距离不是一回事。代数误差对坐标数值敏感,同一组点在放大十倍的坐标系下,代数误差的最优解会偏向远处的点;几何误差则不受尺度影响。这种偏差在噪声小、点分布均匀时几乎看不出来,但在点少、外点多、分布不均的场景下,能差出好几个像素。
标准做法是两步走:DLT 给初值,再用几何误差做非线性精修。目标函数用对称转移误差最稳妥——既算正向 $d(\mathbf{x}', H\mathbf{x})$,也算反向 $d(\mathbf{x}, H^{-1}\mathbf{x}')$,避免单向误差把误差全推给一侧。MATLAB 里可以直接丢给lsqnonlin,初值就是 DLT 的解,或者用工具箱返回的tform作初值。
代价要说清楚:精修能压低重投影误差,但它压的是“内点上的误差”。如果内点集本身选错了(比如全是重复纹理造成的误匹配,但几何上又自洽),精修会让错解更加自洽,误差指标好看,实际完全错位。所以验证不能只看误差数字,得看三件事:内点比例、内点空间分布、四角变换后的落点是否合理。三个都过了,误差才有意义。
另一个取舍是精度和鲁棒性的平衡。MaxDistance调小、MaxRatio调严,精度上去了但可能点不够;放宽则点数多但噪声大。我一般的做法是把这条链路拆成“宽进严出”:匹配阶段放宽一点,保证 RANSAC 有足够的候选;求解阶段收紧阈值,用内点比例和四角检查做最后把关。这样两头都不至于卡死。
从那以后我每次算完 $H$,都强制走一遍四角检查加内点分布可视化,哪怕误差数字只有 0.3 像素也不跳过。因为翻车从来不是因为误差大,而是因为误差小得很有说服力。希望帮到你。
本文还有配套的精品资源,点击获取