ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ubuntu 20.04下Sophus a621ff版本安装与编译避坑指南

Ubuntu 20.04下Sophus a621ff版本安装与编译避坑指南

1. 写在前面的几句实话

先把话说在前面:Sophus这个库,本身不算难装,难的是“装对版本”。很多朋友在跟着教程跑视觉SLAM或者惯导融合相关的项目时,都会遇到一个经典步骤——安装Sophus(commit编号a621ff)。这个commit在教程社区里被高频引用,原因是它对应的是李群李代数非模板版本的稳定形态,代码接口和许多公开课、开源项目里的调用方式是对得上的。如果你装的是后续模板化的Sophus版本,接口差异会导致不少旧代码编译不过去,报错能让你折腾一下午。

这篇内容就是围绕Ubuntu 20.04系统下,如何干净、稳妥地把Sophus的a621ff版本装好并跑通自带测试,同时把安装过程中容易踩的坑挨个说清楚。适合正在学习SLAM、刚接触李群李代数、或者卡在编译报错里的读者参考。我尽量按照实际操作的顺序来讲,先讲依赖,再讲编译配置,最后给出一份常见报错速查表,你可以对照自己的情况直接查。

2. 为什么偏偏是a621ff这个commit

Sophus从2018年之后经历了一次比较大的重构,从原来的非模板版本迁移到了模板版本。很多旧教程、旧代码、旧公开课里写的接口都是基于非模板版本的。比如Sophus::SE3、Sophus::SO3在非模板版本中直接以类型的方式使用,而模板化之后,你看到的往往是Sophus::SE3d、Sophus::SO3d这种带有精度后缀的类型名。接口变化不仅仅是名字上的,构造函数、成员函数、矩阵转换方式都跟着变了。如果你装的是新版本Sophus,再去编译旧代码,几乎必然会遇到接口不匹配的问题。

a621ff这个commit正好卡在非模板版本比较成熟、同时代码风格又相对统一的一个节点上。它在很多课程资料里被引用,是因为教程作者在编写代码时,本地测试用的就是这个版本。你按教程走,装这个版本,编译你下载的示例代码,成功的概率是最高的。如果盲目装最新版,代码层面需要改动的量会非常大,对新手来说并不友好。

再说一下系统和工具链的问题。Ubuntu 20.04自带的GCC是9.x系列,CMake默认是3.16系列,Eigen可以通过apt直接安装3.3.7版本。Sophus的a621ff本身发布于2018年前后,理论上它设计时面向的编译器环境没有现在这么新。在实际编译中,GCC 9配合Eigen 3.3.7去编译这个版本的Sophus,并不会出现什么兼容性障碍,反而是新版本的Eigen有时会因为模板推导规则收紧而带来无谓的警告甚至报错。这也是为什么我建议在Ubuntu 20.04上安装时,优先用apt自带的Eigen版本,而不是去源码编译一个极新的Eigen。

3. 动手前的依赖准备

3.1 基础工具链检查

在开始编译Sophus之前,先把编译工具链确认一遍。毕竟Sophus是一个需要CMake管理的C++库,工具链不齐,后面每一步都会很痛苦。打开终端,依次执行下面几条命令检查环境:

gcc --version g++ --version cmake --version make --version

正常来说,Ubuntu 20.04安装完成后,gcc/g++ 9.x、cmake 3.16、make 4.2这些工具都是有的。如果提示找不到命令,就执行:

sudo apt update sudo apt install build-essential cmake

build-essential这个包会一次性把gcc、g++、make以及一些基础的头文件都装好,属于装任何C++项目前的标配动作。

如果你之前自己编译安装过更高版本的CMake或者GCC,这里要稍微留意一下环境变量是否指向了自定义路径。我在实际操作中就遇到过类似情况:系统自带CMake 3.16,但用户PATH里混了一个编译安装的CMake 3.22,结果Sophus的编译脚本在版本判断上走了不同的分支,生成的文件路径变得很奇怪。为了避免这类问题,建议优先使用apt源里的版本,除非你明确知道自己在做什么。

3.2 Eigen 3.3.7的安装

