增量式SFM自由网平差:消除7自由度奇异性,提升三维重建内部一致性
2026/8/7 7:46:48 网站建设 项目流程

1. 从“自由网”说起:为什么增量式SFM需要它

如果你做过三维重建,尤其是基于图像序列的增量式运动恢复结构,那你一定遇到过这个经典困境:随着新图像不断加入,整个场景的稀疏点云和相机姿态会开始“漂移”。这种漂移不是某个点的误差,而是整个重建的坐标系在缓慢地扭曲、缩放甚至旋转,就像一艘没有锚的船在海上随波逐流。我们辛辛苦苦跑出来的重建结果,可能因为一个微小的累积误差,导致最后生成的模型和真实世界对不上,或者相邻的模型块无法无缝拼接。

这个问题的根源,在于增量式SFM(Structure from Motion)本质上是一个“从无到有”的过程。我们通常从两张图像开始,通过特征匹配和五点法或八点法计算出初始的相机相对姿态和一组三维点。然后,我们不断地“注册”新图像:找到新图像与已有三维点的对应关系,通过PnP(Perspective-n-Point)求解新相机的姿态,再通过三角化生成新的三维点。这个过程就像搭积木,每一块新积木都依赖于之前已经搭好的部分。

然而,这里有一个关键假设:我们求解每一块“积木”时,都认为之前的部分是绝对正确的。但事实上,之前的求解本身就带有误差。这些误差会随着增量过程不断累积和传播。更重要的是,在整个过程中,我们缺乏一个全局的、固定的参考系。初始的两张图像定义了一个任意的局部坐标系,后续所有计算都基于这个“浮动”的坐标系。没有外部的绝对约束(比如已知的GPS坐标、已知尺寸的标定物),这个坐标系本身就可以发生任意的刚体变换(旋转、平移、缩放)而不影响图像投影的重投影误差。这就是所谓的“尺度模糊性”和“坐标系自由度”。

“自由网平差”要解决的,正是这个“浮动”坐标系的问题。它不是引入外部先验信息去“锚定”坐标系,而是承认并处理这种自由度。其核心思想是:在优化过程中,允许整个网络(所有相机和三维点)作为一个整体进行平移、旋转和缩放,但同时通过施加一组最小约束,来消除这些自由度带来的数值不稳定问题,从而得到一个在“最小二乘”意义下最优的、内部一致的重建结果。这个结果虽然仍然缺少绝对的尺度和朝向,但其内部几何关系是最准确的,为后续的绝对定向、模型融合或纹理映射提供了可靠的基础。

所以,当我们谈论“无先验约束下的增量式SFM自由网平差”时,我们讨论的是一套在只有图像数据本身的情况下,如何让增量式重建结果在数学上更严谨、更稳定、内部一致性更高的核心技术。它不是重建流程的替代,而是其精度保障和理论完备性的关键一环。

2. 拆解最小二乘:SFM优化的数学心脏

要理解自由网平差,必须先透彻理解驱动整个SFM的引擎:非线性最小二乘优化。在SFM中,我们最终要优化的目标函数,几乎总是重投影误差的平方和

假设我们有m个相机(姿态),n个三维点。对于第i个相机观察到的第j个三维点,其在图像上的观测像素坐标为u_ij。我们用相机模型(比如常用的针孔模型加径向畸变)将三维点X_j投影到第i个相机的图像平面上,得到预测的像素坐标π(R_i, t_i, X_j),其中R_it_i是第i个相机的旋转和平移。

那么,重投影误差就是观测值与预测值之差:e_ij = u_ij - π(R_i, t_i, X_j)

SFM的Bundle Adjustment(BA)问题,就是寻找所有相机参数{R_i, t_i}和所有三维点{X_j},使得所有观测到的重投影误差的平方和最小:

min Σ_ij || e_ij ||^2

