1. 项目概述从“自由”到“精确”的视觉重建之旅在计算机视觉和摄影测量领域三维重建一直是个迷人的话题。我们手里有一堆从不同角度拍摄的同一场景的照片如何让计算机理解这些二维像素点之间的空间关系并反推出相机的位置、姿态以及场景的三维结构这就是所谓的运动恢复结构Structure from Motion, SFM要解决的核心问题。而“无先验约束下的增量式SFM自由网平差”这个标题听起来很学术但它描述的是一个非常实际且关键的步骤如何在没有GPS、没有已知控制点、甚至不知道相机任何初始参数如焦距的情况下仅凭图像本身构建一个尺度统一、几何一致且精度最优的三维模型。你可以把它想象成给你一堆散乱的乐高积木图像特征点你需要在不看说明书无先验的情况下先拼出一个小角落初始化然后一块一块地、小心翼翼地增量式把整个模型搭建起来并且在搭建过程中不断微调每一块积木的位置让整体结构最稳固、缝隙最小平差。这个过程的核心数学工具就是“最小二乘”。今天我们就来彻底拆解这个技术链条看看它是如何运作的以及在实际操作中会遇到哪些“坑”。2. 核心思路拆解为什么是“增量式”和“自由网”在深入公式和代码之前我们必须先理解两个关键概念“增量式”和“自由网”这决定了整个SFM流程的架构和挑战。2.1 “自由网”意味着什么“自由网平差”是测绘学里的一个经典概念但在SFM中它被赋予了新的内涵。简单说它指的是我们进行优化的那个三维网络由三维点和相机位姿构成没有任何外部绝对控制。没有已知坐标的三维点作为“锚点”也没有已知精确位置和姿态的相机作为“基准”。整个网络在空间中是可以整体平移、旋转和缩放的就像一个漂浮在虚空中的模型它的绝对位置和大小是未知的但其内部各点之间的相对几何关系是确定的。这带来了两个直接后果尺度模糊性我们重建的模型可以是1:1的也可以是1:100的只要内部比例正确就行。这个尺度信息在只有图像的情况下是无法恢复的通常需要引入一个已知尺寸的物体比如一个人、一个硬币来赋予其物理尺度。基准缺失整个模型没有“原点”。我们通常会把初始化时选定的第一个相机的坐标系作为整个重建过程的参考系将其位置设为原点姿态设为单位矩阵但这个选择是任意的。所有后续的相机和三维点都相对于这个“临时基准”进行估计。自由网平差的目标就是在承认并接受这种整体自由度7个3个平移、3个旋转、1个缩放存在的前提下优化网络内部的几何一致性使得所有二维图像观测值特征点反投影到三维空间再投影到其他图像上的误差最小。它不追求绝对位置正确只追求内部相对关系最准。2.2 为什么选择“增量式”策略与“增量式”相对的是“全局式”SFM。全局式方法试图一次性利用所有图像之间的匹配关系直接估算所有相机位姿和三维点通常通过求解一个大型的矩阵分解或优化问题来实现。这种方法理论优美计算效率可能更高但对数据质量匹配的准确性和完整性和初始化非常敏感容易因错误匹配而导致整个系统崩溃。增量式SFM则采取了更稳健、更符合人类认知习惯的“由点到面”策略初始化选择两张视角、重叠度都较好的图像通过对极几何计算它们的相对位姿并三角化出一批初始的三维点云。这相当于建立了重建世界的“第一块基石”。注册新视图从剩余图像中选择一张能看到最多已有三维点的图片利用PnPPerspective-n-Point算法估算该新相机相对于现有模型的位姿。三角化新点利用新注册的相机和已有相机对新的匹配特征点进行三角化扩充三维点云。局部/全局优化平差在每次注册新视图和三角化新点后都对相关的相机位姿和三维点坐标进行光束法平差Bundle Adjustment, BA以最小化重投影误差。这个过程就是“平差”的核心。循环迭代重复步骤2-4直到所有图像都被处理或无法注册为止。增量式的优势在于其鲁棒性。错误在早期被引入时影响范围有限并且可以通过后续的BA进行纠正和剔除例如使用鲁棒核函数。它就像滚雪球从一个可靠的小核心开始逐步扩大并在每一步都压实优化已经构建的部分。当然它的缺点是可能累积误差并且计算量随着模型增大而增加因为BA需要频繁运行。3. 数学核心最小二乘与光束法平差详解一切的核心都归结为一个优化问题最小化重投影误差。理解了这个就理解了SFM平差的灵魂。3.1 重投影误差模型假设我们有n个三维点X_j(j1,...,n) 和m个相机第i个相机的参数内参K_i和外参[R_i | t_i]共同决定了投影函数π。三维点X_j在第i张图像上的理论投影像素坐标为u_ij_pred π(K_i, R_i, t_i, X_j)而在图像上实际检测到的特征点像素坐标为u_ij_obs。重投影误差就是观测值与预测值之间的差通常是欧氏距离的平方e_ij || u_ij_obs - u_ij_pred ||²不是所有的点都被所有的相机看到。我们定义一个可见性集合V其中(i, j) ∈ V表示点j在相机i中是可见的。3.2 光束法平差的目标函数BA的目标是找到所有相机参数通常包括内参和外参和所有三维点坐标使得所有重投影误差的总和最小argmin_{ {K_i, R_i, t_i}, {X_j} } Σ_{(i,j)∈V} ρ( || u_ij_obs - π(K_i, R_i, t_i, X_j) ||² )这里的ρ是一个鲁棒核函数如Huber核用于降低外点错误匹配对整体优化目标的影响这是工程实践中至关重要的一环。这是一个大规模、高度非线性的最小二乘问题。待优化的参数数量巨大一个包含100张图片、10000个三维点的项目参数轻松超过3万相机参数 3万点坐标 6万个。3.3 求解非线性最小二乘与稀疏性我们无法直接求解必须迭代。最常用的方法是列文伯格-马夸尔特Levenberg-Marquardt, LM算法它是高斯-牛顿法的增强版通过引入一个阻尼因子在梯度下降和高斯-牛顿法之间自适应切换保证收敛的稳定性。BA问题的雅可比矩阵具有特殊的稀疏块结构。一个重投影误差只对其对应的一个相机参数块和一个三维点参数块有导数对其他块导数为零。这意味着海森矩阵近似为 JᵀJ也是稀疏的。例如海森矩阵H可以按相机C和点P分块H [ U W ] [ Wᵀ V ]其中U是对角线块矩阵每个块对应一个相机参数与其他相机参数的二阶导因为相机之间通过共同观测的点产生间接关联但在近似海森阵中常被忽略非对角线块或利用舒尔补消元V也是对角线块矩阵每个块对应一个三维点W是相机-点之间的非对角线块。利用这种稀疏性我们可以极大地加速 LM 算法的求解。常见的策略是使用舒尔补Schur Complement消去三维点参数先求解关于相机参数的简化方程规模小得多再回代求解三维点。这就是为什么像 Ceres Solver、g2o 这样的优化库能够高效处理大型 BA 问题的原因。注意在实际编码中我们几乎从不从头实现 LM 算法或稀疏矩阵运算。使用成熟的优化库如 Ceres Solver是标准做法。我们的工作重点是正确地定义参数块、残差块以及提供解析或数值的雅可比矩阵计算。4. 增量式SFM自由网平差全流程实操下面我们结合一个典型的开源SFM管线例如使用 OpenMVG Ceres Solver的思路来拆解每一步的具体操作和关键参数。4.1 数据准备与特征提取首先你需要一个图像序列。假设它们放在./images文件夹下。# 假设使用 OpenMVG 的管线 # 1. 列出图像 openMVG_main_SfMInit_ImageListing -i ./images -o ./matches -d /path/to/camera/sensor_database.txt这里camera/sensor_database.txt文件包含了可能的相机传感器尺寸信息用于提供焦距的初始估计像素单位。即使我们做“无先验自由网”一个合理的焦距初始值例如EXIF中读取的或设为图像宽度的一半也能极大地帮助优化收敛。这就是一个重要的实操技巧完全无先验很难我们通常利用图像EXIF信息或简单假设来设置内参初值在BA中将其作为变量进行优化尤其是焦距和主点。4.2 特征匹配与几何验证# 2. 计算特征点例如SIFT openMVG_main_ComputeFeatures -i ./matches/sfm_data.json -o ./matches -m SIFT # 3. 特征匹配 openMVG_main_ComputeMatches -i ./matches/sfm_data.json -o ./matches -g e-g e表示使用“等价对”匹配模式这是增量式SFM的常见选择它计算所有图像对之间的匹配但会通过几何验证如对极几何滤除错误匹配。几何验证是保证初始化和后续注册成功率的生命线。4.3 增量重建核心循环这是最核心的部分我们简述其逻辑步骤因为具体实现通常被封装在库中。初始化选择算法会评估所有图像对选择一个“最佳”的初始对。评估标准通常是匹配数量多、且通过对极几何验证本质矩阵或单应矩阵的内点比例高、两相机基线视差适中。基线太小会导致三角化深度不确定太大会导致匹配困难。实操心得对于无人机序列或环绕拍摄相邻帧是最佳初始对。对于无序图像集可能需要更复杂的评分策略。初始重建对选定的图像对(I_a, I_b)计算其本质矩阵E分解得到四个可能的[R|t]组合通过三角化点并在两个相机前方深度为正的测试选择正确的解。三角化初始匹配点得到第一批三维点{X_init}。立即执行一次两视图BA优化这两个相机的位姿和初始三维点。此时固定第一个相机的位姿为[I|0]第二个相机的t的模长被归一化例如设为1这定义了初始的、任意的尺度和坐标系。增量注册与三角化循环选择下一张图像在未注册的图像中寻找一张能观察到最多已有三维点{X_existing}的图片。设阈值为min_observations例如至少50个点。PnP求解位姿使用{X_existing}及其在该图像上的2D观测通过RANSACEPnP或UPnP算法求解该相机的位姿[R_new|t_new]。三角化新点对于该图像与已注册图像之间的新匹配对其2D点尚未被三角化利用新的和已有的相机位姿进行三角化。通常使用最小二乘三角化DLT或更稳定的中点法。局部BA新内容加入后执行一次BA。但为了效率并非优化所有参数。通常优化新加入的相机。与新相机共同观测到的所有三维点这些点会被新相机约束。与这些三维点相连的其他相机通常限制在最近注册的N个例如N10。这就是“局部”BA它能有效防止误差累积又不会每次都对全局进行耗时优化。异常值剔除在BA之后检查每个观测的重投影误差。如果误差大于阈值例如5个像素则将该观测标记为异常值并从优化问题中移除。如果某个三维点被少于2个相机观测则删除该点。全局BA与最后优化当所有图像注册完毕或达到某个停止条件后执行一次全局BA优化所有相机参数和三维点坐标。这是最耗时但也是最精确的一步。注意在自由网中全局BA会导致“漂移”或“基准变化”。因为整个模型可以自由平移旋转优化后的坐标系可能与初始坐标系不同。但这不影响内部几何精度。4.4 关键参数与配置以Ceres Solver为例当我们在Ceres中定义BA问题时需要仔细设置。// 伪代码示例 ceres::Problem problem; // 1. 添加相机参数块 (这里使用简单的针孔模型内参f, k1, k2; 外参旋转(四元数), 平移) for (auto camera : cameras) { problem.AddParameterBlock(camera.intrinsics, 3); // f, k1, k2 problem.AddParameterBlock(camera.rotation, 4); // 四元数 [qw, qx, qy, qz] problem.AddParameterBlock(camera.translation, 3); // [tx, ty, tz] // 设置局部参数化四元数需要单位约束 problem.SetParameterization(camera.rotation, new ceres::QuaternionParameterization()); // 如果内参已知可以固定某些参数例如 // if (fix_focal_length) problem.SetParameterBlockConstant(camera.intrinsics); } // 2. 添加三维点参数块 for (auto point : points) { problem.AddParameterBlock(point.xyz, 3); } // 3. 添加残差块重投影误差 ceres::LossFunction *loss_function new ceres::HuberLoss(1.0); // 鲁棒核函数 for (auto observation : observations) { ceres::CostFunction *cost_function SnavelyReprojectionError::Create(observed_x, observed_y); // 自定义代价函数 problem.AddResidualBlock(cost_function, loss_function, observation.camera-intrinsics, observation.camera-rotation, observation.camera-translation, observation.point-xyz); } // 4. 设置优化选项并求解 ceres::Solver::Options options; options.linear_solver_type ceres::SPARSE_SCHUR; // 利用稀疏性这是关键。 options.minimizer_progress_to_stdout true; options.max_num_iterations 100; options.function_tolerance 1e-6; ceres::Solver::Summary summary; ceres::Solve(options, problem, summary);关键参数解析linear_solver_type对于BASPARSE_SCHUR是最高效的选择之一。它利用了我们之前提到的舒尔补稀疏性。LossFunction(HuberLoss)这是处理外点的关键。Huber核函数在误差较小时是二次的在误差较大时是线性的能减少大误差项的影响。参数1.0是阈值需要根据你的重投影误差尺度调整通常设为几个像素。参数块设置注意我们对旋转使用了四元数参数化并添加了QuaternionParameterization以保证优化过程中四元数保持单位长度。这是必须的否则优化会出错。自由度问题在自由网平差中如果我们固定了第一个相机位姿恒定那么整个网络的7个自由度就被锁定了。但有时我们什么也不固定此时海森矩阵是奇异的有7个零特征值对应7个自由度。Ceres等求解器可以处理这种“亏秩”问题但更常见的做法是添加一个微弱的先验比如用一个很小的代价项将第一个相机“轻轻”地锚定在原点而不是硬性固定。这能稳定数值求解同时不影响最终内部几何精度。5. 常见问题、调试技巧与性能优化在实际操作中你会遇到各种各样的问题。下面是一些典型场景和解决思路。5.1 重建失败或模型扭曲症状模型严重变形像被拉扯的橡皮泥或重建到一半崩溃。排查清单特征匹配质量这是万恶之源。检查初始图像对的匹配和内点数量。如果RANSAC内点率太低50%初始化就会失败。尝试调整特征提取参数如SIFT的对比度阈值或使用更强的匹配滤除方法如比率测试、双向匹配。三角化检查在三角化新点时检查三角化角度。两束光线近乎平行时三角化结果极不稳定。可以设置一个最小角度阈值如2度来过滤。BA发散查看优化器输出。如果代价函数不下降反而上升或LM算法的阻尼因子激增说明问题可能出在外点过多加强鲁棒核函数如使用更“硬”的Cauchy核或减小Huber阈值。参数初始化太差特别是新注册相机的位姿。如果PnP求解的位姿误差很大以其为初值进行BA很容易发散。可以尝试用更鲁棒的PnP方法或在BA前先用一个更小的局部BA只优化该新相机和少量点进行微调。数值问题确保三维点坐标和相机平移量的数值尺度在一个合理的范围内例如场景大小在1-100个单位内。过大或过小的数值会导致雅可比矩阵条件数很差。一个常用技巧是对初始三维点云进行归一化将其重心移到原点并缩放使其平均深度为1。5.2 累积误差与尺度漂移症状在长序列重建中虽然局部看起来不错但序列末尾的相机位姿与开头相比会出现明显的漂移或者环路闭合时无法对齐。解决方案更频繁的局部BA不要只在注册新图时做BA。可以设置一个关键帧策略每注册K帧如K10就执行一次包含更多相机和点的局部BA。全局BA最终必须执行全局BA。对于大规模场景可以使用增量式BA或分布式BA来降低计算负担。环路闭合检测这是消除累积误差的终极武器。当系统检测到当前图像与很久之前的图像匹配成功时就形成了一个“环”。这时可以添加一个位姿图优化Pose Graph Optimization约束或者直接将闭环边作为约束加入全局BA强制纠正漂移。在增量SFM中这通常需要额外的模块来处理。5.3 性能瓶颈与优化问题图像数量上千时全局BA速度极慢甚至内存不足。优化策略使用更好的求解器确保使用SPARSE_SCHUR线性求解器。对于超大规模问题可以考虑使用ITERATIVE_SCHUR并结合预处理共轭梯度法。降采样三维点不是所有的特征点都需要参与BA。可以使用网格法或随机采样在保证观测覆盖的前提下减少三维点的数量。使用子图/分层BA将整个场景划分为多个子图分别进行BA然后再对齐子图。或者进行分层BA先优化关键帧和其代表点再优化非关键帧。固定老参数在增量BA中可以固定那些远离当前活动区域的老相机和老三维点的参数只优化最近的部分。5.4 实操心得与技巧记录内参的优化策略对于消费级相机通常优化焦距f和径向畸变k1, k2就足够了。主点(cx, cy)如果初始化得好设为图像中心可以固定。对于手机广角镜头可能需要考虑切向畸变p1, p2或更复杂的畸变模型。切记同时优化所有内参且没有足够多的、分布良好的观测时容易过拟合或导致模型不稳定。尺度恢复的土办法自由网重建的模型没有真实尺度。如果你知道场景中某两点的真实距离D_real而模型中对应距离为D_model那么尺度因子s D_real / D_model。将所有三维点坐标和相机平移量乘以s即可得到有尺度的模型。可视化是王道在开发调试时实时可视化相机位姿用坐标系表示和三维点云至关重要。它能帮你直观地发现位姿错误、尺度问题或扭曲。可以使用 Pangolin、Open3D 或简单的 matplotlib 进行可视化。数据集的选取从简单、规整的数据集开始如桌面物体、有清晰纹理的建筑物避免一开始就挑战弱纹理、重复纹理或动态场景。COLMAP 官网提供的示例数据集是很好的练手材料。增量式SFM自由网平差是一个将理论多视图几何、非线性优化与工程实践鲁棒性、效率、调试紧密结合的领域。理解最小二乘的原理是基础但真正的挑战在于如何处理不完美的真实数据如何设计稳健的流水线以及如何调优每一个环节的参数。它没有唯一的正确答案只有针对特定场景的更优解。希望这篇详尽的拆解能为你搭建自己的三维重建系统提供一份扎实的路线图和避坑指南。