slam定位学习笔记(七)-g2o学习

2023-05-16

主要学习的是这篇文章,但大佬并没有在文章里面仔细的讲g2o,所以我在网上找了这几篇介绍g2o的文章,讲的十分详细,对入门十分友好:文章一、文章二、文章三,这三篇都是一个作者写的,主要是针对编程实际操作。

g2o入门

一、图优化是什么?

区分两个不同的概念:

1)图优化(graph-base optimization)

2)凸优化(convex optimization)

很多时候容易搞混淆,我第一次听到图优化这个词的时候,看到实验室有一本书名叫《凸优化》还以为是这个,但两个是完全不同的概念,在这里进行区分。图优化的图是数据结构里面的图,凸优化里面的凸是凸函数。

slam后端一般有两种方法,第一个是EKF(扩展卡尔曼滤波器)为代表的滤波方法;第二个就是以图优化为代表的非线性方法。当前slam研究热点几乎都是基于图优化的。

图优化里的图就是i数据结构里面的图,一个图有若干个定点(vertex),以及连接这些顶点的边(edge)。举例:一个机器人在房间中移动,它在某个时刻t的位姿(pose)就是一个顶点,这个就是一个待优化变量。而位姿之间的关系就构成一个边,比如t时刻和t+1时刻之间的相对位姿变换矩阵T就是边,边通常表示误差项。

在slam中,图优化一般分解为两步:

第一步:构建图。将机器人的位姿作为顶点,位姿间的关系作为边。

第二步:优化图。调整机器人的位姿(顶点)来尽量满足边的约束,使得误差减少。

举例:

 在机器人运行过程中,将机器人不同时刻t的位姿pose作为顶点(vertex),这个位姿可能来源与机器人自身携带的编码器或者是icp、ndt配准算法求得。图的边就是位姿之间的关系。但在机器人运行的过程中,会出现很多的误差,如图左,然后通过图优化,设置边的约束关系,就可以获得图中,与图右之间的差别就很小了。但我现在不太知道这个图左的轨迹是什么?一个是真值另一个是计算出来的轨迹吗?

二、g2o框架

在slam后端图优化一般有g2o、gtsam和ceres,这里主要介绍g2o,以后应该还会学习gtsam。

g2o全称:General Graph Optimization,是一个用来优化非线性误差函数的c++框架。简单来说就是它把优化的框架搭建好了,使用者只用专注与输入的顶点和边的建立,然后使用它优化的结果。将slam后端优化在工程实现上变的更加简单。

g2o官网:GitHub - RainerKuemmerle/g2o: g2o: A General Framework for Graph Optimization

文献:

《g2o: A General Framework for Graph Optimization》

《A Tutorial on Graph-Based SLAM》

文献以后有机会在看,先会用再说。

这是官网上关于g2o整个框架的介绍,简单明确

首先看向图片的右上角,关于"is a"、"has a"、"has many"这三个箭头的含义。最初看这个图的时候,这三个的意思一直不得要领,怎么理解都很别扭。后来感觉是不是和c++中的继承关系有关,直接按照c++ is a在搜索引擎里面寻找,果然就是我想的这样。这里来简单解释一下,方便之后的理解。

水果Fruit、香蕉Banana、午餐Lunch、米饭Rice

Banana is a kind of Fruit.

上面说的是香蕉是一种水果,is a,将它们抽象到c++的class中的继承关系就是这样的:

class Banana : public Fruit,就是Banana继承自Fruit。

这就是is a的简单理解。

Lunch has a Fruit.(可能语法有问题,不要在意)

说的是午餐有水果,也可能有别的米饭Rice什么的。它们就是一种包含关系,在class中就是这样的。

class Lunch

{

        class Fruit{};

        class Rice{};

...

}

大概就是这个道理,就是表示一种class间的继承关系。

现在来分析这张图:

首先看最左边的SparseOptimizer(稀疏优化器),按向上的箭头阅读,就是说SparseOptimizer is a OptimizableGraph,说SparseOptimizer是一个OptimizableGraph(可优化的图)。而OptimizableGraph is a HyperGraph(超图)。

重点就是这个HyperGraph它连接的是has many的箭头,这些箭头指向了图优化中的顶点(HyperGraph::Vertex)和边(HyperGraph::edge),就是前面午饭和水果、米饭的关系。