这是一个典型的非线性最小二乘问题,因为投影函数π是非线性的。我们通常使用迭代优化算法来解决它,最主流的就是列文伯格-马夸尔特(Levenberg-Marquardt, LM)算法。LM算法可以看作是高斯-牛顿法和最速下降法的结合,通过引入一个阻尼因子来动态调整步长,在远离最优解时像最速下降法一样稳定,在接近最优解时像高斯-牛顿法一样快速收敛。

LM算法的核心是求解一个线性方程组(正规方程):

(J^T J + λ I) δ = -J^T e

这里,J是整个问题关于所有优化变量(所有相机参数和三维点坐标)的雅可比矩阵,e是所有残差向量堆叠而成,δ是我们要求解的增量更新,λ是阻尼因子。

这个雅可比矩阵J的规模非常庞大,但它的结构具有鲜明的特点:它是稀疏的。因为一个三维点通常只被少数几个相机看到,一个相机也只看到一部分三维点。所以,J可以按相机块和点块进行分块。对应地,信息矩阵H = J^T J也是一个稀疏的块矩阵。这种稀疏性是BA能够处理成千上万个相机和数百万个点的关键。

然而,这个信息矩阵H有一个致命的问题:它是奇异的(不可逆的)。原因就是我们开篇提到的自由度问题。整个系统有7个不可观的自由度(在相似变换下不变):

  1. 整体平移(3个自由度)
  2. 整体旋转(3个自由度)
  3. 整体尺度(1个自由度)

这意味着,如果我们对所有的相机和点同时施加一个相同的旋转、平移或缩放,重投影误差不会发生任何改变。从数学上讲,这导致信息矩阵H的零空间维度为7。在求解H δ = -b(忽略阻尼项简化表示)时,由于H奇异,解δ要么不存在,要么不唯一。直接求解会导致数值计算不稳定,迭代更新会朝着无穷大的方向发散,优化失败。

这就是为什么我们不能直接对原始的BA问题进行优化,而必须引入某种形式的约束,来“钉住”这个浮动的网络,消除奇异性。自由网平差,就是施加约束的一种优雅方式。

3. 自由网平差的约束之道:消除奇异性而不引入偏差

既然问题的根源是7个自由度的奇异性,那么最直观的想法就是施加7个约束,把这艘“船”固定住。但如何施加约束,却大有讲究。错误或过强的约束会扭曲优化结果,引入人为的偏差。

自由网平差的核心在于施加最小约束,即恰好能消除7个自由度奇异性所需的最少、最弱的约束。这些约束不应该改变问题在可观测方向上的最优解,只应该消除不可观测方向上的不确定性。常用的方法有以下几种,我们需要深入理解其原理和实现细节:

3.1 固定参数法:简单粗暴但需谨慎

这是最直接的方法:在优化过程中,固定某几个特定的参数不动。例如:

  • 固定第一个相机的旋转矩阵为单位矩阵,平移向量为零向量。这消除了6个自由度(3旋转+3平移)。
  • 固定一个三维点的深度,或者固定某两个三维点之间的距离为1。这消除了第7个尺度自由度。

实现方式:在构建雅可比矩阵J和信息矩阵H时,直接将与这些固定参数对应的行和列移除。等价于在LM算法的增量方程中,将这些参数的增量δ强制设为0。

为什么需要谨慎?

  1. 选择依赖性:结果依赖于你选择固定哪个相机、哪个点。如果恰好选中的那个相机姿态本身估计误差很大,或者选中的那个三维点是个外点,那么以这个“错误”的基准去约束整个网络,会把误差传播给所有其他变量。
  2. 破坏对称性:这种方法人为地赋予了一个相机或点特殊的“基准”地位,这在数学上并不优雅,也可能在后续处理中带来不便。

在实际的增量式SFM中,固定初始两个相机是一种常见做法,因为它简单且为重建提供了一个初始的基准坐标系。但在进行全局BA(自由网平差)时,我们通常希望采用更“公平”、对结果影响更小的方法。

3.2 伪逆法:数学严谨的通用解

