1. 程式人生 > >從零開始一起學習SLAM | 掌握g2o頂點編程套路

從零開始一起學習SLAM | 掌握g2o頂點編程套路

視覺slam 必須 hat 一個 point 公眾 pmap plus 存儲

點“計算機視覺life”關註,置頂更快接收消息! ##

小白:師兄,上一次將的g2o框架《從零開始一起學習SLAM | 理解圖優化,一步步帶你看懂g2o代碼》真的很清晰,我現在再去看g2o的那些優化的部分,基本都能看懂了呢!

師兄:那太好啦,以後多練習練習,加深理解

小白:嗯,我開始編程時,發現g2o的頂點和邊的定義也非常復雜,光看十四講裏面,就有好幾種不同的定義,完全懵圈狀態。。。師兄,能否幫我捋捋思路啊

師兄:嗯,你說的沒錯,入門的時候確實感覺很亂,我最初也是花了些時間才搞懂的,下面分享一下。

g2o的頂點(Vertex) 從哪裏來的?

師兄:在《g2o: A general Framework for (Hyper) Graph Optimization》這篇文檔裏,我們找到那張經典的類結構圖。也就是上次講框架用到的那張結構圖。其中涉及到頂點 (vertex) 的就是下面 加了序號的3個東東了。

技術分享圖片

小白:記得呢,這個圖很關鍵,幫助我理清了很多思路,原來來自這篇文章啊

