惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Latest news
Latest news
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
L
LINUX DO - 热门话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
V
V2EX
博客园 - 司徒正美
B
Blog RSS Feed
AWS News Blog
AWS News Blog
MyScale Blog
MyScale Blog
Scott Helme
Scott Helme
Cisco Talos Blog
Cisco Talos Blog
Last Week in AI
Last Week in AI
NISL@THU
NISL@THU
博客园 - Franky
P
Proofpoint News Feed
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
S
Schneier on Security
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
WordPress大学
WordPress大学
The Hacker News
The Hacker News
T
Threatpost
阮一峰的网络日志
阮一峰的网络日志
A
Arctic Wolf
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
Engineering at Meta
Engineering at Meta
罗磊的独立博客
T
The Blog of Author Tim Ferriss
D
Darknet – Hacking Tools, Hacker News & Cyber Security
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
K
Kaspersky official blog
SecWiki News
SecWiki News
云风的 BLOG
云风的 BLOG
美团技术团队
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Security Latest
Security Latest
C
Cyber Attacks, Cyber Crime and Cyber Security
B
Blog
S
Security Affairs

博客园 - charlee44

3D Gaussian Splatting可视化渲染原理详解 Unity项目适配华为鸿蒙系统的原生库加载问题排查与解决 Unity在安卓端如何调试输出信息 代码之外:一个技术人的职场困境与自我和解 记一次GIS专业职称水平能力测试考试 《GIS基础原理与技术实践》配套案例(Python版) 最小二乘问题详解:目录 最小二乘问题详解22:抗差估计与增量式SFM的工程稳健实现 最小二乘问题详解21:稀疏GCP约束下的自由网平差与弱约束融合 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 最小二乘问题详解19:带先验约束的增量式SFM优化与实现 最小二乘问题详解18:增量式SFM核心流程实现 最小二乘问题详解17:SFM仿真数据生成 最小二乘问题详解15:束平差原理与基础实现 最小二乘问题详解14:鲁棒估计与5点算法求解本质矩阵 最小二乘问题详解13:对极几何中本质矩阵求解 最小二乘问题详解12:三角化中的非线性优化 最小二乘问题详解11:基于李代数的PnP优化 最小二乘问题详解10:PnP问题求解 《实时渲染》第3章-图形处理单元-3.7几何着色器 《实时渲染》第3章-图形处理单元-3.6曲面细分阶段 《实时渲染》第3章-图形处理单元-3.5顶点着色器 《实时渲染》第3章-图形处理单元-3.4可编程着色和及其API的演变 为什么现代 C++ 库都用 PIMPL?一场关于封装、依赖与安全的演进 《实时渲染》第3章-图形处理单元-3.2GPU管线概览 我的GIS实践与思考:新书《GIS基础原理与技术实践》分享 从零实现一个生产级 RAG 语义搜索系统:C++ + ONNX + FAISS 实战 三维模型瓦片服务三剑客:3D Tiles、I3S与S3M全解析
最小二乘问题详解16:束平差工程实践总结
charlee44 · 2026-03-13 · via 博客园 - charlee44

1. 引言

在上一篇文章《最小二乘问题详解15:束平差原理与基础实现》中,我们从几何模型出发,系统推导了 束平差(Bundle Adjustment, BA) 的数学形式,并基于 Ceres Solver 实现了一个基础但完整的 BA 系统。在理想合成数据下,该系统能够将初始存在偏差的相机位姿与 3D 点高效优化至亚像素级精度,充分验证了 BA 作为“全局一致性精修”工具的强大能力。

然而,理论上的优雅并不总能直接转化为工程上的稳定。在真实的 BA 应用场景( SFM / SLAM / 稀疏重建)中不会那么理想,往往会遇到各种问题。其中最为常见的就是由于误匹配造成的外点(Outliers)污染的问题。

2. 实例

为了更直观地说明外点对 BA 优化的影响,以及如何在工程实践中提升 BA 的稳健性,这里通过一个完整的实例进行演示。在该示例中,我们构造一个简单但具有代表性的 多视图重建场景。具体设置如下:

  • 相机数量:5 个
  • 空间点数量:100 个
  • 相机模型:理想 pinhole 相机
  • 观测噪声:像素级高斯噪声
  • 外点比例:10%

在生成观测数据时,我们首先按照真实几何关系计算每个 3D 点在相机中的投影位置,并叠加 高斯像素噪声。随后按照设定的比例随机注入 错误匹配(外点),即直接生成随机像素坐标来替代真实观测值,从而模拟真实特征匹配中不可避免的误匹配问题。具体代码实现如下:

#include <ceres/ceres.h>
#include <ceres/rotation.h>

#include <Eigen/Core>
#include <Eigen/Geometry>
#include <iostream>
#include <random>
#include <vector>

using namespace std;

struct Camera {
  double q[4];
  double t[3];
};

struct Point3D {
  double xyz[3];
};

struct Observation {
  int cam_id;
  int pt_id;
  double x;
  double y;
};

struct ReprojectionError {
  ReprojectionError(double x, double y, double fx, double fy, double cx,
                    double cy)
      : x_(x), y_(y), fx_(fx), fy_(fy), cx_(cx), cy_(cy) {}

  template <typename T>
  bool operator()(const T* const q, const T* const t, const T* const point,
                  T* residuals) const {
    T p[3];
    ceres::QuaternionRotatePoint(q, point, p);

    p[0] += t[0];
    p[1] += t[1];
    p[2] += t[2];

    T xp = p[0] / p[2];
    T yp = p[1] / p[2];

    T u = T(fx_) * xp + T(cx_);
    T v = T(fy_) * yp + T(cy_);

    residuals[0] = u - T(x_);
    residuals[1] = v - T(y_);

    return true;
  }

  static ceres::CostFunction* Create(double x, double y, double fx, double fy,
                                     double cx, double cy) {
    return new ceres::AutoDiffCostFunction<ReprojectionError, 2, 4, 3, 3>(
        new ReprojectionError(x, y, fx, fy, cx, cy));
  }

  double x_, y_;
  double fx_, fy_, cx_, cy_;
};

double ReprojectionErrorValue(const Camera& c, const Point3D& p,
                              const Observation& o, double fx, double fy,
                              double cx, double cy) {
  Eigen::Quaterniond q(c.q[0], c.q[1], c.q[2], c.q[3]);
  Eigen::Vector3d t(c.t[0], c.t[1], c.t[2]);
  Eigen::Vector3d P(p.xyz[0], p.xyz[1], p.xyz[2]);

  Eigen::Vector3d Pc = q * P + t;

  double u = fx * Pc.x() / Pc.z() + cx;
  double v = fy * Pc.y() / Pc.z() + cy;

  double du = u - o.x;
  double dv = v - o.y;

  return sqrt(du * du + dv * dv);
}

double ComputeRMSE(const vector<Camera>& cams, const vector<Point3D>& pts,
                   const vector<Observation>& obs, double fx, double fy,
                   double cx, double cy) {
  double err = 0;
  int n = 0;

  for (auto& o : obs) {
    double e =
        ReprojectionErrorValue(cams[o.cam_id], pts[o.pt_id], o, fx, fy, cx, cy);

    err += e * e;
    n++;
  }

  return sqrt(err / n);
}

