g2o學習——g2o整體框架:https://blog.csdn.net/wubaobao1993/article/details/79319215
g2o學習——再看頂點和邊:https://blog.csdn.net/wubaobao1993/article/details/79328569
g2o學習——頂點和邊之外的solver: https://blog.csdn.net/wubaobao1993/article/details/79336069
寫在前面
進來對g2o優化庫進行了學習,雖然才模仿着寫了兩個例程,但是對於整個g2o的理解和使用方面還是多了不少的感觸,特此寫下博客,對這些天的學習進行記錄。
g2o的整體結構
說到整體的結構,不得不用一張比較概括的圖來說明:
這張圖最好跟着畫一下,這樣能更好的理解和掌握,例如我第一次看的時候根本沒有注意說箭頭的類型等等的細節。
那么從圖中我們其實比較容易的就看出來整個庫里面較為重要的類之間的繼承以及包含關系,也可以看出整個框架里面最重要的東西就是SparseOptimizer這個類(或者說實例)。
頂點(Vertex)和邊(Edge)
順着圖往上看,可以看到我們所使用的優化器最終是一個超圖(hyperGrahp),而這個超圖包含了許多頂點(Vertex)和邊(Edge)。這兩個類型是我們在看程序和寫程序中比較關注的東西了,g2o不像Ceres,內部很多東西其實作者都已經寫好了(此刻一鞠躬),很適合我們這些新興的懶惰青年,但是同時,我們也失去了一個比較完整的了解內部關系的機會,不過這個東西我們可以通過看內部的實現補回來(但是編程實踐是沒有機會了),順便看看大牛寫的代碼~
那么扯回來,在圖優化中,頂點代表了要被優化的變量,而邊則是連接被優化變量的橋梁,因此,也就造成了說我們在程序中見得較多的就是這兩種類型的初始化和賦值。
在整個優化過程中,頂點的值會越來越趨近於最有值,優化完畢后則可以將頂點的優化值作為最優值進行使用;邊則是連接頂點的類型,在SLAM問題中,一般是邊連接要被優化的空間點(Point)和機器人的位姿(Pose),當然,邊還可以連接一個頂點(類似與參數估計,邊的數量由量測的數量決定),也可以連接多個頂點(超圖,這里筆者還沒有遇到過,就不妄言了),邊在圖優化中的一個很大的作用就是計算誤差(視覺SLAM中計算的就是空間點的映射誤差),同時計算該誤差對於被優化變量的jacobian矩陣,也是比較重要的存在。
自定義頂點(Vertex)和邊(Edge)
我們在用g2o的時候,不會一帆風順的就能適合自身機器人的實際情況,總會遇到自己獨特的頂點類型和邊類型,此時我們需要對頂點和邊進行重寫,那么重寫也比較簡單,這里簡單進行記錄。
在整體框架圖中,可以看到不管是頂點還是邊,都可以說是繼承自baseXXX這個類的,因此我們在自定義的時候,也可以仿照着繼承這兩個類,當然也可以繼承自g2o中較為“成熟”的類,不管怎樣,都要重寫下述的函數。
自定義頂點
- 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比較重要,我們根據增量方程計算出增量之后,就是通過這個函數對估計值進行調整的,因此這個函數的內容一定要寫對,否則會造成一直優化卻得不到好的優化結果的現象。
自定義邊
- virtual bool read(std::istream& is);
- virtual bool write(std::ostream& os) const;
- virtual void computeError();
- virtual void linearizeOplus();
read和write函數同上,computeError函數是使用當前頂點的值計算的測量值與真實的測量值之間的誤差,linearizeOplus函數是在當前頂點的值下,該誤差對優化變量的偏導數,即jacobian。
自定義的總結
不管是自定義邊還是頂點,除了自己加入的一些變量,還都要對一些g2o框架要調用的函數進行覆寫,這些函數用戶可以聲明為實函數(即不加virtual),但是筆者還是建議聲明為虛函數。
優化算法(Algorithm,BlockSolver,linearSolver)
順着整體結構圖往下看,可以看到這部分其實算是整個g2o里面比較隱晦的部分,設計到優化的算法,塊求解器,線性求解器等等部分,在程序中,這部分通常位於g2o算法的開頭配置部分,一般情況下我們可以隨着一個例程進行配置即可,這里對這部分進行了稍微淺顯的理解,特意寫在這里(下面的東西純屬個人理解了,如有不妥還請大神指出:)。
linearSolver,線性求解器
我們知道在求解增量方程HdeltaX=-b的時候,通常情況下想到線性求解,很簡單嘛,deltaX=-H.inv*b,的確,當H的維度較小的時候,上述問題變得簡單,只需要矩陣的求逆就能解決問題,但是當H的維度較大時,問題變得復雜,此時我們就需要一些特殊的方法對矩陣進行求逆,g2o中主要有圖中所示的三種方法,PCG,CSparse和Cholmod方法。
注意,這里說再多,線性求解器僅僅只是完成了一個求解的功能,可以說是整個優化中比較靠后的計算部分了。
BlockSolver,塊求解器(參數塊求解器?)
塊求解器是包含線性求解器的存在,之所以是包含,是因為塊求解器會構建好線性求解器所需要的矩陣塊(也就是H和b),之后給線性求解器讓它進行運算,邊的jacobian也就是在這個時候發揮了自己的光和熱。
這里再記錄下一個比較容易混淆的問題,也就是在初始化塊求解器的時候的參數問題。
大部分的例程在初始化塊求解器的時候都會使用如下的程序代碼:
- std::unique_ptr<g2o::BlockSolver_6_3::LinearSolverType> linearSolver = g2o::make_unique<g2o::LinearSolverCholmod<g2o::BlockSolver_6_3::PoseMatrixType>>();
其中的BlockSolver_6_3有兩個參數,分別是6和3,在定義的時候可以看到這是一個模板的重命名(模板類的重命名只能用using)
- template<int p, int l>
- using BlockSolverPL = BlockSolver< BlockSolverTraits<p, l> >;
其中p代表pose的維度,l表示landmark的維度,且這里都表示的是增量的維度(這里筆者也不是很確定,但是從后續的程序中可以看出是增量的維度而並非是狀態變量的維度)。
因此(后面的話以SLAM情況為例),對於僅僅優化位姿的應用而言,這里l的值是沒有太大影響的,因為H矩陣中並沒有Hll的塊,因此這里的維度也就沒有用武之地了。
Algorithm,優化算法
這里就不多講了,從圖中可以看到主要有三種方法:GN,LM,PSD,不同的方法主要表現在最終的H矩陣構造不同。
總結
1. 對g2o優化庫的整體框架有了更好的了解
2. 對圖中的頂點和邊有了更加深刻的認識
3. 理解了g2o初始化部分的程序與意圖
以上觀點僅僅代表個人學習觀點,如有不對還請各位大神指正!這里不勝感激!