在HyperGraph::Vertex(顶点)可以看到OptimizableGraph::Vertex指向了它,说明OptimizableGraph::Vertex is a HyperGraph::Vertex。OptimizableGraph::Vertex继承自HyperGraph::Vertex,类似的可以推出BaseVertex<D,T>继承自OptimizableGraph::Vertex。具体请看后面对g2o的源码分析。这里的继承自也可以理解为通过xx来实现。

再通过SparseOptimizer往下看,SparseOptimizer has a OptimizationAlgorithm(优化算法)。然后这个OptimizationWithHessian is a OptimizationAlgorithm。就是说OptimizationAlgorithm 是通过OptimizationWithHessian来实现的。然后OptimizationWithHessian有三个迭代的方法:Gauss-Newton(高斯牛顿法,简称GN), Levernberg-Marquardt(简称LM法), Powell's dogleg。之后就是对于OptimizationWithHessian类的的内容进行分析,OptimizationWithHessian has a Solver,OptimizationWithHessian类里面有一个Solver(求解器)。然后对这个Solver进行分析。Solver is a BlockSolver。BlockSolver包含了两个类SparseBlockMatrix<T>用于计算稀疏的雅可比Hessian矩阵LinearSolver它用于计算迭代过程中最关键的一步HΔx=−b。LinearSolveru有三个PCG, CSparse, Choldmod方法。

三、g2o运行流程

在前面的框架图中,介绍是从上到下的。在g2o的运行过程中是从下到上的。

 整个流程是这样的:

第一步:先确定采用什么线性求解器。

第二步:使用第一步的线性求解器初始化BlockSlover<>。

第三步:从三个迭代方法中选择合适的并使用第二步获得的BlockSlover<>来初始化Solver。

第四步:创建核心SparseOptimizer.

第五步:定义顶点和边,然后添加到SparseOptimizer中去。

根据高博十四讲里面介绍使用g2o的源码分析:
 

 // 构建图优化,先设定g2o
  typedef g2o::BlockSolver<g2o::BlockSolverTraits<3, 1>> BlockSolverType;  // 每个误差项优化变量维度为3,误差值维度为1
  typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType; // 线性求解器类型

   //这里将前三步合并到一起了
  // 梯度下降方法,可以从GN, LM, DogLeg 中选
  auto solver = new g2o::OptimizationAlgorithmGaussNewton(
    g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
  // 第四步
  g2o::SparseOptimizer optimizer;     // 图模型
  optimizer.setAlgorithm(solver);   // 设置求解器
  optimizer.setVerbose(true);       // 打开调试输出

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

  // 往图中增加边
  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_sigma * w_sigma)); // 信息矩阵:协方差矩阵之逆
    optimizer.addEdge(edge);
  }

高博这一版的源码前三步有点难理解,其实就是将第一步和第二步使用智能指针和第三步放在一行了,在文章一中有更加清晰的流程:

typedef g2o::BlockSolver< g2o::BlockSolverTraits<3,1> > Block;  // 每个误差项优化变量维度为3,误差值维度为1

// 第1步:创建一个线性求解器LinearSolver
Block::LinearSolverType* linearSolver = new g2o::LinearSolverDense<Block::PoseMatrixType>(); 

// 第2步:创建BlockSolver。并用上面定义的线性求解器初始化
Block* solver_ptr = new Block( linearSolver );      

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

// 第4步:创建终极大boss 稀疏优化器(SparseOptimizer)
g2o::SparseOptimizer optimizer;     // 图模型
optimizer.setAlgorithm( solver );   // 设置求解器
optimizer.setVerbose( true );       // 打开调试输出

// 第5步:定义图的顶点和边。并添加到SparseOptimizer中
CurveFittingVertex* v = new CurveFittingVertex(); //往图中增加顶点
v->setEstimate( Eigen::Vector3d(0,0,0) );
v->setId(0);
optimizer.addVertex( v );
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_sigma*w_sigma) ); // 信息矩阵:协方差矩阵之逆
  optimizer.addEdge( edge );
}

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

具体解析:

第一步:创建一个线性求解器LinearSolver

求解增量方程是:H△X=-b,通常是直接求逆。即,△X=H.inv()*(-b)。一般如果H的维度小可以这么做,如果维度大就不能这样做。所以要使用其它的办法来求逆。g2o收集了多种求解方法放在g2o/solvers文件下。

有博主总结了它们的差异:

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

主要是针对不同求逆的办法。