Sophus底层所有运算都是基于Eigen完成的。Eigen是一个头文件为主的模板库,安装起来相对简单。Ubuntu 20.04的apt源里就有Eigen 3.3.7,直接装就好了:

sudo apt install libeigen3-dev

装完之后,Eigen的头文件会被放到/usr/include/eigen3目录下,CMake里通过find_package(Eigen3)来查找它,Sophus的CMake脚本会自动处理这个问题,不需要你手动指定路径。

这里有一个非常关键的细节:某些教程会让你去官网下载最新版Eigen并手动放到/usr/local/include下,千万不要这么做。不是最新版不好,而是新版本的Eigen在某些模板推导规则上更严格,和a621ff这个老版本Sophus搭配编译时,极容易触发一堆模板报错,报错信息看起来就像天书一样。实测下来,Eigen 3.3.7 + Sophus a621ff是一对非常稳妥的组合。除非你有特殊需求,否则不要轻易更换Eigen版本。

注意:如果你系统里之前已经装过其他版本的Eigen,建议先用dpkg -l | grep eigen查看一下。如果有多个版本混在一起,编译时头文件搜索顺序会非常混乱,报错也很难排查。最稳妥的办法是把多余版本清理掉,只保留apt安装的3.3.7。

3.3 fmt库的安装问题

Sophus的a621ff版本在打印输出时使用了fmt库来实现格式化字符串功能。这个依赖在安装时容易被忽视,因为很多教程只是轻描淡写地提一句“需要fmt”,却没说清楚怎么装、装哪个版本。实际上,Sophus的CMake脚本里默认会把fmt的源码下载到ThirdParty目录下进行捆绑编译,但这个捆绑版本和系统已有的fmt版本之间可能会产生冲突。

在Ubuntu 20.04上,我推荐直接通过apt安装系统版本的fmt:

sudo apt install libfmt-dev

然后编译Sophus时需要显式关闭捆绑第三方库的选项,具体操作方法在下一节说明。这里想强调的是,如果你忽略了这一项,编译过程中可能会出现fmt/format.h not found或者莫名其妙的链接错误,原因就是CMake在捆绑编译fmt时,没有找到对应的头文件路径,或者找到了多个冲突版本。

fmt库本身是一个纯头文件加少量实现代码的库,安装很快,不会有任何负担。你只需要记住:先装好fmt,再编译Sophus,顺序别反了。

4. 编译安装Sophus全流程

4.1 下载指定commit的源码

下载源码这一步,不要直接clone最新分支。很多人习惯性执行git clone https://github.com/strasdat/Sophus.git,然后发现编译报错、接口对不上,最后才意识到版本不对。正确做法是先clone完整仓库,再checkout到指定commit。

git clone https://github.com/strasdat/Sophus.git cd Sophus git checkout a621ff

执行完git checkout a621ff之后,可以用git log -1确认一下当前HEAD是否确实指向这个commit:

git log -1

正常情况下,输出信息里会显示commit编号a621ff对应的完整哈希值以及提交信息。看到类似a621ff开头的哈希就说明切换成功了。

有的教程会建议你只git clone然后git reset --hard a621ff,效果上是等价的,但checkout表达更直观,不容易引起歧义。如果你的网络环境不太好,clone整个仓库会比较慢,这时可以考虑加上--depth参数浅克隆。不过浅克隆之后可能拿不到历史版本信息,所以建议还是完整clone,Sophus仓库本身不算大,几百兆以内,正常网络条件下很快。

4.2 修改CMakeLists版本限制

这一步是整个安装过程中最容易被忽略但实际最重要的操作。打开Sophus目录下的CMakeLists.txt文件,找到关于Eigen版本要求的那一行:

find_package(Eigen3 3.3 REQUIRED)

在a621ff这个版本中,这一行通常写的是:

find_package(Eigen3 3.4 REQUIRED)

而Ubuntu 20.04的apt源Eigen版本是3.3.7,如果保持3.4的要求,CMake会在配置阶段直接报错,提示找不到满足条件的Eigen版本。

解决办法很简单,把这一行改成:

find_package(Eigen3 3.3 REQUIRED)

保存退出即可。

