zoukankan      html  css  js  c++  java
  • SLAM-G2O分析

    简介:由图中的关系,可以知道,g2o问题的食物链顶层是SparseOptimizer,一个SparseOptimizer包含优化算法,g2o里面有三种优化算法,分别为GN , LM , Dogleg,而每一种算法都需要执行Ax=b的求解,而求解这些矩阵就需要求解器Solver,用的最多的就是线性求解器,g2o里面有几种线性求解器分别为cholmod, csparse, dense, eigen, pcg。这些求解器针对什么问题后面会详细介绍。下面可以说图优化的流程了:

    1.构建SparseOptimizer , 选一种优化算法,一共有三种分别为 OptimizationAlgorithmLevenberg,OptimizationAlgorithmDogleg,OptimizationAlgorithmGaussNewton,然后在给选定的算法一个求解器。这个过程的代码如下,我们是以BA为例:

      

        g2o::SparseOptimizer optimizer;
        g2o::OptimizationAlgorithmLevenberg* solver = new g2o::OptimizationAlgorithmLevenberg(
            new g2o::BlockSolverX(
                new g2o::LinearSolverEigen<g2o::BlockSolverX::PoseMatrixType>()));
       optimizer.setAlgorithm(solver);

    2:构建属于本问题的节点,我们首先要明白,我们的节点一共有几种,所谓的节点就是我们需要优化的变量,对于BA问题我们要优化相机的位姿和地图点的三维坐标,先说相机的位姿:

      2.1 构建位姿节点

        (1)继承BaseVertex,写自己的节点,这个时候要清楚,自己的优化变量多少维度,类型是什么,比如g2o里面提供的位姿节点VertexSE3Expmap,就是6维的,然后类型是SE3Quat;  

        (2)设置节点的输入输出流

        (3)设定默认值,这个时候操纵变量_estimate

        (4)最重要的一步oplusImpl()函数,在这个函数里面参数是传入的是节点的更新量,在这个函数里面要完成之前的估计值_estimate和这个增量作用之后得到的估计值,将这个估计值再设置给_estimate,完成节点的更新。

         

    class  VertexSE3Expmap : public BaseVertex<6, SE3Quat>{ /////第一个参数是节点的维度,第二个参数是节点的类型
    public:
      EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    
      VertexSE3Expmap();
    
      bool read(std::istream& is);
    
      bool write(std::ostream& os) const;
    
      virtual void setToOriginImpl() {
        _estimate = SE3Quat(); 
      }
    
      virtual void oplusImpl(const double* update_)  {
        Eigen::Map<const Vector6d> update(update_);
        setEstimate(SE3Quat::exp(update)*estimate());//////setEstimate就是用来设置节点值的
      }
    };
    

       2.2 构建地图点节点

     class VertexSBAPointXYZ : public BaseVertex<3, Vector3d>
    {
      public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW    
        VertexSBAPointXYZ();
        virtual bool read(std::istream& is);
        virtual bool write(std::ostream& os) const;
    
        virtual void setToOriginImpl() {
          _estimate.fill(0.);
        }
    
        virtual void oplusImpl(const double* update)
        {
          Eigen::Map<const Vector3d> v(update);
          _estimate += v;
        }
    };
    

     3.构建边,每一个边就是误差函数,既然我们要估计两种变量,那就是两元边,这个边链接了相机的位姿和地图点。两元边的设置需要设置,观测维度,观测类型,关联节点类型两个种。自定义二元边需要继承BaseBinaryEdge.这里我们以EdgeSE3ProjectXYZ为例说一下继承之后需要重写哪些函数,,首先computeError是一个比较重要的函数,如何根据节点的值计算误差,那有可能人有人问,既然两个类型节点,我怎么知道计算的时候用哪一个,是这样的,_vertices[0]代表VertexSBAPointXYZ,_vertices[1]代表VertexSE3Expmap,;其实这里我们要保证一个约定就是,我们在给边添加节点的时候一般用setVertex函数,里面有指定序号的,这里的序号指定和我们public BaseBinaryEdge<2, Vector2d, VertexSBAPointXYZ, VertexSE3Expmap>中的节点顺序,以及我们利用_vertices取值的时候的索引先后关系要一一对应好。如下面代码:

      

        
    
    class  EdgeSE3ProjectXYZ: public  BaseBinaryEdge<2, Vector2d, VertexSBAPointXYZ, VertexSE3Expmap>{ /// 2代表观测维度为2,也就是我们的像素x y ; Vector2d是观测量的类型,VertexSBAPointXYZ是一个节点的类型对应_vertices[0], VertexSE3Expmap是另一个节点的类型
    public:
      EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    
      EdgeSE3ProjectXYZ();
    
      bool read(std::istream& is);
    
      bool write(std::ostream& os) const;
    
      void computeError()  {
        const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
        const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
        Vector2d obs(_measurement);
        _error = obs-cam_project(v1->estimate().map(v2->estimate()));
      }
    
      bool isDepthPositive() {
        const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
        const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
        return (v1->estimate().map(v2->estimate()))(2)>0.0;
      }
        
    
      virtual void linearizeOplus();
    
      Vector2d cam_project(const Vector3d & trans_xyz) const;
    
      double fx, fy, cx, cy;
    };            
    

    3.1 linearizeOplus()也是一个很重要的函数,对应雅克比矩阵,对于二元边,我们要得到误差函数相对于每一个状态变量的雅克比,那二元边就有两种雅克比,那这两种雅克比我们都要设定,如下面的代码:_jacobianOplusXi 对应第0个状态变量的雅克比矩阵,和_vertices[0]对应,_jacobianOplusXj是第二个状态变量的雅克比矩阵,这是二元边的专属。对于一元边linearizeOplus()中只需要设置_jacobianOplusXi就可以了。

    void EdgeSE3ProjectXYZ::linearizeOplus() {
      VertexSE3Expmap * vj = static_cast<VertexSE3Expmap *>(_vertices[1]);
      SE3Quat T(vj->estimate());
      VertexSBAPointXYZ* vi = static_cast<VertexSBAPointXYZ*>(_vertices[0]);
      Vector3d xyz = vi->estimate();
      Vector3d xyz_trans = T.map(xyz);
    
      double x = xyz_trans[0];
      double y = xyz_trans[1];
      double z = xyz_trans[2];
      double z_2 = z*z;
    
      Matrix<double,2,3> tmp;
      tmp(0,0) = fx;
      tmp(0,1) = 0;
      tmp(0,2) = -x/z*fx;
    
      tmp(1,0) = 0;
      tmp(1,1) = fy;
      tmp(1,2) = -y/z*fy;
    
      _jacobianOplusXi =  -1./z * tmp * T.rotation().toRotationMatrix();
    
      _jacobianOplusXj(0,0) =  x*y/z_2 *fx;
      _jacobianOplusXj(0,1) = -(1+(x*x/z_2)) *fx;
      _jacobianOplusXj(0,2) = y/z *fx;
      _jacobianOplusXj(0,3) = -1./z *fx;
      _jacobianOplusXj(0,4) = 0;
      _jacobianOplusXj(0,5) = x/z_2 *fx;
    
      _jacobianOplusXj(1,0) = (1+y*y/z_2) *fy;
      _jacobianOplusXj(1,1) = -x*y/z_2 *fy;
      _jacobianOplusXj(1,2) = -x/z *fy;
      _jacobianOplusXj(1,3) = 0;
      _jacobianOplusXj(1,4) = -1./z *fy;
      _jacobianOplusXj(1,5) = y/z_2 *fy;
    }
    

     4. 这一步就要给我们的优化器添加节点了:我们这里要注意节点的id是连续增长的。

      4.1 先添加位姿节点

        

            g2o::VertexSE3Expmap * vSE3 = new g2o::VertexSE3Expmap();
            vSE3->setEstimate(Converter::toSE3Quat(pKF->GetPose()));////设置估计值
            vSE3->setId(pKF->mnId);////设置节点id
            vSE3->setFixed(pKF->mnId==0);/////节点是否固定
            optimizer.addVertex(vSE3);//添加进优化器
    

       4.2 添加地图点节点

      

            g2o::VertexSBAPointXYZ* vPoint = new g2o::VertexSBAPointXYZ();
            vPoint->setEstimate(Converter::toVector3d(pMP->GetWorldPos()));
            const int id = pMP->mnId+maxKFid+1;
            vPoint->setId(id);
            vPoint->setMarginalized(true);
            optimizer.addVertex(vPoint);
    

     5. 下面我们要设置边,每一个关键帧的一个位姿节点和很多地图点之间可以构建边,构建部分包括 设置边的节点,设置观测,设置信息矩阵,设置核函数:

       g2o::EdgeSE3ProjectXYZ* e = new g2o::EdgeSE3ProjectXYZ();
    
                    e->setVertex(0, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(id)));
                    e->setVertex(1, dynamic_cast<g2o::OptimizableGraph::Vertex*>(optimizer.vertex(pKF->mnId)));
                    //e->setVertex(2, dynamic_cast<g2o::OptimizableGraph::Vertex*>(vCam));
                    e->setMeasurement(obs);
                    const float &invSigma2 = pKF->mvInvLevelSigma2[kpUn.octave];
                    e->setInformation(Eigen::Matrix2d::Identity()*invSigma2);
    
                    if(bRobust)
                    {
                        g2o::RobustKernelHuber* rk = new g2o::RobustKernelHuber;
                        e->setRobustKernel(rk);
                        rk->setDelta(thHuber2D);
                    }
    
                    e->fx = pKF->fx;
                    e->fy = pKF->fy;
                    e->cx = pKF->cx;
                    e->cy = pKF->cy;
    
                    optimizer.addEdge(e);
    

     6. 开始优化

        optimizer.initializeOptimization();
        optimizer.optimize(nIterations);
    

     7:优化完成之后我们可以通过优化器中取出对应序号的节点,里面estimate()函数得到估计值,如下:

            g2o::VertexSE3Expmap* vSE3 = static_cast<g2o::VertexSE3Expmap*>(optimizer.vertex(pKF->mnId));
            g2o::SE3Quat SE3quat = vSE3->estimate();
    

     8:优化完成后,对每一条边都进行检查,剔除误差较大的边(认为是错误的边),并设置setLevel为0,即下次不再对该边进行优化,其中vpEdgesMono里面保存着边的指针。另外,用chi2函数来获得(加权)最小二乘误差等,这些可以用来判断某条边的误差是否过大。最后,边允许获得/设置level,将边设置为不同的level,在优化时就可以分开优化不同level的边(orb-slam2的优化就采用了这种multi-level optimization)。其实chi()就是误差和信息矩阵的加权乘积,如下:

          virtual double chi2() const 
          {
            return _error.dot(information()*_error);
          }
     
    // 优化完成后,进行Edge的检查
    for(size_t i=0, iend=vpEdgesMono.size(); i<iend;i++)
    {
        g2o::EdgeSE3ProjectXYZ* e = vpEdgesMono[i];
        MapPoint* pMP = vpMapPointEdgeMono[i];
    
        if(pMP->isBad())
            continue;
    
        // 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)
        // 第二个判断条件,用于检查构成该边的MapPoint在该相机坐标系下的深度是否为正?
        if(e->chi2()>5.991 || !e->isDepthPositive())
        {
            e->setLevel(1);// 不优化
        }
    
        // 因为剔除了错误的边,所以下次优化不再使用核函数
        e->setRobustKernel(0);
    }
    

     9:其实有时候我们每进行一次迭代,之后都可以检查误差,然后设置边的级别,然后再优化这样子。

    杂项解析:

    每种edge都需要用户来实现computeError函数。

    1: 由于只链接一个节点,因此BaseUnaryEdge只多了一个模版参数:节点类型。并且多了一个雅可比矩阵的成员变量:_jacobianOplusXi。此外,由于HyperGraph::Edge类中用了vector<Vertex *>来存储一条边所连接的节点,因此,BaseUnaryEdge类在初始化的时候会将这个数组resize为1(对应地,BaseBinaryEdge类初始化时会将之resize为2)。

    2: 其最主要的函数是计算雅可比矩阵的linearizeOplus函数。可以由用户提供雅可比矩阵的解析解,而默认是g2o自己进行数值求导:g2o使用中值求导方法,其步长delta为常数值。函数对节点的_estimate加上和减去对应步长,再计算对应的误差值,再除以2*delta来计算误差函数对于特定误差项的导数。此时,为了保存节点原来的值,就会使用push/pop函数往_backup变量中保持修改前的节点估计值。(数值求导部分我们后面再细说。)

    3: 另一个比较主要的函数是constructQuadraticForm函数。该函数用来计算该误差项的系数矩阵H和对应的b向量。以BaseUnaryEdge为例,函数先调用chi2函数获得误差 ( [公式] ),如果没有使用鲁棒核函数,直接使用 [公式] 来计算系数矩阵H,用 [公式] 来计算b向量;否则,调用对应函数得到鲁棒核函数的值,及其一阶导、二阶导(rho,见后面的鲁棒核函数)。b向量可以直接通过 [公式] 来计算,但系数矩阵H则调用robustInformation函数来重新计算加权信息矩阵 (weightedOmega)。

    如下面:

          InformationType robustInformation(const Eigen::Vector3d& rho)
          {
            InformationType result = rho[1] * _information;
            //ErrorVector weightedErrror = _information * _error;
            //result.noalias() += 2 * rho[2] * (weightedErrror * weightedErrror.transpose());
            return result;
          }
    
    rho[0] = rho
    rho[1] = rho'
    rho[2]=rho''
    新的权重计算公式为: rho'  +2rho'' * _error * _error^t.
    然后最后的信息矩阵W= ( rho' +2rho'' * _error * _error^t )* _information(原来程序设置的信息矩阵).
    这样原来的 H=J^t W J.
    原来的 b = -rho'J^t_information_error.

    4:BaseBinaryEdge链接两个节点,因此比BaseEdge多两个模版参数。对应的,自然也会有两个雅可比矩阵:_jacobianOplusXi和_jacobianOplusXj。其余本质上和BaseUnaryEdge并没有差别。需要注意的是,BaseBinaryEdge类也定义了一个_hessian变量,但这个海塞矩阵并不是残差对于观测(节点)进行求导得到的雅可比J得到的 [公式] ,而是通过 [公式] 得到的,显然,这是_Hpl矩阵,即位姿和路标点的协方差矩阵(或位姿和位姿间的协方差矩阵)。

    BaseMultiEdge由于链接的节点数目是不固定的,因此其直接继承BaseEdge类而不在需要额外的模版参数。相关的雅可比矩阵也变成了一个vector变量:_jacobianOplus。链接多个节点并没有带来多大的不同。一些变量如_hessian和_jacobianOplus定义成了vector。而在计算雅可比矩阵(linearizeOplus)时,BaseUnaryEdge只计算一个雅可比矩阵,BaseBinaryEdge计算两个,而BaseMulitEdge利用循环计算了多个雅可比矩阵,分别存储_jacobianOplus[I]中;constructQuadraticForm函数也是同理。

    关于方程求解器:

    在这里插入图片描述
    总结:
    LinearSolverCholmod :使用sparse cholesky分解法。继承自LinearSolverCCS
    LinearSolverCSparse:使用CSparse法。继承自LinearSolverCCS
    LinearSolverDense :使用dense cholesky分解法。继承自LinearSolver
    LinearSolverEigen: 依赖项只有eigen,使用eigen中sparse Cholesky 求解,因此编译好后可以方便的在其他地方使用,性能和CSparse差不多。继承自LinearSolver
    LinearSolverPCG :使用preconditioned conjugate gradient 法,继承自LinearSolver

  • 相关阅读:
    2020 CCPC Wannafly Winter Camp Day2 E阔力梯的树(树上启发式合并)
    牛客练习赛73D 离别(线段树)
    从零开始部署图书管理系统
    linux下安装nginx(编译安装)及反向代理及负载均衡
    linux下MariaDB安装
    linux下virtualenvwrapper安装
    linux下安装虚拟环境
    linux下安装django2.2
    linux下安装nginx(yum源安装)
    linux系统优化命令--day03
  • 原文地址:https://www.cnblogs.com/mataiyuan/p/12884953.html
Copyright © 2011-2022 走看看