void SolveBA(vector<Camera>& cams, vector<Point3D>& pts,
             const vector<Observation>& obs, double fx, double fy, double cx,
             double cy, bool robust) {
  ceres::Problem problem;

  ceres::Manifold* q_manifold = new ceres::QuaternionManifold();

  for (auto& c : cams) {
    problem.AddParameterBlock(c.q, 4, q_manifold);
    problem.AddParameterBlock(c.t, 3);
  }

  for (auto& p : pts) problem.AddParameterBlock(p.xyz, 3);

  for (auto& o : obs) {
    ceres::CostFunction* cost =
        ReprojectionError::Create(o.x, o.y, fx, fy, cx, cy);

    ceres::LossFunction* loss = nullptr;

    if (robust) loss = new ceres::HuberLoss(1.0);

    problem.AddResidualBlock(cost, loss, cams[o.cam_id].q, cams[o.cam_id].t,
                             pts[o.pt_id].xyz);
  }

  problem.SetParameterBlockConstant(cams[0].q);
  problem.SetParameterBlockConstant(cams[0].t);

  ceres::Solver::Options options;
  options.linear_solver_type = ceres::SPARSE_SCHUR;
  options.max_num_iterations = 50;
  options.minimizer_progress_to_stdout = true;

  ceres::Solver::Summary summary;
  ceres::Solve(options, &problem, &summary);

  cout << summary.BriefReport() << endl;
}

int main() {
  int num_cams = 5;
  int num_pts = 100;

  double fx = 800, fy = 800;
  double cx = 320, cy = 240;

  // 外点比例
  double outlier_ratio = 0.1;

  vector<Camera> gt_cams(num_cams);
  vector<Point3D> gt_pts(num_pts);
  vector<Observation> obs;

  mt19937 rng(0);

  uniform_real_distribution<double> uni(-1.0, 1.0);
  uniform_real_distribution<double> uni01(0.0, 1.0);
  uniform_real_distribution<double> pixel_x(0.0, 640.0);
  uniform_real_distribution<double> pixel_y(0.0, 480.0);

  normal_distribution<double> noise(0.0, 1.0);

  // 生成相机
  for (int i = 0; i < num_cams; i++) {
    double ang = i * 0.2;

    Eigen::Quaterniond q(Eigen::AngleAxisd(ang, Eigen::Vector3d::UnitY()));

    gt_cams[i].q[0] = q.w();
    gt_cams[i].q[1] = q.x();
    gt_cams[i].q[2] = q.y();
    gt_cams[i].q[3] = q.z();

    gt_cams[i].t[0] = i * 0.5;
    gt_cams[i].t[1] = 0;
    gt_cams[i].t[2] = 0;
  }

  // 生成3D点
  for (int i = 0; i < num_pts; i++) {
    gt_pts[i].xyz[0] = uni(rng);
    gt_pts[i].xyz[1] = uni(rng);
    gt_pts[i].xyz[2] = 4.0 + uni(rng);
  }

  // 生成观测
  for (int i = 0; i < num_cams; i++) {
    Eigen::Quaterniond q(gt_cams[i].q[0], gt_cams[i].q[1], gt_cams[i].q[2],
                         gt_cams[i].q[3]);

    Eigen::Vector3d t(gt_cams[i].t[0], gt_cams[i].t[1], gt_cams[i].t[2]);

    for (int j = 0; j < num_pts; j++) {
      Eigen::Vector3d P(gt_pts[j].xyz[0], gt_pts[j].xyz[1], gt_pts[j].xyz[2]);

      Eigen::Vector3d Pc = q * P + t;

      double u = fx * Pc.x() / Pc.z() + cx;
      double v = fy * Pc.y() / Pc.z() + cy;

      Observation o;

      o.cam_id = i;
      o.pt_id = j;

      o.x = u + noise(rng);
      o.y = v + noise(rng);

      // 注入外点
      if (uni01(rng) < outlier_ratio) {
        o.x = pixel_x(rng);
        o.y = pixel_y(rng);
      }

      obs.push_back(o);
    }
  }

  vector<Camera> est_cams = gt_cams;
  vector<Point3D> est_pts = gt_pts;

  for (auto& c : est_cams)
    for (int k = 0; k < 3; k++) c.t[k] += noise(rng) * 0.2;

  for (auto& p : est_pts)
    for (int k = 0; k < 3; k++) p.xyz[k] += noise(rng) * 0.2;

  cout << "Initial RMSE: "
       << ComputeRMSE(est_cams, est_pts, obs, fx, fy, cx, cy) << endl;

  cout << "\n===== BA without robust =====\n";
  SolveBA(est_cams, est_pts, obs, fx, fy, cx, cy, false);

  cout << "RMSE: " << ComputeRMSE(est_cams, est_pts, obs, fx, fy, cx, cy)
       << endl;

  cout << "\n===== BA with Huber =====\n";
  est_cams = gt_cams;
  est_pts = gt_pts;
  SolveBA(est_cams, est_pts, obs, fx, fy, cx, cy, true);

  cout << "RMSE: " << ComputeRMSE(est_cams, est_pts, obs, fx, fy, cx, cy)
       << endl;

  cout << "\n===== Outlier Filtering =====\n";

  vector<Observation> filtered;

  for (auto& o : obs) {
    double e = ReprojectionErrorValue(est_cams[o.cam_id], est_pts[o.pt_id], o,
                                      fx, fy, cx, cy);

    if (e < 5.0) filtered.push_back(o);
  }

  cout << "Observations: " << obs.size() << " -> " << filtered.size() << endl;

  SolveBA(est_cams, est_pts, filtered, fx, fy, cx, cy, true);

  cout << "Final RMSE: "
       << ComputeRMSE(est_cams, est_pts, filtered, fx, fy, cx, cy) << endl;
}

在这个案例中,我们按照 10% 的比例随机注入外点。这些外点的像素位置与真实投影完全无关,因此其重投影误差往往会达到 数百像素级别,从而对 BA 优化产生显著干扰。具体的优化流程如下:

  1. 加入像素噪声:模拟真实图像测量误差。
  2. 加入初始位姿误差:在真实相机位姿基础上叠加随机扰动,模拟 SFM / SLAM 中不准确的初始估计。
  3. 注入错误匹配(Outliers):按 10% 的比例随机生成错误观测。
  4. 直接进行 BA 优化:不使用鲁棒核函数,观察外点对优化结果的影响。
  5. 引入鲁棒核函数(Huber Loss):在 BA 中对大残差进行权重抑制。
  6. BA 后进行外点剔除:根据重投影误差过滤异常观测。
  7. 再次执行 BA 优化:在清洗后的数据上进行最终优化。

整体流程可以总结为一个典型的 稳健 BA pipeline

Initial
  ↓
BA
  ↓
Robust BA
  ↓
Outlier Filtering
  ↓
Final BA

程序的实际运行结果如下所示:

Initial RMSE: 295.759

