《SLAM14讲》后端知识汇总及非线性优化

本文深入探讨了g2o非线性优化库的内部结构和使用流程,包括SparseOptimizer、顶点、边、求解器的选择以及优化步骤。通过实例展示了如何配置线性求解器、迭代算法和优化参数,并详细解释了图优化中各个节点和边的类型。最后,文章以ORB-SLAM2为例,阐述了在全局BA中所用的边、节点和求解器类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、介绍一些你熟悉的非线性优化库

        非线性优化库一般有ceres和g2o两种,我比较熟悉的是g2o,看下g2o的结构:

        它表示了g2o中的类结构。 首先根据前面的代码经验可以发现,我们最终使用的optimizer是一个SparseOptimizer对象,因此我们要维护的就是它(对它进行各种操作)。 一个SparseOptimizer是一个可优化图(OptimizableGraph),也是一个超图(HyperGraph)。而图中有很多顶点(Vertex)和边(Edge)。顶点继承于BaseVertex,边继承于BaseUnaryEdge、BaseBinaryEdge或BaseMultiEdge。它们都是抽象的基类,实际有用的顶点和边都是它们的派生类。我们用SparseOptimizer.addVertex和SparseOptimizer.addEdge向一个图中添加顶点和边,最后调用SparseOptimizer.optimize完成优化。
 

在优化之前还需要制定求解器和迭代算法。一个SparseOptimizer拥有一个OptimizationAlgorithm,它继承自Gauss-Newton, Levernberg-Marquardt, Powell’s dogleg三者之一。同时,这个OptimizationAlgorithm拥有一个Solver,它含有两个部分。一个是 SparseBlockMatrix,用于计算稀疏的雅可比和海塞矩阵;一个是线性方程求解器,可从PCG、CSparse、Choldmod三选一,用于求解迭代过程中最关键的一步:
H Δ x = − b 

        因此理清了g2o的结构,也就知道了其使用流程。在之前已经说过了,这里就再重复一遍:
(1)选择一个线性方程求解器,PCG、CSparse、Choldmod三选一,来自g2o/solvers文件夹
(2)选择一个BlockSolver,用于求解雅克比和海塞矩阵,来自g2o/core文件夹
(3)选择一个迭代算法,GN、LM、DogLeg三选一,来自g2o/core文件夹
参考G2O图优化基础和SLAM的Bundle Adjustment(光束法平差)

 代码如下:

