G2O - 入门踩坑记录

G2O入门踩坑记录

在刚开始学习g2o的时候,遇到不少问题,包括代码和编译。在运行tutorial example的时候也出现一些问题,这里做一个记录。同时,把g2o官方的一个入门例子tutorial_slam2d单独拿出来,并进行部分注释和改动,方便后续回顾。

本文主要内容如下:

  1. G2O编译与安装(编译时如何带上g2o_viewer)
  2. CMake项目如何链接G2O库
  3. 解决创建cache时自定义类型找不到的问题
  4. tutorial_slam2d例子(作为单独项目并依赖安装好的g2o)

一、G2O编译与安装

1.1 编译时生成g2o_viewer

参考

g2o可以将优化图(Graph/Edge/Vertex)保存为.g2o文件,然后使用g2o_viewer可视化。但是默认情况下,g2o_viewer并不会被编译,需要安装额外的依赖。

g2o - README.md中说明了一些可选依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
### Optional requirements

- spdlog <https://github.com/gabime/spdlog>
- suitesparse <http://faculty.cse.tamu.edu/davis/suitesparse.html>
- Qt5 <http://qt-project.org>
- libQGLViewer <http://www.libqglviewer.com>

On Ubuntu / Debian these dependencies are resolved by installing the
following packages.

- libspdlog-dev
- libsuitesparse-dev
- qtdeclarative5-dev
- qt5-qmake
- libqglviewer-dev-qt5

需要满足以上依赖才会编译生成g2o_viewer

依赖安装:

1
sudo apt-get install libsuitesparse-dev qtdeclarative5-dev qt5-qmake libqglviewer-dev-qt5

1.2 编译与安装

g2o源码地址

1
2
3
4
5
cd g2o
mkdir build
cd build
cmake ..
make -j4

检查build/bin目录下是否有g2o_viewer可执行文件。然后执行安装到系统:

1
sudo make install

g2o默认会安装到/usr/local下(/usr/local/lib,/usr/local/include

二、CMake项目链接G2O库

!!!g2o默认会安装到/usr/local下(/usr/local/lib,/usr/local/include),这个路径并不是CMake默认搜索路径。同时,g2o提供的g2oConfig.cmake似乎也不太对(没有细究),所以需要g2o源码中:cmake_modules文件夹下的FindG2O.cmake文件。同时,g2o依赖CSparse,还需要FindCSparse.cmake文件。

  1. FindG2O.cmake文件放到项目的cmake_modules文件夹下
  2. CMakeLists.txt中添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake_modules)
find_package(G2O REQUIRED)
# CSparse
find_package(CSparse REQUIRED)
include_directories(${G2O_INCLUDE_DIR} ${CSPARSE_INCLUDE_DIR})

message(STATUS "G2O_FOUND: ${G2O_FOUND}")

set(G2O_LIBRARIES
${G2O_STUFF_LIBRARY}
${G2O_CORE_LIBRARY}
${G2O_CLI_LIBRARY}
${G2O_SOLVER_CHOLMOD}
${G2O_SOLVER_CSPARSE}
${G2O_SOLVER_CSPARSE_EXTENSION}
${G2O_SOLVER_DENSE}
${G2O_SOLVER_PCG}
${G2O_SOLVER_SLAM2D_LINEAR}
${G2O_SOLVER_STRUCTURE_ONLY}
${G2O_SOLVER_EIGEN}
${G2O_TYPES_DATA}
${G2O_TYPES_ICP}
${G2O_TYPES_SBA}
${G2O_TYPES_SCLAM2D}
${G2O_TYPES_SIM3}
${G2O_TYPES_SLAM2D}
${G2O_TYPES_SLAM3D})

# message(STATUS "G2O_LIBRARIES: ${G2O_LIBRARIES}")

target_link_libraries(project ${G2O_LIBRARIES} fmt)

三、编译时fmt错误

g2o目前依赖fmt,但是似乎没有默认链接,编译的时候会报错:

1
2
/usr/bin/ld: CMakeFiles/curve_fitting.dir/main.cpp.o: undefined reference to symbol '_ZN3fmt2v86detail18throw_format_errorEPKc'
/usr/bin/ld: /lib/x86_64-linux-gnu/libfmt.so.8: error adding symbols: DSO missing from command line

解决方法:在CMakeLists.txt中链接fmt库到目标可执行文件:

1
target_link_libraries(project ${G2O_LIBRARIES} fmt)

四、运行时一些自定义类型找不到

4.1 报错信息

如果将代码的一部分编译成库,再链接到可执行文件,在编译时不会报错,但是在运行时可能会出现一些自定义类型在被g2o使用时找不到的错误,例如:

1
2
3
4
5
fatal error in creating cache of type TUTORIAL_CACHE_SE2_OFFSET
addEdge: FATAL, cannot resolve caches for edge 0x23416e0
g2o::Cache* g2o::CacheContainer::createCache(const g2o::Cache::CacheKey&)
fatal error in creating cache of type TUTORIAL_CACHE_SE2_OFFSET
addEdge: FATAL, cannot resolve caches for edge 0x23419a0

相关issue

这个问题是因为库被编译成静态库,编译器优化了一些其认为没有必要的代码;主要是在createCache()函数中下面两行:

1
2
Factory* f = Factory::instance();
HyperGraph::HyperGraphElement* e = f->construct(key.type());

上面这个工厂方法在编译阶段返回的是nullptr,因为自定义类型,比如上面的报错信息中TUTORIAL_CACHE_SE2_OFFSET,在编译阶段并没有被注册进工厂,也就是下面这个代码没有被执行:

1
G2O_REGISTER_TYPE(TUTORIAL_CACHE_SE2_OFFSET, CacheSE2Offset);

4.2 解决方法1:将库编译成动态库

将库编译成动态库,编译器就不会优化掉上面的代码,这样就可以正常运行了。

1
add_library(project SHARED ${PROJECT_SOURCE_FILES})

4.3 解决方法2:显示调用G2O_REGISTER_TYPE

根据上面的issue,问题出现在由于我们没有显式地调用注册某些类型,主要是CacheCacheEdge中是通过一中比较隐秘的方式被创建(Factory),个人理解是编译时并不指定具体类型,而是在运行时根据CacheKey来创建,所以在编译时并不会报错,但是在运行时就会出现上面的问题。

所以解决方法是在编译时显式地调用G2O_REGISTER_TYPE,例如在Main中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace g2o {
namespace tutorial {

G2O_REGISTER_TYPE_GROUP(tutorial_slam2d);

G2O_REGISTER_TYPE(TUTORIAL_VERTEX_SE2, VertexSE2);
G2O_REGISTER_TYPE(TUTORIAL_VERTEX_POINT_XY, VertexPointXY);

G2O_REGISTER_TYPE(TUTORIAL_PARAMS_SE2_OFFSET, ParameterSE2Offset);

G2O_REGISTER_TYPE(TUTORIAL_CACHE_SE2_OFFSET, CacheSE2Offset);

G2O_REGISTER_TYPE(TUTORIAL_EDGE_SE2, EdgeSE2);
G2O_REGISTER_TYPE(TUTORIAL_EDGE_SE2_POINT_XY, EdgeSE2PointXY);
}
} // end namespace

然后就可以照常使用静态链接(target_link_libraries默认是静态链接)。

五、tutorial_slam2d例子

g2o源码中的tutorial_slam2d例子,是作为项目的一部分进行编译,所以不用解决g2o安装及链接等问题,为了让例子更符合实际应用,将该例子单独拿出来,作为单独一个项目。依赖系统安装的g2o进行编译和运行。

同时,该例子为了展示自定义类型的实现方法,所使用的EdgeVertex为自定义类型,但是例子里的实现似乎还缺少一些可视化方法的实现,导致无法在g2o_viewer中显示。 为此,在例子中又添加一个子文件夹,使用内置的数据类型进行实现,使得生成的.g2o文件可以在g2o_viewer中显示。

5.1 代码

项目代码:https://github.com/zeal-up/Slam2d_g2o_example

5.2 g2o_viewer以及其他g2o工具如何加载自定义类型

这一点其实在g2o的官方文档中有说明,但是我看的时候没有注意到,所以这里把原文贴一下

7.4 Plug-in Architecture Both tools support the loading of types and optimization algorithms at run-time from dynamic libraries. This is realized as follows. The tools load from the libs folder all libraries matching “* types ” and “ solver ” to register types and optimization algorithms, respectively. We assume that by loading the libraries the types and the algorithms register via their respective constructors to the system. Listing 1 shows how to register types to the system and Listing 2 is an example, which shows how to register an optimization algorithm via the plug-in architecture. For loading dynamic library containing types or optimization algorithms, we support two different methods: ˆ The tools recognize the command line switch -typeslib and -solverlib to load a specific library. ˆ You may specify the environment variables G2O_TYPES_DIR and G2O_SOLVER_DIR which are scanned at start and libraries matching “ types ” and “ solver *” are automatically loaded.

这里顺便把g2o源码中包含的官方文档也贴一下(需要用latex编译):g2o.pdf

5.3 运行结果

例子中实现一个仿真器,模拟机器人在正方形网格中运行。所以真实的轨迹是一些正方形轨迹。但是由于测量误差,轨迹会有一些偏差,所以需要通过优化来估计真实轨迹。

优化前:

优化前位姿图

优化后:

优化后位姿图

可以明显看到优化后的轨迹更像一堆“正方形”。