===== BA without robust =====
iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  2.186829e+07    0.00e+00    5.19e+06   0.00e+00   0.00e+00  1.00e+04        0    5.52e-04    1.37e-03
   1  1.102282e+09   -1.08e+09    5.19e+06   0.00e+00  -7.89e+01  5.00e+03        1    1.04e-03    2.59e-03
   2  7.761862e+10   -7.76e+10    5.19e+06   0.00e+00  -5.67e+03  1.25e+03        1    3.09e-04    3.07e-03
   3  2.597820e+08   -2.38e+08    5.19e+06   0.00e+00  -1.74e+01  1.56e+02        1    3.35e-04    3.54e-03
   4  5.889476e+09   -5.87e+09    5.19e+06   0.00e+00  -4.32e+02  9.77e+00        1    3.11e-04    3.98e-03
   5  1.061306e+07    1.13e+07    1.13e+06   0.00e+00   8.92e-01  1.88e+01        1    6.78e-04    4.77e-03
   6  9.469239e+06    1.14e+06    1.10e+06   4.45e+01   6.42e-01  1.92e+01        1    6.48e-04    5.51e-03
   7  8.256255e+06    1.21e+06    2.75e+05   1.14e+02   9.29e-01  5.24e+01        1    7.67e-04    6.45e-03
   8  1.052581e+07   -2.27e+06    2.75e+05   5.23e+02  -1.44e+00  2.62e+01        1    4.22e-04    7.20e-03
   9  8.731070e+06   -4.75e+05    2.75e+05   3.68e+02  -3.35e-01  6.54e+00        1    2.92e-04    7.66e-03
  10  7.442423e+06    8.14e+05    5.54e+05   1.51e+02   8.10e-01  8.60e+00        1    7.57e-04    8.70e-03
  11  6.690504e+06    7.52e+05    3.93e+05   2.07e+02   1.11e+00  2.58e+01        1    6.74e-04    9.51e-03
  12  1.583961e+07   -9.15e+06    3.93e+05   8.84e+02  -1.08e+01  1.29e+01        1    2.74e-04    9.88e-03
  13  1.601285e+09   -1.59e+09    3.93e+05   5.77e+02  -2.47e+03  3.23e+00        1    3.49e-04    1.05e-02
  14  6.374333e+06    3.16e+05    4.11e+05   1.92e+02   1.12e+00  9.68e+00        1    6.33e-04    1.14e-02
  15  5.985169e+06    3.89e+05    1.60e+06   3.16e+02   7.17e-01  1.05e+01        1    7.66e-04    1.25e-02
  16  4.864876e+06    1.12e+06    2.78e+05   2.13e+02   1.17e+00  3.16e+01        1    6.26e-04    1.32e-02
  17  4.497102e+06    3.68e+05    8.45e+05   3.91e+02   4.41e-01  3.16e+01        1    6.38e-04    1.42e-02
  18  4.218995e+06    2.78e+05    3.98e+05   3.12e+02   4.43e-01  3.15e+01        1    7.15e-04    1.50e-02
  19  4.120921e+06    9.81e+04    9.27e+04   3.49e+02   7.55e-01  3.63e+01        1    7.22e-04    1.58e-02
  20  4.602581e+06   -4.82e+05    9.27e+04   4.92e+02  -7.51e+00  1.81e+01        1    3.33e-04    1.63e-02
  21  4.080593e+06    4.03e+04    5.43e+04   2.72e+02   1.06e+00  5.44e+01        1    7.30e-04    1.74e-02
  22  4.029644e+06    5.09e+04    2.21e+05   6.65e+02   8.69e-01  9.12e+01        1    7.73e-04    1.82e-02
  23  3.991119e+06    3.85e+04    2.64e+05   1.01e+03   8.74e-01  1.57e+02        1    7.79e-04    1.93e-02
  24  3.969039e+06    2.21e+04    2.21e+05   1.11e+03   8.23e-01  2.14e+02        1    6.81e-04    2.02e-02
  25  3.959024e+06    1.00e+04    7.61e+04   1.17e+03   8.29e-01  2.99e+02        1    6.91e-04    2.10e-02
  26  3.955334e+06    3.69e+03    1.68e+04   1.64e+03   8.11e-01  3.93e+02        1    6.40e-04    2.18e-02
  27  3.953939e+06    1.39e+03    3.35e+03   2.22e+03   8.20e-01  5.33e+02        1    6.28e-04    2.26e-02
  28  3.953339e+06    6.00e+02    1.22e+03   2.37e+03   8.40e-01  7.77e+02        1    6.86e-04    2.35e-02
  29  3.952988e+06    3.51e+02    6.41e+02   2.46e+03   8.37e-01  1.12e+03        1    8.45e-04    2.44e-02
  30  3.952734e+06    2.54e+02    3.17e+02   2.45e+03   8.34e-01  1.59e+03        1    6.99e-04    2.55e-02
  31  3.952528e+06    2.06e+02    1.89e+02   2.47e+03   8.25e-01  2.20e+03        1    6.50e-04    2.62e-02
  32  3.952359e+06    1.69e+02    1.17e+02   2.57e+03   8.21e-01  2.99e+03        1    7.71e-04    2.71e-02
  33  3.952223e+06    1.37e+02    8.09e+01   2.74e+03   8.22e-01  4.08e+03        1    7.05e-04    2.80e-02
  34  3.952113e+06    1.10e+02    8.33e+01   3.00e+03   8.26e-01  5.65e+03        1    7.68e-04    2.90e-02
  35  3.952024e+06    8.89e+01    9.15e+01   3.26e+03   8.33e-01  8.03e+03        1    6.43e-04    3.01e-02
  36  3.951950e+06    7.44e+01    1.09e+02   3.67e+03   8.33e-01  1.14e+04        1    7.37e-04    3.11e-02
  37  3.951888e+06    6.16e+01    1.15e+02   4.13e+03   8.37e-01  1.64e+04        1    6.83e-04    3.21e-02
  38  3.951836e+06    5.17e+01    1.24e+02   4.69e+03   8.42e-01  2.42e+04        1    7.78e-04    3.35e-02
  39  3.951792e+06    4.43e+01    1.41e+02   5.44e+03   8.43e-01  3.58e+04        1    6.88e-04    3.46e-02
  40  3.951755e+06    3.68e+01    1.34e+02   6.23e+03   8.60e-01  5.72e+04        1    7.06e-04    3.57e-02
  41  3.951722e+06    3.35e+01    1.85e+02   7.66e+03   8.60e-01  9.12e+04        1    6.43e-04    3.67e-02
  42  3.951692e+06    2.99e+01    2.15e+02   9.20e+03   8.58e-01  1.44e+05        1    6.08e-04    3.75e-02
  43  3.951666e+06    2.59e+01    1.77e+02   1.07e+04   8.62e-01  2.32e+05        1    5.69e-04    3.84e-02
  44  3.951643e+06    2.31e+01    2.32e+02   1.27e+04   8.62e-01  3.74e+05        1    6.14e-04    3.93e-02
  45  3.951623e+06    2.03e+01    2.76e+02   1.47e+04   8.63e-01  6.04e+05        1    6.65e-04    4.01e-02
  46  3.951605e+06    1.78e+01    3.48e+02   1.70e+04   8.64e-01  9.82e+05        1    6.93e-04    4.11e-02
  47  3.951589e+06    1.56e+01    4.40e+02   1.94e+04   8.66e-01  1.61e+06        1    6.15e-04    4.19e-02
  48  3.951575e+06    1.36e+01    5.62e+02   2.19e+04   8.68e-01  2.68e+06        1    6.86e-04    4.27e-02
  49  3.951564e+06    1.17e+01    7.24e+02   2.46e+04   8.71e-01  4.53e+06        1    9.08e-04    4.38e-02
  50  3.951554e+06    9.88e+00    9.35e+02   2.73e+04   8.75e-01  7.82e+06        1    6.47e-04    4.47e-02
