Skip to content

光束法平差与 Ceres 优化库实践

在计算机视觉、视觉 SLAM(同步定位与建图)以及三维重建领域,光束法平差(Bundle Adjustment, BA) 是一项极其核心的后端优化技术。本文结合 Ceres Solver,探讨 BA 的基本概念及其基于非线性最小二乘法的代码实践。

1. 光束法平差 (Bundle Adjustment) 的核心概念

1.1 什么是光束法平差?

光束法平差的本质是一个非线性优化问题

  • 输入:一系列相机在不同视角下拍摄到的图像,以及通过特征匹配获取的二维“观测点”。
  • 目标:同时估计(或优化)相机的内参/外参(位姿)三维空间点(Map Points)的坐标
  • “光束”的直观理解:想象从每一个三维空间点,向观察到它的各个相机光心连线,这些光线就构成了一束束的“光束”。我们的目标是调整相机和点的位置,使得这些光束能够最完美地交汇在一起。

1.2 重投影误差 (Reprojection Error)

如何衡量“最完美地交汇”?我们使用重投影误差来作为代价函数(Cost Function)。

  1. 拿当前的相机参数和三维点坐标,通过相机的投影模型,计算出一个“理论上的二维投影坐标”。
  2. 将这个理论坐标与实际在图像中提取到的“实际观测坐标”求欧氏距离,这就是重投影误差。
  3. BA 优化的最终目标,就是最小化所有观测点的重投影误差平方和

(其中 为观测值, 为理论投影值)。

2. 基于 Ceres Solver 的 BA 求解器设计

Ceres Solver 是 Google 开源的极其强大的非线性优化库,专门用于解决非线性最小二乘问题。以下是使用 Ceres 解决 BA 问题的标准流程(参考 simple_bundle_adjuster 示例)。

2.1 数据解析与管理 (BALProblem 类)

在开始优化前,我们需要加载并解析数据集(例如 BAL - Bundle Adjustment in the Large 数据集)。数据通常包含:

  • 整体信息:相机数量、地图点数量、观测数据总数。
  • 观测数据:每个观测关联的 相机索引地图点索引,以及 二维观测坐标 (x, y)
  • 初始估计值:每个相机初始的旋转矩阵、平移向量、焦距及畸变参数;每个三维点的初始空间坐标

我们可以构建一个 BALProblem 类来统一管理这些在内存中连续存放的参数指针:

  • mutable_camera_for_observation(int i):返回第 i 个观测对应的相机参数地址。
  • mutable_point_for_observation(int i):返回第 i 个观测对应的三维点坐标地址。

2.2 定义残差块 (Residual Block / Cost Function)

在 Ceres 中,必须定义如何计算残差。对于 BA,我们需要定义投影模型 SnavelyReprojectionError

通过 Ceres 提供的自动求导(AutoDiff)功能,我们只需要编写如何从参数计算残差的仿函数(Functor),无需手动推导雅可比矩阵(Jacobian):

cpp
struct SnavelyReprojectionError {
    SnavelyReprojectionError(double observed_x, double observed_y)
        : observed_x(observed_x), observed_y(observed_y) {}

    // 重载括号运算符,模板化以便于 Ceres 进行自动求导计算
    template <typename T>
    bool operator()(const T* const camera,
                    const T* const point,
                    T* residuals) const {
        // 1. 相机坐标系转换 (旋转 + 平移)
        // 2. 透视除法 (Perspective Division)
        // 3. 应用径向畸变 (Radial Distortion)
        // 4. 计算残差: residuals[0] = predicted_x - observed_x;
        //             residuals[1] = predicted_y - observed_y;
        return true;
    }

    // 工厂函数,返回一个自动求导的 CostFunction
    static ceres::CostFunction* Create(const double observed_x,
                                       const double observed_y) {
        return (new ceres::AutoDiffCostFunction<SnavelyReprojectionError, 2, 9, 3>(
            new SnavelyReprojectionError(observed_x, observed_y)));
    }

    double observed_x;
    double observed_y;
};

(注:此处 2 代表残差维度,9 代表相机参数维度,3 代表三维点坐标维度)

2.3 构建并求解 Problem

当数据和残差计算模型准备完毕后,就可以利用 ceres::Problem 组织优化流程:

cpp
ceres::Problem problem;

// 遍历所有的观测数据
for (int i = 0; i < bal_problem.num_observations(); ++i) {
    // 创建针对当前观测的代价函数
    ceres::CostFunction* cost_function =
        SnavelyReprojectionError::Create(
            bal_problem.observations()[2 * i + 0],
            bal_problem.observations()[2 * i + 1]);
            
    // 将代价函数及对应的参数块添加到 problem 中
    problem.AddResidualBlock(cost_function,
                             nullptr /* 不使用核函数(如Huber),使用标准平方误差 */,
                             bal_problem.mutable_camera_for_observation(i),
                             bal_problem.mutable_point_for_observation(i));
}

// 配置求解器参数
ceres::Solver::Options options;
options.linear_solver_type = ceres::DENSE_SCHUR; // Schur消元对于BA问题非常高效
options.minimizer_progress_to_stdout = true;

// 执行优化
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);

// 输出优化报告
std::cout << summary.FullReport() << "\n";

3. 总结

在深入理解了第一篇文档中提到的“最小二乘法拟合”的基础上,光束法平差实际上是将线性最小二乘法扩展到了非线性领域、并大幅提升了问题规模。借助 Ceres Solver,科研和工程人员能够屏蔽底层极其复杂的雅可比推导与矩阵求解算法(例如 Schur 消元和 Levenberg-Marquardt 算法),通过纯粹的物理与数学模型描述,即可实现高精度的相机标定与三维场景重建。

基于 VitePress 强力驱动 | 记录技术与生活