第二步:创建BlockSolver。并用上面定义的线性求解器初始化。

BlockSolver 内部包含 LinearSolver,用上面我们定义的线性求解器LinearSolver来初始化。它的定义在如下文件夹内:g2o/g2o/core/block_solver.h

我下载的这一版g2o全部使用模板类重写了,和之前文章的源码差别好大。。。

template <int p, int l>
using BlockSolverPL = BlockSolver<BlockSolverTraits<p, l>>;

// variable size solver
using BlockSolverX = BlockSolverPL<Eigen::Dynamic, Eigen::Dynamic>;

// solver for BA/3D SLAM
using BlockSolver_6_3 = BlockSolverPL<6, 3>;

// solver fo BA with scale
using BlockSolver_7_3 = BlockSolverPL<7, 3>;

// 2Dof landmarks 3Dof poses
using BlockSolver_3_2 = BlockSolverPL<3, 2>;

这里的BlockSolver有两种定义模式,第一种是BlockSolverPL是固定尺度,P表示pose而L表示Landmark。第二种是BlockSolverX是变换尺度,在某些应用场景,我们的Pose和Landmark在程序开始时并不能确定,那么此时这个块状求解器就没办法固定变量,此时使用这个可变尺寸的solver,所有的参数都在中间过程中被确定。

第三步:创建总求解器solver。并从GN, LM, DogLeg 中选一个,再用第二步得到求解器BlockSolver初始化。

还是在g2o/g2o/core文件夹下:

 可以看到之间框架图里面提到的三种迭代方法。点开其中一个,就会发现它们都是继承于OptimizationWithHessian类。

这和前面的框架图里面的箭头是匹配的。

第四步:创建核心SparseOptimizer。

  g2o::SparseOptimizer optimizer;     // 图模型
  optimizer.setAlgorithm(solver);   // 设置求解器
  optimizer.setVerbose(true);       // 打开调试输出

第五步:添加顶点和边。

 这张图比较清晰,直接看源码,在g2o/core/hyper_graph.h里面是对HyperGraph类的定义。它里面有两个类分别是Vertex和Edge,符合上图关系。里面还涉及了抽象类,简单来说就是如果一个类里面它有一个纯虚函数或则继承的基类是抽象类且没有对虚函数进行定义则它们都是抽象类。

class G2O_CORE_API HyperGraph {
 public:
  /**
   * \brief enum of all the types we have in our graphs
   */
  enum G2O_CORE_API HyperGraphElementType {
    HGET_VERTEX,
    HGET_EDGE,
    HGET_PARAMETER,
    HGET_CACHE,
    HGET_DATA,
    HGET_NUM_ELEMS  // keep as last elem
  };
......
//! abstract Vertex, your types must derive from that one
  class G2O_CORE_API Vertex : public HyperGraphElement {
   public:
    //! creates a vertex having an ID specified by the argument
    explicit Vertex(int id = InvalidId);
    virtual ~Vertex();
    //! returns the id
    int id() const { return _id; }
    virtual void setId(int newId) { _id = newId; }
    //! returns the set of hyper-edges that are leaving/entering in this vertex
    const EdgeSet& edges() const { return _edges; }
    //! returns the set of hyper-edges that are leaving/entering in this vertex
    EdgeSet& edges() { return _edges; }
    virtual HyperGraphElementType elementType() const { return HGET_VERTEX; }

   protected:
    int _id;
    EdgeSet _edges;
  };
......
class G2O_CORE_API Edge : public HyperGraphElement {
   public:
    //! creates and empty edge with no vertices
    explicit Edge(int id = InvalidId);
    virtual ~Edge();
......
}

而OptimizableGraph::Vertex is a HyperGraph::Vertex,说明Optimizable::Vertex继承自HyperGraph::Vertex,源码中也是这样的。


struct G2O_CORE_API OptimizableGraph : public HyperGraph {
  enum ActionType {
    AT_PREITERATION,
    AT_POSTITERATION,
    AT_NUM_ELEMENTS,  // keep as last element
  };
......
  class G2O_CORE_API Vertex : public HyperGraph::Vertex,
                              public HyperGraph::DataContainer {
   private:
    friend struct OptimizableGraph;
......
}

最后是使用最多的BaseVertex,它在这里g2o/core/base_vertex.h,也是继承自OptimizableGraph::Vertex。

/**
 * \brief Templatized BaseVertex
 *
 * Templatized BaseVertex
 * D  : minimal dimension of the vertex, e.g., 3 for rotation in 3D. -1 means
 * dynamically assigned at runtime. T  : internal type to represent the
 * estimate, e.g., Quaternion for rotation in 3D
 */
template <int D, typename T>
class BaseVertex : public OptimizableGraph::Vertex {
 public:
  typedef T EstimateType;
  typedef std::stack<
      EstimateType,
      std::vector<EstimateType, Eigen::aligned_allocator<EstimateType> > >
      BackupStackType;

D代表了vertex的最小维度,比如3D空间中旋转是3维的,那么这里 D = 3。

T表示待估计vertex的数据类型,比如用四元数表达三维旋转的话,T就是Quaternion 类型。

