视觉SLAM 十四讲——后端设计II(Pose Graph, Factor Graph)

主要内容

1. 位姿图
    1.1) 引出
        BA问题中,特征点在优化问题中占据了绝大部分,因此在优化过程中,倾向于把收敛的特征点固定住,只把它们看做位姿估计的约束,而不再实际的优化它们的位置。
        当机器人在更大范围的时间和空间中运动时,从减小计算量的角度出发有一下解决方案:
            a 滑动窗口法,丢弃一些历史数据;
            b 位姿图,舍弃对路标点的优化,只保留相机位姿。

    1.2) Pose Graph优化
        1.2.1) 构建误差方程:
       

    其中,第一项表示i和j时刻之间的一个运动变换,这个是已知的,可以通过前后两帧图像计算得到的;后面两项分别为i时刻和j时刻的位姿。
        1.2.2) 推到误差关于优化变量的导数(雅克比矩阵):
         
    其中两项分别如下所示:
        

   

     

       
    由于se(3)上的左右雅克比Jr形式过去复杂,如果误差接近于0,可近似为如下形式:
       (会有一定的损失)
    其中,伴随矩阵求解如下:
        
        1.2.3)  构造最优化问题
            所有的位姿顶点和位姿-位姿边构成了一个图优化,本质上是一个最小二乘问题,优化变量为各个顶点的位姿,边来自于位姿观测约束。优化方法可以选用高斯牛顿法、列文伯格-马夸尔特方法等求解。