師兄:對,下面我們一步步來看吧。先來看看上圖中和vertex有關的第①個類: HyperGraph::Vertex,在g2o的GitHub上(https://github.com/RainerKuemmerle/g2o),它在這個路徑

g2o/core/hyper_graph.h

這個 HyperGraph::Vertex 是個abstract vertex,必須通過派生來使用。如下圖所示

技術分享圖片

然後我們看g2o 類結構圖中第②個類,我們看到HyperGraph::Vertex 是通過類OptimizableGraph 來繼承的, 而OptimizableGraph的定義在

g2o/core/optimizable_graph.h

我們找到vertex定義,發現果然,OptimizableGraph 繼承自 HyperGraph,如下圖所示

技術分享圖片

不過,這個OptimizableGraph::Vertex 也非常底層,具體使用時一般都會進行擴展,因此g2o中提供了一個比較通用的適合大部分情況的模板。就是g2o 類結構圖中 對應的第③個類:

BaseVertex<D, T>

那麽它在哪裏呢? 在這個路徑:

g2o/core/base_vertex.h

技術分享圖片

小白:哇塞,原來是這樣抽絲剝繭的呀,學習了,授人以魚不如授人以漁啊!

師兄:嗯,其實就是根據那張圖結合g2o GitHub代碼就行了

g2o的頂點(Vertex) 參數如何理解?

小白:那是不是就可以開始用了?

師兄:別急,我們來看看參數吧,這個很關鍵。

我們來看一下模板參數 D 和 T,翻譯一下上圖紅框:

D是int 類型的,表示vertex的最小維度,比如3D空間中旋轉是3維的,那麽這裏 D = 3

T是待估計vertex的數據類型,比如用四元數表達三維旋轉的話,T就是Quaternion 類型

小白:哦哦,大概理解了,但還是有點模糊

師兄:我們進一步來細看一下D, T。這裏的D 在源碼裏面是這樣註釋的

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

可以看到這個D並非是頂點(更確切的說是狀態變量)的維度,而是其在流形空間(manifold)的最小表示,這裏一定要區別開,另外,源碼裏面也給出了T的作用

typedef T EstimateType;
EstimateType _estimate;

可以看到,這裏T就是頂點(狀態變量)的類型,跟前面一樣。

小白:Got it!

如何自己定義頂點?

小白:師兄,我們是不是可以開始寫頂點定義了?

師兄:嗯,我們知道了頂點的基本類型是 BaseVertex<D, T>,那麽下一步關心的就是如何使用了,因為在不同的應用場景(二維空間,三維空間),有不同的待優化變量(位姿,空間點),還涉及不同的優化類型(李代數位姿、李群位姿)

小白:這麽多啊,那要自己根據 BaseVertex 一個個實現嗎?

師兄:那不需要!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(/尷尬臉),還有每次定義都要重新寫這幾個函數嗎?

師兄:是的,這幾個是主要要改的地方。我們來看一下他們都是什麽意義:

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

/**

 \* \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, SE3Quat 分別是什麽意思?

師兄:書中都寫了,以下來自十四講的介紹:

第一個參數6 表示內部存儲的優化變量維度,這是個6維的李代數

第二個參數是優化變量的類型,這裏使用了g2o定義的相機位姿類型:SE3Quat。

在這裏可以具體查看g2o/types/slam3d/se3quat.h

它內部使用了四元數表達旋轉,然後加上位移來存儲位姿,同時支持李代數上的運算,比如對數映射(log函數)、李代數上增量(update函數)等操作

說完了,那我現在問你個問題,為啥這裏更新時沒有像上面那樣直接加上去?

小白:這個表示位姿,好像是不能直接加的我記得,原因有點忘了

師兄:嗯,是不能直接加,原因是變換矩陣不滿足加法封閉。那我再問你,為什麽相機位姿頂點類VertexSE3Expmap使用了李代數表示相機位姿,而不是使用旋轉矩陣和平移矩陣?

小白:不造啊。。

師兄:其實也是上述原因的拓展:這是因為旋轉矩陣是有約束的矩陣,它必須是正交矩陣且行列式為1。使用它作為優化變量就會引入額外的約束條件,從而增大優化的復雜度。而將旋轉矩陣通過李群-李代數之間的轉換關系轉換為李代數表示,就可以把位姿估計變成無約束的優化問題,求解難度降低。

小白:原來如此啊,以前學的東西都忘了。。

師兄:以前學的要多看,溫故而知新。我們繼續看例子,剛才是位姿的例子,下面是三維點的例子,空間點位置 VertexPointXYZ,維度為3,類型是Eigen的Vector3,比較簡單,就不解釋了

 class G2O_TYPES_SBA_API VertexSBAPointXYZ : public BaseVertex<3, Vector3>
{
  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 number_t* update)
    {
      Eigen::Map<const Vector3> v(update);
      _estimate += v;
    }
};

如何向圖中添加頂點?

師兄:往圖中增加頂點比較簡單,我們還是先看看第一個曲線擬合的例子,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 );
    }

至此,我們講完了g2o 的頂點的來源,定義,自定義方法,添加方法,基本上你以後再看到頂點就不會陌生啦!

小白:太感謝啦!

編程練習

  • 題目:給定一組世界坐標系下的3D點(p3d.txt)以及它在相機中對應的坐標(p2d.txt),以及相機的內參矩陣。

    使用bundle adjustment 方法(g2o庫實現)來估計相機的位姿T。初始位姿T為單位矩陣。

  • 本程序學習目標:熟悉g2o庫編寫流程,熟悉頂點定義方法。

代碼框架、數據及預期結果已經為你準備好了,公眾號「計算機視覺life」後臺回復:頂點,即可獲得。

歡迎留言討論,更多學習視頻、文檔資料、參考答案等關註計算機視覺life公眾號,,菜單欄點擊“知識星球”查看「從零開始學習SLAM」星球介紹,快來和其他小夥伴一起學習交流~

本文參考:

高翔《視覺SLAM十四講》

https://www.jianshu.com/p/e16ffb5b265d

推薦閱讀

從零開始一起學習SLAM | 為什麽要學SLAM?
從零開始一起學習SLAM | 學習SLAM到底需要學什麽?
從零開始一起學習SLAM | SLAM有什麽用?
從零開始一起學習SLAM | C++新特性要不要學?
從零開始一起學習SLAM | 為什麽要用齊次坐標?
從零開始一起學習SLAM | 三維空間剛體的旋轉
從零開始一起學習SLAM | 為啥需要李群與李代數?
從零開始一起學習SLAM | 相機成像模型
從零開始一起學習SLAM | 不推公式,如何真正理解對極約束?
從零開始一起學習SLAM | 神奇的單應矩陣
從零開始一起學習SLAM | 你好,點雲
從零開始一起學習SLAM | 給點雲加個濾網
從零開始一起學習SLAM | 點雲平滑法線估計
從零開始一起學習SLAM | 點雲到網格的進化
從零開始一起學習SLAM | 理解圖優化,一步步帶你看懂g2o代碼
零基礎小白,如何入門計算機視覺?
SLAM領域牛人、牛實驗室、牛研究成果梳理
我用MATLAB擼了一個2D LiDAR SLAM
可視化理解四元數,願你不再掉頭發
最近一年語義SLAM有哪些代表性工作?
視覺SLAM技術綜述
匯總 | VIO、激光SLAM相關論文分類集錦

從零開始一起學習SLAM | 掌握g2o頂點編程套路