从线性代数的角度看,对于奇异系统H δ = -b,虽然经典逆不存在,但我们可以求其摩尔-彭罗斯伪逆H⁺。伪逆给出的解δ = -H⁺ b是所有可能解中范数最小的那个(最小范数解)。

数学原理:奇异值分解(SVD)是求解伪逆的稳定方法。对H进行SVD:H = U Σ V^T,其中Σ是对角矩阵,包含奇异值。由于H的秩亏为7,那么Σ中会有7个零(或接近零,由于数值误差)的奇异值。伪逆H⁺ = V Σ⁺ U^T,其中Σ⁺是将Σ中非零奇异值取倒数,零奇异值保持不变(或设为一个极小的阈值)。

这个最小范数解在数学上是良好的,但它有一个潜在的缺点:它给出的更新量δ可能并不是我们“期望”的方向。在BA迭代中,我们更关心的是降低重投影误差,而不是更新量的范数最小。不过,在LM算法中,由于阻尼项λI的存在,即使H奇异,(H + λI)也可能是可逆的,此时LM算法实际上隐式地给出了一个正则化解。

实操注意:对于大规模的BA问题,对完整的H矩阵进行SVD计算代价极高,通常不可行。因此,伪逆法更多是一种理论指导,在实际大规模优化中,我们采用下面这种等价但更高效的方法。

3.3 添加先验约束法:软化固定,更鲁棒

这是一种更灵活、更鲁棒的方法,可以看作是固定参数法的“软化”版本。我们不强行将某些参数固定为常数,而是为它们添加一个很弱的先验约束。

具体做法是,在原有的BA代价函数后面,增加一个惩罚项:min Σ_ij || e_ij ||^2 + Σ_k w_k || c_k - c_k0 ||^2其中,c_k是我们要约束的参数(例如,第一个相机的平移向量),c_k0是我们希望它靠近的值(例如,零向量),w_k是一个很小的权重(例如,1e-6)。

为什么有效?这个额外的惩罚项相当于在信息矩阵H的对角线对应位置,加上了一个很小的值w_k。这直接解决了H的奇异性问题,因为现在(H + diag(...))是正定的。同时,由于权重w_k非常小,只要优化本身有明确的趋势,这个弱约束几乎不会影响最终结果的方向,它仅仅起到了“稳定器”的作用,防止优化在不可观的方向上乱跑。

如何选择约束参数?通常,我们选择7个参数来对应7个自由度。一个稳健的策略是:

  • 约束所有相机平移的质心为零。这需要3个约束,对应整体平移。具体来说,添加惩罚项w * || (Σ_i t_i) / m ||^2,其中m是相机数量。这比固定单个相机的平移更“公平”。
  • 约束所有相机旋转的“平均旋转”为单位矩阵。这需要3个约束,对应整体旋转。实现上更复杂一些,可以通过约束所有相机旋转矩阵的李代数之和为零来近似。
  • 约束所有三维点质心到原点的平均距离为1。这需要1个约束,对应整体尺度。例如,添加惩罚项w * || (Σ_j ||X_j||) / n - 1 ||^2

这种方法在流行的BA库(如ceres-solver, g2o)中很容易实现,只需在构建残差块时,额外添加这些先验残差块即可。它是工程实践中最常用、最推荐的方法。

3.4 舒尔补消元与边缘化:规模缩减与自由度的关系

在大型BA中,为了加速求解,我们经常使用舒尔补技巧来消去三维点参数,只求解相机参数。这是因为点的数量远多于相机,且每个点只被少数相机看到,消去点参数可以极大减小方程规模。

过程如下:

  1. 将参数分为相机块δ_c和点块δ_p,将正规方程写为分块形式:
    [ H_cc H_cp ] [ δ_c ] = [ b_c ] [ H_pc H_pp ] [ δ_p ] [ b_p ]
    其中H_pp是块对角矩阵,很容易求逆。
  2. 通过高斯消元(舒尔补),消去δ_p,得到关于相机参数δ_c的约化方程:(H_cc - H_cp * H_pp^{-1} * H_pc) δ_c = b_c - H_cp * H_pp^{-1} * b_pS = H_cc - H_cp * H_pp^{-1} * H_pc,这个S矩阵称为舒尔补矩阵。
  3. 求解约化方程得到δ_c,再回代得到δ_p