你可能会问:既然Sophus只用了Eigen的基础功能,为什么不直接装高版本Eigen让CMake检查通过呢?原因是高版本Eigen和这个老版本Sophus在模板实例化上存在兼容性问题,强行搭配使用可能导致编译错误,反而不如把版本要求调低,让CMake通过检查。这个操作没有任何逻辑风险,纯粹是版本门槛的调整,放心改就好。

4.3 配置CMake选项

进入Sophus目录,创建build目录并执行CMake配置。此时需要显式指定关闭捆绑第三方库:

mkdir build cd build cmake -DCMAKE_BUILD_TYPE=Release -DUSE_BUNDLED_THIRD_PARTY=OFF ..

解释一下USE_BUNDLED_THIRD_PARTY这个选项。默认状态下,Sophus会尝试把fmt、maybe_serialized等第三方依赖捆绑编译,这样在隔离性上更好,但会遇到两个问题。其一,捆绑下载依赖时需要访问外网,网络不稳定时容易失败;其二,捆绑编译出的fmt版本可能和系统libfmt-dev冲突,链接阶段两个版本的符号混在一起,轻则警告,重则链接失败。关闭捆绑,明确使用系统已有的fmt库,整个流程会顺畅得多。这也是我在实际操作后最终确定下来的方案。

配置完成后,会生成完整的Makefile工程文件。接下来执行编译:

make -j4

-j4是并行编译的线程数,如果你的CPU核心数较多,可以适当调大,比如-j8。这个版本的Sophus代码量不大,编译时间通常在1到3分钟以内。如果编译过程中没有报错,继续执行安装:

sudo make install

安装完成后,Sophus的头文件会被拷贝到/usr/local/include/Sophus目录下,库文件会被拷贝到/usr/local/lib目录下。

注意:安装目录默认是/usr/local,如果你的系统之前安装过其他版本的Sophus,建议先检查/usr/local/include/Sophus和/usr/local/lib下的旧文件,手动清理后再执行安装。否则新旧头文件混在一起,编译自己的项目时会出现各种匪夷所思的错误。

4.4 验证安装是否成功

安装完毕后,别急着关终端。用一个小例子验证一下Sophus是否真的可用。

在Sophus目录下有个examples文件夹,里面存放了一些示例代码。你可以尝试编译其中最简单的那个,比如hello_so3.cpp:

cd ../examples g++ hello_so3.cpp -o hello_so3 -I/usr/local/include -I/usr/include/eigen3 -L/usr/local/lib -lSophus

编译成功后,运行:

./hello_so3

如果输出正常,没有报错,就说明Sophus安装成功。这个示例代码会创建SO3旋转,并打印一些变换结果,看到数字输出基本就稳了。

如果没有examples文件夹或者编译失败,这里给一个最简单的测试代码,你可以直接保存成test_sophus.cpp来验证:

#include <Sophus/SO3.hpp> #include <iostream> int main() { Sophus::SO3<double> R; std::cout << "Sophus SO3 identity matrix:\n" << R.matrix() << std::endl; std::cout << "Sophus installation works!" << std::endl; return 0; }

编译命令同上。注意这里使用的是Sophus::SO3<double>这个模板形式,对应新版Sophus的接口,和a621ff版本里的Sophus::SO3也是兼容的——在变形发生前,非模板版本内部就是通过模板别名来实现的,所以大部分情况两种写法都成立。

看到Sophus installation works!输出,就可以放心地往下进行你项目里的编译工作了。

5. 安装完成后,代码里该注意的接口细节

5.1 SO3和SE3的构造与初始化

安装只是第一步,真正让新手头疼的是在自己的代码里调用Sophus的接口。a621ff这个版本中,SO3类和SE3类的使用方式和后续模板版本几乎一致,但有一个容易被忽略的细节:SO3构造函数的入参类型。一个经典写法:

#include <Sophus/SO3.hpp> #include <Eigen/Geometry> Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI / 4, Eigen::Vector3d::UnitZ()).toRotationMatrix(); Sophus::SO3<double> SO3_R(R);

这里SO3_R变量名是我临时起的,实际项目中你可以换成任何语义化的名字。注意构造时传入的是Eigen的Matrix3d矩阵,Sophus内部会将其转换为李代数对应的旋转矩阵形式。很多新手在这里会尝试直接传入欧拉角或者四元数,导致编译报错“no matching function call”。实际上应该先通过Eigen将该旋转表示为旋转矩阵,再传给SO3构造函数。