void bundleAdjustment (
        const vector< Point3f > points_3d,
        const vector< Point2f > points_2d,
        Mat& K   )
{
    // creat g2o
    // new g2o version. Ref:https://www.cnblogs.com/xueyuanaichiyu/p/7921382.html

    typedef g2o::BlockSolver< g2o::BlockSolverTraits<6, 3> > Block;  // pose 维度为 6, landmark 维度为 3
    // 第1步:创建一个线性求解器LinearSolver
    Block::LinearSolverType* linearSolver = new g2o::LinearSolverCSparse<Block::PoseMatrixType>();
    // 第2步:创建BlockSolver。并用上面定义的线性求解器初始化
    Block* solver_ptr = new Block (  std::unique_ptr<Block::LinearSolverType>(linearSolver) );
    // Block* solver_ptr = new Block ( linearSolver );

    // 第3步:创建总求解器solver。并从GN, LM, DogLeg 中选一个,再用上述块求解器BlockSolver初始化
    g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( std::unique_ptr<Block>(solver_ptr) );
    // g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );

    // 第4步:创建稀疏优化器
    g2o::SparseOptimizer optimizer;
    optimizer.setAlgorithm ( solver );

//    // old g2o version
//    typedef g2o::BlockSolver< g2o::BlockSolverTraits<6,3> > Block;  // pose 维度为 6, landmark 维度为 3
//    Block::LinearSolverType* linearSolver = new g2o::LinearSolverCSparse<Block::PoseMatrixType>(); // 线性方程求解器
//    Block* solver_ptr = new Block ( linearSolver );     // 矩阵块求解器
//    g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg ( solver_ptr );
//    g2o::SparseOptimizer optimizer;
//    optimizer.setAlgorithm ( solver );

    // 第5步:定义图的顶点和边。并添加到SparseOptimizer中
	// ----------------------开始你的代码:设置并添加顶点,初始位姿为单位矩阵
    g2o::VertexSE3Expmap* pose = new g2o::VertexSE3Expmap();    // camera pose, 李代数位姿顶点
    Eigen::Matrix3d R_mat = Eigen::Matrix3d::Identity();        // 设置为单位阵
    pose->setId(0);
    //pose->setEstimate(g2o::SE3Quat(
    //                        R_mat,
    //                        Eigen::Vector3d::Identity()));    // 这里初值设置错误
    pose->setEstimate(g2o::SE3Quat(
            R_mat,
            Eigen::Vector3d::Zero()));                          // 平移部分设置为0
    optimizer.addVertex( pose );

    int index = 1;
    for ( const Point3f p : points_3d )   // landmarks,第一幅图坐标系下的三维空间点
    {
        g2o::VertexSBAPointXYZ* point = new g2o::VertexSBAPointXYZ();
        point->setId( index ++ );
        point->setEstimate( Eigen::Vector3d( p.x, p.y, p.z ) );
        point->setMarginalized( true );     // g2o中必须设置marg参见第10讲内容
        optimizer.addVertex( point );
    }
	// ----------------------结束你的代码
	
	
    // 设置相机内参
    g2o::CameraParameters* camera = new g2o::CameraParameters (
            K.at<double> ( 0,0 ), Eigen::Vector2d ( K.at<double> ( 0,2 ), K.at<double> ( 1,2 ) ), 0);
    camera->setId ( 0 );
    optimizer.addParameter ( camera );

    // 设置边
    index = 1;
    for ( const Point2f p:points_2d )
    {
        g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();
        edge->setId ( index );
        edge->setVertex ( 0, dynamic_cast<g2o::VertexSBAPointXYZ*> ( optimizer.vertex ( index ) ) );
        edge->setVertex ( 1, pose );
        edge->setMeasurement ( Eigen::Vector2d ( p.x, p.y ) );  //设置观测值
        edge->setParameterId ( 0,0 );
        edge->setInformation ( Eigen::Matrix2d::Identity() );
        optimizer.addEdge ( edge );
        index ++;
    }

    // 第6步:设置优化参数,开始执行优化
    optimizer.setVerbose ( false );
    optimizer.initializeOptimization();
    optimizer.optimize ( 100 );

    // 输出优化结果
    cout<<endl<<"after optimization:"<<endl;
    cout<<"T="<<endl<<Eigen::Isometry3d ( pose->estimate() ).matrix() <<endl;
}

这里我补充下:
        注意到上面的结构图中,节点Basevertex<D,T>,BaseBinaryEdge<D,E,VertexXi,VertexXj>和BlockSolver<>等都是模板类,我们可以根据自己的需要初始化不同类型的节点和边以及求解器,以ORB SLAM2为例,分析下后端最典型的全局BA所用的边、节点和求解器:
        (1)边是EdgeSE3ProjectXYZ,它其实就是继承自BaseBinaryEdge<2, Vector2d, VertexSBAPointXYZ, VertexSE3Expmap>,其模板类型里第一个参数是观测值维度,这里的观测值是其实就是我们的像素误差u , v u,vu,v,第二个参数就是我们观测值的类型,第三个第四个就是我们边两头节点的类型;
        (2)相机节点VertexSE3Expmap,它其实就是继承自BaseVertex<6, SE3Quat>,其模板类第一个参数就是其维度,SE3是六维的这没毛病,第二个就是节点的类型,SE3Quat就是g2o自定义的SE3的类,类里面写了各种SE3的计算法则;
        (3)空间点节点VertexSBAPointXYZ,它其实就是继承自BaseVertex<3, Vector3d>,其模板类第一个参数是说明咱空间点的维度是三维,第二个参数说明这个点的类型是Vector3d;
        (4)求解器是BlockSolver_6_3,它其实就是BlockSolver< BlockSolverTraits<6, 3> >,6,3分别指代的就是边两边的维度了。

        我记得我刚开始学习SLAM的时候自己想办法写后端的时候很纳闷这个图是怎么构建起来的,在ORB或者SVO里面,所有的地图点和关键帧都是以类的形式存在的,例如在ORB中是先将关键帧的节点添加起来,然后添加空间点,然后遍历空间点中记录的与哪些关键帧有关系,然后相应ID的关键帧的节点和空间点的节点连接起来,然后就把图建立起来了,我觉得不写类好像没有什么其他更好的办法了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

家门Jm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值