这里的关键是:约化后的舒尔补矩阵S继承了原信息矩阵H的奇异性。也就是说,即使消去了点参数,相机参数系统仍然有7个自由度的奇异性。因此,自由网平差的约束必须施加在相机参数空间,或者施加在消元后系统的层面上。通常,我们在构建最终的S矩阵并求解δ_c之前,采用“添加先验约束法”来处理S的奇异性。

注意:在增量式SFM中,我们经常使用“滑动窗口”BA来平衡精度和效率。当窗口滑动,有旧的相机和点被边缘化出去时,这个过程会将它们的约束信息以先验的形式保留在剩下的参数中。此时,自由网平差的约束需要施加在当前活跃的优化窗口上,同时要考虑边缘化带来的先验信息可能已经部分约束了某些自由度,需要仔细分析当前系统的零空间。

4. 融入增量流程:何时、何地、如何执行平差

理解了自由网平差的原理,接下来就要把它嵌入到增量式SFM的流水线中。这不是一个一次性步骤,而是一个需要精心设计触发时机和策略的持续过程。

4.1 局部BA与全局BA的分工

一个健壮的增量式SFM系统通常包含两种粒度的BA:

  • 局部BA(Local BA):在注册新图像后立即执行。它只优化一个滑动窗口内的相机(例如最新注册的10个相机)以及它们观测到的所有三维点。窗口外的相机和点保持不变。局部BA速度快,能及时纠正最新引入的误差,防止误差过快累积。在局部BA中,由于窗口外的参数固定,它们实际上为窗口内的优化提供了一个“临时”的基准,因此通常不需要显式的自由网约束,或者只需要很弱的约束来防止窗口内的奇异性。
  • 全局BA(Global BA):在重建进行到一定阶段(例如每注册50张图像后),或者重建完成时执行。它优化所有的相机和三维点。这是计算量最大的一步,也是自由网平差最主要的应用场景。只有在这里,我们才面对完整的、具有7自由度奇异性的系统。

4.2 增量式SFM中的平差流程设计

一个结合了自由网平差的增量式SFM流程可以如下设计:

  1. 初始化:选择两张图像,计算初始相对姿态和三维点。此时,固定第一个相机姿态(旋转为单位阵,平移为零),将第二个相机和三维点作为优化变量,进行一次小规模的BA。这建立了初始的浮动坐标系。
  2. 图像注册循环: a.新图像姿态估计:通过2D-3D匹配,用EPnP等鲁棒方法估计新相机姿态。 b.三角化新点:用新相机和已有相机三角化生成新的三维点。 c.几何验证:对新的匹配和三维点进行外点滤除(如RANSAC)。 d.局部BA:将新相机和受影响的点(以及滑动窗口内的相机)加入优化。此时,窗口外的相机和点固定,作为基准。如果窗口内参数开始出现奇异性迹象(如信息矩阵条件数过大),可为窗口内所有相机平移的质心添加一个微弱的先验约束。 e.点云与相机筛选:滤除重投影误差过大的点和不稳定的相机。
  3. 全局BA触发
    • 周期性触发:每注册N张图像后(如N=100)。
    • 关键帧触发:当累计的旋转或平移变化超过阈值时。
    • 闭环检测触发:当检测到闭环(当前图像与很久以前的图像匹配成功)时,这是执行全局BA的最佳时机,因为闭环提供了强烈的全局约束。
  4. 执行全局自由网平差: a.构建全局BA问题:将所有相机姿态{R_i, t_i}和三维点{X_j}设为优化变量。 b.添加最小约束:采用“添加先验约束法”。 - 约束1:所有相机平移的质心为零。残差 = weight * (sum(t_i) / m)。 - 约束2:所有相机旋转的李代数均值为零。残差 = weight * (sum(log(R_i)) / m)。这里log是从旋转矩阵到李代数的映射。 - 约束3:所有三维点到原点的平均距离为1。残差 = weight * ((sum(||X_j||) / n) - 1)。 c.配置求解器:使用LM算法,设置合理的迭代次数和收敛阈值。由于是全局优化,初始的阻尼因子λ可以设大一些。 d.执行优化:调用后端优化库(如Ceres, g2o)进行迭代求解。 e.后处理:检查优化后各相机和点的重投影误差是否均匀下降。如果某些误差异常高,可能需要将其标记为外点并移除,然后重新优化。
  5. 闭环融合:如果全局BA是由闭环触发的,在BA之后,需要将闭环边的约束正式加入到图中,并可能进行一次额外的BA来吸收闭环信息。