Ceres Solver Report: Iterations: 51, Initial cost: 2.186829e+07, Final cost: 3.951554e+06, Termination: NO_CONVERGENCE
RMSE: 125.723

===== BA with Huber =====
iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  3.458088e+04    0.00e+00    1.05e+04   0.00e+00   0.00e+00  1.00e+04        0    4.48e-04    8.73e-04
   1  3.437237e+04    2.09e+02    1.46e+03   0.00e+00   1.30e+00  3.00e+04        1    1.09e-03    2.21e-03
   2  3.423587e+04    1.37e+02    1.13e+03   9.98e-01   1.78e+00  9.00e+04        1    7.72e-04    3.58e-03
   3  3.400336e+04    2.33e+02    9.65e+02   2.87e+00   1.48e+00  2.70e+05        1    7.87e-04    4.74e-03
   4  3.371180e+04    2.92e+02    7.20e+02   1.26e+01   1.03e+00  8.10e+05        1    6.67e-04    5.53e-03
   5  3.346516e+04    2.47e+02    7.92e+02   1.03e+02   6.37e-01  8.27e+05        1    7.58e-04    6.54e-03
   6  3.330819e+04    1.57e+02    6.80e+02   3.89e+03   3.54e-01  8.07e+05        1    7.31e-04    7.80e-03
   7  3.315351e+04    1.55e+02    1.87e+04   1.41e+04   5.75e-01  8.10e+05        1    6.95e-04    8.73e-03
   8  3.306804e+04    8.55e+01    4.06e+03   3.92e+03   3.64e-01  7.94e+05        1    7.00e-04    9.54e-03
   9  3.303941e+04    2.86e+01    3.48e+04   1.62e+04   2.48e-01  7.04e+05        1    8.07e-04    1.07e-02
  10  3.285352e+04    1.86e+02    2.05e+04   3.03e+04   1.08e+00  2.11e+06        1    6.85e-04    1.16e-02
  11  3.279611e+04    5.74e+01    3.81e+04   3.59e+03   7.09e-01  2.28e+06        1    7.13e-04    1.24e-02
  12  3.266054e+04    1.36e+02    2.74e+04   3.28e+03   1.17e+00  6.83e+06        1    6.60e-04    1.32e-02
  13  3.256993e+04    9.06e+01    1.72e+04   5.55e+03   1.34e+00  2.05e+07        1    7.28e-04    1.41e-02
  14  3.252382e+04    4.61e+01    1.69e+04   1.19e+04   1.27e+00  6.15e+07        1    8.01e-04    1.52e-02
  15  3.250368e+04    2.01e+01    2.43e+04   2.17e+04   8.23e-01  8.42e+07        1    7.96e-04    1.64e-02
  16  3.247707e+04    2.66e+01    2.04e+04   1.48e+04   1.01e+00  2.53e+08        1    7.79e-04    1.75e-02
  17  3.248113e+04   -4.06e+00    2.04e+04   3.16e+04  -2.07e-01  1.26e+08        1    3.11e-04    1.79e-02
  18  3.245748e+04    1.96e+01    2.03e+04   1.54e+04   9.99e-01  3.79e+08        1    8.18e-04    1.89e-02
  19  3.246806e+04   -1.06e+01    2.03e+04   3.37e+04  -6.02e-01  1.90e+08        1    3.57e-04    1.95e-02
  20  3.244144e+04    1.60e+01    2.21e+04   1.66e+04   9.13e-01  4.35e+08        1    6.39e-04    2.05e-02
  21  3.244195e+04   -5.12e-01    2.21e+04   2.78e+04  -2.99e-02  2.17e+08        1    3.19e-04    2.15e-02
  22  3.242364e+04    1.78e+01    1.93e+04   1.36e+04   1.04e+00  6.52e+08        1    7.44e-04    2.24e-02
  23  3.243337e+04   -9.73e+00    1.93e+04   3.26e+04  -7.28e-01  3.26e+08        1    4.20e-04    2.35e-02
  24  3.241143e+04    1.22e+01    2.17e+04   1.61e+04   9.13e-01  7.48e+08        1    7.31e-04    2.43e-02
  25  3.241300e+04   -1.56e+00    2.17e+04   2.84e+04  -1.21e-01  3.74e+08        1    3.00e-04    2.54e-02
  26  3.239821e+04    1.32e+01    1.90e+04   1.40e+04   1.02e+00  1.12e+09        1    6.04e-04    2.63e-02
  27  3.240644e+04   -8.23e+00    1.90e+04   3.40e+04  -8.53e-01  5.61e+08        1    3.82e-04    2.70e-02
  28  3.238958e+04    8.63e+00    2.11e+04   1.68e+04   8.95e-01  1.11e+09        1    7.26e-04    2.80e-02
  29  3.238695e+04    2.63e+00    3.22e+04   2.58e+04   2.98e-01  1.04e+09        1    6.80e-04    2.91e-02
  30  3.237595e+04    1.10e+01    2.15e+04   1.64e+04   9.33e-01  2.96e+09        1    6.68e-04    2.99e-02
  31  3.237954e+04   -3.59e+00    2.15e+04   3.71e+04  -6.40e-01  1.48e+09        1    2.53e-04    3.04e-02
  32  3.237076e+04    5.19e+00    2.01e+04   1.84e+04   9.27e-01  3.92e+09        1    7.26e-04    3.14e-02
  33  3.237276e+04   -2.00e+00    2.01e+04   3.78e+04  -4.86e-01  1.96e+09        1    3.03e-04    3.20e-02
  34  3.236666e+04    4.10e+00    1.89e+04   1.88e+04   9.95e-01  5.88e+09        1    6.54e-04    3.29e-02
  35  3.237015e+04   -3.49e+00    1.89e+04   4.34e+04  -1.16e+00  2.94e+09        1    2.90e-04    3.33e-02
  36  3.236388e+04    2.78e+00    2.12e+04   2.16e+04   9.29e-01  8.01e+09        1    6.84e-04    3.41e-02
  37  3.236591e+04   -2.03e+00    2.12e+04   4.34e+04  -7.93e-01  4.00e+09        1    3.73e-04    3.46e-02
  38  3.236132e+04    2.56e+00    2.11e+04   2.16e+04   1.00e+00  1.20e+10        1    7.00e-04    3.55e-02
  39  3.236405e+04   -2.73e+00    2.11e+04   4.78e+04  -1.42e+00  6.00e+09        1    2.92e-04    3.61e-02
  40  3.235946e+04    1.86e+00    2.38e+04   2.38e+04   9.67e-01  1.80e+10        1    6.15e-04    3.69e-02
  41  3.236259e+04   -3.13e+00    2.38e+04   5.06e+04  -1.93e+00  9.01e+09        1    3.22e-04    3.74e-02
  42  3.235796e+04    1.50e+00    2.75e+04   2.52e+04   9.29e-01  2.44e+10        1    7.42e-04    3.85e-02
  43  3.235998e+04   -2.02e+00    2.75e+04   4.69e+04  -1.47e+00  1.22e+10        1    2.97e-04    3.93e-02
  44  3.235662e+04    1.34e+00    2.92e+04   2.34e+04   9.74e-01  3.66e+10        1    6.41e-04    4.02e-02
  45  3.236032e+04   -3.70e+00    2.92e+04   4.92e+04  -3.53e+00  1.83e+10        1    2.72e-04    4.06e-02
  46  3.235583e+04    7.91e-01    4.01e+04   2.46e+04   7.55e-01  2.11e+10        1    6.07e-04    4.14e-02
  47  3.235484e+04    9.88e-01    3.93e+04   1.92e+04   9.59e-01  6.33e+10        1    5.75e-04    4.22e-02
  48  3.235830e+04   -3.46e+00    3.93e+04   4.25e+04  -4.96e+00  3.17e+10        1    3.02e-04    4.27e-02
  49  3.235449e+04    3.48e-01    6.21e+04   2.12e+04   4.99e-01  3.17e+10        1    7.20e-04    4.37e-02
  50  3.235378e+04    7.15e-01    5.75e+04   1.49e+04   8.70e-01  5.32e+10        1    6.76e-04    4.46e-02