SE3类似,构造函数接收一个旋转矩阵或四元数,以及一个平移向量:

Sophus::SE3<double> SE3_Rt(R, t);

这里的t是一个Eigen::Vector3d类型。注意参数顺序是旋转在前、平移在后,不要写反了。写反的后果是编译不报错但语义完全错误,这种bug排查起来非常耗时。

5.2 常见报错对照速查表

安装Sophus的过程中,不同人会踩到不同的坑。这里整理一个速查表,基本覆盖了我见过的绝大多数问题。

报错信息可能原因解决方案
Could not find Eigen3Eigen未安装或CMake找不到版本安装libeigen3-dev,并确认版本不低于3.3
Could not find a package configuration file依赖缺失检查fmt是否安装,执行sudo apt install libfmt-dev
fatal error: Sophus/SO3.hpp: No such file头文件路径未包含编译时添加-I/usr/local/include
undefined reference to Sophus::SO3d未链接Sophus库编译时添加-L/usr/local/lib -lSophus
一堆模板编译错误,指向eigen头文件内部Eigen版本过新改用3.3.7版本的Eigen
打印输出时出现奇怪的格式异常fmt版本冲突关闭捆绑第三方库,使用系统libfmt
安装时提示Permission denied没有sudo权限加sudo执行安装

这张表是我在实际操作过程中踩过的坑浓缩出来的,基本覆盖了90%以上的新手问题。遇到报错先对照这个表自查,比盲目搜索整段报错信息要高效得多。

5.3 头文件引用方式的历史遗留问题

还有一个很玄学的问题。部分教程会让你在代码里写#include <sophus/so3.h>,注意这里的路径是小写sophus,文件名也是小写的so3.h。a621ff这个commit对应的头文件布局其实是Sophus/SO3.hpp这种大写开头的形式。通常大写开头的头文件里面会#include对应的小写路径头文件以兼容老的调用方式。但实际测试下来,直接用大写开头的写法最省心,不要混着来:

#include <Sophus/SO3.hpp> #include <Sophus/SE3.hpp>

如果你看到某个教程里写的是小写路径,别急着模仿,优先换成大写形式。两种写法指向的声明内容基本相同,但大写形式的头文件是当前版本的标准入口,编译时路径解析更可靠。

6. 关于编译选项和链接路径的一些补充

6.1 为什么推荐Release构建

我在前面给出了CMake的配置命令:

cmake -DCMAKE_BUILD_TYPE=Release -DUSE_BUNDLED_THIRD_PARTY=OFF ..

有人可能会注意到CMAKE_BUILD_TYPE这个参数,并好奇为什么一定要指定为Release。其实不指定也行,Debug模式也能编译通过。但Sophus这类几何运算密集的库,在Debug模式下会保留大量调试符号和断言检查,运行速度明显变慢。更重要的是,很多依赖Sophus的项目在编译时会默认使用Release模式。

为了避免之后你项目中因为库的构建类型不一致而产生交叉编译的问题,统一使用Release是更安全的选择。这部分经验来自一次实际遇见的棘手问题:用户项目使用Release,但Sophus装的是Debug模式编译,链接时对STL符号的解析出现冲突,报错信息指向不明确,排查了好几个小时才定位到构建类型不匹配。所以装库这种事,从一开始就把编译选项固定下来,能省掉后续很多麻烦。

6.2 动态库和静态库的选择

Sophus安装结束后,/usr/local/lib下会生成libSophus.so这样的动态库文件。链接时使用:

-lSophus

动态库的优势是运行时共享,磁盘占用小,但如果你的可执行文件要部署到没有Sophus的机器上,就需要额外处理动态链接的问题。对于学习阶段或者本机开发来说,动态库完全够用。如果你后续有打包部署的需求,可以考虑在CMake配置时加一个-DBUILD_SHARED_LIBS=OFF选项来生成静态库:

cmake -DCMAKE_BUILD_TYPE=Release -DUSE_BUNDLED_THIRD_PARTY=OFF -DBUILD_SHARED_LIBS=OFF ..