4.3 工程实现要点与避坑指南

在实际编码中,以下几个细节决定了自由网平差的成败:

约束权重的选择: 权重w的选择是一门艺术。太大会扭曲结果,太小则约束不足。一个经验法则是:让先验残差项的量级远小于典型的重投影误差项。例如,如果重投影误差在像素坐标下是几个像素(即残差范数在个位数),那么可以将先验权重设为1e-61e-8量级。最好的方式是进行敏感性测试:在同一个数据集上,用不同的权重跑几次BA,观察相机轨迹和点云的整体形状(特别是尺度)是否发生显著变化。如果没有,说明权重是合适的。

参数化与流形: 三维空间的旋转不属于欧几里得空间,不能直接用3x3矩阵的9个参数去优化(会破坏正交性)。必须使用流形上的优化。常用的参数化有:

  • 四元数 + 局部参数化:用单位四元数表示旋转,在优化时,对其使用一个三维的切空间增量(李代数)进行更新。
  • 角轴/李代数:直接用三维向量表示旋转(旋转向量)。 在Ceres Solver中,可以使用EigenQuaternionParameterizationLocalParameterization来实现。在g2o中,有专门的VertexSE3Expmap等节点类型。错误地参数化旋转是BA不收敛或结果错误的常见原因。

处理大规模问题的策略: 当相机和点数量达到上万甚至百万时,直接求解S δ_c = b可能内存不足。此时需要:

  • 使用稀疏求解器:如SuiteSparse的CHOLMOD,或Eigen的稀疏Cholesky分解。
  • 使用迭代法:如共轭梯度法(CG),特别是预处理共轭梯度法(PCG)。BA的舒尔补矩阵S通常是病态的,需要好的预处理器(如块对角预处理器)。
  • 采用更高级的优化库:如Ceres Solver,它内部自动处理稀疏性、提供多种求解器选项(DENSE_SCHUR, SPARSE_SCHUR, ITERATIVE_SCHUR),并支持流形优化,极大降低了实现难度。

诊断与调试

  • 监控信息矩阵的条件数:在优化前后计算S矩阵的条件数。自由网平差应该能显著降低条件数(从无穷大或极大值变为一个有限值),但不会改变问题本身的性质。
  • 可视化更新量:在LM算法的每次迭代中,观察相机平移增量δ_t和点坐标增量δ_X的范数。一个健康的优化过程,这些增量范数应该随着迭代单调下降(阻尼LM可能震荡下降)。如果出现某个参数增量异常巨大,可能是约束不足或数值不稳定。
  • 检查零空间:对于最终优化后的信息矩阵S_final,可以计算其最小的7个特征值。在理想情况下,它们应该非常接近于零(由于数值计算和弱约束,会是一个很小的正值)。如果它们显著大于零,说明约束可能过强了。

5. 从理论到实践:一个简化的代码示例与结果分析

为了更具体地说明,我们用一个高度简化的二维模拟例子,并使用Ceres Solver来演示自由网平差的实现。这个例子包含5个相机和10个三维点,相机观测带有高斯噪声。