Ceres Solver Report: Iterations: 51, Initial cost: 3.458088e+04, Final cost: 3.235378e+04, Termination: NO_CONVERGENCE
RMSE: 252.662

===== Outlier Filtering =====
Observations: 500 -> 412
iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  4.013538e+02    0.00e+00    2.47e+04   0.00e+00   0.00e+00  1.00e+04        0    3.67e-04    7.70e-04
   1  3.411137e+02    6.02e+01    1.94e+05   0.00e+00   1.04e+00  3.00e+04        1    8.65e-04    1.80e-03
   2  4.370727e+02   -9.60e+01    1.94e+05   1.54e+04  -2.00e+00  1.50e+04        1    3.00e-04    2.58e-03
   3  3.280579e+02    1.31e+01    3.52e+05   8.71e+03   3.73e-01  1.48e+04        1    5.07e-04    3.25e-03
   4  3.014199e+02    2.66e+01    3.33e+05   8.45e+03   6.33e-01  1.50e+04        1    6.36e-04    4.19e-03
   5  2.779264e+02    2.35e+01    2.77e+05   7.74e+03   6.90e-01  1.59e+04        1    5.85e-04    5.13e-03
   6  2.592852e+02    1.86e+01    2.35e+05   7.14e+03   7.13e-01  1.72e+04        1    5.45e-04    6.08e-03
   7  2.434548e+02    1.58e+01    2.03e+05   6.63e+03   7.60e-01  2.01e+04        1    5.41e-04    7.05e-03
   8  2.307131e+02    1.27e+01    1.83e+05   6.27e+03   7.77e-01  2.42e+04        1    6.59e-04    7.92e-03
   9  2.206938e+02    1.00e+01    1.59e+05   5.59e+03   8.03e-01  3.11e+04        1    6.66e-04    9.01e-03
  10  2.136451e+02    7.05e+00    1.29e+05   4.88e+03   8.29e-01  4.35e+04        1    5.58e-04    9.95e-03
  11  2.087576e+02    4.89e+00    9.58e+04   4.13e+03   8.81e-01  7.81e+04        1    6.68e-04    1.13e-02
  12  2.058755e+02    2.88e+00    6.52e+04   3.34e+03   9.18e-01  1.88e+05        1    6.90e-04    1.21e-02
  13  2.044417e+02    1.43e+00    2.53e+04   2.03e+03   1.09e+00  5.63e+05        1    6.71e-04    1.31e-02
  14  2.040352e+02    4.07e-01    5.44e+03   8.63e+02   1.32e+00  1.69e+06        1    6.71e-04    1.42e-02
  15  2.039235e+02    1.12e-01    1.57e+03   3.48e+02   1.54e+00  5.07e+06        1    6.44e-04    1.53e-02
  16  2.038868e+02    3.68e-02    9.67e+02   1.58e+02   1.58e+00  1.52e+07        1    8.33e-04    1.68e-02
  17  2.038721e+02    1.46e-02    5.72e+02   7.34e+01   1.67e+00  4.56e+07        1    5.61e-04    1.80e-02
  18  2.038652e+02    6.88e-03    3.74e+02   3.87e+01   1.70e+00  1.37e+08        1    5.57e-04    1.90e-02
  19  2.038617e+02    3.51e-03    2.56e+02   2.21e+01   1.72e+00  4.10e+08        1    5.76e-04    1.97e-02
  20  2.038599e+02    1.88e-03    1.80e+02   1.34e+01   1.73e+00  1.23e+09        1    6.62e-04    2.08e-02
  21  2.038589e+02    9.39e-04    1.42e+02   8.68e+00   1.68e+00  3.69e+09        1    6.37e-04    2.16e-02
  22  2.038584e+02    4.75e-04    1.14e+02   5.90e+00   1.74e+00  1.11e+10        1    5.21e-04    2.24e-02
  23  2.038582e+02    2.59e-04    9.18e+01   3.44e+00   1.74e+00  3.32e+10        1    4.93e-04    2.31e-02
Ceres Solver Report: Iterations: 24, Initial cost: 4.013538e+02, Final cost: 2.038582e+02, Termination: CONVERGENCE
Final RMSE: 1.09353

从该输出结果可以观察到几个非常典型的现象:

首先,在未使用鲁棒核函数的 BA 中,优化过程虽然能够降低整体误差,但最终 RMSE 仍然维持在 百像素级别。这是因为外点的重投影误差非常大,在最小二乘框架下会被平方放大,从而主导整个优化目标函数,使优化结果偏离真实解。

随后,在引入 Huber Loss 后,优化结果出现了一定程度的改善。Huber 核函数会在残差较小时保持二次损失,而在残差超过阈值后转为线性增长,从而有效降低外点的影响权重。然而需要注意的是,仅依赖鲁棒核函数通常仍然不足以完全消除外点的影响,因此 RMSE 仍然保持在较高水平。

接下来,通过计算当前模型下的重投影误差,对观测进行简单的阈值过滤(例如 5 像素),可以将明显不符合几何约束的观测点剔除。在本实验中,观测数量从 500 降低到 412,说明约 20% 的观测被过滤。这是因为随机生成的外点往往具有极大的投影误差,因此能够被可靠地识别并剔除。