  static const int Dimension =  D;  
///< dimension of the estimate (minimal) in the manifold space
  typedef T EstimateType;
  EstimateType _estimate;

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>

如果没有,也可以自行定义,但要重写这些函数:

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*/;
      }
  }

这是一个自己定义的顶点的格式,符合前面所有的要求,这里的增量是相加的。又比如高博g2o的内容:

class CurveFittingVertex : public g2o::BaseVertex<3, Eigen::Vector3d> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW

  // 重置
  virtual void setToOriginImpl() override {
    _estimate << 0, 0, 0;
  }

  // 更新
  virtual void oplusImpl(const double *update) override {
    _estimate += Eigen::Vector3d(update);
  }

  // 存盘和读盘:留空
  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}
};

这里也是因为它是向量,所以也是可以相加的。但遇到不能相加的,李代数的例子。比如:

g2o/types/sba/types_six_dof_expmap.h

/**

 \* \brief 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());        //更新方式
  }
};

其中6表示内部存储的优化变量维度,这是个6维的李代数。SE3Quat是优化变量的类型,是g2o定义的相机位姿类型。这里就不能相加,因为传递矩阵没有加法,要采用别的更新办法。

将顶点的数据格式定义好了后,添加顶点的操作就比较简单了,还是以高博的代码为例:

    CurveFittingVertex* v = new CurveFittingVertex();
    v->setEstimate( Eigen::Vector3d(0,0,0) );
    v->setId(0);
    optimizer.addVertex( v );

CurveFittingVertex是自己定义的顶点的类,然后就是初始化的操作,最后就是直接optimizer.addVertex(v)来添加顶点。

关于边Edge,这里就不去查看它们源码之间的继承关系了,按框架图中的表示就可以了。

BaseUnaryEdge,BaseBinaryEdge,BaseMultiEdge 分别表示一元边,两元边,多元边

一元边可以理解为一条边只连接一个顶点,两元边理解为一条边连接两个顶点,多元边理解为一条边可以连接多个(3个以上)顶点。

相关参数:
D 是 int 型,表示测量值的维度 (dimension)
E 表示测量值的数据类型
VertexXi,VertexXj 分别表示不同顶点的类型

 BaseBinaryEdge<2, Vector2D, VertexSBAPointXYZ, VertexSE3Expmap>

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

然后是自己定义边的需要写的函数:

virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;
virtual void computeError();
virtual void linearizeOplus();

read,write:分别是读盘、存盘函数,一般情况下不需要进行读/写操作的话,仅仅声明一下就可以。
computeError函数:非常重要,是使用当前顶点的值计算的测量值与真实的测量值之间的误差。
linearizeOplus函数:非常重要,是在当前顶点的值下,该误差对优化变量的偏导数,也就是我们说的Jacobian。

还有一些比较重要的:

_measurement:存储观测值
_error:存储computeError() 函数计算的误差
_vertices[]:存储顶点信息,比如二元边的话,_vertices[] 的大小为2,存储顺序和调用setVertex(int, vertex) 是设定的int 有关(0 或1)
setId(int):来定义边的编号(决定了在H矩阵中的位置)
setMeasurement(type) 函数来定义观测值
setVertex(int, vertex) 来定义顶点
setInformation() 来定义协方差矩阵的逆

模板:

 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
  }

实际例子,还是使用高博的源码。

// 误差模型 模板参数:观测值维度,类型,连接顶点类型
class CurveFittingEdge : public g2o::BaseUnaryEdge<1, double, CurveFittingVertex> {
public:
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW

  CurveFittingEdge(double x) : BaseUnaryEdge(), _x(x) {}

  // 计算曲线模型误差
  virtual void computeError() override {
    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 void linearizeOplus() override {
    const CurveFittingVertex *v = static_cast<const CurveFittingVertex *> (_vertices[0]);
    const Eigen::Vector3d abc = v->estimate();
    double y = exp(abc[0] * _x * _x + abc[1] * _x + abc[2]);
    _jacobianOplusXi[0] = -_x * _x * y;
    _jacobianOplusXi[1] = -_x * y;
    _jacobianOplusXi[2] = -y;
  }

  virtual bool read(istream &in) {}

  virtual bool write(ostream &out) const {}

public:
  double _x;  // x 值, y 值为 _measurement
};

雅克比矩阵是用来求解误差的。

然后就是添加边,比较简单:

  // 往图中增加边
  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_sigma * w_sigma)); // 信息矩阵:协方差矩阵之逆
    optimizer.addEdge(edge);
  }

这是添加一个顶点,v是前面生成的顶点。

还有一个两元边的例子。

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

这里的0和1分别代表了不同的顶点。0表示的是VertexSBAPointXYZ 类型的顶点,1对应的是VertexSE3Expmap 类型的顶点就是位姿pose。g2o不会区分顶点的类型需要自己区分。

这里准备一个练习,使用g2o完成一次优化,把上面提到的流程走一遍。

图优化数学理论

四、图优化理论来源

主要学习高博的两篇博客:文章一、文章二。两篇博客写的非常详细,这里作一些简单的笔记。

优化理论前提:

优化问题有三个最重要的因素:目标函数、优化变量、优化约束。一个简单的优化问题可以描述如下:

\min_{x}F(x)

 其中x为优化变量,而F(x)是优化函数。此问题是无优化问题,因为没有任何约束形式,而slam中大多数都是无约束的优化问题。当F(x)有特殊性质时,对应的优化问题也可以用一些特殊的解法。例如,当F(x)为一个线性函数时,则为线性优化问题。反之为非线性优化,对于无约束的非线性优化,如果我们知道它梯度的解析形式,就能直接求那些梯度为零的点,来解决这个优化:

\frac{\mathrm{dF(x)} }{\mathrm{d} x}=0

梯度为零的地方可能是函数的极大值、极小值或者鞍点。但不知道F(x)的形式,就遍历所有的极值点,找到最小的作为最优解。但并不是所有的工程问题都可以得到具体的F(x)的解析式。所以一般使用迭代的方法求解。包括梯度下降法,反复迭代,直到求出最优解。一般有两种迭代方法:Gauss-Newton (GN)法Levenberg-Marquardt (LM)法

slam问题和图相结合:

slam的核心根据已有的观测数据,计算机器人的运动轨迹和地图。

假设在时刻k,机器人在位置^{x_{k}}处,用传感器进行了一次观测,得到了数据^{z_{k}}。传感器的观测方程为:

z_{k}=h(x_{k})

算上误差:

{e_{k}}=z_{k}-h(x_{k})

^{x_{k}}为优化变量,以\min_{​{x_{k}}}F_{k}({x_{k}}) = \left \| e_k \right \|为目标函数,就可以求出^{x_{k}}的估计值。

观测方程有多种形式:

  • 机器人两个Pose之间的变换;
  • 机器人在某个Pose处用激光测量到了某个空间点,得到了它离自己的距离与角度;
  • 机器人在某个Pose处用相机观测到了某个空间点,得到了它的像素坐标;

与图相结合:

在图中,以顶点表示优化变量,以边表示观测方程。由于边可以连接一个或多个顶点,所以我们把它的形式写成更广义的 z_k=h(x_{k1},x_{k2},\left. ... \right \),以表示不限制顶点数量的意思。而上面提到的三种观测方程就表示为:

机器人两个Pose之间的变换;——一条Binary Edge(二元边),顶点为两个pose,边的方程为T_1 = \Delta T*T_2,这也是边的约束方程。

机器人在某个Pose处用激光测量到了某个空间点,得到了它离自己的距离与角度;——Binary Edge,顶点为一个2D Pose:[x,y,\Theta ]^{T}和一个Point:[\lambda x,\lambda y]^T,观测数据是距离r和角度b,那么观测方程为:

 第三个类似。

然后这是没有带上误差的理想情况,而优化的主要任务就是算出优化变量误差最小。

接下来的内容全是公式推导,建议直接看原文,讲的很清楚,csdn上面实在是不好处理公式。我在纸上推导了一遍。

最后可以得到最开始要求进行g2o的那个公式:H*\Delta x = -b。直接跟着上面的流程走就行了。

高博直接帮我们总结了:

小结

  最后总结一下做图优化的流程。

  1. 选择你想要的图里的节点与边的类型,确定它们的参数化形式;
  2. 往图里加入实际的节点和边;
  3. 选择初值,开始迭代;
  4. 每一步迭代中,计算对应于当前估计值的雅可比矩阵和海塞矩阵;
  5. 求解稀疏线性方程HkΔx=−bk,得到梯度方向;
  6. 继续用GN或LM进行迭代。如果迭代结束,返回优化值。

  实际上,g2o能帮你做好第3-6步,你要做的只是前两步而已。下节我们就来尝试这件事。

五、g2o使用实例

在这里直接看了下任佬的关于g2o的源码,然后发现对于我来说好复杂,好像刚刚弄明白1+1 = 2,突然就要算两位数的乘法了(还是太菜了)。然后我在网上又找到一篇讲的特别好的,还没发现讲的比这篇文章还要清晰的入门g2o的文章:文章。

总结的特别好,这篇文章的作者完成了这篇文章举的第一个例子,我在他的代码上更换了几个数据就直接算出了第二个例子的数值。

答案数值:

 所以对于g2o来说,将点和边的定义设置好就让它直接帮你算出结果。

发现使用g2o最难的可能是对应版本的问题,解决库的问题真的好复杂。。。先记录到这里

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

slam定位学习笔记(七)-g2o学习 的相关文章

  • Docker 安装 mysql 8.0.29

    文章目录 安装拉取镜像启动容器 配置 xff08 可跳过 xff0c 建议弄上 xff09 使用启动控制台登录navicat远程登录 删除 安装 拉取镜像 span class token function docker span pull
  • SpringBoot 启动打印 Banner:佛祖保佑,永无BUG!

    最近新学了一招 打印完了 xff0c 一点bug不出 xff0c 果然 xff0c 程序的尽头是玄学 教程 在Resources目录下新建 banner txt 输入内容如下 span class token comment ooOoo s
  • 一文就懂AQS!

    文章目录 AQS介绍AQS概念AQS模式分类AQS核心思想 AQS源码结构CLH同步队列state同步状态独占式同步状态获取与释放流程图总结 xff1a 共享式同步状态获取与释放 看了很多帖子 xff0c 原理说啥的都有 xff0c 算了还
  • 手把手教你构建源码级组件——Java互斥不可重入锁

    文章目录 构造同步组件的步骤1 定义内部类Syn2 继承同步器 xff0c 重写指定方法3 调用同步器方法 互斥不可重入锁实现代码实现测试Demo运行结果结果分析 构造同步组件的步骤 之前的学习中我们学习了AQS的原理 xff0c 其中有许
  • 手把手教你构建源码级组件——Java指定共享线程数目的共享锁

    文章目录 构造同步组件的步骤1 定义内部类Syn2 继承同步器 xff0c 重写指定方法3 调用同步器方法 指定共享线程数目的共享锁实现代码实现测试Demo运行结果结果分析 构造同步组件的步骤 之前的学习中我们学习了AQS的原理 xff0c
  • Anaconda下载及安装保姆级教程(详细图文)

    文章目录 先导知识Anaconda 下载Anaconda 安装Anaconda 配置环境变量测试配置 Anaconda 其他操作安装第三方包卸载第三方包查看环境包信息导入导出环境其他命令 先导知识 Anaconda指的是一个开源的Pytho
  • 解决微信支付申请扫描提示“操作超时,请重新扫码确认问题”

    文章目录 问题原因解决 问题 在申请微信支付账号的时候 xff0c 按照微信网站的提示 操作超时 xff0c 请重新扫码确认 原因 因为浏览器的原因 xff0c 浏览器不兼容 xff0c 醉了 xff0c 这个年代了还有这个兼容的问题 解决
  • npm ERR! cb.apply is not a function npm ERR! A complete log of this run can be found in: npm ERR!

    问题 原因 npm版本过旧 解决 windows下cmd安装指令 xff1a cnpm span class token function install span g span class token function npm span
  • 深度学习从入门到出门(1)——概念与发展概况

    文章目录 深度学习 xff1a 从概念到实践深度学习的起源深度学习的应用场景图像识别自然语言处理推荐系统视频游戏 深度学习的核心思想模型数据优化算法 深度学习的实践方法结论 深度学习 xff1a 从概念到实践 在过去几年里 xff0c 深度
  • ubuntu: su root 报错 su: Authentication failure

    问题 xff1a 输入 sudo su进入root系统 xff0c 输入密码后提示 su Authentication failure 解决方法 xff1a 输入 xff1a sudo passwd root 设置密码 之后重新 sudo
  • 树莓派4b引脚图

  • Ubuntu16.04下安装openssh-server 依赖问题

    1 安装时报依赖错误 因为Ubuntu 自带openssh client xff0c 自带的client和要安装的openssh server 所依赖的client版本不一致导致的 先卸载openssh client 然后在安装即可 sud
  • 《智能控制技术》学习笔记-1.绪论,智能控制定义、分类、发展及应用场景

    一 引言 先修内容 xff1a 经典控制理论 现代控制理论 MATLAB 引入 xff1a 智能理论是新兴学科 xff0c 随着数字计算机和人工智能发展而来 xff0c 发展得益于许多学科 xff1a 人工智能 认知科学 现代自适应控制 最
  • 【STM32】基础篇 ST-Link下载器接线方式和SWD,JTAG协议简介

    在开发STM32等基于arm内核的单片机时 xff0c 选择一款载调试器必不可少 市面上有各式各样的下载调试器可供我们选择 xff0c 常用的下载器包括J Link xff0c ST Link xff0c J Link0B xff0c CM
  • 【Linux学习】正点原子裸机篇 C语言LED实验实现

    上一篇使用汇编语言编写LED等驱动实验 xff0c 在实际工作中较少使用汇编编写嵌入式驱动 xff0c 毕竟汇编过于底层 xff0c 难度较大 绝大部分情况下都是使用C语言编写主体程序 xff0c 只是开始部分使用汇编来初始化C语言环境 x
  • Nginx

    1 基本介绍 Nginx是由俄罗斯的设计师开发的 Nginx不像Apache那样 xff0c 不论功能是否常用 xff0c 统统都给你 自带了 xff0c 虽然功能 很强大 xff0c 但是也很消耗性能 xff0c 而Nginx只是自带了常
  • linux运维经典面试题总结

    1 Linux常见的日志文件都有哪些 xff0c 各自的用途 xff1f 日志轮询配置文件在哪里 xff1f 欢迎界面配置文件在哪里 xff1f 答 xff1a var log messages 内核及公共消息日志 var log cron
  • openair-cn-cups

    Github openair cn cups openair cn cups descriptions openair cn cups是在openair cn的基础上将spgw控制面与用户面分离 xff0c 实现从LTE EPC向5G Co
  • 5GS NAS SM Elementary Procedures(EP) -- General

    参考 xff1a clause 6 2 3GPP TS24 501 1 Procedure Transaction Identifier PTI PTI procedure transaction identifier用来指示message
  • keil5(MDK)美化文本编辑区

    1 常规的改法 xff1a 在MDK菜单栏中选择 编辑 gt 配置 gt Color amp Fonts xff0c 如下界面 xff1a 可以在以上界面配置对应的选项更改代码编辑区数字 xff08 number xff09 关键字 xff

随机推荐

  • oai-5gcn-smf prototype testbed

    Deployment Scenario Repository Of SMF git clone https gitlab eurecom fr oai oai cn5g smf git SMF gt build amp run cd oai
  • OAI LTE系统搭建 -- OAI EPC

    OAI EPC搭建 1 系统环境 xff1a Ubuntu 16 04 2 基础软件安装 xff1a vim git ssh server sudo apt get install vim sudo apt get install git
  • OAI LTE系统搭建 -- OAI eNB

    OAI eNB 搭建 接着OAI EPC搭建教程 1 下载源代码 1 1 加载gitlab eurecom fr证书 echo n openssl s client showcerts connect gitlab eurecom fr 4
  • 端到端5G核心网测试方式

    1 系统搭建 2 网元功能运行 2 1 运行顺序 xff1a UDM Server SMF AMF UPF VPP UPF N4 Huawei CPE配置保存 gNB 2 2 UDM Server运行方式 密码123 1 可执行文件位置 s
  • 无线通信知识回顾(2) - 《5G移动通信系统设计与标准详解》

    Reference 5G移动通信系统设计与标准详解 王映民等 第10章 功率控制 10 1 概述 功率控制对基站或者终端发送信号功率进行调节 xff0c 达到有效地实现路径损耗补偿 克服阴影衰落 抑制干扰等目的 xff1b 如果单纯提高信号
  • python讲解from ctypes import *调用C语言动态链接库

    文章目录 前言一 from ctypes import 二 调用举例1 代码 前言 顾名思义 一 from ctypes import 在写python程序时 有时会用到C语言库函数 Python 的 ctypes 要使用 C 函数 xff
  • Docker随笔,从基础到入门

    实验环境 xff1a centos7 7 防火墙和selinux提前关闭 一 Docker基本概念 Docker系统有两个程序 xff1a docker服务端和docker客户端 docker服务端是一个服务进程 xff0c 管理着所有的容
  • cuda、torch、torchvision对应版本以及安装

    查找torch与torchvision对应版本 匹配情况如下 xff1a 1 在线下载 xff1a 在pytorch官网选择相应的历史版本 xff0c 使用conda或者pip安装 xff0c 使用官网的镜像下载很慢 xff0c 建议使用其
  • CUDA在VS下编程出现MSB3721错误

    CUDA在VS下编程出现MSB3721错误 一 错误示范二 解决方式 一 错误示范 我们在VS环境下进行CUDA编程的时候可能会出现如下MSB1721的错误 二 解决方式 首先确保好是在x64平台下运行 xff1a 确定无误然后如果还有问题
  • ROS:关于node启动问题

    相关代码 xff1a lt node pkg 61 34 turn on wheeltec robot 34 type 61 34 wheeltec robot node 34 name 61 34 wheeltec robot 34 ou
  • Altium Designer 的一些使用技巧(纯属爱好,不定期更新)

    目录 1 鼠标所到之处的 net 高亮显示 2 导出PCB所有图层的快捷键 3 同一个工程中不同的原理图导入到不同的PCB中 4 设置元件旋转步进角为45 5 添加LOGO的方法 6 删除已经放置的 LOGO 的方法 7 一些快捷键 1 鼠
  • C++中vector的size()类型

    span class token keyword int span span class token function main span span class token punctuation span span class token
  • 【论文笔记】FashionBERT: Text and Image Matching with Adaptive Loss for Cross-modal Retrieval

    文章来源 xff1a SIGIR 20 摘要 文章基于BERT提出了一个跨模态检索模型 xff0c 该模型并不是一个通用的检索模型 xff0c 主要用于电商领域时尚用品 xff08 Fashon xff09 检索 xff0c 作者是阿里巴巴
  • 多模态数据集预处理方法

    摘要 对文本是单个词语的一般采用词袋特征 图片可以采用多种灵活的处理方式 对文本是句子甚至是段落的则需要采用较为复杂的处理方式 xff0c 参考文献 1 2 中对Wiki和Pascal Sentence数据集的处理方式 Wiki 参考文献
  • Numpy删除指定行

    删除Numpy数组中指定的某些行 函数 xff1a np delete span class token comment 示例 span span class token keyword import span numpy span cla
  • Pytorch排查训练过程中出现nan值的位置

    span class token keyword import span torch span class token comment 正向传播 span torch span class token punctuation span au
  • JupyterLab配置远程访问

    新版本的jupyter lab似乎和之前版本的不大一样 目录 生成配置文件设置密码安装插件将 96 Test 96 环境写入jupyterlab设置远程访问其他 生成配置文件 jupyter lab generate config 设置密码
  • Anaconda多用户配置指北

    适用于实验室深度学习服务器配置 创建用户组 groupadd stu配置conda xff0c 参考这里新创建的用户加入stu组 xff0c 并指定家目录useradd d home media m g stu xpt xff0c 为不同用
  • Hexo/Github.io 配置腾讯云CDN

    前言 使用Github io搭建自己的博客时可能会遇到访问速度不理想的情况 xff0c 可以采用如下几种方案解决 xff1a 将Hexo部署到阿里云 xff08 点击查看 xff09 xff0c 直接访问阿里云 xff0c 但这种方式会暴露
  • slam定位学习笔记(七)-g2o学习

    主要学习的是这篇文章 xff0c 但大佬并没有在文章里面仔细的讲g2o xff0c 所以我在网上找了这几篇介绍g2o的文章 xff0c 讲的十分详细 xff0c 对入门十分友好 xff1a 文章一 文章二 文章三 xff0c 这三篇都是一个