#include <ceres/ceres.h> #include <Eigen/Dense> #include <vector> #include <iostream> // 1. 定义重投影误差代价函数(针孔模型,忽略内参和畸变,简化到2D) struct ReprojectionError { ReprojectionError(double observed_x, double observed_y) : observed_x(observed_x), observed_y(observed_y) {} template <typename T> bool operator()(const T* const camera_rotation, // 用角度表示旋转 const T* const camera_translation, // [tx, ty] const T* const point, // [x, y] T* residuals) const { // 旋转点: R * point (2D旋转) T cos_r = cos(camera_rotation[0]); T sin_r = sin(camera_rotation[0]); T rotated_x = cos_r * point[0] - sin_r * point[1]; T rotated_y = sin_r * point[0] + cos_r * point[1]; // 平移并投影(假设焦距=1,光心在(0,0)) T predicted_x = rotated_x + camera_translation[0]; T predicted_y = rotated_y + camera_translation[1]; residuals[0] = predicted_x - T(observed_x); residuals[1] = predicted_y - T(observed_y); return true; } double observed_x, observed_y; }; // 2. 定义先验约束代价函数 // 约束1:所有相机平移的质心为零 struct TranslationCenterPrior { TranslationCenterPrior(double weight) : weight(weight) {} template <typename T> bool operator()(const T* const translation_i, T* residual) const { // 这个残差会在所有相机上求和。为了“公平”,我们约束每个相机的平移向量本身。 // 更精确的做法是构建一个约束所有平移之和的残差,这里为简化,约束每个平移接近零。 // 注意:这实际上施加了过强的约束(3m个约束),但权重很小时,效果类似于约束质心。 residual[0] = T(weight) * translation_i[0]; residual[1] = T(weight) * translation_i[1]; return true; } double weight; }; // 约束2:所有相机旋转角度均值为零(简化版,2D下旋转只有一个角度) struct RotationMeanPrior { RotationMeanPrior(double weight) : weight(weight) {} template <typename T> bool operator()(const T* const rotation_i, T* residual) const { residual[0] = T(weight) * rotation_i[0]; return true; } double weight; }; // 约束3:所有三维点到原点的平均距离为1 struct ScalePrior { ScalePrior(double weight, int total_points) : weight(weight), inv_n(1.0/total_points) {} template <typename T> bool operator()(const T* const point_j, T* residual) const { // 这个残差计算该点到原点的距离,并减去1。需要在所有点上求和。 // 为了在Ceres中实现对所有点的平均约束,一种方法是为每个点创建一个残差块, // 计算 (||point_j|| - 1),然后通过权重来控制其对总代价的影响。 // 另一种更准确的方法是构建一个单独的残差块,接收所有点,计算平均距离。 // 这里采用第一种简化方法,为每个点添加一个弱先验。 T norm = sqrt(point_j[0]*point_j[0] + point_j[1]*point_j[1]); residual[0] = T(weight) * (norm - T(1.0)); return true; } double weight; double inv_n; }; int main() { // 生成模拟数据(略) std::vector<double> camera_rotations(5, 0.0); std::vector<double> camera_translations(5*2, 0.0); std::vector<double> points(10*2, 0.0); std::vector<std::pair<int, int>> observations; // (camera_idx, point_idx) // ... 此处填充模拟数据,为相机和点赋予真值,并添加观测和噪声 ... // 3. 构建Ceres优化问题 ceres::Problem problem; // 添加重投影误差残差块 for (const auto& obs : observations) { int cam_id = obs.first; int pt_id = obs.second; ceres::CostFunction* cost_function = new ceres::AutoDiffCostFunction<ReprojectionError, 2, 1, 2, 2>( new ReprojectionError(observed_x, observed_y)); // observed_x,y需从数据中读取 problem.AddResidualBlock(cost_function, nullptr, // 使用默认的平方损失 &camera_rotations[cam_id], &camera_translations[cam_id*2], &points[pt_id*2]); } // 4. 添加自由网平差的最小约束(关键步骤) double weak_weight = 1e-6; // 约束相机平移质心(简化:为每个相机平移添加弱先验,使其靠近当前值) // 注意:更好的做法是添加一个约束所有平移之和的残差块。这里为演示采用简化版。 for (int i = 0; i < 5; ++i) { ceres::CostFunction* trans_prior_cost = new ceres::AutoDiffCostFunction<TranslationCenterPrior, 2, 2>( new TranslationCenterPrior(weak_weight)); problem.AddResidualBlock(trans_prior_cost, nullptr, &camera_translations[i*2]); } // 约束相机旋转均值(简化:为每个相机旋转添加弱先验) for (int i = 0; i < 5; ++i) { ceres::CostFunction* rot_prior_cost = new ceres::AutoDiffCostFunction<RotationMeanPrior, 1, 1>( new RotationMeanPrior(weak_weight)); problem.AddResidualBlock(rot_prior_cost, nullptr, &camera_rotations[i]); } // 约束尺度(为每个三维点添加弱先验,使其到原点距离接近1) for (int j = 0; j < 10; ++j) { ceres::CostFunction* scale_prior_cost = new ceres::AutoDiffCostFunction<ScalePrior, 1, 2>( new ScalePrior(weak_weight, 10)); problem.AddResidualBlock(scale_prior_cost, nullptr, &points[j*2]); } // 5. 配置并运行求解器 ceres::Solver::Options options; options.linear_solver_type = ceres::DENSE_SCHUR; // 小规模问题用DENSE options.minimizer_progress_to_stdout = true; options.max_num_iterations = 50; ceres::Solver::Summary summary; ceres::Solve(options, &problem, &summary); std::cout << summary.FullReport() << "\n"; // 6. 分析结果 // 计算并比较添加约束前后的重投影误差总和、相机轨迹的质心、点云的平均尺度 double total_reproj_error = 0; // ... 计算代码 ... Eigen::Vector2d trans_center(0,0); for (int i=0; i<5; ++i) trans_center += Eigen::Map<Eigen::Vector2d>(&camera_translations[i*2]); trans_center /= 5.0; double avg_scale = 0; for (int j=0; j<10; ++j) avg_scale += Eigen::Map<Eigen::Vector2d>(&points[j*2]).norm(); avg_scale /= 10.0; std::cout << "After optimization with free-network adjustment:\n"; std::cout << " Total reprojection error: " << total_reproj_error << "\n"; std::cout << " Center of camera translations: " << trans_center.transpose() << " (should be near 0)\n"; std::cout << " Average point distance from origin: " << avg_scale << " (should be near 1)\n"; return 0; }