最后,在清洗后的数据上再次执行 BA 优化,系统最终收敛到:Final RMSE ≈ 1.09 pixel。这一误差已经接近我们在数据生成阶段设定的像素噪声水平(σ≈1 pixel),说明优化结果已经基本恢复了真实的几何结构。

3. 问题

通过上述实验可以看到,尽管最终能够获得较好的优化结果,但在整个过程中仍然出现了一些值得注意的现象。例如:

  • 外点比例仅为 10%,但最终被剔除的观测却高达 20%
  • 引入 Huber Loss 后,RMSE 反而 上升

这些现象在实际 BA 系统中其实非常常见,其背后反映了鲁棒优化与外点处理的一些重要特点。下面分别进行分析。

3.1 剔除外点

在本代码实现中,外点的真实比例仅为 10%,但在外点过滤阶段,却有大约 20% 的观测被剔除。这种“过度剔除”的原因在于:外点不仅会自身产生巨大误差,还会拖累整个优化结果。

在没有鲁棒机制的情况下,BA 的优化目标是最小化:

\[\sum_i r_i^2 \]

由于外点的残差往往达到数百像素,平方后会被极度放大,因此优化过程会试图同时“兼顾”这些错误观测。结果是:

  • 相机位姿被拉偏
  • 三维点位置被扭曲
  • 许多原本正确的观测也会产生较大的重投影误差

因此,在进行外点过滤时,实际上被删除的不仅仅是原始外点,还包括一些由于模型被污染而产生较大残差的“次级异常观测”。换句话说:外点会污染模型,而污染后的模型又会制造更多大残差观测。在本例的实现中,使用的是固定的外点阈值,很容易就会造成这些其实是内点的“次级异常观测”也被剔除。那么,如何如何避免过度剔除呢?

3.1.1 统计方法进行外点检测

由于使用统一使用固定阈值,可能会造成在高质量数据中可能误删大量正常观测,在噪声较大的数据中又可能保留大量外点。因此,许多视觉系统会采用基于 统计分布的自适应外点检测 方法,根据当前残差分布自动确定合理阈值。

1. Median Absolute Deviation

Median Absolute Deviation(MAD)是一种常见的鲁棒统计量,在视觉系统中被广泛用于外点检测。

首先计算所有观测的重投影误差:

\[r_1, r_2, ..., r_n \]

然后计算残差的 中位数

\[m = \text{median}(r) \]

接下来计算每个残差与中位数之间的绝对偏差:

\[|r_i - m| \]

再对这些偏差取中位数:

\[MAD = \text{median}(|r_i - m|) \]

最后根据经验公式得到外点阈值:

\[threshold = k \cdot 1.4826 \cdot MAD \]

其中:

  • (1.4826) 是将 MAD 转换为高斯标准差的系数
  • (k) 通常取 2–3

于是可以得到外点判断规则:

\[|r_i - m| > threshold \Rightarrow \text{outlier} \]

MAD 方法的优势在于由于使用的是中位数而不是均值,不会被少量极端外点严重影响。因此即使存在大量大残差,阈值仍然能够稳定反映 数据主体部分的统计特性

2. χ² 检验

在束平差中,我们通常假设图像特征点的定位误差是由高斯噪声引起的。也就是说,每个像素坐标的测量误差可以看作是从均值为 0、标准差为 \(\sigma\) 的正态分布中随机采样得到的:

\[\text{noise} \sim N(0, \sigma^2) \]

对于一个二维的重投影残差 \(\mathbf{r} = [r_x, r_y]^\top\),如果这个残差确实只由上述高斯噪声引起(即它是内点),那么它的归一化平方长度

\[\frac{\|\mathbf{r}\|^2}{\sigma^2} = \frac{r_x^2 + r_y^2}{\sigma^2} \]

在统计上会服从自由度为 2 的卡方分布(记作 \(\chi^2(2)\))。这是概率论中的一个经典结论:两个独立的标准正态变量的平方和,就服从 \(\chi^2(2)\)

利用这一性质,我们可以设定一个统计意义上的合理阈值来判断某个观测是否“太离谱”而可能是外点。例如,在 95% 的置信水平下,\(\chi^2(2)\) 分布的临界值约为 5.99。这意味着:如果一个观测确实是内点(仅受高斯噪声影响),那么有 95% 的概率,其 \(\frac{\|\mathbf{r}\|^2}{\sigma^2}\) 会小于等于 5.99;只有约 5% 的情况会偶然超过这个值。因此,我们把超过该阈值的观测视为统计上不太可能来自高斯噪声,从而判定为外点:

\[\frac{\|\mathbf{r}\|^2}{\sigma^2} > 5.99 \quad \Rightarrow \quad \text{outlier} \]

进一步推论,如果已知特征点定位的噪声标准差为 \(\sigma = 1\) 像素(这是一个常见的经验值),那么对应的重投影误差阈值就是:

\[\|\mathbf{r}\| > \sqrt{5.99} \approx 2.45 \text{ 像素} \]

换句话说,在 1 像素噪声水平下,超过 2.45 像素的重投影误差就有理由被怀疑是外点。与简单使用固定阈值(比如一律用 5 像素)相比,χ² 检验的优势在于:

  • 显式考虑了噪声水平 \(\sigma\),当标定更精确或图像质量更高(\(\sigma\) 更小)时,阈值自动收紧;
  • 基于概率模型,具有明确的统计意义,避免了“拍脑袋”设阈值的随意性。

正因为这些优点,χ² 检验被广泛应用于 SFM、SLAM 等系统中,作为 BA 前后外点过滤的重要依据。

3. 残差分布分析

在实际工程系统中,束平差优化后的重投影残差通常呈现出一种典型的混合分布形态

  • 主体部分集中在较小误差范围内(如 0–2 像素),近似服从高斯分布,对应于正确的 2D-3D 匹配;
  • 少量观测则散布在较大误差区域(如 >5 像素),形成明显的长尾(long tail),主要由误匹配、动态物体、遮挡或低纹理区域的不稳定特征引起。

这种“尖峰 + 长尾”的分布特征可以通过绘制残差直方图直观展现:

残差 (像素) | 观测数量
------------|----------
0 – 1       | ████████████████ (大量)
1 – 2       | ██████████
2 – 3       | ████
3 – 5       | ██
> 5         | █ (稀疏但显著)

通过分析该分布,我们可以:

  1. 定性判断 BA 质量:若长尾过重(如 >10% 的观测残差 >5 像素),说明前端匹配质量较差,可能需要加强 RANSAC 或特征筛选;
  2. 辅助设定外点阈值:理想情况下,阈值应设在“主体分布”与“长尾”之间的谷底(valley),以最大化内点保留率并最小化外点污染;
  3. 验证噪声模型假设:若主体部分明显偏离高斯(如偏斜、多峰),可能暗示存在系统性误差(如未校正的畸变、时序不同步等)。

因此,在关键系统(如自动驾驶、测绘)中,可以在 BA 后自动绘制残差直方图或计算统计指标(如中位数、MAD、95% 分位数),作为重建质量的诊断工具。

3.1.2 空间均衡外点剔除

仅根据误差大小进行外点过滤,还可能带来另一个问题:观测的空间分布被破坏

