【视觉SLAM十四讲】后端 2

文章目录

后端 1 中提到如何构建 BA 问题,即同时优化所有的相机位姿和路径点,而本讲主要探讨的就是如何保证在大规模场景下 BA 的实时性

BA 的理想情况是将所有时刻的所有相机和路径点都放进 H H H 矩阵优化,但是随着时间的推移将产生大量数据,这将严重影响实时性乃至超出计算机的处理能力

为了解决此问题,有两种较为常见的方法

  • 滑动窗口法:将 BA 固定在一个时间窗口,每当新的关键帧加入时,将窗口内最早的关键帧丢弃
  • 共视图:仅优化与当前帧有足够共视路标点的关键帧

滑动窗口

在使用滑动窗口算法的时候,当加入新的关键帧时,旧的关键帧的处理便成了问题,如果直接删除,那么这一帧的约束信息将直接消失,导致协方差估计过于乐观,降低精度。因此我们需要利用边缘化提炼出旧帧的信息保留下来

边缘化与 Schur 补

边缘化的本质是利用 Schur 补将被删除变量的信息投影到剩余变量上

回顾 BA 中的线性方程: H Δ x = g H\Delta x=g HΔx=g

H m m H m r H r m H r r \] ⋅ \[ Δ x m Δ x r \] = \[ b m b r \] \\begin{bmatrix} H_{mm}\&H_{mr}\\\\H_{rm}\&H_{rr} \\end{bmatrix} \\cdot \\begin{bmatrix} \\Delta x_{m}\\\\ \\Delta x_{r} \\end{bmatrix}= \\begin{bmatrix} b_{m}\\\\b_{r} \\end{bmatrix} \[HmmHrmHmrHrr\]⋅\[ΔxmΔxr\]=\[bmbr

  • x m x_{m} xm :将被边缘化的变量
  • x r x_{r} xr :保留的剩余变量
    利用高斯消元得到关于 Δ x \Delta x Δx 的方程:
    ( H r r − H r m H m m − 1 H m r ) ⏟ H n e w Δ x r = b r − H r m H m m − 1 b m \underbrace{(H_{rr}-H_{rm}H_{mm}^{-1}H_{mr})}{H{new}}\Delta x_{r}=b_{r}-H_{rm}H^{-1}{mm}b{m} Hnew (Hrr−HrmHmm−1Hmr)Δxr=br−HrmHmm−1bm
    H n e w H_{new} Hnew 即边缘化后剩余变量 x r x_{r} xr 的新信息矩阵
    传递对象:
  • 滑动窗口内较新的关键帧:如果新帧与被边缘化的旧帧观测到了相同的路标点,或者有直接的里程计约束,那么旧帧的信息会作为先验信息传递给新帧
  • 滑动窗口内的路标点:更新被边缘化的旧帧观测的路标点位置
特性 BA 边缘化
场景 求解 H Δ x = g H\Delta x=g HΔx=g 移除旧帧并保留信息
消除变量 路标点 Δ x p \Delta x_{p} Δxp 旧关键帧 Δ x m \Delta x_{m} Δxm
保留变量 相机位姿 Δ x e \Delta x_{e} Δxe 剩余帧+路标 Δ x r \Delta x_{r} Δxr
Schur补 S = B − E C − 1 E T S=B-EC^{-1}E^T S=B−EC−1ET H n e w = H r r − H r m H m m − 1 H m r H_{new}=H_{rr}-H_{rm}H^{-1}{mm}H{mr} Hnew=Hrr−HrmHmm−1Hmr
目的 加速计算,求解相机位姿 信息传递,将旧帧的约束传给新帧

S S S 矩阵其实就是边缘化掉路标点的相机位姿信息矩阵( H n e w H_{new} Hnew)

稀疏性的破坏(Fill in)

让我们回忆一下后端 1 中提到的

H = [ B E E T C ] H=\begin{bmatrix} B & E \\ E^T & C \end{bmatrix} \\ H=[BETEC]

其中 C 矩阵是对角块矩阵,因为路径点与路径点之间并无联系,所以仅有对角上用 3 × 3 3\times 3 3×3 矩阵存储路径点的信息矩阵,其余点都为0

对于对角矩阵 C i i C_{ii} Cii ,其存储的是所有相机关于该路标点位置的二阶导数之和 ,物理意义上,它描述的是该路标点的确定程度

路标点之间的无关性正是 C 矩阵稀疏性的来源,基于这点,BA 才能利用 Schur 消元加速计算

当我们使用边缘化移除旧帧 T o l d T_{old} Told 时:

  1. T o l d T_{old} Told 同时观测到的所有路径点,都与 T o l d T_{old} Told 存在着约束
  2. 当我们边缘化 T o l d T_{old} Told 时,Schur 补会将这些点都联系起来
  3. 这意味着原先互不相关的路径点之间,产生了直接的数学联系
    这将使得 C 矩阵中原先非对角的元素不再为 0 ,C 矩阵变成了稠密矩阵,在下一次 BA 过程中,无法利用 C 矩阵的稀疏性加速计算

位姿图

如果不希望处理复杂的边缘化,或者需要进行大范围的全局调整,可以采用位姿图优化

位姿图优化的核心思想就是不再优化 BA 中的路标点,而是仅把它们看成对姿态节点的约束

位姿图优化

位姿图中的节点标示相机位姿,以 T 1 , ⋯   , T n T_{1},\cdots,T_{n} T1,⋯,Tn 表示,边则是两个位姿节点之间的相对运动估计。

假设我们估计 T i T_{i} Ti 和 T j T_{j} Tj 之间的一个相对运动 T i j T_{ij} Tij ,表达式如下
T i j = T i − 1 T j T_{ij}=T_{i}^{-1}T_{j} Tij=Ti−1Tj

或者写成李代数形式( ∘ \circ ∘ 表示"在李群上做逆和乘法,对应到李代数上的运算", ∨ ^\vee ∨ 表示矩阵转向量)
Δ ξ i j = ξ i − 1 ∘ ξ j = ln ⁡ ( T i − 1 T j ) ∨ \Delta \xi_{ij}=\xi_{i}^{-1}\circ\xi_{j}=\ln(T_{i}^{-1}T_{j})^\vee Δξij=ξi−1∘ξj=ln(Ti−1Tj)∨

该等式不会严格成立,所以我们还要引入最小二乘,先构建误差项 e i j e_{ij} eij
e i j = ln ⁡ ( T i j − 1 T i − 1 T j ) ∨ e_{ij}=\ln(T^{-1}{ij}T^{-1}{i}T_{j})^\vee eij=ln(Tij−1Ti−1Tj)∨

  • T i j − 1 T_{ij}^{-1} Tij−1 :两个相机之间的相对运动测量值
  • T i , T j T_{i},T_{j} Ti,Tj :两个相机在世界坐标系下的估计位姿

理想情况下, T i j = T i − 1 T j T_{ij}=T_{i}^{-1}T_{j} Tij=Ti−1Tj
李群和李代数中提到过,李群无法直接做减法,这里用"乘逆"来表示减法

如果估计没有偏差, T i j − 1 ⋅ ( T i − 1 T j ) = I T_{ij}^{-1}\cdot(T_{i}^{-1}T_{j})=I Tij−1⋅(Ti−1Tj)=I

反之则是得到一个残差矩阵

要优化的变量有两个: ξ i \xi_{i} ξi 和 ξ j \xi_{j} ξj ,因此我们要求 e i j e_{ij} eij 关于这两个变量的导数,给 T i T_{i} Ti 和 T j T_{j} Tj 各自左乘扰动 δ ξ i \delta\xi_i δξi 和 δ ξ j \delta\xi_j δξj ,此时误差变为 ( ( A B ) − 1 = B − 1 A − 1 (AB)^{-1}=B^{-1}A^{-1} (AB)−1=B−1A−1)
e ^ i j = ln ⁡ ( T i j − 1 T i − 1 exp ⁡ ( − δ ξ i ∧ ) exp ⁡ ( δ ξ j ∧ ) ) ∨ \hat{e}{ij}=\ln\left(T{ij}^{-1}T_{i}^{-1}\exp\left(-\delta\xi_{i}^\wedge\right)\exp(\delta\xi_{j}^\wedge)\right)^\vee e^ij=ln(Tij−1Ti−1exp(−δξi∧)exp(δξj∧))∨

为了利用李群和李代数,我们需要将式子化为诸如 ln ⁡ ( A ⋅ exp ⁡ ( δ ∧ ) ) ∨ \ln(A\cdot \exp(\delta^\wedge))^\vee ln(A⋅exp(δ∧))∨ 的形式

定义 Δ ξ = d e f δ ξ j − δ ξ i \Delta\xi \stackrel{def}{=}\delta\xi_{j}-\delta\xi_{i} Δξ=defδξj−δξi

先进行线性近似 ( exp ⁡ ( A ) exp ⁡ ( B ) = exp ⁡ ( A + B ) ) (\exp(A)\exp(B)=\exp(A+B)) (exp(A)exp(B)=exp(A+B)),得到
e ^ i j ≈ ln ⁡ ( T i j − 1 T i − 1 exp ⁡ ( Δ ξ ∧ ) T j ) ∨ \hat{e}{{ij}}\approx\ln\left(T{ij}^{-1}T_{i}^{-1}\exp\left(\Delta\xi^\wedge\right)T_{j}\right)^\vee e^ij≈ln(Tij−1Ti−1exp(Δξ∧)Tj)∨

在下一步推导之前,我们还需要了解一下**共轭和伴随性质**

不过 T i − 1 exp ⁡ ( ( Δ ξ ) ∧ ) T j T_{i}^{-1}\exp\left((\Delta\xi)^\wedge\right)T_{j} Ti−1exp((Δξ)∧)Tj 并不是一个共轭式子,只是一个约束链,因为共轭式子的要求是左右两侧的矩阵必须是互为逆的同一位姿,但这里是不同时刻的两个位姿

先来剖析一下这个式子:
P j ⏟ j 坐标系 → T j P W ⏟ 世界坐标系 → exp ⁡ ( Δ ξ ∧ ) P W ′ ⏟ 施加扰动后 → T i − 1 P i ⏟ i 坐标系 \underbrace{ P_{j} }{ j~\text{}{坐标系} } \xrightarrow{T{j}} \underbrace{ P_{W} }{ \text{世界坐标系} } \xrightarrow{\exp(\Delta\xi^\wedge)}\underbrace{ P{W}' }{ \text{施加扰动后} } \xrightarrow{T{i}^{-1}} \underbrace{ P_{i} }_{ i~\text{坐标系} } j 坐标系 PjTj 世界坐标系 PWexp(Δξ∧) 施加扰动后 PW′Ti−1 i 坐标系 Pi

这显然不满足条件,因为误差项应当是在 j 坐标系下的,所以还要利用伴随性质纠正坐标系转换

已知:
exp ⁡ ( ξ ∧ ) T = T exp ⁡ ( ( A d ( T − 1 ) ξ ) ∧ ) \exp(\xi^\wedge)T=T\exp\left((Ad(T^{-1})\xi)^\wedge\right) exp(ξ∧)T=Texp((Ad(T−1)ξ)∧)

此时可以凑出标准的 BCH 形式
e ^ i j = ln ⁡ ( T i j − 1 T i − 1 T j ⏟ E i j (原误差) ⋅ exp ⁡ ( ( A d ( T j − 1 ) Δ ξ ) ∧ ⏟ 转换后扰动 ) ) ∨ \hat{e}{ij}=\ln\left(\underbrace{ T{ij}^{-1}T_{i}^{-1}T_{j} }{ E{ij} \text{(原误差)}}\cdot \exp\left(\underbrace{ (Ad(T_{j}^{-1})\Delta \xi)^\wedge}_{ \text{转换后扰动} }\right)\right)^\vee e^ij=ln Eij(原误差) Tij−1Ti−1Tj⋅exp 转换后扰动 (Ad(Tj−1)Δξ)∧ ∨

根据 S E ( 3 ) SE(3) SE(3) 上的 BCH 近似:
ln ⁡ ( A ⋅ exp ⁡ ( δ ∧ ) ) ∨ ≈ ln ⁡ ( A ) ∨ + J r − 1 ( ln ⁡ ( A ) ∨ ) ⋅ δ \ln(A\cdot \exp(\delta^\wedge))^\vee \approx \ln(A)^\vee+J_{r}^{-1}(\ln(A)^\vee)\cdot\delta ln(A⋅exp(δ∧))∨≈ln(A)∨+Jr−1(ln(A)∨)⋅δ

代入得到最终的线性化误差公式:
e ^ i j ≈ e i j + J r − 1 ( e i j ) ⋅ A d ( T j − 1 ⋅ Δ ξ ) \hat{e}{ij}\approx e{ij}+J_{r}^{-1}(e_{ij})\cdot Ad(T_{j}^{-1}\cdot\Delta \xi) e^ij≈eij+Jr−1(eij)⋅Ad(Tj−1⋅Δξ)

展开得到
e ^ i j ≈ e i j + J r − 1 ( e i j ) A d ( T j − 1 ) ⋅ δ ξ j ⏟ 对 j 的导数项 − J r − 1 ( e i j ) A d ( T j − 1 ) ⋅ δ ξ i ⏟ 对 i 的导数项 \hat{e}{ij}\approx e{ij}+\underbrace{ J_{r}^{-1}(e_{ij})Ad(T^{-1}{j})\cdot\delta \xi{j} }{ \text{对} j \text{的导数项} }-\underbrace{ J{r}^{-1}(e_{ij})Ad(T_{j}^{-1})\cdot\delta \xi_{i} }_{ \text{对 } i \text{ 的导数项} } e^ij≈eij+对j的导数项 Jr−1(eij)Ad(Tj−1)⋅δξj−对 i 的导数项 Jr−1(eij)Ad(Tj−1)⋅δξi

于是我们得到了误差关于两个位姿的雅可比矩阵

  • 对 T i T_{i} Ti 的雅可比:
    ∂ e i j ∂ δ ξ i = − J r − 1 ( e i j ) ⋅ A d ( T j − 1 ) \frac{\partial e_{ij}}{\partial\delta \xi_{i}}=-J_{r}^{-1}(e_{ij})\cdot Ad(T_{j}^{-1}) ∂δξi∂eij=−Jr−1(eij)⋅Ad(Tj−1)
  • 对 T j T_{j} Tj 的雅可比: ∂ e i j ∂ δ ξ j = J r − 1 ( e i j ) ⋅ A d ( T j − 1 ) \frac{\partial e_{ij}}{\partial\delta \xi_{j}}=J_{r}^{-1}(e_{ij})\cdot Ad(T_{j}^{-1}) ∂δξj∂eij=Jr−1(eij)⋅Ad(Tj−1)
    由于 s e ( 3 ) \mathfrak{se}(3) se(3) 上的左右雅可比形式过于复杂,所以通常会取近似值,当误差接近于 0 时,我们可以近似其为 I I I 或
    J r − 1 ( e i j ) ≈ I + 1 2 [ ϕ e ∧ ρ e ∧ 0 ϕ e ∧ ] J_{r}^{-1}(e_{ij}) \approx I + \frac{1}{2}\begin{bmatrix}\phi_{e}^\wedge&\rho_{e}^\wedge\\0&\phi_{e}^\wedge\end{bmatrix} Jr−1(eij)≈I+21[ϕe∧0ρe∧ϕe∧]
    不用过于担心近似引进的误差,因为在迭代算法里,我们其实需求的不是实际精确的值,而是一个优化方向,而且随着迭代的推进,误差减小的时候,雅可比矩阵也会靠近我们近似的 I I I

g2o 与位姿图优化

\[g2o入门\]

原生 g2o 代码实例
cpp 复制代码
#include <iostream>
#include <memory>
#include <eigen3/Eigen/Core>
#include <eigen3/Eigen/Geometry>
#include <sophus/se3.hpp>
#include <sophus/so3.hpp>
#include <g2o/core/base_binary_edge.h>
#include <g2o/core/base_vertex.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/dense/linear_solver_dense.h>
#include <g2o/types/slam3d/vertex_se3.h>
#include <g2o/types/slam3d/edge_se3.h>

int main()
{
    // 1. 数据生成
    // 半径 10 米的圆周轨迹,每步走一度
    std::vector<Eigen::Isometry3d> poses;
    double radius = 10.0;
    for (int i = 0; i < 360; i++)
    {
        double theta = i * M_PI / 180.0;
        // 位置
        Eigen::Vector3d trans(radius * cos(theta), radius * sin(theta), 0);
        // 姿态
        Eigen::Quaterniond q;
        q = Eigen::AngleAxisd(theta + M_PI_2, Eigen::Vector3d::UnitZ());

        Eigen::Isometry3d pose = Eigen::Isometry3d::Identity();
        pose.rotate(q);
        pose.pretranslate(trans);
        poses.push_back(pose);
    }

    // 2. 构建 g2o 优化器
    typedef g2o::BlockSolver<g2o::BlockSolverTraits<6, 6>> BlockSolverType;

    typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;

    auto solver = std::make_unique<g2o::OptimizationAlgorithmLevenberg>(
        std::make_unique<BlockSolverType>(std::make_unique<LinearSolverType>())
    );

    g2o::SparseOptimizer optimizer;
    optimizer.setAlgorithm(solver.release());
    optimizer.setVerbose(true);

    // 3. 添加位姿顶点
    // 添加噪声
    Eigen::Isometry3d current_pose = poses[0];

    // 随机数生成器
    double trans_noise = 0.1;  // 平移噪声标准差
    double rot_noise = 0.01;   // 旋转噪声标准

    for (int i = 0; i < poses.size(); i++)
    {
        g2o::VertexSE3* v = new g2o::VertexSE3();
        v->setId(i);

        if (i == 0)
        {
            v->setEstimate(current_pose);
            v->setFixed(true); // 固定第一个位姿
        }
        else
        {
            // 计算相对运动真实值
            Eigen::Isometry3d relative_motion = poses[i - 1].inverse() * poses[i];

            // 添加噪声
            Eigen::Vector3d noise_trans = Eigen::Vector3d::Random() * trans_noise;
            Eigen::Vector3d noise_rot = Eigen::Vector3d::Random() * rot_noise;
            Eigen::Quaterniond noise_q = Sophus::SO3d::exp(noise_rot).unit_quaternion();

            Eigen::Isometry3d noisy_relative_motion = Eigen::Isometry3d::Identity();
            noisy_relative_motion.rotate(noise_q * relative_motion.rotation());
            noisy_relative_motion.pretranslate(relative_motion.translation() + noise_trans);

            // 更新当前位姿
            current_pose = current_pose * noisy_relative_motion;
            v->setEstimate(current_pose);

            // 添加边
            auto edge = std::make_unique<g2o::EdgeSE3>();
            edge->setVertex(0, optimizer.vertex(i - 1));
            edge->setVertex(1, v);
            edge->setMeasurement(noisy_relative_motion);

            // 信息矩阵
            Eigen::Matrix<double, 6, 6> information = Eigen::Matrix<double, 6, 6>::Identity();
            information.block<3,3>(0, 0) *= 1.0 / (trans_noise * trans_noise);
            information.block<3,3>(3, 3) *= 1.0 / (rot_noise * rot_noise);
            edge->setInformation(information);

            optimizer.addEdge(edge.release());
        }
        optimizer.addVertex(v);
    }

    auto* last_vertex = static_cast<g2o::VertexSE3*>(optimizer.vertex(359));
    Eigen::Vector3d end_error = last_vertex->estimate().translation() - poses[359].translation();
    std::cout << "Before optimization: End pose error distance = " 
              << end_error.norm() 
              << " m" << std::endl;

    // 添加回环边
    g2o::EdgeSE3* loop_edge = new g2o::EdgeSE3();
    loop_edge->setVertex(0, optimizer.vertex(359)); // 回环到第一个顶点
    loop_edge->setVertex(1, optimizer.vertex(0));

    Eigen::Isometry3d loop_measurement = poses[359].inverse() * poses[0];
    loop_edge->setMeasurement(loop_measurement);

    Eigen::Matrix<double, 6, 6> loop_information = Eigen::Matrix<double, 6, 6>::Identity() * 1000.0;
    loop_edge->setInformation(loop_information);

    optimizer.addEdge(loop_edge);

    // 4. 优化
    std::cout << "Start optimization" << std::endl;
    optimizer.initializeOptimization();
    optimizer.optimize(100);    

    auto* new_last_vertex = static_cast<g2o::VertexSE3*>(optimizer.vertex(359));
    Eigen::Vector3d new_end_error = new_last_vertex->estimate().translation() - poses[359].translation();
    std::cout << "After optimization: End pose error distance = " 
              << new_end_error.norm() 
              << " m" << std::endl;

    optimizer.save("pose_graph_result.g2o");
}

效果图如下:

李代数代码实例
cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <memory>
#include <eigen3/Eigen/Core>
#include <eigen3/Eigen/Geometry>
#include <sophus/se3.hpp>

#include <g2o/core/base_vertex.h>
#include <g2o/core/base_binary_edge.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/eigen/linear_solver_eigen.h>

#include <g2o/core/factory.h>
#include <g2o/stuff/command_args.h>

typedef Eigen::Matrix<double, 6, 6> Matrix6d;
typedef Eigen::Matrix<double, 6, 1> Vector6d;

// J_R^{-1} 的近似
Matrix6d JRinv(const Sophus::SE3d& e)
{
    Matrix6d J;
    J.block(0, 0, 3, 3) = Sophus::SO3d::hat(e.so3().log());
    J.block(0, 3, 3, 3) = Sophus::SO3d::hat(e.translation());
    J.block(3, 0, 3, 3) = Eigen::Matrix3d::Zero();
    J.block(3, 3, 3, 3) = Sophus::SO3d::hat(e.so3().log());
    J = J * 0.5 + Matrix6d::Identity();
    return J;
}

class VertexSE3LieAlgebra : public g2o::BaseVertex<6, Sophus::SE3d>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW

    virtual void setToOriginImpl() override
    {
        _estimate = Sophus::SE3d();
    }

    virtual void oplusImpl(const double* update) override
    {
        Vector6d update_eigen;
        update_eigen << update[0], update[1], update[2], update[3], update[4], update[5];
        _estimate = Sophus::SE3d::exp(update_eigen) * _estimate;
    }

    virtual bool read(std::istream& in) override
    {
        double data[7];
        for (int i = 0; i < 7; i++)
            in >> data[i];
        setEstimate(Sophus::SE3d(
            Eigen::Quaterniond(data[6], data[3], data[4], data[5]).normalized(),
            Eigen::Vector3d(data[0], data[1], data[2])
        ));
        return true;
    }

    virtual bool write(std::ostream& os) const override
    {
        Eigen::Quaterniond q = _estimate.unit_quaternion();
        os << _estimate.translation().transpose() << " ";
        os << q.x() << " " << q.y() << " " << q.z() << " " << q.w() << std::endl;
        return true;
    }
};

// 
class EdgeSE3LieAlgebra : public g2o::BaseBinaryEdge<6, Sophus::SE3d, VertexSE3LieAlgebra, VertexSE3LieAlgebra> {
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW

    virtual void computeError() override
    {
        Sophus::SE3d v1 = (static_cast<VertexSE3LieAlgebra*>(_vertices[0]))->estimate();
        Sophus::SE3d v2 = (static_cast<VertexSE3LieAlgebra*>(_vertices[1]))->estimate();
        
        _error = (_measurement.inverse() * (v1.inverse() * v2)).log();
    }

    // 雅可比计算( J_r^{-1} \approx I)
    virtual void linearizeOplus() override
    {
        Sophus::SE3d v2 = (static_cast<VertexSE3LieAlgebra*>(_vertices[1]))->estimate();
        
        Matrix6d J = JRinv(Sophus::SE3d::exp(_error));
        J = J * v2.inverse().Adj();

        _jacobianOplusXi = -J;
        _jacobianOplusXj = J;
    }

    virtual bool read(std::istream& in) override
    {
        double data[7];
        for (int i = 0; i < 7; i++)
            in >> data[i];
        Eigen::Quaterniond q(data[6], data[3], data[4], data[5]);
        q.normalize();
        setMeasurement(Sophus::SE3d(
            q,
            Eigen::Vector3d(data[0], data[1], data[2])
        ));

        for (int i = 0; i < information().rows(); i++)
        {
            for (int j = 0; j < information().cols(); j++)
            {
                in >> information()(i, j);
                // 保持对称性
                if (i != j)
                {
                    information()(j, i) = information()(i, j);
                }
            }
        }

        return true;
    }

    virtual bool write(std::ostream& os) const override
    {
        VertexSE3LieAlgebra *v1 = static_cast<VertexSE3LieAlgebra*>(_vertices[0]);
        VertexSE3LieAlgebra *v2 = static_cast<VertexSE3LieAlgebra*>(_vertices[1]);

        Eigen::Quaterniond q = _measurement.unit_quaternion();
        os << _measurement.translation().transpose() << " ";
        os << q.x() << " " << q.y() << " " << q.z() << " " << q.w() << " ";
        for (int i = 0; i < information().rows(); ++i)
        {
            for (int j = i; j < information().cols(); ++j)
            {
                os << information()(i, j) << " ";
            }
        }
        os << std::endl;
        return true;
    }
};

int main()
{
    // 1. 读取位姿图
    std::fstream fin("../example/sphere.g2o");
    if (!fin)
    {
        std::cerr << "File not found." << std::endl;
        return -1;
    }

    // 2. 构建优化器
    typedef g2o::BlockSolver<g2o::BlockSolverTraits<6, 6>> BlockSolverType;
    typedef g2o::LinearSolverEigen<BlockSolverType::PoseMatrixType> LinearSolverType;

    auto solver = new g2o::OptimizationAlgorithmLevenberg(
        std::make_unique<BlockSolverType>(std::make_unique<LinearSolverType>())
    );

    g2o::SparseOptimizer optimizer;
    optimizer.setAlgorithm(solver);
    optimizer.setVerbose(true);

    // 3. 从文件中加载顶点和边
    int vertex_count = 0, edge_count = 0;
    while (!fin.eof())
    {
        std::string name;
        fin >> name;
        if (name == "VERTEX_SE3:QUAT")
        {
            int idx = 0;
            double data[7];
            fin >> idx;
            for (int i = 0; i < 7; i++)
                fin >> data[i];

            Eigen::Quaterniond q(data[6], data[3], data[4], data[5]);
            q.normalize();
            Eigen::Vector3d t(data[0], data[1], data[2]);   
            Sophus::SE3d pose(q, t);

            VertexSE3LieAlgebra* v = new VertexSE3LieAlgebra();
            v->setId(idx);
            v->setEstimate(pose);
            optimizer.addVertex(v);
            vertex_count++;

            if (idx == 0)
                v->setFixed(true); // 固定第一个节点
        }
        else if (name == "EDGE_SE3:QUAT")
        {
            int idx1, idx2;
            fin >> idx1 >> idx2;
            double data[7];
            for (int i = 0; i < 7; ++i)
                fin >> data[i];

            Eigen::Quaterniond q(data[6], data[3], data[4], data[5]);
            q.normalize();
            Eigen::Vector3d t(data[0], data[1], data[2]);
            Sophus::SE3d measurement(q, t);

            Matrix6d information = Matrix6d::Zero();
            for (int i = 0; i < 6; i++)
            {
                for (int j = i; j < 6; j++)
                {
                    fin >> information(i, j);
                    if (i != j)
                        information(j, i) = information(i, j);
                }
            }

            EdgeSE3LieAlgebra* e = new EdgeSE3LieAlgebra();
            e->setVertex(0, optimizer.vertex(idx1));
            e->setVertex(1, optimizer.vertex(idx2));
            e->setMeasurement(measurement);
            e->setInformation(information);
            optimizer.addEdge(e);
            edge_count++;
        }

        if (!fin.good())
            break;
    }

    std::cout << "Read total " << vertex_count << " vertices, " << edge_count << " edges." << std::endl;

    // 4. 优化
    std::cout << "Start optimization" << std::endl;
    optimizer.initializeOptimization();
    optimizer.optimize(100);

    // 5. 保存结果
    optimizer.save("sphere_se3_result.g2o");
}

G2O_REGISTER_TYPE_NAME("VERTEX_SE3:QUAT", VertexSE3LieAlgebra);
G2O_REGISTER_TYPE_NAME("EDGE_SE3:QUAT", EdgeSE3LieAlgebra);

优化前

优化后

相关推荐
踏雪Vernon42 分钟前
[论文][环境]DA3环境搭建_Ubuntu24
计算机视觉
三炭先生1 小时前
计算机视觉算法--第一章:概述
人工智能·算法·计算机视觉
棒棒的皮皮1 小时前
【OpenCV】Python图像处理之图像加密解密
图像处理·python·opencv·计算机视觉
唯道行1 小时前
计算机图形学·21 梁友栋-Barsky直线裁剪算法与三维直线裁剪
人工智能·算法·机器学习·计算机视觉·计算机图形学·opengl
唯道行1 小时前
计算机图形学·20 绘制(Implementation)1与Cohen-Sutherland算法
人工智能·算法·计算机视觉·计算机图形学·opengl
CV爱数码2 小时前
【宝藏数据集】MCOD:多光谱伪装目标检测首个挑战性基准
人工智能·深度学习·目标检测·计算机视觉·目标跟踪·数据集
某林2122 小时前
基于ROS2与EKF的四轮差速机器人里程计精度优化:解决建图漂移与重影问题
linux·stm32·嵌入式硬件·slam·智能小车
Rotion_深2 小时前
几何光学基本定律与成像概念
计算机视觉
自然语2 小时前
完整的 OpenCV 点云可视化版本
人工智能·opencv·计算机视觉