结果分析要点: 在没有添加先验约束的情况下,直接对带有噪声的模拟数据进行BA,求解器可能会报告失败(矩阵奇异),或者得到一组解,但这组解的整体平移、旋转和尺度是任意的。每次运行结果可能都不一样。 添加了弱先验约束后,优化应该能成功收敛。你会观察到:

  1. 最终的重投影误差会降低到一个稳定值。
  2. 所有相机平移的质心非常接近(0,0)
  3. 所有三维点到原点的平均距离非常接近1
  4. 如果你有真值数据(在模拟中你知道),你可以计算优化后的结构与真值结构之间的相似变换误差(即先求一个最佳拟合的相似变换,然后计算对齐后的误差)。这个误差应该很小,它衡量的是重建的内部几何精度。而整体的平移、旋转和尺度差异则被自由网平差“吸收”掉了,这正是我们想要的——我们得到了一个内部一致的最优结构,尽管它位于一个任意的坐标系中。

这个简单的例子揭示了自由网平差的本质:它不追求与某个绝对坐标系的吻合,而是追求观测数据内部几何关系的最优拟合。它为后续的绝对定向(如果需要)提供了一个坚实、无扭曲的基础。在实际的视觉SLAM或三维重建系统中,这套机制被封装在优化后端内部,开发者通过配置参数来启用和调整约束,但其背后的数学原理和工程考量,正是我们作为从业者需要深刻理解的。

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

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

立即咨询