例如,在某些区域由于纹理较弱或匹配不稳定,观测误差可能整体偏大。如果直接按照误差阈值删除观测,这些区域的点可能会被全部剔除,例如原始观测分布:

+--------------------+
|  ●  ●   ●   ●   ●  |
|  ●  ●   ●   ●   ●  |
|  ●  ●   ●   ●   ●  |
|  ●  ●   ●   ●   ●  |
+--------------------+

过滤后:

+--------------------+
|  ●  ●              |
|  ●  ●              |
|                    |
|                    |
+--------------------+

此时观测点集中在图像的一小部分区域,可能会导致如下问题:

  1. 结构退化:例如只有图像左半部分有点,相机位姿约束变弱。
  2. 局部最优:优化会倾向于只拟合某一部分数据,例如左侧结构正确,右侧结构漂移。
  3. 深度不稳定:三角化需要视差和空间分布。

因此,许多视觉系统会在外点过滤中加入 空间均衡策略(Spatially Balanced Filtering)

一种常见做法是将图像划分为多个网格区域,例如:

+----+----+----+
| G1 | G2 | G3 |
+----+----+----+
| G4 | G5 | G6 |
+----+----+----+
| G7 | G8 | G9 |
+----+----+----+

然后在每个网格内部单独进行外点筛选,例如:按重投影误差进行排序,保留误差最小的前 K 个观测。伪代码如下:

for each grid cell:
    collect observations in cell
    sort by reprojection error
    keep first K observations

这样就可以保证每个区域都保留一定数量的观测,从而使得整体空间分布更加均匀,避免结构信息完全丢失。

在实际视觉系统中,空间均衡策略被广泛使用。例如:

  • ORB-SLAM:通过图像网格限制每个区域的特征数量
  • 视觉里程计系统:使用 feature bucketing 保证特征均匀分布
  • SFM 系统:在匹配或 BA 过滤阶段限制每个区域的观测数量

这些方法的核心思想就是外点剔除不仅要考虑误差大小,还需要关注观测的空间分布。保持均匀的观测分布通常比单纯增加观测数量更有利于 BA 的稳定收敛。

3.2 鲁棒核函数

在实验结果中还可以观察到一个看似反直觉的现象:

BA without robust : RMSE ≈ 125
BA with Huber     : RMSE ≈ 253

也就是说,在引入 Huber Loss 后,整体 RMSE 反而略微增大。这一现象其实并不奇怪,其原因在于 RMSE 的统计方式与鲁棒优化的目标并不完全一致。标准 BA 优化的是:

\[\min \sum r_i^2 \]

而鲁棒 BA 优化的是:

\[\min \sum \rho(r_i^2) \]

其中 \(\rho(\cdot)\) 为鲁棒核函数。当残差较小时:

\[\rho(r^2) \approx r^2 \]

当残差较大时:

\[\rho(r^2) \approx |r| \]

这意味着 大残差会被显著降权。因此,在鲁棒 BA 中, 外点不会再强烈影响优化,优化主要关注大多数正常观测。但如果我们仍然使用包含所有外点的原始 RMSE 进行评估,那么被降权的外点依然存在,并且这些外点仍然具有很大的残差,因此 RMSE 可能 看起来更大。换句话说:鲁棒优化的目标是恢复正确的几何结构,而不是最小化所有观测的 RMSE。

在视觉 BA 中,常见的鲁棒核函数包括:

  1. Huber Loss:Huber Loss是最常见、最稳定的鲁棒核函数,其形式为:$ \rho(r) = \begin{cases} \frac{1}{2}r^2 & |r| \le \delta \ \delta(|r| - \frac{1}{2}\delta) & |r| > \delta \end{cases} $。它的特点是在残差较小时,能保持 二次损失;而在残差较大时,就成为了 线性损失。因此既保持了最小二乘的效率,又具有一定的鲁棒性。
  2. Cauchy Loss:Cauchy Loss 的形式为:\(\rho(r^2) = c^2 \log\left(1 + \frac{r^2}{c^2}\right)\)。它的特点是大残差会被 更强烈压制,对极端外点更鲁棒,但也可能带来 收敛速度下降 的问题。

在实际的工程实践中,常见的经验是:先使用鲁棒核函数稳健 BA,然后结合过滤机制去除外点,最后在最终 BA 中关闭鲁棒核函数进行精修。并且这个过程可以多次迭代:

RANSAC
 ↓
BA + robust
 ↓
filter
 ↓
BA
 ↓
filter
 ↓
BA

通过这种策略,即使在存在大量误匹配的情况下,系统仍然能够稳定地恢复准确的相机位姿和三维结构。

4. 挑战

前面的实践中讨论了外点鲁棒性这一核心问题,并通过 Huber Loss 与残差滤波展示了典型修复流程。然而,在真实的 SFM、SLAM 或三维重建系统中,束平差还面临一系列无法在简单合成数据中复现、但对系统稳定性与精度至关重要的工程挑战。

4.1 零空间自由度(Gauge Freedom)

束平差优化的是相机位姿与 3D 点的相对几何关系,而整个场景在 3D 空间中可进行任意刚体变换(旋转 + 平移)而不改变重投影误差。这意味着优化问题存在 6 个不可观测自由度(3 旋转 + 3 平移),导致 Hessian 矩阵天然奇异。

  • 现象:若不固定任何参数,Ceres 可能报错 The Jacobian is rank deficient,或优化过程震荡不收敛。
  • 解决方案
    • 固定第一帧相机:最常用方法,将第一个相机的位姿设为世界坐标系原点(即 SetParameterBlockConstant);
    • 固定一个 3D 点 + 相机朝向:适用于无全局参考的纯运动恢复;
    • 施加软约束(Soft Prior):在某些 SLAM 系统中,通过先验信息(如 IMU)提供弱约束,而非硬固定。
  • 注意:在增量式 SFM 中,每次新增相机后都需确保当前子图有足够约束,避免局部零空间。

4.2 相机内参与畸变模型

在本文的案例实现中,假设相机内参 \(\mathbf{K}\) 已知且图像无畸变。但真实相机往往存在:

  • 标定误差:内参(\(f_x, f_y, c_x, c_y\))不精确;

  • 镜头畸变:径向/切向畸变导致针孔模型失效。

  • 后果:即使所有匹配正确,重投影误差仍系统性偏大,BA 无法收敛到高精度。

  • 解决方案

    1. 显式去畸变:在 BA 前将像素点反投影到无畸变平面(OpenCV 的 undistortPoints);
    2. 联合优化内参与畸变:将 \(f_x, f_y, k_1, k_2, p_1, p_2\) 等作为额外变量加入 BA,并施加合理正则化(如内参变化不应过大);
    3. 使用更复杂相机模型:如 Kannala-Brandt(鱼眼)、双球模型(wide-angle)等。

4.3 大规模问题的可扩展性

