zoukankan      html  css  js  c++  java
  • 从零开始学g2o(2)

    1、g2o顶点编程

    1.1 g2o自带的顶点类型

    g2o本身内部定义了一些常用的顶点类型

    VertexSE2 : public BaseVertex<3, SE2>  //2D pose Vertex, (x,y,theta)
    VertexSE3 : public BaseVertex<6, Isometry3>  //6d vector (x,y,z,qx,qy,qz) (note that we leave out the w part of the quaternion)
    VertexPointXY : public BaseVertex<2, Vector2>
    VertexPointXYZ : public BaseVertex<3, Vector3>
    VertexSBAPointXYZ : public BaseVertex<3, Vector3>
    
    // SE3 Vertex parameterized internally with a transformation matrix and externally with its exponential map
    VertexSE3Expmap : public BaseVertex<6, SE3Quat>
    
    // SBACam Vertex, (x,y,z,qw,qx,qy,qz),(x,y,z,qx,qy,qz) (note that we leave out the w part of the quaternion.
    // qw is assumed to be positive, otherwise there is an ambiguity in qx,qy,qz as a rotation
    VertexCam : public BaseVertex<6, SBACam>
    
    // Sim3 Vertex, (x,y,z,qw,qx,qy,qz),7d vector,(x,y,z,qx,qy,qz) (note that we leave out the w part of the quaternion.
    VertexSim3Expmap : public BaseVertex<7, Sim3>
    

    1.2 自定义顶点类型

    重新定义顶点一般需要考虑重写如下函数:

    virtual bool read(std::istream& is);
    virtual bool write(std::ostream& os) const;
    virtual void oplusImpl(const number_t* update);
    virtual void setToOriginImpl();
    

    其中:
    read,write:分别是读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可以。

    setToOriginImpl:顶点重置函数,设定被优化变量的原始值。

    oplusImpl:顶点更新函数。非常重要的一个函数,主要用于优化过程中增量△x 的计算。我们根据增量方程计算出增量之后,就是通过这个函数对估计值进行调整的,因此这个函数的内容一定要重视。

    自己定义 顶点一般是下面的格式

    class myVertex: public g2::BaseVertex<Dim, Type>
      {
          public:
          EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    
          myVertex(){}
    
          virtual void read(std::istream& is) {}
          virtual void write(std::ostream& os) const {}
    
          virtual void setOriginImpl()
          {
              _estimate = Type();
          }
          virtual void oplusImpl(const double* update) override
          {
              _estimate += /*update*/;
          }
      }
    

    举个例子:
    来自十四讲中的曲线拟合,来源如下

    ch6/g2o_curve_fitting/main.cpp

    // 曲线模型的顶点,模板参数:优化变量维度和数据类型

    class CurveFittingVertex: public g2o::BaseVertex<3, Eigen::Vector3d>
    {
    public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW
        virtual void setToOriginImpl() // 重置
        {
            _estimate << 0,0,0;
        }
    
        virtual void oplusImpl( const double* update ) // 更新
        {
            _estimate += Eigen::Vector3d(update);
        }
        // 存盘和读盘:留空
        virtual bool read( istream& in ) {}
        virtual bool write( ostream& out ) const {}
    };
    

    我们可以看到下面代码中顶点初值设置为0,更新时也是直接把更新量 update 加上去的。
    更新不就是 x + △x ,因此对于这个例子是可以直接加,因为顶点类型是Eigen::Vector3d,属于向量,是可以通过加法来更新的。但是但是有些例子就不行,比如下面这个复杂点例子:李代数表示位姿VertexSE3Expmap:

    来自g2o官网,在这里
    
    g2o/types/sba/types_six_dof_expmap.h
    
    /**
    
     * rief SE3 Vertex parameterized internally with a transformation matrix
    
     and externally with its exponential map
    
     */
    
    class G2O_TYPES_SBA_API 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 number_t* update_)  {
        Eigen::Map<const Vector6> update(update_);
        setEstimate(SE3Quat::exp(update)*estimate());        //更新方式
      }
    };
    

    1.3 如何向图中添加顶点?

    往图中增加顶点比较简单,我们还是先看看第一个曲线拟合的例子,setEstimate(type) 函数来设定初始值;setId(int) 定义节点编号

    // 往图中增加顶点
    CurveFittingVertex* v = new CurveFittingVertex();
    v->setEstimate( Eigen::Vector3d(0,0,0) );
    v->setId(0);
    optimizer.addVertex( v );
    

    这个是添加 VertexSBAPointXYZ 的例子,都很容易看懂

    /ch7/pose_estimation_3d2d.cpp

    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 ); 
        optimizer.addVertex ( point );
    }
    

    2、 g2o边的编程

    BaseUnaryEdge,BaseBinaryEdge,BaseMultiEdge 分别表示一元边,两元边,多元边。一元边你可以理解为一条边只连接一个顶点,两元边理解为一条边连接两个顶点,也就是我们常见的边啦,多元边理解为一条边可以连接多个(3个以上)顶点
    边有几个主要参数:D, E, VertexXi, VertexXj,他们的分别代表:

    D 是 int 型,表示测量值的维度 (dimension)
    E 表示测量值的数据类型
    VertexXi,VertexXj 分别表示不同顶点的类型
    比如我们用边表示三维点投影到图像平面的重投影误差,就可以设置输入参数如下:

     BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>
    

    首先这个是个二元边。第1个2是说测量值是2维的,也就是图像像素坐标x,y的差值,对应测量值的类型是Vector2D,两个顶点也就是优化变量分别是三维点 VertexSBAPointXYZ,和李群位姿VertexSE3Expmap。

    边的几个重要的成员函数:

    //read,write:分别是读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可以
    virtual bool read(std::istream& is);
    virtual bool write(std::ostream& os) const;
    virtual void computeError();//非常重要,是使用当前顶点的值计算的测量值与真实的测量值之间的误差
    virtual void linearizeOplus();//非常重要,是在当前顶点的值下,该误差对优化变量的偏导数,也就是我们说的Jacobian
    
    //其他成员函数和成员变量
    _measurement:存储观测值
    _error:存储computeError() 函数计算的误差
    _vertices[]:存储顶点信息,比如二元边的话,_vertices[] 的大小为2,存储顺序和调用setVertex(int, vertex) 是设定的int 有关(01setId(int):来定义边的编号(决定了在H矩阵中的位置)
    setMeasurement(type) 函数来定义观测值
    setVertex(int, vertex) 来定义顶点
    setInformation() 来定义协方差矩阵的逆
    

    2.1 如何自定义g2o的边

    模板如下:

     class myEdge: public g2o::BaseBinaryEdge<errorDim, errorType, Vertex1Type, Vertex2Type>
      {
          public:
          EIGEN_MAKE_ALIGNED_OPERATOR_NEW      
          myEdge(){}     
          virtual bool read(istream& in) {}
          virtual bool write(ostream& out) const {}      
          virtual void computeError() override
          {
              // ...
              _error = _measurement - Something;
          }      
          virtual void linearizeOplus() override
          {
              _jacobianOplusXi(pos, pos) = something;
              // ...         
              /*
              _jocobianOplusXj(pos, pos) = something;
              ...
              */
          }      
          private:
          // data
      }
    

    先来看一个简单例子,地址在
    https://github.com/gaoxiang12/slambook/blob/master/ch6/g2o_curve_fitting/main.cpp
    这个是个一元边,主要是定义误差函数了,如下所示,你可以发现这个例子基本就是上面例子的一丢丢扩展,是不是感觉so easy?

    // 误差模型 模板参数:观测值维度,类型,连接顶点类型
    class CurveFittingEdge: public g2o::BaseUnaryEdge<1,double,CurveFittingVertex>
    {
    public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW
        CurveFittingEdge( double x ): BaseUnaryEdge(), _x(x) {}
        // 计算曲线模型误差
        void computeError()
        {
            const CurveFittingVertex* v = static_cast<const CurveFittingVertex*> (_vertices[0]);
            const Eigen::Vector3d abc = v->estimate();
            _error(0,0) = _measurement - std::exp( abc(0,0)*_x*_x + abc(1,0)*_x + abc(2,0) ) ;
        }
        virtual bool read( istream& in ) {}
        virtual bool write( ostream& out ) const {}
    public:
        double _x;  // x 值, y 值为 _measurement
    };
    

    下面是一个复杂一点例子,3D-2D点的PnP 问题,也就是最小化重投影误差问题,这个问题非常常见,使用最常见的二元边。代码在g2o的GitHub上这个地方可以看到
    g2o/types/sba/types_six_dof_expmap.h

    //继承了BaseBinaryEdge类,观测值是2维,类型Vector2D,顶点分别是三维点、李群位姿
    class G2O_TYPES_SBA_API EdgeProjectXYZ2UV : public  BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>{
      public:
        EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
        //1. 默认初始化
        EdgeProjectXYZ2UV();
        //2. 计算误差
        void computeError()  {
          //李群相机位姿v1
          const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
          // 顶点v2
          const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
          //相机参数
          const CameraParameters * cam
            = static_cast<const CameraParameters *>(parameter(0));
         //误差计算,测量值减去估计值,也就是重投影误差obs-cam
         //估计值计算方法是T*p,得到相机坐标系下坐标,然后在利用camera2pixel()函数得到像素坐标。
          Vector2D obs(_measurement);
          _error = obs-cam->cam_map(v1->estimate().map(v2->estimate()));
        }
        //3. 线性增量函数,也就是雅克比矩阵J的计算方法
        virtual void linearizeOplus();
        //4. 相机参数
        CameraParameters * _cam; 
        bool read(std::istream& is);
        bool write(std::ostream& os) const;
    };
    

    有一个地方比较难理解

    _error = obs - cam->cam_map(v1->estimate().map(v2->estimate()));
    

    其实就是:误差 = 观测 - 投影

    捋捋思路。我们先来看看cam_map 函数,它的定义在
    g2o/types/sba/types_six_dof_expmap.cpp
    cam_map 函数功能是把相机坐标系下三维点(输入)用内参转换为图像坐标(输出),具体代码如下所示

    Vector2  CameraParameters::cam_map(const Vector3 & trans_xyz) const {
      Vector2 proj = project2d(trans_xyz);
      Vector2 res;
      res[0] = proj[0]*focal_length + principle_point[0];
      res[1] = proj[1]*focal_length + principle_point[1];
      return res;
    }
    

    然后看 .map函数,它的功能是把世界坐标系下三维点变换到相机坐标系,函数在
    g2o/types/sim3/sim3.h
    具体定义是

    Vector3 map (const Vector3& xyz) const {
    return s*(r*xyz) + t;
    }

    因此下面这个代码

    v1->estimate().map(v2->estimate())
    

    就是用V1估计的pose把V2代表的三维点,变换到相机坐标系下。

    2.2 如何向图中添加边?

    一元边的添加方法

    下面代码来自GitHub上,仍然是前面曲线拟合的例子
    slambook/ch6/g2o_curve_fitting/main.cpp

    // 往图中增加边
    for ( int i=0; i<N; i++ )
    {
    CurveFittingEdge* edge = new CurveFittingEdge( x_data[i] );
    edge->setId(i);
    edge->setVertex( 0, v ); // 设置连接的顶点
    edge->setMeasurement( y_data[i] ); // 观测数值
    edge->setInformation( Eigen::Matrix<double,1,1>::Identity()1/(w_sigmaw_sigma) ); // 信息矩阵:协方差矩阵之逆
    optimizer.addEdge( edge );
    }

    setMeasurement 函数的输入的观测值具体是指什么呢?
    对于这个曲线拟合,观测值就是实际观测到的数据点。对于视觉SLAM来说,通常就是我们我们观测到的特征点坐标,
    下面就是一个例子。这个例子比刚才的复杂一点,因为它是二元边,需要用边连接两个顶点
    代码来自GitHub上
    slambook/ch7/pose_estimation_3d2d.cpp

     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++;
        }
    

    这里的setMeasurement函数里的p来自向量points_2d,也就是特征点的图像坐标(x,y)了。setVertex 有两个一个是 0 和 VertexSBAPointXYZ 类型的顶点,一个是1 和pose。这里的0和1是什么意思?能否互换呢?

    这个是setVertex在g2o官网的定义:

    // set the ith vertex on the hyper-edge to the pointer supplied
    void setVertex(size_t i, Vertex* v) { assert(i < _vertices.size() && "index out of bounds"); _vertices[i]=v;}
    

    这段代码在
    g2o/core/hyper_graph.h
    里可以找到。你看 _vertices[i] 里的i就是我们这里的0和1,我们再去看看这里边的类型: g2o::EdgeProjectXYZ2UV
    的定义,前面我们也放出来了,就这两句

    class G2O_TYPES_SBA_API EdgeProjectXYZ2UV 
    .....
     //李群相机位姿v1
    const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
    // 顶点v2
    const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*>(_vertices[0]);
    

    你看 _vertices[0] 对应的是 VertexSBAPointXYZ 类型的顶点,也就是三维点,_vertices[1] 对应的是VertexSE3Expmap 类型的顶点,也就是位姿pose。因此前面 1 对应的就应该是 pose,0对应的 应该就是三维点。

    参考链接:
    https://mp.weixin.qq.com/s/12V8iloLwVRPahE36OIPcw
    https://mp.weixin.qq.com/s/etFYWaZ6y4XPiXrfqCm53Q

  • 相关阅读:
    weexpack build android 和 weexpack run android 报错 及 解决方案
    weexapp 开发流程(三)其他页面创建
    svn 创建分支、切换分支 及 合并分支 操作
    github 新建远程仓库 及 删除远程仓库
    photoshop 前端常用技巧
    vue2.0 常用的 UI 库
    weex 小结
    Android-studio 连接真机 调试weex项目
    js中Math之random,round,ceil,floor的用法总结
    基于canvas图像处理的图片 灰色图像
  • 原文地址:https://www.cnblogs.com/long5683/p/12885783.html
Copyright © 2011-2022 走看看