改了这个选项之后,生成的libSophus.a文件就可以直接静态链接进入你的可执行程序。两种方式各有优劣,没有绝对的好坏,根据实际场景选就行。

7. 集成到你的CMake项目中

Sophus装好之后,最终要在你自己的项目里发挥作用。这里给一个最标准、最省事的CMake集成模板。假设你的项目根目录下有CMakeLists.txt,在文件里加入:

cmake_minimum_required(VERSION 3.10) project(MySLAMProject) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Sophus REQUIRED) find_package(Eigen3 REQUIRED) add_executable(my_slam_main main.cpp) target_link_libraries(my_slam_main Sophus::Sophus Eigen3::Eigen)

这里Sophus::Sophus和Eigen3::Eigen是基于CMake的target链接机制,相比直接使用变量SOPHUS_LIBRARIES、EIGEN_INCLUDE_DIRS要干净得多。如果你在项目里看到了target链接的用法,建议沿用这套方式,不要混用老式的include和link变量。

有一点要注意,find_package(Sophus REQUIRED)在CMake中依赖/usr/local/lib/cmake/Sophus目录下的cmake配置文件。正常执行make install之后,这个路径会自动生成,所以不需要额外指定Sophus_DIR。如果你看到CMake报错说找不到Sophus的配置文件,几乎可以肯定安装步骤没有完整执行到最后一步。

7.1 一个最小示例的完整编译流程

这里给出一个最小示例,用来验证你集成了Sophus的CMake工程是否真的能跑。在项目目录下新建main.cpp:

#include <Sophus/SO3.hpp> #include <iostream> int main() { Sophus::SO3d R = Sophus::SO3d::rotX(0.3); std::cout << "Rotation matrix:\n" << R.matrix() << std::endl; std::cout << "SO(3) hat of v=(1,2,3):\n" << Sophus::SO3d::hat(Eigen::Vector3d(1,2,3)) << std::endl; return 0; }

CMAKE工程文件如上节所示,编译运行:

mkdir build && cd build cmake .. make ./my_slam_main

如果能输出旋转矩阵和hat运算的结果,说明你的CMake集成链路是完整通的。这个验证过程非常值得做,因为它把“Sophus可用”和“Sophus可在我的项目中可用”这两件事一次验证完毕,为后续代码开发奠定了可靠基础。

8. 我的真实操作体会

装Sophus这个库,本质上不是装完就一劳永逸的事情。a621ff这个commit之所以被翻来覆去地使用,是因为它在很多SLAM相关的课程和项目中已经成了一个事实标准。但有很多人装完Sophus之后,发现自己的项目还是编译不过,这时候问题往往不在Sophus本身,而在别的地方。

我遇到过最典型的一个案例是:某开发者在自己的项目里同时引用了高版本的OpenCV、PCL、Sophus等多个第三方库,各个库之间对Eigen的版本要求不一致,编译时头文件搜索路径互相打架。最后定位下来,是某个库头文件目录里自带了旧版Eigen,导致系统Eigen被覆盖遮挡。这种问题不会在单装Sophus时暴露出来,但在完整项目中非常常见。

所以如果你编译自己的项目时遇到奇怪的头文件冲突,建议先检查一下系统的/usr/include/eigen3和/usr/local/include/eigen3里是否存在多个Eigen版本。另外,如果你的项目用CMake管理,试着在CMakeLists里显式指定Eigen的搜索路径,优先搜索系统路径:

set(EIGEN3_INCLUDE_DIR "/usr/include/eigen3") include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR})

这样可以在很大程度上避免搜索顺序导致的问题。

最后再分享一个个人习惯:每次在新机器上装Sophus,我都会把git checkout a621ff之后的一整套CMake配置命令记录成脚本,保存到一个专门的环境搭建笔记里。这样每次装新环境时只需要复制粘贴执行一遍,省去了手动敲命令、记参数的时间。如果你在之后的项目开发过程中遇到过类似的依赖管理问题,也可以试试这个方法——把环境搭建的步骤脚本化、文档化,你会发现“配环境”这件事的耗时能被压缩到极短。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进