当相机数 \(N > 100\)、3D 点数 \(M > 10^4\) 时,即使使用 SPARSE_SCHUR,内存和计算时间仍可能成为瓶颈。

  • 挑战
    • Schur 补矩阵 \(\mathbf{B} - \mathbf{E}\mathbf{C}^{-1}\mathbf{E}^\top\) 虽为 \(6N \times 6N\),但通常稠密,Cholesky 分解复杂度 \(O(N^3)\)
    • 内存消耗随 \(N^2\) 增长,易超限。
  • 解决方案
    • 滑动窗口 BA(Local BA):仅优化最近 \(K\) 帧(如 K=10)及其共视 3D 点,其余固定;
    • 关键帧机制:只保留信息量大的帧参与全局 BA;
    • 迭代求解器:使用 ITERATIVE_SCHUR(预条件共轭梯度法),内存占用低,适合超大规模问题;
    • 分布式 BA:如 Theia、OpenMVG 支持多线程/多机并行。
  • 典型应用:ORB-SLAM 使用 Local BA 实现实时性;COLMAP 在全局 BA 中结合关键帧与稀疏求解器。

4.4 初始值敏感性与分层优化

BA 是非凸优化,对初始值敏感。若前端(PnP/三角化)误差过大(如 >50 像素),BA 可能陷入局部极小。可能的对策是:

  • 分层优化(Coarse-to-Fine)
    1. 先固定相机,仅优化 3D 点(快速收敛);
    2. 再联合优化相机与点;
  • 阻尼因子控制:Ceres 的 LM 算法默认已处理,但可手动调整 initial_trust_region_radius
  • 多尺度 BA:先在低分辨率图像上优化,再迁移到高分辨率。

4.5 动态物体与非刚性场景

BA 假设场景静态。若存在移动物体(行人、车辆)、非刚性变形(人脸、旗帜),其对应点会表现为“伪外点”。

  • 影响:即使匹配正确,残差仍很大,被误判为外点,或污染整体结构。
  • 解决方案
    • 前端动态分割:使用光流、语义分割或几何一致性检测动态区域;
    • 多模型 BA:为不同运动物体分配独立运动模型(如 SE(3) + 刚性片段);
    • 鲁棒核函数 + 时空一致性:结合时间连续性判断是否为真外点。
  • 注意:这类问题通常需在 BA 之前解决,而非依赖 BA 自身鲁棒性。

4.6 系统集成:何时触发

BA 计算开销大,不能每帧都执行。可能的对策是:

  • 局部 BA:新增关键帧后,优化该帧 + 共视关键帧 + 共视点;
  • 全局 BA:闭环检测成功后触发,用于纠正累积漂移;
  • 异步 BA:在后台线程运行,不影响主线程实时性。

4.7 深度退化(Degenerate Geometry)

在某些几何配置下,BA 的参数会变得 弱可观测甚至不可观测。常见的退化场景包括:

4.7.1 纯旋转运动

如果相机只发生旋转而没有平移。此时所有特征点的视差接近于零,三角化深度无法确定。结果导致3D 点深度发散、BA 难以稳定收敛、以及相机位姿估计不稳定。

解决方案就是检测视差(parallax),若视差过小,就暂停三角化,只优化旋转。这也是为什么很多 SLAM 系统要求 minimum parallax才会三角化。

4.7.2 近平面场景

如果大部分特征点都位于同一平面,例如:建筑墙面、地面、书页,此时系统更适合用 单应矩阵(Homography) 而不是完整的 3D 模型。因为在这种情况下可能出现深度估计不稳定和 3D 点漂移的问题。具体来说,可在初始化阶段通过 单应矩阵(H)与基础矩阵(E)的模型选择机制(例如 Nister 的五点法 vs 四点法得分比较),仅在 E 模型显著优于 H 模型时建立 3D 结构——这正是 ORB-SLAM 初始化阶段的做法。

4.8 观测权重不一致(Heteroscedastic Noise)

在理想情况下,BA 假设所有观测具有相同噪声 $\sigma_i = \sigma $。但真实系统中,观测质量差异很大,例如:

观测来源 误差
中心区域特征
边缘畸变区域
远距离点
运动模糊

如果所有观测使用同一权重,低质量观测会污染优化结果。解决方法是可以引入 加权 BA(Weighted BA)

\[\min \sum_i w_i r_i^2 \]

其中:

\[w_i = \frac{1}{\sigma_i^2} \]

权重可以根据特征匹配置信度、角点响应值(Harris / FAST score)、视差大小或者深度不确定性进行估计。一些系统甚至会使用:weight = cos(view_angle)来降低斜视角观测的权重。

4.9 参数尺度问题(Parameter Scaling)

BA 中不同变量的量纲差异很大:

参数 量级
rotation ~1
translation ~1–10
3D point ~1–100
focal length ~1000

如果直接优化[rotation, translation, point, focal]这样的值,Hessian 的条件数会变差,可能会造成 LM 步长异常、收敛速度慢以及数值不稳定的问题。解决方法如下:

  1. 参数归一化。例如point /= scene_scale,使场景尺寸接近 1–10
  2. 使用合理的参数化。例如不直接使用 rotation 参数,而是用李代数或者四元数。
  3. Solver 内部 scaling。例如 Ceres 内部也会进行Jacobian column scaling改善数值稳定性。

4.10 数据关联漂移(Data Association Drift)

在束平差中,每个观测都隐含着一个关键假设:图像中的 2D 特征点与同一个真实 3D 点正确对应。然而在真实系统中,这种数据关联往往来自特征匹配或特征跟踪,而这些过程不可避免地会产生误匹配。例如,重复纹理、视角变化、遮挡、动态物体或描述子相似度不足,都可能导致特征在不同帧之间被错误关联。随着系统运行时间增加,这类错误匹配可能在特征轨迹(track)中逐渐累积并传播,形成所谓的数据关联漂移。与随机外点不同,这种误差往往具有持续性:一个 3D 点可能在某些帧中对应真实物体 A,而在另一些帧中被误匹配到物体 B,从而破坏 BA 所依赖的几何一致性,导致三维结构被拉扯、相机位姿偏移,甚至在局部区域形成错误但“自洽”的几何结构。

由于数据关联漂移往往不会产生极端大的残差,因此仅依赖鲁棒核函数通常难以彻底解决。工程系统通常通过多层机制抑制这一问题,例如:在前端进行更严格的几何验证(如极线约束或RANSAC筛选匹配),在中间层通过多视图一致性检测特征轨迹长度过滤删除不稳定的观测(例如只保留被多帧观测的点),并在系统层面引入回环检测或图结构一致性检查以纠正长期漂移。总体而言,束平差本身主要用于精化已经基本正确的几何结构,而可靠的数据关联管理才是保证 BA 能够稳定工作的关键前提。

4.11 总结

束平差虽是“精修”工具,但其工程落地远不止“调用 Ceres”那么简单。一个健壮的 BA 系统需要综合考虑:

  • 数据质量(外点、噪声、分布)
  • 模型完整性(内参、畸变、自由度)
  • 计算效率(稀疏性、规模、求解器)
  • 系统集成(触发时机、失败回滚、动态处理)

掌握 BA 的原理只是起点;真正的挑战在于如何在复杂、不完美的现实条件下,灵活组合上述要素,构建稳定可靠的系统。

上一篇 | 目录