ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

光束平差法详解:从重投影误差到三维重建精度优化

光束平差法详解:从重投影误差到三维重建精度优化 做三维重建的朋友无论你是搞SfM运动恢复结构、SLAM还是近景摄影测量最终大概率都会撞上同一个名字光束平差法Bundle AdjustmentBA。我最早接触BA是在做一组建筑物照片的三维重建时当时用了现成的SfM流程跑出稀疏点云结果模型弯曲得厉害后来才知道问题不在特征匹配而在于优化环节没做好。从那时起我就意识到BA不是“库里的一个函数”而是整个重建管线里真正决定精度上限的核心。所以这篇笔记不打算复述教科书上的数学推导而是从“我实际用它解决什么问题”出发把BA的工作方式、数学直觉、坑点以及调参经验梳理清楚。不管你是刚入行的学生还是自己搭重建流程的工程师希望这篇内容能帮你少走弯路。1. 光束平差法到底在解决什么问题1.1 从一次弯曲的模型说起先说一个我自己的案例。当时我在做一个室外建筑的环绕拍摄重建共采集了约300张照片用增量式SfM流程做重建得到的稀疏点云肉眼可见地向下凹陷整个建筑像被重力压过一样。刚开始我以为是相机内参标定不准反复用不同标定板重标也换了特征匹配的策略问题依旧。后来我把整个重建流程拆开一步步检查每个环节的输出——特征提取、匹配、基础矩阵估计、三角化。最终锁定问题出在最后的全局优化环节当时使用的流程默认关闭了BA中的畸变参数优化导致相机模型和实际镜头不匹配误差累积后形成了这种系统性弯曲。这个经历让我意识到BA并不仅仅是“把误差变小”的操作它对整个重建结果起的是骨架级的作用如果骨架歪了细节再好看也没用。深入理解BA之后我认为它的核心价值在于将相机位姿、三维点坐标、相机内参、畸变系数放在一个统一框架下同时优化通过最小化“观测到的像素位置”和“预测出的像素位置”之间的差异让整个场景一致地变“直”。1.2 重投影误差一切优化的出发点BA的目标函数是重投影误差。什么叫重投影简单说就是你有一个三维点比如空间中的一个墙角它在真实世界里有一个坐标这个坐标如果投影到相机成像平面上会得到一个像素位置。但由于相机位姿估计不准、三维点坐标不准这个“预测出的像素位置”和你在图像里实际“看到的位置”往往不重合——这两个位置的差值就是重投影误差。以我常用的双目重建流程为例一个三维点会同时出现在左图和右图中。理想情况下同一个三维点投影到左右两幅图像中的像素位置分别和实际观测位置完全重合。但实际总会有几像素甚至几十像素的偏差BA要做的就是不断调整所有未知数让这些偏差的某种总和最小。用公式表达就是E Σ ρ_i( || π(C_k, X_i) - x_ik ||^2 )其中X_i是第i个三维点C_k是第k个相机包括外参和内参π是投影函数x_ik是第i个三维点在第k个相机中的实际观测像素坐标ρ是鲁棒核函数。所有“点-相机”可见性对都要参与求和。这个公式看起来简单但它包含了整个重建里最核心的矛盾所有变量互相耦合。你改了一个相机位姿所有可见点的重投影误差都会变你改了一个三维点的坐标投影到所有相机的误差也会变。如何处理这种大规模、非线性、强耦合的优化问题就是BA的核心技术点。1.3 为什么叫“光束”和“平差”这个名字初看有点抽象但拆开就很好理解。所谓“光束Bundle”指的是从每个三维点出发射向所有能观测到它的相机光心的那束光线。每个三维点对应一组光线像一束光线从空间中发散出去。而“平差Adjustment”是测绘和摄影测量领域的术语指的是通过冗余观测来修正原始观测值中的误差让所有测量结果在内保持一致。所以“光束平差法”字面意思就是同时调整所有光束的位置和方向让它们最终都能精确汇聚到对应的像素点上。这个类比我一直觉得很准确因为在实际迭代中视觉上感受到的正是这种“光束”逐渐聚焦的过程——初始时投影点散落各处随着迭代收敛投影点逐渐逼近实际观测点误差云团收缩到一个很小的半径。2. BA的数学原理与核心构成2.1 从最小二乘到LM优化算法BA本质上是一个非线性最小二乘问题。它涉及投影函数、旋转矩阵、畸变模型等非线性环节导致误差函数对参数求导后不是线性的无法用一次线性求解完成只能迭代逼近。当前主流的优化策略是Levenberg-MarquardtLM算法它能自适应地在“梯度下降法”和“高斯牛顿法”之间切换。高斯牛顿法假设目标函数近似二次型收敛速度快但对初始值敏感梯度下降法稳定但收敛慢。LM算法通过一个阻尼因子 λ 在两者之间动态平衡当迭代远离最优解时λ 变大算法偏向梯度下降保证稳定当接近最优解时λ 变小算法偏向高斯牛顿加快收敛。我在实际使用Ceres Solver做BA时经常会调整这个 λ 的初始值和更新策略。Ceres默认的初始 λ 通常表现良好但遇到病态问题或初值误差大时手动调整min_trust_region_radius或max_trust_region_radius会显著改变收敛速度和最终精度。2.2 参数化与自由度BA中需要优化的参数主要包括三类相机外参旋转和平移。每帧6个自由度旋转部分通常用旋转矩阵、四元数或李代数表示。实际常用罗德里格斯向量3参数或四元数4参数带归一化约束来避免万向锁问题。相机内参焦距、主点、畸变参数。这里要区分不同的畸变模型常见的是鱼眼模型如Kannala-Brandt和针孔模型含径向畸变k1, k2, k3和切向畸变p1, p2。三维点坐标每个点3个自由度。一个典型场景的规模有多大假设有100帧重建图像每帧有500个可见三维点那么未知参数数量大约是100×6 4 (100×500×3)算下来是15万个量级。这个规模在BA中属于小规模真正的大规模BA例如整个城市的航空影像重建涉及的未知参数可达数亿。如此庞大的参数空间暴力求解完全不可行因此必须利用矩阵的稀疏结构来提高计算效率。2.3 稀疏性BA能大规模应用的关键BA的误差方程有一个非常重要的结构特征一个误差项只与一个三维点和一个相机相关。这意味着在计算Hessian矩阵或近似Hessian时矩阵中大部分元素是零。把相机参数块和三维点参数块分开排列Hessian矩阵会呈现一个有趣的模式左上角是相机-相机块对角矩阵右下角是三维点-三维点块对角矩阵右上角和左下角则是相机-点交叉的非零块。利用这种结构可以在求解时先对三维点部分做消元只求解相机参数然后再回代求解三维点这就是经典的Schur消元技巧。Ceres Solver提供的SPARSE_SCHUR求解器就是利用了这一性质。在节点规模较大时SPARSE_SCHUR比DENSE_QR快数倍到数十倍。我自己在跑一个包含2000帧的重建项目时把求解器从DENSE_QR切换成SPARSE_SCHUR单轮BA耗时直接从无法忍受降到几分钟级优化精度反而略有提升。了解这一点之后遇到耗时问题先看求解器选项是否正确而不要盲目加大迭代次数。2.4 一个手动计算的小例子为了展示BA的基本工作过程我写了一个非常小的模拟场景。假设只有1个相机和3个三维点相机位姿固定在第1帧作为参考帧不优化需要优化的是三维点坐标。初始三维点坐标设为真实值加噪声后的结果每个点重投影误差初始约为15像素。使用LM优化迭代10次后误差降到0.3像素以内三维点坐标与真实值的偏差小于1毫米假设场景尺度为1米。这里我把关键代码段贴出来。使用Ceres Solver实现时核心是自定义一个代价函数类struct ReprojectionError { ReprojectionError(double observed_u, double observed_v, double fx, double fy, double cx, double cy) : observed_u_(observed_u), observed_v_(observed_v), fx_(fx), fy_(fy), cx_(cx), cy_(cy) {} template typename T bool operator()(const T* const camera, const T* const point, T* residuals) const { // camera: 旋转罗德里格斯向量3参数 平移3参数 // point: 三维坐标3参数 T p[3]; ceres::AngleAxisRotatePoint(camera, point, p); p[0] camera[3]; p[1] camera[4]; p[2] camera[5]; // 透视投影 T xp p[0] / p[2]; T yp p[1] / p[2]; // 加畸变此处省略畸变模型直接用针孔投影 T predicted_u fx_ * xp cx_; T predicted_v fy_ * yp cy_; residuals[0] predicted_u - T(observed_u_); residuals[1] predicted_v - T(observed_v_); return true; } double observed_u_; double observed_v_; double fx_, fy_, cx_, cy_; };在这个例子中AngleAxisRotatePoint是Ceres自带的罗德里格斯旋转工具函数。它的作用是把三维点坐标根据旋转向量进行旋转相当于用指数映射实现了R * point的操作。因为罗德里格斯向量只有3个参数没有冗余约束在LM迭代中非常方便。实际做BA时代价函数要写得更复杂需要处理畸变模型、不同相机型号的投影差异但核心思想不变给定一组参数预测像素位置计算与观测像素的差值。3. BA在三维重建流程中的角色与设计思路3.1 三维重建管线里BA的位置一套典型的增量式三维重建流程大致是这样的特征提取与匹配如SIFT特征基础矩阵或本质矩阵估计相机位姿初始化通常在两张影像之间三角化生成初始三维点光束平差法全局优化增量式流程中边增长边BA稠密匹配与深度估计网格生成与纹理映射BA嵌在第5步。很多人会问前面几步已经完全确定了相机位姿和三维坐标为什么还需要做BA原因是之前所有步骤基本上都是“局部估计”——一次只考虑两张或少数几张影像误差会随着影像数量的增加而逐渐累积形成轨迹漂移或尺度漂移。BA把所有影像放在一起最小化全局误差从整体上消除这种累积误差。这种设计思路其实和我们在工程里做标定类似单独配对做误差很小但闭环一检查发现整体已经产生了系统性偏移。BA就像“全局闭合差调整”把分散在各处的微小误差重新分配到所有参数上让整体达到自洽。3.2 增量BA与全局BA的取舍在SfM流程中BA的使用策略有几种不同的流派全局BA所有相机位姿和点同时参与优化。优点是精度高缺点是计算量巨大且容易陷入局部最优。增量BA每加入一组新的影像和三维点就执行一次BA但只优化与新影像关联的参数。优点是效率高适合逐步构建的场景缺点是需要精心设计新影像的选取顺序和局部优化策略。在我做的建筑扫描项目中全局BA跑一次可能需要20分钟而增量BA几乎实时。但全局BA的精度往往更稳定尤其在场景中有较多闭环结构时。我的实践建议是在增量式重建过程中使用增量BA保证效率在得到大致的位姿和点云后再整体执行一次全局BA来收紧误差。这个策略在多个数据集上都取得了不错的效果。还有一个细节值得提就是BA中是否允许三维点在迭代过程中被移除。我的习惯是每轮迭代后统计每个点的重投影误差如果某个点的误差超过中位数误差的3倍以上就标记它为外点并在后续迭代中剔除。这不是BA标准库自带的功能但实际效果很好——能显著减少粗差对最终结果的污染。3.3 为什么需要鲁棒核函数实际获取的图像数据里常常存在少量错误的特征匹配。如果你用纯最小二乘一个错误匹配可能把整个优化结果拉偏几十个像素。因此BA中一般会用鲁棒核函数来衰减大误差项的影响。我常用的是Huber核和Cauchy核。Huber核在误差较小时等同于最小二乘误差超过阈值时从二次函数切换为线性函数从而衰减大误差项的权重。Cauchy核则在整个误差范围内都进行衰减对粗差的抑制更强。鲁棒核函数的选择对重建精度影响很大我个人的经验是在特征匹配质量较好例如使用学习型特征描述子时用Huber核效果足够且收敛稳定在进行快速重建或匹配质量较差时改用Cauchy核能更有效地压制异常值。阈值参数scale也很关键它决定了从哪个误差量级开始衰减一般设到2~4像素比较稳妥。4. 实操过程中的关键细节与工具使用4.1 初始化BA成败的第一道门槛数一下我踩过的坑排第一的绝对是初始化问题。很多初学者拿BA库直接优化却发现结果发散或收敛到局部最优原因往往是初始值太差。BA是基于梯度的局部优化算法如果初始相机位姿偏差过大误差函数的梯度方向可能指向完全错误的方向。一个典型的案例我在处理一个环形拍摄序列时由于前几帧匹配质量差估计出的初始旋转矩阵几乎全错了。我试图通过增大LM迭代次数来让BA“自行修正”结果跑了200次迭代后误差还是非常大。后来我把第一帧作为参考帧固定住手动给后续帧设置一个基于时间序列的平滑运动初始值BA很快收敛到了正确的解。所以我的建议是BA前一定要保证初始位姿已经大致正确哪怕粗糙一点都可以但不能方向性错误。如果是在SfM流程中做增量BA新加入的帧必须先用PnPPerspective-n-Point算法估计位姿再进入BA而不是直接给全零值或随机值。在对称场景或重复纹理区域更要注意初始旋转的一致性必要时引入闭环检测结果作为约束。4.2 损失函数与迭代的策略在Ceres Solver中配置BA问题我通常会这样设置ceres::Problem problem; ceres::LossFunction* loss new ceres::HuberLoss(2.0); // 为每个观测添加残差块 for (auto obs : observations) { ceres::CostFunction* cost_function new ceres::AutoDiffCostFunctionReprojectionError, 2, 6, 3( new ReprojectionError(obs.u, obs.v, fx, fy, cx, cy)); problem.AddResidualBlock(cost_function, loss, camera_params[obs.camera_id].data(), point_params[obs.point_id].data()); } // 固定参考相机位姿 problem.SetParameterBlockConstant(camera_params[0].data()); ceres::Solver::Options options; options.linear_solver_type ceres::SPARSE_SCHUR; options.max_num_iterations 100; options.function_tolerance 1e-4; options.gradient_tolerance 1e-10; options.parameter_tolerance 1e-8; ceres::Solver::Summary summary; ceres::Solve(options, problem, summary);这个配置里有几个细节值得说明AutoDiffCostFunction的模板参数2, 6, 3分别代表残差维度2u,v方向、相机参数块维度6旋转3平移3、三维点参数块维度3。HuberLoss的阈值设为2.0像素意思是误差小于2像素时按最小二乘处理超过2像素则开始衰减权重。这个阈值不是固定的如果图像分辨率高、标注一致性好可以设得更小比如1像素如果图像质量差、噪声大可以放宽到4~5像素。SetParameterBlockConstant固定参考帧非常重要。如果不固定整个场景存在一个7自由度的尺度/旋转/平移退化方向优化问题变成了病态问题可能出现整体漂移但误差很小的荒谬解。function_tolerance、gradient_tolerance、parameter_tolerance这三个收敛条件的设置要匹配。设置过于严格会导致迭代次数过多但精度提升微不足道设置过于宽松则可能提前停止迭代。实际操作中可以先跑一次看summary里的termination_type如果显示CONVERGENCE表示正常收敛如果是NO_CONVERGENCE就需要检查初值或增大迭代上限。4.3 更大场景的优化四元数与李代数的选择参数化方式对BA的数值稳定性影响很大。旋转矩阵本身9个参数但有6个约束直接优化矩阵会引入大量冗余欧拉角虽然参数少但有万向锁问题。我在不同项目中用过三种常见方案罗德里格斯向量3参数参数紧凑无冗余约束但参数空间不是全局单射——当旋转角达到π时会出现奇异。适合增量式重建中旋转变化较小的场景。四元数4参数 单位长度约束全局无奇异但需要额外处理归一化约束。Ceres提供了QuaternionParameterization来在流形空间上做优化效果很好。李代数 so(3)在数学上最优雅但需要自己实现指数映射和对数映射代码复杂度稍高。我个人在大多数重建项目中推荐用四元数加QuaternionParameterization因为它在大型旋转变化下稳定且Ceres原生支持良好。罗德里格斯向量在小旋转场景比如连续帧之间位姿变化不太大的视频重建下用起来更方便代码也更直观。4.4 工具链选型Ceres、g2o、GTSAM对比BA工具链的选择会直接影响开发效率。这三个库我都试过简单对比一下工具库适用场景优点缺点Ceres SolverSfM、标定、任意非线性最小二乘自动求导接口灵活支持大规模稀疏求解需要自己管理参数块和残差块学习曲线略陡g2oSLAM图优化图结构清晰自带多种顶点/边类型在SLAM社区应用广泛对SfM这种带大量三维点的场景表达不够直观GTSAM因子图优化概率建模能力强适合带先验约束的复杂场景抽象层次高初次使用不太容易上手如果你做的是纯三维重建项目我强烈建议从Ceres入手。它的自动求导机制能省去大量手推雅可比的麻烦而且SPARSE_SCHUR求解器在稀疏BA问题上几乎是量身定制的。g2o更适合SLAM场景因为它对位姿图有很好的封装。两个库我都用过如果项目需要和IMU、轮速计等传感器融合GTSAM会让你后续扩展更轻松。5. 常见问题与排查技巧实录5.1 问题速查表以下是我在部署BA过程中遇到的高频问题及排查方案症状可能原因排查与解决迭代次数用完仍未收敛初值误差过大 / 参数块未正确固定检查初值、固定参考相机、增大max_num_iterations优化结果误差很小但场景形状明显错误缺少参考帧固定导致整体漂移固定一个相机的6自由度参数或加入先验约束某些三维点误差极大匹配外点污染引入鲁棒核函数增大HuberLoss阈值或用工件统计外点并剔除不同图像重叠区域的点云错位相机内参未参与优化开启内参优化块并考虑优化畸变系数运行内存溢出矩阵规模过大切换到SPARSE_SCHUR或分块BA策略加入闭环后误差反而变大闭环匹配错误或初值错误单独验证闭环边的匹配质量剔除明显错误闭环5.2 一个具体的误差爆炸排查过程有一段时间我在做房间级重建每新增几帧就执行一次局部BA整体节点大小在500帧左右。某个版本运行到约200帧时优化误差突然从2像素跳到20像素但看起来并没有明显的错误匹配。排查过程是这样的我先把所有残差大于10像素的观测项打印出来发现异常集中在新加入的一帧图像上。单独验证该帧的角点坐标发现是特征提取环节产生了亚像素不稳定——同一组特征在不同尺度空间下提取到的坐标发生了偏移导致观测值本身是错的。这种问题不是BA能解决的。BA是在给定观测的前提下调整参数如果观测本身有系统误差再怎么调也只是“强行把错误拟合进去”。后来我改进特征提取策略对提取的角点用LK光流或直接法做亚像素精化误差异常立即消失。这个案例给我的教训是BA不是万能的它优化的是“给定观测下的最佳参数”但如果观测本身脏了必须在输入环节清洗。我现在的流程里每次特征匹配后都会做一步一致性检查用基础矩阵约束过滤明显错误的匹配对再进入BA。5.3 关于参数取舍的实战心得在调整BA参数时有一个常见但容易忽略的问题相机内参是否参与优化。默认情况下很多流程把内参当常数处理只优化位姿和三维点。在标定良好的摄影场景中这样做没有问题但如果相机存在温度漂移、镜头松动或标定不够精确固定内参就会限制精度上限。我倾向于在BA中同时优化焦距和畸变系数但要分阶段进行第一轮只优化位姿和点内参固定收敛后解锁内参继续第二轮优化。这种策略比较稳定不会在初始阶段因内参变化过大而引发震荡。主点坐标则要慎重在稀疏重建中主点难以约束容易漂移一般只在有足够多影像覆盖时放开优化。三维点的参数块优化也有门道。对于远处的地物点视觉视差小深度的可观测性差如果没有先验约束三维点可能在优化中被推到很远的位置。一种做法是给三维点坐标加一个弱先验约束让它在小范围内变化防止过度漂移。6. 从BA到全局一致性的扩展思考BA解决的不仅是局部精度问题更是全局一致性问题。在完整的重建系统中BA与闭环检测、位姿图优化、回环修正紧密配合。很多SLAM系统在BA之后还会做一次轻量级的位姿图优化把loop closure的约束传播到所有关键帧上。在我自己的一个户外扫描项目中我把BA与GPS先验约束结合为每个相机位姿加了弱先验误差项。结果是传统BA得到的结果在局部细节上表现良好但整体轨迹有轻微漂移加入GPS先验后轨迹被拉回到真实走向上重建出的建筑轮廓也更符合实际丈量数据。这说明BA并不是孤立的它的效果取决于你给它提供了什么样的约束和先验信息。另外BA也正在与深度学习方法结合。近年来有一些工作用神经网络预测初始深度或位姿再用BA模块做精化比如可微BA在被集成到端到端三维重建网络中。我在实验中尝试过用学习型特征配合传统BA整体重建精度比纯手工特征提升了约30%。这说明BA作为“几何优化的基石”仍然有很强的生命力——无论前端特征怎么换后端几何优化这一环很难被跳过。如果你正在做大规模场景的三维重建我还有一个具体的建议尽量不要在一个BA中优化所有帧和所有点而是采用分层策略。先对局部子图各自BA再把子图的相机位姿作为虚拟节点合并成上层图最后做全局BA。这种“局部精化 全局松弛”的思路在计算资源和精度之间取得了很好的平衡。7. 最后分享一点我的体会光束平差法这个东西刚接触时会觉得只是套公式调用库但用久了会发现真正决定效果的是你对误差来源的判断和对参数的物理理解。调好一个BA问题不只是调整迭代次数、损失函数阈值这些表面参数更是要理解你的数据是怎么来的哪些环节容易引入错误哪些参数在数学上是不可观的。我个人的工作流中始终会保留这样几个习惯一是固定参考相机杜绝整体漂移二是用鲁棒核函数压制粗差而不是盲目信任所有匹配三是在BA前后分别做质量分析对比重投影误差的分布变化而不是只看最终均值。这三个习惯帮我节省了大量的排查时间。还有一个算是个人的“手工技巧”BA跑完后我会随机抽取几组“相机-三维点”对应关系手动在图像上画出预测的投影点位置和实际观测点做目视对比。虽然看起来很土但一旦发现问题定位速度远超单纯看数值。这个习惯在复杂场景中救过我很多次。希望这篇关于光线平差法的笔记能对正在做三维重建或者准备深入了解BA朋友有所帮助。如果后续有时间我会再整理一篇关于增量式SfM的完整实现笔记把从特征匹配到BA输出的细节串起来那将是另一个有意思的话题。
返回列表