1. 从单应矩阵的物理意义说起为什么需要分解它单应矩阵Homography在视觉SLAM和计算机视觉里出现的频率极高但很多人第一次接触它时只知道两张平面图像之间的映射可以用一个3x3矩阵表示却说不清楚这个矩阵到底藏了哪些信息。我在带新人的时候经常遇到一个典型场景有人用cv::findHomography算出了矩阵然后直接拿它做透视变换任务就算完成了。可一旦被问到这个矩阵能不能告诉我相机转了多少度、平移了多少就答不上来了。这就是cv::decomposeHomographyMat存在的意义。它做的事情是把一个已经算出来的单应矩阵拆解成旋转矩阵、平移向量、平面法向量这几组物理量。换句话说单应矩阵本身是一个混合体它同时编码了相机的运动旋转平移和场景平面的朝向法向量而分解函数就是把这个混合体还原成可解释的几何参数。要理解这件事得先回到单应矩阵的定义。假设空间中有一个平面它在相机坐标系下的方程为n^T · X d其中n是平面法向量d是相机原点到平面的距离。同一个平面上的点在两个不同相机位姿下投影到各自图像平面两个像素坐标之间就存在一个单应变换关系H R (t · n^T) / d这个公式是整个分解问题的核心。可以看到H由旋转R、平移t、法向量n和距离d共同决定。注意这里有个关键点t和d是耦合的也就是说你无法从H中单独恢复出真实的平移量和真实的平面距离只能恢复出它们的比值。这是单应分解的一个固有尺度模糊后面讲源码时会反复看到这个问题的处理方式。那为什么SLAM里需要这个分解我总结下来主要有三个用途。第一是平面初始化在基于平面的SLAM系统中比如一些AR应用需要知道平面在相机坐标系下的朝向才能把虚拟物体正确地贴在平面上。第二是运动估计的交叉验证当你用特征点法算出了本质矩阵又用平面点算出了单应矩阵两者的分解结果可以互相印证提高鲁棒性。第三是纯旋转检测当相机只旋转不平移时单应矩阵退化成旋转矩阵这个特性在 panoramio 拼接和全景应用中非常有用。提示单应分解得到的解通常不止一组这是由几何本身的多解性决定的不是算法缺陷。理解这一点才不会在拿到四组解时感到困惑。2. 分解的数学原理从H到R、t、n的推导链路2.1 为什么会有四组解在深入源码之前必须先把数学上的多解性讲清楚否则看代码时会觉得为什么平白无故要返回四个结果。前面给出的公式H R (t · n^T) / d如果我们把H乘以任意非零尺度因子等式依然成立因为单应矩阵本身就是在齐次坐标下定义的尺度不确定。所以第一步要做的是归一化通常用H的某个元素或者奇异值来定尺度。归一化之后问题转化为已知H求R、t、n。数学上可以证明满足这个方程的(R, t, n)组合一般有四组。这四组的来源是平面法向量n可以指向平面的两侧正负号同时平移t的方向也可以有两种选择组合起来就是2×24种。这四组解在代数上都满足方程但只有一组在物理上是正确的——也就是能让所有重建点都落在相机前方的那个解。我在实际项目里验证过一个经验四组解中通常有两组会导致重建点跑到相机后面直接通过点在相机前方这个约束就能筛掉。剩下两组则需要额外的先验信息比如已知平面大致朝向或者用更多帧的数据来投票。2.2 奇异值分解在其中的角色cv::decomposeHomographyMat的数学核心是奇异值分解SVD。具体做法是对归一化后的H做SVD得到H U · Σ · V^T。由于H是由旋转和秩为1的平移-法向量外积组成的它的奇异值有特殊结构。理论上归一化后的H的奇异值应该满足σ1 σ2 σ3的某种关系在无噪声理想情况下中间奇异值和两端奇异值有特定比例。源码里正是利用这个结构来构造中间矩阵再通过一系列旋转矩阵的构造和符号组合枚举出四组候选解。这里有个细节值得注意SVD的符号不确定性会直接影响最终解的符号所以源码里对U和V的符号做了显式处理确保构造出的旋转矩阵行列式为1即真正的旋转而非反射。2.3 平移与距离的尺度耦合前面提到的尺度模糊在数学上表现为你只能求出t/d这个组合无法分开。源码返回的平移向量实际上是归一化后的平移它的模长并不对应真实的物理平移距离。这一点在实际使用时极其重要——如果你拿这个平移向量去算真实的米数结果一定是错的。我见过有人用分解出的平移向量做尺度估计然后发现结果和实际差了一个数量级排查半天才发现是这个问题。正确的做法是要么用外部尺度信息比如已知平面上的实际尺寸来恢复尺度要么就只用平移的方向不用它的大小。输出量是否含尺度模糊实际可用信息旋转矩阵 R无模糊完整的旋转姿态平移向量 t含模糊与d耦合仅方向可靠平面法向量 n无模糊完整的平面朝向距离 d无法单独恢复需外部尺度3. 源码逐层拆解decomposeHomographyMat到底做了什么3.1 函数签名与输入输出约定先看函数原型以OpenCV的接口为例int cv::decomposeHomographyMat( InputArray H, InputArray K, OutputArrayOfArrays rotations, OutputArrayOfArrays translations, OutputArrayOfArrays normals );这里有几个容易踩坑的点。第一H必须是归一化的如果你传入的是原始的单应矩阵函数内部会先做归一化但归一化的方式可能和你的预期不一致建议自己先处理好。第二K是相机内参矩阵这个参数绝对不能省因为分解是在归一化相机坐标系下进行的需要先用K把H转换过去。第三返回值是解的个数通常是4但在退化情况下可能更少。我特别想强调K的作用。很多人以为单应分解和相机内参无关这是错的。H是在像素坐标系下的而R、t、n是在相机坐标系下的两者之间差了一个K的变换H_normalized K^{-1} · H · K。源码里这一步是隐式完成的但如果你自己实现分解这一步漏掉的话结果会完全错误。3.2 归一化与SVD分解的具体实现源码的第一步是计算H的范数并归一化。这里用的不是简单的除以某个元素而是用Frobenius范数或者最大奇异值来归一化目的是让后续的SVD数值更稳定。归一化之后对H做SVDcv::SVD::compute(H_normalized, w, u, vt);得到的奇异值w理论上应该满足某种比例关系。源码里会检查这些奇异值是否满足分解的前提条件如果不满足比如奇异值比例异常说明输入的H可能不是来自一个真实的平面运动这时候分解结果就不可信。接下来是构造中间变量。源码里定义了几个关键的中间矩阵利用U和V的列向量来构造。这部分是整个函数最数学的地方涉及大量的矩阵乘法和符号判断。我的建议是不要试图逐行读懂每一个矩阵乘法而是抓住它的逻辑主线——先构造出候选的旋转矩阵再用旋转矩阵反推平移和法向量。3.3 四组解的枚举与符号处理源码里有一个循环枚举四种符号组合。对于每一种组合构造出一个候选的旋转矩阵R然后计算对应的平移t和法向量n。这里的关键是保证R是正交且行列式为1源码里通过显式的符号修正来做到这一点。我实测过如果不做这个符号修正得到的旋转矩阵可能是反射矩阵用它去变换点会得到镜像的结果非常隐蔽。这也是为什么自己手写分解时容易出错的地方——数学推导看起来对但符号处理一疏忽就全错了。四组解生成后源码会做一个可见性检查对于每一组解检查重建出的平面点是否在相机前方。这个检查是通过计算平面到相机的距离符号来完成的。只有距离为正点在相机前方的解才会被保留。这就是为什么有时候返回的解少于4个。注意可见性检查依赖输入的H对应的点确实在相机前方。如果H是从一组虚拟对应点算出来的这个检查可能失效。3.4 返回值的组织方式函数通过OutputArrayOfArrays返回三个数组的数组分别对应rotations、translations、normals。每个数组里有N个元素N是解的个数。使用的时候需要这样取std::vectorcv::Mat rotations, translations, normals; cv::decomposeHomographyMat(H, K, rotations, translations, normals); // rotations[0], translations[0], normals[0] 是第一组解这里有个坑返回的平移向量是归一化的前面已经强调过。另外返回的法向量也是单位向量但它的正负号对应平面的两侧需要根据实际场景判断哪一侧是正面。4. 实战中的坑与验证方法怎么确认分解结果是对的4.1 用合成数据做闭环验证我最推荐的验证方法是合成数据闭环测试。具体做法是自己设定一组已知的R、t、n、d构造出H然后调用分解函数看返回的四组解里是否包含你设定的那一组。这个测试能帮你确认函数的基本正确性也能让你直观感受多解性。构造H的代码大概是这样cv::Mat R ...; // 已知旋转 cv::Mat t ...; // 已知平移 cv::Mat n ...; // 已知法向量 double d ...; // 已知距离 cv::Mat H R t * n.t() / d;然后分解对比结果。注意对比的时候要考虑尺度模糊平移向量只能比方向不能比模长。我一般用夹角来衡量两个平移向量的接近程度夹角小于1度就算匹配。4.2 真实数据下的筛选策略真实数据没有真值怎么选正确的解我总结了一套实用的筛选流程可见性筛选先用函数自带的可见性检查这一步能筛掉大约一半的解。法向量一致性如果场景中平面的朝向大致已知比如桌面是水平的用法向量和已知朝向的夹角来筛选。时序连续性在视频序列中相邻帧的分解结果应该变化平缓用上一帧的结果来约束当前帧的选择。重投影误差把每组解代回去计算重投影误差选误差最小的。这套流程我在多个项目里用过实测下来在纹理丰富的平面上非常稳定。但如果平面纹理稀疏或者H本身估计得不准筛选就会变得困难。4.3 数值不稳定时的处理单应分解在以下几种情况下会数值不稳定平面接近退化比如法向量几乎平行于光轴、H的条件数很大、相机内参估计不准。遇到这些情况我的经验是不要硬分解而是退回到更鲁棒的方法比如用多帧的PnP或者直接做BA优化。还有一个细节OpenCV的分解函数对H的尺度很敏感。如果你传入的H数值范围很大比如元素值在1e6量级SVD的精度会下降。建议先做归一化把H的元素缩放到1附近。问题现象可能原因处理方式返回解少于4个可见性检查筛掉了检查H对应的点是否在相机前方旋转矩阵行列式不为1符号处理问题检查输入H是否归一化平移方向明显错误尺度耦合误解只信方向不信模长结果抖动大H估计噪声大多帧平滑或改用BA5. 从分解结果反推应用平面AR与运动估计的落地5.1 平面法向量的直接应用分解出的法向量n最直接的用途是平面AR。假设你要在桌面上放一个虚拟物体需要知道桌面的朝向才能让物体的底面贴合桌面。n就是桌面在相机坐标系下的法向量有了它构造一个旋转矩阵把物体的局部坐标系对齐到桌面就能实现正确的贴合。具体做法是以n为z轴构造一组正交基得到从物体坐标系到相机坐标系的旋转。这个旋转再和相机的位姿组合就能把虚拟物体渲染到正确的位置和朝向。我在做这类应用时发现法向量的符号需要特别注意——n和-n都满足数学方程但只有一个对应桌面的上表面。5.2 旋转矩阵用于姿态初始化分解出的旋转R可以直接作为相机姿态的初始估计。在SLAM的初始化阶段如果场景中有一个明显的平面用单应分解得到的R比用本质矩阵分解更稳定因为平面点提供了更强的几何约束。不过要注意这个R是相对于第一帧的旋转不是绝对姿态。在视觉惯性系统中它可以和IMU的预积分结果做对齐快速完成初始化。5.3 平移方向的辅助判断虽然平移的模长不可靠但方向是可靠的。在纯平移场景中平移方向就是相机的运动方向。这个信息可以用来做运动分割比如区分相机是在前进还是侧移。我试过用平移方向来辅助视觉里程计的外点剔除效果不错——如果某组特征点的运动方向和整体平移方向严重不符很可能是动态物体。5.4 与本质矩阵分解的对比单应分解和本质矩阵分解是两条并行的路线。本质矩阵分解得到R和t同样有尺度模糊但它不依赖平面假设适用于一般场景。单应分解依赖平面假设但在平面场景下更稳定、更精确。我的选择策略是如果场景中平面占比超过60%优先用单应分解否则用本质矩阵分解。两者都算出来做交叉验证是提高鲁棒性的好办法。6. 自己动手实现一个简化版分解6.1 核心步骤的代码骨架如果你想真正吃透这个函数我强烈建议自己实现一个简化版。核心步骤不多// 1. 归一化H cv::Mat H_norm H / cv::norm(H); // 2. SVD分解 cv::Mat w, u, vt; cv::SVD::compute(H_norm, w, u, vt); // 3. 构造中间变量这里省略具体公式 // 4. 枚举四组解 // 5. 可见性检查关键难点在第3步的公式推导和第4步的符号处理。我的建议是先从论文里的公式出发一步步推导然后用合成数据验证每一步的中间结果。6.2 验证每一步的中间结果自己实现时最容易出错的是中间矩阵的构造。我的做法是每算出一个中间量就和OpenCV的结果对比。比如SVD得到的U和V虽然符号可能不同但奇异值应该一致。通过这种逐步对比能快速定位错误。还有一个技巧用单位旋转RI和简单平移t[1,0,0]构造H这时候分解结果应该很容易验证。从简单情况入手逐步增加复杂度比一上来就用真实数据调试高效得多。6.3 性能与精度的权衡自己实现的版本在精度上通常不如OpenCV因为OpenCV做了很多数值优化。但自己实现的价值在于理解。我在实际项目中除非有特殊需求比如要嵌入到不支持OpenCV的平台否则还是直接用OpenCV的函数。自己实现主要用于教学和调试。如果确实需要自己实现注意用双精度double而不是单精度float单精度在SVD这一步的误差会明显放大。另外矩阵乘法的顺序也会影响数值稳定性尽量把小的矩阵先乘。7. 几个容易被忽略的边界情况7.1 纯旋转退化当相机只旋转不平移时t0单应矩阵退化为HR。这时候分解函数依然能工作但返回的平移向量是零向量法向量则完全不确定因为平面信息丢失了。这种情况下不要试图从分解结果中提取平面信息它不存在。7.2 平面过相机中心如果平面恰好经过相机光心d0公式H R t·n^T/d会出现除零。实际数据中d不会精确为零但会非常小导致数值不稳定。遇到这种情况分解结果基本不可用需要换方法。7.3 内参矩阵的错误传播K的误差会直接传播到分解结果。如果K的焦距估计偏了5%分解出的法向量可能偏好几度。在标定不准的情况下建议先做一次内参标定或者用自标定方法在线优化K。我在一个项目里遇到过因为K的错误导致法向量完全反向的情况排查了很久才发现是标定参数的问题。从那以后我养成了一个习惯每次用分解函数前先检查K的合理性比如焦距是否在合理范围内主点是否接近图像中心。7.4 多平面场景的误用单应分解的前提是单一平面。如果场景中有多个不同朝向的平面用一个H去分解得到的结果没有物理意义。这种情况下应该先用分割方法把不同平面的点分开对每个平面单独算H再分解。判断场景是否适合单应分解我的经验是看H的重投影误差分布。如果误差在某些区域明显偏大说明这些区域可能不属于同一个平面。8. 我在实际项目中的几点体会踩过几次坑之后我对单应分解的使用形成了几个固定习惯。第一永远不单独使用分解结果一定要和别的信息交叉验证比如IMU、轮速计或者多帧的BA结果。第二平移向量只用方向模长从来不直接使用需要尺度就从外部引入。第三法向量的符号要显式判断不能默认取正。还有一个很实用的技巧如果场景中平面的朝向在短时间内不变比如相机对着墙面移动可以把多帧分解出的法向量做平均能显著降低噪声。我试过在纹理较弱的墙面上单帧法向量抖动能有十几度平均五帧之后降到两三度效果立竿见影。最后说一个调试技巧。当你怀疑分解结果不对时把四组解都画出来——在图像上画出每组解对应的平面法向量方向以及重建出的平面点。视觉化之后哪组解是合理的往往一目了然。这个方法帮我省下了大量盯着数字发呆的时间。