位姿图总结:

  一般认为,位姿图是是结构最简单的图之一。在不假设机器人如何运动的前提下,很难再进一步讨论它的稀疏性。例如直线往前运动,形成带状的位姿图,是稀疏的,又如形成大量的小型回环需要优化,从而变成像试验中“球”那样比较稠密的地图。

  当位姿点比较多,且整体位姿图是稠密的,那么整体计算也是比较耗费时间的,相比前端VO因此在PTAM中,将前端和后端分开,分别运行与两个线程中,历史上称为跟踪(Tracking)和建图(Mapping

位姿图参考链接

g2o Error in `g2o_viewer': realloc(): invalid pointer: 0x00007f6911493820

 

2. 因子图

  2.1) 参考文献

    [1] Koller D , Friedman N . Probabilistic Graphical Models: Principles and Techniques[M]. MIT Press, 2009.

    [2] Kaess M, Ranganathan A, Dellaert F. iSAM: Incremental smoothing and mapping[J]. IEEE Transactions on Robotics, 2008, 24(6): 1365-1378.

    [3] Kaess M, Johannsson H, Roberts R, et al. iSAM2: Incremental smoothing and mapping using the Bayes tree[J]. The International Journal of Robotics Research, 2012, 31(2): 216-235.

    [4] Rosen D M, Kaess M, Leonard J J. RISE: An incremental trust-region method for robust online sparse least-squares estimation[J]. IEEE Transactions on Robotics, 2014, 30(5): 1091-1108.

 

  2.2) 贝叶斯网络

    贝叶斯网络(Bayes Network)是一种概率图,由随机变量的节点和表达随机变量条件独立性的边组成,形成一个有向无环图。

    在SLAM中,运动方程和观测方程恰好表示了状态变量之间的条件概率,因此,SLAM可以自然地表达成一个动态贝叶斯网络(Dynamic Bayes Network, DBN)。

    在贝叶斯网络中,各线条表示了它们之间的关系,箭头表示依赖关系,比如x1指向x2,可以用条件概率来表示P(x2|x1)SLAM中运动方程和观测方程可以用条件概率表示,即可以用贝叶斯网络来表达。

    后端优化的目标是在这些既有约束之上,通过调整贝叶斯网络中随机变量的取值,使整个后验概率达到最大。可以使用概率图模型中的算法进行求解

 

  2.3) 因子图概念

    因子图是一种无向图,由两种节点组成,表示优化变量的变量节点,以及表示因子的因子节点。

    对因子图的优化,就是调整各变量的值,使它们的因子之乘积最大化——对应着一个优化问题,在该问题中,把各个因子的条件概率去高斯分布,则可显示的表达因子图优化的目标函数

    运动方程和观测方程作为因子存在于图中,除此之外,对于某些相机的位姿可能具有先验信息(GPS测量的位置或者速度信息等),那么就可以对这些待优化变量位姿添加先验因子。且由于不同的先验信息具有不同的概率分布,因此因子图也可以定义许多不同的因子,比如里程计,IMU的测量等。

 

  2.4) 增量特性

    Kaess等人提出的iSAMincremental Smooth and Mapping)中,对因子图进行了更加细致的处理,使得它可以增量式地处理后端优化。

 

    2.4.1) 原有问题:

      随着机器人运动,新的节点和边加入到图中,使得整体规模不断增长。是否每次新加一个节点,就需要重新计算一遍所有节点的更新量?

 

    2.4.2) 增量特性:

      新加节点,受影响的节点可以近似看成只有最后一个与之相连的节点(实际情况,新增节点还是会对之前的估计产生影响的,只是对最近的数据影响最大,对较远的数据影响很小,可以忽略掉,节省计算量,避免增加新节点对整个图进行优化

      按回环检测增加新节点,受影响范围为回环开始到当前帧这一段中的所有节点,也就是整段轨迹都有可能被重新调整,回环以外的节点是不受影响的。

 

    2.4.3) 实际实现

      在每次优化中保存中间结果,而当新的变量和因子加入时,首先分析它们因子图之间的连接和影响关系,考虑之前存储的信息有哪些可以继续利用,哪些必须重新计算,最后处理对增量的优化。

      实际中,尽管有增量分析,但必须清楚的一点这里受影响的节点是近似的,当图的规模发生一定程度的改变时,需要再做一次全图优化。

 

因子图参考链接:

  gtsam:从入门到使用

  GTSAM 库的学习建议

 

3. 位姿图优化——g2o原生位姿图实现

  3.1数据说明

    位姿图数据是由g2o自带的create sphere 程序仿真生成的

    真实轨迹为一个球体,由从下往上的多个层组成

    仿真程序生成了t-1t时刻的边,称为odometry边(里程计),此外,又生成了层与层之间的边,称为loop closure (回环)

    在每条边上添加噪声,根据里程计边的噪声,重新设置节点的初始值,这样就生成了带有累计误差的位姿图数据。

  3.2) 位姿图信息读取

    包括各个位姿点的位姿信息;(ID + 平移向量+ 四元数)

    两两位姿点之间的量测信息以及R阵信息(其中R阵是非对角线元素是一样的,所以给的是三角阵)

  3.3) 图优化问题构建

    求解器的设置,优化问题构建

    typedef g2o::BlockSolver<g2o::BlockSolverTraits<6,6>> Block;  // 6x6 BlockSolver
    Block::LinearSolverType* linearSolver = new g2o::LinearSolverCholmod<Block::PoseMatrixType>(); // 线性方程求解器
    Block* solver_ptr = new Block( linearSolver );      // 矩阵块求解器
    // 梯度下降方法,从GN, LM, DogLeg 中选
    g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg( solver_ptr );
    g2o::SparseOptimizer optimizer;     // 图模型
    optimizer.setAlgorithm( solver );   // 设置求解器

    位姿顶点采用g2o::VertexSE3

      其中顶点数据初始化采用read函数读文件数据实现

        if ( name == "VERTEX_SE3:QUAT" )
        {
            // SE3 顶点
            g2o::VertexSE3* v = new g2o::VertexSE3();
            int index = 0;
            fin>>index;
            v->setId( index );
            v->read(fin);
            optimizer.addVertex(v);
            vertexCnt++;
            if ( index==0 )
                v->setFixed(true);
        }

    边的类型采用g2o::EdgeSE3

      其中需要注意边类型中:

        computeError 误差函数计算(两测的两两顶点的位姿变化和实际位姿变化的残差计算)

        linearizeOplus线性化函数实现(雅克比矩阵求解):computeEdgeSE3Gradient函数(细节需要在查看一下?????

        else if ( name=="EDGE_SE3:QUAT" )
        {
            // SE3-SE3 边
            g2o::EdgeSE3* e = new g2o::EdgeSE3();
            int idx1, idx2;     // 关联的两个顶点
            fin>>idx1>>idx2;
            e->setId( edgeCnt++ );
            e->setVertex( 0, optimizer.vertices()[idx1] );
            e->setVertex( 1, optimizer.vertices()[idx2] );
            e->read(fin);
            optimizer.addEdge(e);
        }

优化结果保存:调用优化类g2o::SparseOptimizer实现的save函数实现:

     optimizer.save("result.g2o");

 

  3.4) 测试结果如下:(上面一幅图为原始结果,下面一幅图为优化以后的结果)

read total 2500 vertices, 9799 edges.
prepare optimizing ...
calling optimizing ...
iteration= 0     chi2= 1023011093.967642     time= 0.284138     cumTime= 0.284138     edges= 9799     schur= 0     lambda= 805.622433     levenbergIter= 1
iteration= 1     chi2= 385118688.233189     time= 0.226302     cumTime= 0.51044     edges= 9799     schur= 0     lambda= 537.081622     levenbergIter= 1
iteration= 2     chi2= 166223726.693659     time= 0.224116     cumTime= 0.734555     edges= 9799     schur= 0     lambda= 358.054415     levenbergIter= 1
iteration= 3     chi2= 86610874.269316     time= 0.235826     cumTime= 0.970382     edges= 9799     schur= 0     lambda= 238.702943     levenbergIter= 1
iteration= 4     chi2= 40582782.710189     time= 0.2299     cumTime= 1.20028     edges= 9799     schur= 0     lambda= 159.135295     levenbergIter= 1
iteration= 5     chi2= 15055383.753040     time= 0.226908     cumTime= 1.42719     edges= 9799     schur= 0     lambda= 101.425210     levenbergIter= 1
iteration= 6     chi2= 6715193.487654     time= 0.23027     cumTime= 1.65746     edges= 9799     schur= 0     lambda= 37.664667     levenbergIter= 1
iteration= 7     chi2= 2171949.168383     time= 0.226018     cumTime= 1.88348     edges= 9799     schur= 0     lambda= 12.554889     levenbergIter= 1
iteration= 8     chi2= 740566.827049     time= 0.229358     cumTime= 2.11284     edges= 9799     schur= 0     lambda= 4.184963     levenbergIter= 1
iteration= 9     chi2= 313641.802464     time= 0.225855     cumTime= 2.33869     edges= 9799     schur= 0     lambda= 2.583432     levenbergIter= 1
iteration= 10     chi2= 82659.743578     time= 0.231751     cumTime= 2.57044     edges= 9799     schur= 0     lambda= 0.861144     levenbergIter= 1
iteration= 11     chi2= 58220.369189     time= 0.228829     cumTime= 2.79927     edges= 9799     schur= 0     lambda= 0.287048     levenbergIter= 1
iteration= 12     chi2= 52214.188561     time= 0.236754     cumTime= 3.03603     edges= 9799     schur= 0     lambda= 0.095683     levenbergIter= 1
iteration= 13     chi2= 50948.580336     time= 0.230022     cumTime= 3.26605     edges= 9799     schur= 0     lambda= 0.031894     levenbergIter= 1
iteration= 14     chi2= 50587.776729     time= 0.278807     cumTime= 3.54485     edges= 9799     schur= 0     lambda= 0.016436     levenbergIter= 1
iteration= 15     chi2= 50233.038802     time= 0.238338     cumTime= 3.78319     edges= 9799     schur= 0     lambda= 0.010957     levenbergIter= 1
iteration= 16     chi2= 49995.082839     time= 0.256058     cumTime= 4.03925     edges= 9799     schur= 0     lambda= 0.007305     levenbergIter= 1
iteration= 17     chi2= 48876.738967     time= 0.453673     cumTime= 4.49292     edges= 9799     schur= 0     lambda= 0.009298     levenbergIter= 2
iteration= 18     chi2= 48806.625522     time= 0.237011     cumTime= 4.72993     edges= 9799     schur= 0     lambda= 0.006199     levenbergIter= 1
iteration= 19     chi2= 47790.891373     time= 0.456466     cumTime= 5.1864     edges= 9799     schur= 0     lambda= 0.008265     levenbergIter= 2
iteration= 20     chi2= 47713.626582     time= 0.242076     cumTime= 5.42848     edges= 9799     schur= 0     lambda= 0.005510     levenbergIter= 1
iteration= 21     chi2= 46869.323688     time= 0.494889     cumTime= 5.92336     edges= 9799     schur= 0     lambda= 0.007347     levenbergIter= 2
iteration= 22     chi2= 46802.585510     time= 0.242211     cumTime= 6.16558     edges= 9799     schur= 0     lambda= 0.004898     levenbergIter= 1
iteration= 23     chi2= 46128.758042     time= 0.456365     cumTime= 6.62194     edges= 9799     schur= 0     lambda= 0.006489     levenbergIter= 2
iteration= 24     chi2= 46069.133541     time= 0.59885     cumTime= 7.22079     edges= 9799     schur= 0     lambda= 0.004326     levenbergIter= 1
iteration= 25     chi2= 45553.862165     time= 0.544436     cumTime= 7.76523     edges= 9799     schur= 0     lambda= 0.005595     levenbergIter= 2
iteration= 26     chi2= 45511.762617     time= 0.282814     cumTime= 8.04804     edges= 9799     schur= 0     lambda= 0.003730     levenbergIter= 1
iteration= 27     chi2= 45122.762999     time= 0.465221     cumTime= 8.51326     edges= 9799     schur= 0     lambda= 0.004690     levenbergIter= 2
iteration= 28     chi2= 45095.174401     time= 0.236706     cumTime= 8.74997     edges= 9799     schur= 0     lambda= 0.003127     levenbergIter= 1
iteration= 29     chi2= 44811.248504     time= 0.470759     cumTime= 9.22073     edges= 9799     schur= 0     lambda= 0.003785     levenbergIter= 2
saving optimization results ...

 

 

 4. 位姿图优化——g2o 李代数实现

   4.1) g2o求解配置

   注意 :BlockSolver<g2o::BlockSolverTraits<6,6>>的维度情况

    typedef g2o::BlockSolver<g2o::BlockSolverTraits<6,6>> Block;  // BlockSolver为6x6
    Block::LinearSolverType* linearSolver = new g2o::LinearSolverCholmod<Block::PoseMatrixType>(); // 线性方程求解器
    Block* solver_ptr = new Block ( linearSolver );     // 矩阵块求解器
    g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );
    // 试试G-N或Dogleg?
    // g2o::OptimizationAlgorithmDogleg* solver = new g2o::OptimizationAlgorithmDogleg( solver_ptr );
    // g2o::OptimizationAlgorithmGaussNewton* solver = new g2o::OptimizationAlgorithmGaussNewton ( solver_ptr );
    
    g2o::SparseOptimizer optimizer;     // 图模型
    optimizer.setAlgorithm ( solver );  // 设置求解器

  4.2) 顶点实现及添加

    4.2.1) 顶点实现

      其中,需要注意,顶点更新函数,以及定点信息从文件中输入和输出的函数

// 李代数顶点
typedef Eigen::Matrix<double, 6, 1> Vector6d;
class VertexSE3LieAlgebra: public g2o::BaseVertex<6, SE3>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    bool read ( istream& is )
    {
        double data[7];
        for ( int i=0; i<7; i++ )
            is>>data[i];
        setEstimate ( SE3 (
                Eigen::Quaterniond ( data[6],data[3], data[4], data[5] ),
                Eigen::Vector3d ( data[0], data[1], data[2] )
        ));
    }

    bool write ( ostream& os ) const
    {
        os<<id()<<" ";
        Eigen::Quaterniond q = _estimate.unit_quaternion();
        os<<_estimate.translation().transpose()<<" ";
        os<<q.coeffs()[0]<<" "<<q.coeffs()[1]<<" "<<q.coeffs()[2]<<" "<<q.coeffs()[3]<<endl;
        return true;
    }
    
    virtual void setToOriginImpl()
    {
        _estimate = Sophus::SE3();
    }
    // 左乘更新
    virtual void oplusImpl ( const double* update )
    {
        Sophus::SE3 up (
            Sophus::SO3 ( update[3], update[4], update[5] ),
            Eigen::Vector3d ( update[0], update[1], update[2] )
        );
        _estimate = up*_estimate;
    }
};

    4.2.2) 定点信息读入    

        if ( name == "VERTEX_SE3:QUAT" )
        {
            // 顶点
            VertexSE3LieAlgebra* v = new VertexSE3LieAlgebra();
            int index = 0;
            fin>>index;
            v->setId( index );
            v->read(fin);
            optimizer.addVertex(v);
            vertexCnt++;
            vectices.push_back(v);
            if ( index==0 )
                v->setFixed(true);
        }

  4.3) 边实现及添加

    4.3.1) 李代数边实现

    其中需要关注 g2o::BaseBinaryEdge<6, SE3, VertexSE3LieAlgebra, VertexSE3LieAlgebra> 类模板参数中,包含边的维度及类型,以及 两个顶点类型

    残差计算函数computeError实现(和书上的公式一致)

    线性化函数 linearizeOplus(雅克比矩阵求解):(和书上的公式一致)

      这边需要注意公式中Jr^-1的求解实现:(书上的近似公式)

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

// 给定误差求J_R^{-1}的近似
Matrix6d JRInv( SE3 e )
{
    Matrix6d J;
    J.block(0,0,3,3) = SO3::hat(e.so3().log());
    J.block(0,3,3,3) = SO3::hat(e.translation());
    J.block(3,0,3,3) = Eigen::Matrix3d::Zero(3,3);
    J.block(3,3,3,3) = SO3::hat(e.so3().log());
    J = J*0.5 + Matrix6d::Identity();
    return J;
}

      注:雅克比矩阵计算方式:a: 不提供雅克比计算函数,g2o自动计算数值雅克比;b: 提供完整的或近似的雅克比计算过程(当前实现是提供近似的雅克比计算过程)

 

    边信息的读入以及 协方差阵信息的读入及设置

    边信息的输出,包括连接顶点的ID,边对应的残差信息(对应平移和四元数形式),量测信息的协方差矩阵

// 两个李代数节点之边
class EdgeSE3LieAlgebra: public g2o::BaseBinaryEdge<6, SE3, VertexSE3LieAlgebra, VertexSE3LieAlgebra>
{
public:
    EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    bool read ( istream& is )
    {
        double data[7];
        for ( int i=0; i<7; i++ )
            is>>data[i];
        Eigen::Quaterniond q ( data[6], data[3], data[4], data[5] );
        q.normalize();
        setMeasurement (
            Sophus::SE3 ( q, Eigen::Vector3d ( data[0], data[1], data[2] ) ) 
        );
        for ( int i=0; i<information().rows() && is.good(); i++ )
            for ( int j=i; j<information().cols() && is.good(); j++ )
            {
                is >> information() ( i,j );
                if ( i!=j )
                    information() ( j,i ) =information() ( i,j );
            }
        return true;
    }
    bool write ( ostream& os ) const
    {
        VertexSE3LieAlgebra* v1 = static_cast<VertexSE3LieAlgebra*> (_vertices[0]);
        VertexSE3LieAlgebra* v2 = static_cast<VertexSE3LieAlgebra*> (_vertices[1]);
        os<<v1->id()<<" "<<v2->id()<<" ";
        SE3 m = _measurement;
        Eigen::Quaterniond q = m.unit_quaternion();
        os<<m.translation().transpose()<<" ";
        os<<q.coeffs()[0]<<" "<<q.coeffs()[1]<<" "<<q.coeffs()[2]<<" "<<q.coeffs()[3]<<" ";
        // information matrix 
        for ( int i=0; i<information().rows(); i++ )
            for ( int j=i; j<information().cols(); j++ )
            {
                os << information() ( i,j ) << " ";
            }
        os<<endl;
        return true;
    }

    // 误差计算与书中推导一致
    virtual void computeError()
    {
        Sophus::SE3 v1 = (static_cast<VertexSE3LieAlgebra*> (_vertices[0]))->estimate();
        Sophus::SE3 v2 = (static_cast<VertexSE3LieAlgebra*> (_vertices[1]))->estimate();
        _error = (_measurement.inverse()*v1.inverse()*v2).log();
    }
    
    // 雅可比计算
    virtual void linearizeOplus()
    {
        Sophus::SE3 v1 = (static_cast<VertexSE3LieAlgebra*> (_vertices[0]))->estimate();
        Sophus::SE3 v2 = (static_cast<VertexSE3LieAlgebra*> (_vertices[1]))->estimate();
        Matrix6d J = JRInv(SE3::exp(_error));
        // 尝试把J近似为I?
        _jacobianOplusXi = - J* v2.inverse().Adj();
        _jacobianOplusXj = J*v2.inverse().Adj();
    }
};

      4.3.2) 边的信息添加

        else if ( name=="EDGE_SE3:QUAT" )
        {
            // SE3-SE3 边
            EdgeSE3LieAlgebra* e = new EdgeSE3LieAlgebra();
            int idx1, idx2;     // 关联的两个顶点
            fin>>idx1>>idx2;
            e->setId( edgeCnt++ );
            e->setVertex( 0, optimizer.vertices()[idx1] );
            e->setVertex( 1, optimizer.vertices()[idx2] );
            e->read(fin);
            optimizer.addEdge(e);
            edges.push_back(e);
        }

 

  4.4)最终优化结果写入文件:

    // 因为用了自定义顶点且没有向g2o注册,这里保存自己来实现
    // 伪装成 SE3 顶点和边,让 g2o_viewer 可以认出
    ofstream fout("result_lie.g2o");
    for ( VertexSE3LieAlgebra* v:vectices )
    {
        fout<<"VERTEX_SE3:QUAT ";
        v->write(fout);
    }
    for ( EdgeSE3LieAlgebra* e:edges )
    {
        fout<<"EDGE_SE3:QUAT ";
        e->write(fout);
    }
    fout.close();

 

   4.5) 优化结果如下:

Starting: /home/qifarui/code/slambook-master/ch11/build/pose_graph_g2o_lie sphere.g2o
read total 2500 vertices, 9799 edges.
prepare optimizing ...
calling optimizing ...
iteration= 0     chi2= 781963143.389706     time= 0.369662     cumTime= 0.369662     edges= 9799     schur= 0     lambda= 6706.585223     levenbergIter= 1
iteration= 1     chi2= 236521032.458036     time= 0.334284     cumTime= 0.703946     edges= 9799     schur= 0     lambda= 2235.528408     levenbergIter= 1
iteration= 2     chi2= 142934798.398777     time= 0.323039     cumTime= 1.02699     edges= 9799     schur= 0     lambda= 745.176136     levenbergIter= 1
iteration= 3     chi2= 84490229.050136     time= 0.342605     cumTime= 1.36959     edges= 9799     schur= 0     lambda= 248.392045     levenbergIter= 1
iteration= 4     chi2= 42690811.624642     time= 0.326033     cumTime= 1.69562     edges= 9799     schur= 0     lambda= 82.797348     levenbergIter= 1
iteration= 5     chi2= 17934045.397014     time= 0.330691     cumTime= 2.02632     edges= 9799     schur= 0     lambda= 27.599116     levenbergIter= 1
iteration= 6     chi2= 8366798.477212     time= 0.338484     cumTime= 2.3648     edges= 9799     schur= 0     lambda= 18.399411     levenbergIter= 1
iteration= 7     chi2= 3482772.310310     time= 0.328993     cumTime= 2.69379     edges= 9799     schur= 0     lambda= 7.575799     levenbergIter= 1
iteration= 8     chi2= 1729487.328109     time= 0.329889     cumTime= 3.02368     edges= 9799     schur= 0     lambda= 5.050533     levenbergIter= 1
iteration= 9     chi2= 782974.656516     time= 0.312246     cumTime= 3.33593     edges= 9799     schur= 0     lambda= 1.717118     levenbergIter= 1
iteration= 10     chi2= 480960.530109     time= 0.302879     cumTime= 3.63881     edges= 9799     schur= 0     lambda= 1.144745     levenbergIter= 1
iteration= 11     chi2= 240895.040158     time= 0.301409     cumTime= 3.94022     edges= 9799     schur= 0     lambda= 0.734938     levenbergIter= 1
iteration= 12     chi2= 168428.093802     time= 0.31991     cumTime= 4.26013     edges= 9799     schur= 0     lambda= 0.380169     levenbergIter= 1
iteration= 13     chi2= 148772.984808     time= 0.343125     cumTime= 4.60325     edges= 9799     schur= 0     lambda= 0.163337     levenbergIter= 1
iteration= 14     chi2= 144381.312705     time= 0.333406     cumTime= 4.93666     edges= 9799     schur= 0     lambda= 0.054446     levenbergIter= 1
iteration= 15     chi2= 141820.019983     time= 0.338513     cumTime= 5.27517     edges= 9799     schur= 0     lambda= 0.018149     levenbergIter= 1
iteration= 16     chi2= 138280.601565     time= 0.379272     cumTime= 5.65444     edges= 9799     schur= 0     lambda= 0.012099     levenbergIter= 1
iteration= 17     chi2= 134737.613765     time= 0.311259     cumTime= 5.9657     edges= 9799     schur= 0     lambda= 0.008066     levenbergIter= 1
iteration= 18     chi2= 131592.574358     time= 0.32877     cumTime= 6.29447     edges= 9799     schur= 0     lambda= 0.005377     levenbergIter= 1
iteration= 19     chi2= 129088.012127     time= 0.328451     cumTime= 6.62292     edges= 9799     schur= 0     lambda= 0.003585     levenbergIter= 1
iteration= 20     chi2= 127881.146473     time= 0.326231     cumTime= 6.94915     edges= 9799     schur= 0     lambda= 0.002137     levenbergIter= 1
iteration= 21     chi2= 127607.121623     time= 0.333534     cumTime= 7.28269     edges= 9799     schur= 0     lambda= 0.000712     levenbergIter= 1
iteration= 22     chi2= 127578.889888     time= 0.33181     cumTime= 7.6145     edges= 9799     schur= 0     lambda= 0.000237     levenbergIter= 1
iteration= 23     chi2= 127578.158794     time= 0.326293     cumTime= 7.94079     edges= 9799     schur= 0     lambda= 0.000079     levenbergIter= 1
iteration= 24     chi2= 127578.157859     time= 0.456538     cumTime= 8.39733     edges= 9799     schur= 0     lambda= 0.000053     levenbergIter= 1
iteration= 25     chi2= 127578.157859     time= 0.395115     cumTime= 8.79244     edges= 9799     schur= 0     lambda= 0.000035     levenbergIter= 1
iteration= 26     chi2= 127578.157859     time= 1.46339     cumTime= 10.2558     edges= 9799     schur= 0     lambda= 0.001501     levenbergIter= 4
iteration= 27     chi2= 127578.157859     time= 0.781467     cumTime= 11.0373     edges= 9799     schur= 0     lambda= 0.002002     levenbergIter= 2
iteration= 28     chi2= 127578.157859     time= 0.35155     cumTime= 11.3888     edges= 9799     schur= 0     lambda= 0.001334     levenbergIter= 1
iteration= 29     chi2= 127578.157859     time= 2.98557     cumTime= 14.3744     edges= 9799     schur= 0     lambda= 48074889408591.265625     levenbergIter= 10
saving optimization results ...

   结果分析:

     a:  从结果中可以看出,算法在23次迭代的时候,基本收敛,总体误差保持不变。

     b:  而前一个实验中,g2o原生优化的方法,总体误差一直在收敛。(因为定义的误差计算方式不一样,因此不能直接比较两次的数值大小

     c: 用g2o_viewer result_lie.g2o 打开李代数优化以后的结果,点击优化按钮,查看用g2o原生的整体误差度量方法,评价当前方案优化出来的结果如下:

     从该结果中可以看出,前面实验用 30 次迭代优化的总体误差为44811.248504; 而本次实验在 23 次 迭代就收敛到 44360.492972的水平,说明用李代数实现的方法可以在更少的迭代次数下得到了更好的结果。

loaded result_lie.g2o with 2500 vertices and 9799 measurements
graph is fixed by node 2499
# Using CHOLMOD poseDim -1 landMarkDim -1 blockordering 0
Preparing (no marginalization of Landmarks)
iteration= 0     chi2= 44360.492972     time= 0.223326     cumTime= 0.223326     edges= 9799     schur= 0
iteration= 1     chi2= 44360.454460     time= 0.163356     cumTime= 0.386682     edges= 9799     schur= 0
iteration= 2     chi2= 44360.454460     time= 0.165951     cumTime= 0.552633     edges= 9799     schur= 0
iteration= 3     chi2= 44360.454460     time= 0.169297     cumTime= 0.72193     edges= 9799     schur= 0
iteration= 4     chi2= 44360.454460     time= 0.217875     cumTime= 0.939805     edges= 9799     schur= 0
iteration= 5     chi2= 44360.454460     time= 0.219421     cumTime= 1.15923     edges= 9799     schur= 0
iteration= 6     chi2= 44360.454460     time= 0.185975     cumTime= 1.3452     edges= 9799     schur= 0
iteration= 7     chi2= 44360.454460     time= 0.169911     cumTime= 1.51511     edges= 9799     schur= 0
iteration= 8     chi2= 44360.454460     time= 0.175694     cumTime= 1.69081     edges= 9799     schur= 0
iteration= 9     chi2= 44360.454460     time= 0.159107     cumTime= 1.84991     edges= 9799     schur= 0

 

5. 因子图优化实现——gtsam使用

  5.1) 初始值变量gtsam::Values给定

    读取顶点信息,设置初始值变量,通过insert函数设置id和位姿信息(gtsam::Pose3

  5.2) 因子图构建(gtsam::NonlinearFactorGraph

    读取文件中边的信息,往因子图中增加量测因子,并设置因子的噪声大小(协方差阵)

    量测因子构建:new gtsam::BetweenFactor<gtsam::Pose3> ,参数为连接的两个变量ID,以及量测信息(gtsam::Pose3),以及量测噪声阵(gtsam::SharedNoiseModel

    注:g2o的信息矩阵定义方式与gtsam不同

  5.3) 给定初值值,在因子图中增加先验因子

    先验因子构建:gtsam::PriorFactor<gtsam::Pose3>:参数为变量键值,变量值以及噪声模型

    此处噪声模型:gtsam::noiseModel::Diagonal::Variances

  5.4) 因子图优化优化器设置

    其中不同的求解方法对应这不同的类型,构建该类型需要的参数:构建的因子图,给定的初始值以及该求解方法的配置信息

    注:在稀疏化处理方面,gtsam可以选择使用QR分解或者CHOLESKY分解,具体差别可以查看参数,源代码进行了解(由于单纯的位姿图没有太多的稀疏性可以利用,所以这里区别不大。)

  5.5) 调用优化器 函数optimize进行优化,通过返回值返回优化优化值

  5.6) 从最终优化结果中输出位姿图文件

    其中包括变量信息和因子信息的输出,包括其中的量测噪声信息。

 

   5.7)整体代码如下

#include <iostream>
#include <fstream>
#include <string>
#include <Eigen/Core>

#include <sophus/se3.h>
#include <sophus/so3.h>

#include <gtsam/slam/dataset.h>
#include <gtsam/slam/BetweenFactor.h>
#include <gtsam/slam/PriorFactor.h>
#include <gtsam/nonlinear/GaussNewtonOptimizer.h>
#include <gtsam/nonlinear/LevenbergMarquardtOptimizer.h>

using namespace std;
using Sophus::SE3;
using Sophus::SO3;

/************************************************
 * 本程序演示如何用 gtsam 进行位姿图优化
 * sphere.g2o 是人工生成的一个 Pose graph,我们来优化它。
 * 与 g2o 相似,在 gtsam 中添加的是因子,相当于误差
 * **********************************************/

int main ( int argc, char** argv )
{
    if ( argc != 2 )
    {
        cout<<"Usage: pose_graph_gtsam sphere.g2o"<<endl;
        return 1;
    }
    ifstream fin ( argv[1] );
    if ( !fin )
    {
        cout<<"file "<<argv[1]<<" does not exist."<<endl;
        return 1;
    }

    gtsam::NonlinearFactorGraph::shared_ptr graph ( new gtsam::NonlinearFactorGraph );  // gtsam的因子图
    gtsam::Values::shared_ptr initial ( new gtsam::Values ); // 初始值
    // 从g2o文件中读取节点和边的信息
    int cntVertex=0, cntEdge = 0;
    cout<<"reading from g2o file"<<endl;
    
    while ( !fin.eof() )
    {
        string tag;
        fin>>tag;
        if ( tag == "VERTEX_SE3:QUAT" )
        {
            // 顶点
            gtsam::Key id;
            fin>>id;
            double data[7];
            for ( int i=0; i<7; i++ ) fin>>data[i];
            // 转换至gtsam的Pose3
            gtsam::Rot3 R = gtsam::Rot3::Quaternion ( data[6], data[3], data[4], data[5] );
            gtsam::Point3 t ( data[0], data[1], data[2] );
            initial->insert ( id, gtsam::Pose3 ( R,t ) );       // 添加初始值
            cntVertex++;
        }
        else if ( tag == "EDGE_SE3:QUAT" )
        {
            // 边,对应到因子图中的因子
            gtsam::Matrix m = gtsam::I_6x6;     // 信息矩阵
            gtsam::Key id1, id2;
            fin>>id1>>id2;
            double data[7];
            for ( int i=0; i<7; i++ ) fin>>data[i];
            gtsam::Rot3 R = gtsam::Rot3::Quaternion ( data[6], data[3], data[4], data[5] );
            gtsam::Point3 t ( data[0], data[1], data[2] );
            for ( int i=0; i<6; i++ )
                for ( int j=i; j<6; j++ )
                {
                    double mij;
                    fin>>mij;
                    m ( i,j ) = mij;
                    m ( j,i ) = mij;
                }
                
            // g2o的信息矩阵定义方式与gtsam不同,这里对它进行修改
            gtsam::Matrix mgtsam = gtsam::I_6x6;
            mgtsam.block<3,3> ( 0,0 ) = m.block<3,3> ( 3,3 ); // cov rotation
            mgtsam.block<3,3> ( 3,3 ) = m.block<3,3> ( 0,0 ); // cov translation
            mgtsam.block<3,3> ( 0,3 ) = m.block<3,3> ( 0,3 ); // off diagonal
            mgtsam.block<3,3> ( 3,0 ) = m.block<3,3> ( 3,0 ); // off diagonal
            
            gtsam::SharedNoiseModel model = gtsam::noiseModel::Gaussian::Information ( mgtsam );        // 高斯噪声模型
            gtsam::NonlinearFactor::shared_ptr factor ( 
                new gtsam::BetweenFactor<gtsam::Pose3> ( id1, id2, gtsam::Pose3 ( R,t ), model ) // 添加一个因子  BetweenFactor
            );
            graph->push_back ( factor );            // 指针push 
            cntEdge++;
        }
        if ( !fin.good() )
            break;
    }
    
    cout<<"read total "<<cntVertex<<" vertices, "<<cntEdge<<" edges."<<endl;
    // 固定第一个顶点,在gtsam中相当于添加一个先验因子 
    gtsam::NonlinearFactorGraph graphWithPrior = *graph;
    gtsam::noiseModel::Diagonal::shared_ptr priorModel = 
        gtsam::noiseModel::Diagonal::Variances (
            ( gtsam::Vector ( 6 ) <<1e-6, 1e-6, 1e-6, 1e-6, 1e-6, 1e-6 ).finished() 
        );
    gtsam::Key firstKey = 0;
    for ( const gtsam::Values::ConstKeyValuePair& key_value: *initial )
    {
        cout<<"Adding prior to g2o file "<<endl;
        graphWithPrior.add ( gtsam::PriorFactor<gtsam::Pose3> ( 
            key_value.key, key_value.value.cast<gtsam::Pose3>(), priorModel )   // PriorFactor
        );
        break;
    }

    // 开始因子图优化,配置优化选项
    cout<<"optimizing the factor graph"<<endl;
    // 我们使用 LM 优化
    gtsam::LevenbergMarquardtParams params_lm;
    params_lm.setVerbosity("ERROR");
    params_lm.setMaxIterations(20);
    params_lm.setLinearSolverType("MULTIFRONTAL_QR");
    gtsam::LevenbergMarquardtOptimizer optimizer_LM( graphWithPrior, *initial, params_lm );
    
    // 你可以尝试下 GN
    // gtsam::GaussNewtonParams params_gn;
    // params_gn.setVerbosity("ERROR");
    // params_gn.setMaxIterations(20);
    // params_gn.setLinearSolverType("MULTIFRONTAL_QR");
    // gtsam::GaussNewtonOptimizer optimizer ( graphWithPrior, *initial, params_gn );
    
    gtsam::Values result = optimizer_LM.optimize();
    cout<<"Optimization complete"<<endl;
    cout<<"initial error: "<<graph->error ( *initial ) <<endl;
    cout<<"final error: "<<graph->error ( result ) <<endl;

    cout<<"done. write to g2o ... "<<endl;
    // 写入 g2o 文件,同样伪装成 g2o 中的顶点和边,以便用 g2o_viewer 查看。
    // 顶点咯
    ofstream fout ( "result_gtsam.g2o" );
    for ( const gtsam::Values::ConstKeyValuePair& key_value: result )
    {
        gtsam::Pose3 pose = key_value.value.cast<gtsam::Pose3>();
        gtsam::Point3 p = pose.translation();
        gtsam::Quaternion q = pose.rotation().toQuaternion();
        fout<<"VERTEX_SE3:QUAT "<<key_value.key<<" "
            <<p.x() <<" "<<p.y() <<" "<<p.z() <<" "
            <<q.x()<<" "<<q.y()<<" "<<q.z()<<" "<<q.w()<<" "<<endl;
    }
    // 边咯 
    for ( gtsam::NonlinearFactor::shared_ptr factor: *graph )
    {
        gtsam::BetweenFactor<gtsam::Pose3>::shared_ptr f = dynamic_pointer_cast<gtsam::BetweenFactor<gtsam::Pose3>>( factor );
        if ( f )
        {
            gtsam::SharedNoiseModel model = f->noiseModel();
            gtsam::noiseModel::Gaussian::shared_ptr gaussianModel = dynamic_pointer_cast<gtsam::noiseModel::Gaussian>( model );
            if ( gaussianModel )
            {
                // write the edge information 
                gtsam::Matrix info = gaussianModel->R().transpose() * gaussianModel->R();
                gtsam::Pose3 pose = f->measured();
                gtsam::Point3 p = pose.translation();
                gtsam::Quaternion q = pose.rotation().toQuaternion();
                fout<<"EDGE_SE3:QUAT "<<f->key1()<<" "<<f->key2()<<" "
                    <<p.x() <<" "<<p.y() <<" "<<p.z() <<" "
                    <<q.x()<<" "<<q.y()<<" "<<q.z()<<" "<<q.w()<<" ";
                gtsam::Matrix infoG2o = gtsam::I_6x6;
                infoG2o.block(0,0,3,3) = info.block(3,3,3,3); // cov translation
                infoG2o.block(3,3,3,3) = info.block(0,0,3,3); // cov rotation
                infoG2o.block(0,3,3,3) = info.block(0,3,3,3); // off diagonal
                infoG2o.block(3,0,3,3) = info.block(3,0,3,3); // off diagonal
                for ( int i=0; i<6; i++ )
                    for ( int j=i; j<6; j++ )
                    {
                        fout<<infoG2o(i,j)<<" ";
                    }
                fout<<endl;
            }
        }
    }
    fout.close();
    cout<<"done."<<endl;
}

  5.8) 结果如下:

  

Starting: /home/qifarui/code/slambook-master/ch11/build/pose_graph_gtsam sphere.g2o
reading from g2o file
read total 2500 vertices, 9799 edges.
Adding prior to g2o file 
optimizing the factor graph
Initial error: 4.7724e+09
newError: 6.07118e+08
errorThreshold: 6.07118e+08 > 0
absoluteDecrease: 4165284178.46 >= 1e-05
relativeDecrease: 0.872785675288 >= 1e-05
newError: 5568201.60391
errorThreshold: 5568201.60391 > 0
absoluteDecrease: 601549707.176 >= 1e-05
relativeDecrease: 0.990828467546 >= 1e-05
newError: 66469.3394102
errorThreshold: 66469.3394102 > 0
absoluteDecrease: 5501732.2645 >= 1e-05
relativeDecrease: 0.988062691666 >= 1e-05
newError: 63794.2086834
errorThreshold: 63794.2086834 > 0
absoluteDecrease: 2675.13072678 >= 1e-05
relativeDecrease: 0.0402460856467 >= 1e-05
newError: 63793.9289983
errorThreshold: 63793.9289983 > 0
absoluteDecrease: 0.279685093155 >= 1e-05
relativeDecrease: 4.38417685441e-06 < 1e-05
converged
errorThreshold: 63793.9289983 <? 0
absoluteDecrease: 0.279685093155 <? 1e-05
relativeDecrease: 4.38417685441e-06 <? 1e-05
iterations: 5 >? 20
Optimization complete
initial error: 4772402087.24
final error: 63793.9289982
done. write to g2o ... 
done.

  从结果中可以看出:

    算法在经过5次迭代以后收敛,整体误差每次迭代逐步减小,从最初的4772402087.24减小到 63793.9289982(该绝对值和g2o的整体误差直接比较没有意义

    用g2o_viewer result_gtsam.g2o查看误差情况: 44360.747407,和李代数实现收敛基本一致,但是,该方法收敛速更快,迭代的次数更少。

注:

    本例程为体现出gtsam的增量特性(如果将这些位姿点按时间顺序,逐步放入,可能对起增量特性进行验证和了解)

 

 

posted @ 2021-08-18 14:59  博客园—哆啦A梦  阅读(3765)  评论(0)    收藏  举报