先说结论:Sophus 这个库在 Ubuntu 20.04 下装起来看似简单,真正动手时你会发现它有一堆历史包袱。尤其是当你拿到的不是最新 main 分支,而是某个被大量项目锁定的特定提交 a621ff 时,编译环境稍有不齐就会卡在 fmt 库、Eigen 路径这些莫名其妙的环节上。这篇文章把我实操过的完整流程、踩过的坑、以及事后总结的排查思路全部整理出来,给正准备在这个版本上装 Sophus 的人一个可以直接照着走的参考。
如果你只是为了跑一个固定课程或开源项目的代码,不用犹豫,直接看向这个 a621ff 提交。它处于 Sophus 由旧版非模板类向新版模板类迁移的过渡位置,既保留了相对稳定的接口,又已经被很多工程验证过。问题是这个提交对应时代的周边依赖,和现在 Ubuntu 20.04 默认仓库里的版本存在细微错位,于是安装过程就变成了一场依赖匹配游戏。
1. 这个库为什么让人又爱又恨
1.1 Sophus 在工程里的真实定位
Sophus 本质上是一套李群和李代数相关的 C++ 工具库。做视觉 SLAM、机器人状态估计、位姿插值的人基本绕不开它。它封装了 SO(2)、SO(3)、SE(2)、SE(3) 以及对应的李代数类型,让我们不需要手动推导一堆矩阵指数、对数映射、伴随变换的公式,直接调用现成类型就能完成旋转矩阵、变换矩阵的构建和运算。
举个例子,你要表示一个刚体在三维空间中的位姿,最常见的做法是用一个 4x4 的变换矩阵,包含旋转和平移。直接操作这个矩阵本身不算难,难的是做优化、插值、扰动模型时,你必须引入李代数。Sophus 把这些东西封装成 T、SO3、SE3 这样的类型,代码写起来非常直观。你声明一个Sophus::SE3d变量,传入旋转矩阵和平移向量,就能完成变换,而且内部保证数学上的正确性。
很多库都是这个定位:它不解决领域里的所有问题,但把最常用、最容易被写错的部分抽象掉。Sophus 的用户绝大多数是机器人、自动驾驶、三维重建方向的开发者,少部分是做姿态传感器融合的嵌入式工程师。大家共同的特点是:不想在每一段代码里重复推导李群公式,只想赶紧把上层逻辑跑通。
这在工程上是个典型的核心基础设施依赖。所以它的安装问题影响面很大,不是你一个人装不上那么简单。一个组里十台机器,只要有一台环境特殊装不上,整个联调进度就得卡住。这也是为什么我特别想把这个安装过程里所有已知的坑集中写清楚。
1.2 为什么非要锁 a621ff 这个提交
Sophus 的代码历史有个很明显的分水岭:旧版是直接定义SO3、SE3这样的非模板类,新版全部改成了模板类,比如SO3<double>、SE3<double>。这中间有一次大规模重写,连带依赖也变了。你要是直接拉最新的 main 分支,接口和你手上代码可能完全对不上;你要是用最老的版本,又缺少一些新特性。
a621ff 刚好处在一个有意思的位置:它已经完成了模板化改造,但底层实现还保留了旧版本惯用的代码风格,很多中间产物对学习目的特别友好。更关键的是,不少课程、开源项目、教学案例都固定在这个提交上,为了沿用现成的测试用例和示例代码,大家会主动把版本锁在这里。
我也见过有人不看版本直接克隆最新分支,结果项目原有的头文件引用方式失效,编译报错几十行,最后才意识到是版本错位导致的。装库这事,锁定版本不是洁癖,而是团队协作时的最低成本选择。你锁一个大家都认可的版本,环境差异就只剩下系统依赖层面,排查起来容易得多。
2. 环境准备与依赖安装
2.1 装之前先看清楚系统
在 Ubuntu 20.04 下装 Sophus 之前,我建议先花两分钟把系统状态确认清楚,避免后面所有报错都归因错误。一个最基本的检查列表是这样的:
lsb_release -a gcc --version cmake --version apt-cache policy libfmt-dev apt-cache policy libeigen3-dev第一个命令确认系统确实是 20.04。第二个和第三个确认编译工具链版本,Ubuntu 20.04 默认自带 GCC 9.3.0 和 CMake 3.16.3,这两个版本完全满足 Sophus a621ff 的编译要求。最后两个命令看 apt 源里可用的依赖版本,这往往能预判后续会不会出问题。
Ubuntu 20.04 官方源里 libfmt-dev 默认是 6.1.2,libeigen3-dev 默认是 3.3.7。这两个版本对 a621ff 来说属于兼容范围内,不用额外折腾 PPA。很多人一上来就安装各种第三方源里的 Eigen 或者 fmt,反而把环境搞得混乱不堪。我的建议是:能用官方 apt 解决的就不要自己编译,除非项目明确要求特定版本。
检查完系统之后,下一步是把编译工具装齐。Sophus 本体是纯 C++ 项目,加一个 CMake 构建流程,严格来说只要这几个包:build-essential、cmake、git、libeigen3-dev、libfmt-dev。build-essential 提供了 gcc、g++、make 这些核心工具,libeigen3-dev 是 Eigen 头文件包,libfmt-dev 提供格式化库。
sudo apt update sudo apt install -y build-essential cmake git libeigen3-dev libfmt-dev有的教程会让你额外装 libgoogle-gtest-dev,因为 Sophus 仓库里带了基于 GTest 的单元测试。但我编译 a621ff 时通常不需要跑完整测试套件,能编译过库本身就够了。如果你后面确实需要跑测试,再补装也不迟,不影响主流程。
2.2 Eigen 和 fmt:容易踩的两个坑
Eigen 这个库比较特殊,它几乎全是头文件,没有独立的编译产物。所谓“安装 Eigen”,本质上就是把一堆.hpp文件放到系统能找到的 include 路径里。Ubuntu 的 libeigen3-dev 会把头文件放在/usr/include/eigen3,路径里多了一层eigen3目录。这层目录结构是历来最容易出问题的地方。
如果你的编译器默认在/usr/include下找头文件,它不会直接找到Eigen/Core,因为实际路径是/usr/include/eigen3/Eigen/Core。Sophus 的 CMake 文件通常会用find_package(Eigen3 REQUIRED)来找 Eigen,然后通过 include 目录把eigen3这层拼进去,所以正常情况下没问题。但如果你自己手动编译测试程序,忘记加-I/usr/include/eigen3,就会碰到fatal error: Eigen/Core: No such file or directory。
fmt 库则是另一个坑。a621ff 这个版本的 Sophus 内部用了 fmt 来做格式化输出,如果你系统里没装 libfmt-dev,编译就会立刻失败,报错信息是找不到fmt/format.h。装了但版本太新也可能出问题,因为 fmt 库的 API 在不同大版本之间有过调整,老代码调用fmt::format的签名可能和新版本的实现不匹配。Ubuntu 20.04 自带的 6.1.2 版本和 a621ff 时代比较接近,反而是最省事的选项。
我见过有人在 conda 环境里装了 fmt 新版,又在系统里装了旧版,两个环境混杂之后编译出的链接错误非常难查。所以统一用系统 apt 版本是避免头疼的好策略。如果是个人学习机,别在这两个依赖上追求最新版,稳才是最重要的。
3. 拉取指定版本源码与正确构建
3.1 锁定版本的正确操作姿势
Sophus 的源码托管在 GitHub 上,仓库名就叫 Sophus。安装的第一步是克隆仓库,然后切换到 a621ff 这个提交。这里有一个细节值得强调:不要直接git clone之后用最新分支,除非你非常清楚最新接口的用法。正确做法是先克隆,再 checkout 到指定提交。
cd ~ git clone https://github.com/strasdat/Sophus.git cd Sophus git checkout a621ff执行git checkout a621ff之后,工作区会进入 detached HEAD 状态,也就是不挂在任何分支上。这是正常的,不用慌。你只是在这个提交上编译安装,不需要新建分支,也不需要 push。有个别人习惯性地加一句git branch或者git switch -c,这纯粹是多余的,反而会留下无意义的本地分支。
如果你之前已经克隆过这个仓库,而且里面改过文件,切换提交前最好先git status看一眼。工作区有未提交的改动会导致 checkout 失败,报错信息会提示你冲突文件。处理方法无非是git stash或者git reset --hard,后者会丢弃所有本地改动,用之前要想清楚。
还有一点,仓库克隆下来之后体积不大,几百兆以内,网络正常的几分钟就完成了。如果 clone 速度特别慢,可以只拉取单个提交的历史,用git clone --depth 1 --branch方式,但这种方式对面这个老提交不一定友好。我建议普通用户直接完整克隆,不要为了省这点时间折腾浅克隆导致的缺失历史问题。
3.2 检查源码结构与环境
切换到 a621ff 之后,建议先花一分钟浏览一下仓库根目录。通常你会看到Sophus/这个子目录,里面放着so2.hpp、so3.hpp、se2.hpp、se3.hpp这些核心头文件,以及对应的.cpp源文件。根目录还有CMakeLists.txt,它会定义构建规则。
打开 CMakeLists.txt 看两眼,重点确认它对依赖的查找方式。a621ff 版本会要求find_package(Eigen3 REQUIRED)和find_package(fmt REQUIRED),如果你的系统这些包安装位置不在标准路径,CMake 可能会找不到,需要额外指定。Ubuntu 20.04 的 apt 安装默认位置都在标准路径,所以一般不需要人为干预。
源码检查这一步常被省略,但我觉得很有必要。它就像你拿到一个陌生工程之后先读一遍 README 和构建脚本,可以提前发现已经存在的问题,而不是等到 make 报错才开始头疼。比如我之前在一台机器上就发现,CMakeLists 里写死了某个 fmt 的 target 名称,而系统里那个名称只存在于特定版本,这种信息提前看到就能省时间。
如果你看到根目录下有test或者sophus_test相关目录,不要急着编译它们。a621ff 的库本身编译完成就达到目的了,测试代码只是辅助验证功能,默认 CMake 配置不一定触发它们,但你开启 BUILD_TESTING 选项后就要多编译很多东西,也更容易报奇奇怪怪的测试依赖错误。
3.3 cmake 与 make 全流程
确认环境基本没问题后,开始正式的构建流程。Sophus 用的是标准 CMake 构建方式,和大多数 C++ 项目没什么两样。我习惯在源码目录下建一个单独的 build 目录,而不是直接在源码目录里编译,这样生成的中间文件不会污染源码,后续想重新编译也可以直接删掉 build 目录重来。
mkdir build cd build cmake ..cmake 这一步会打印配置信息,注意观察有没有出现Could NOT find之类的警告。如果找不到 fmt,配置阶段就会失败,报错信息里会直接告诉你目标是 fmt。如果找不到 Eigen,也会卡在这里。配置通过后,接下来编译:
make -j4关于-j参数,我建议保守一点,用-j2或者-j4,不要贪心直接-j$(nproc)。这个项目代码量不算大,但模板实例化很重,并行任务太多容易把内存吃满,虚拟机或者低配置机器会直接卡死。我自己的笔记本 8 核,用-j4编译这个版本大概一两分钟就完成了,没必要追求极致的并行度。
编译成功之后,最后一步是安装:
sudo make install安装默认路径是/usr/local/include和/usr/local/lib。头文件会出现在/usr/local/include/sophus/,动态库是libSophus.so。用sudo是因为默认安装位置超出普通用户的写权限。如果你不想动系统目录,也可以给 cmake 传一个自定义安装前缀:
cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/soft/sophus但这样后面你自己的项目引用 Sophus 的时候,就得手动指定头文件和库文件的搜索路径,麻烦不少。个人开发和学习场景,装到/usr/local最省心。
4. 编译错误大赏与处理方案
4.1 fmt 相关链接错误
整个安装流程里,我遇到最多的错误都和 fmt 有关。第一类是配置阶段就找不到 fmt,报错类似Could NOT find fmt。这个原因通常很简单:没装 libfmt-dev,或者装了但是 CMake 搜索路径里没有。解决办法是回去执行sudo apt install libfmt-dev,然后重新从 cmake 阶段开始。
第二类是编译阶段报找不到头文件,fatal error: fmt/format.h: No such file or directory。这个多半是 fmt 装到了非标准路径,或者你之前手动编译安装了新版 fmt,头文件放到了/usr/local/include/fmt,但 CMake 找到的 include 路径不是这一条。排查时可以手动确认一下/usr/local/include/fmt/format.h或/usr/include/fmt/format.h是否存在,找到了把对应路径交给 cmake。
第三类是最隐蔽的链接错误,编译库本身没问题,你自己的项目里去链接 Sophus 时突然报undefined reference to fmt::v6::vformat这一类。看到fmt::v6基本可以确定是版本命名空间的问题,你的程序或者 Sophus 库编译时用的 fmt 版本,和最终链接时用的 fmt 版本不一致。最典型的场景是系统同时有 apt 版 fmt 和手动编译的 fmt,链接器选错了。
这种问题的解决办法就是统一版本。我建议清除手动安装的 fmt,只保留 apt 版本,然后重新编译 Sophus 和你的项目。注意这两步都要重来,因为库里的链接信息已经带上旧版本的符号了。
4.2 Eigen3 路径不匹配
Eigen 相关的错误也分两个场景。一个场景是 CMake 配置找不到 Eigen3,报错信息会明确提示Could NOT find Eigen3。Ubuntu 20.04 的 apt 包里自带 CMake config 文件,通常不会出这个问题,但如果你是从源码手动编译安装的 Eigen,config 文件路径可能没被 CMake 找到,需要手动指定-D Eigen3_DIR=/path/to/eigen3/cmake。
另一个场景是编译器报找不到头文件,比如fatal error: Eigen/Core: No such file or directory。我在前面提过,这大概率是路径少了eigen3这一层。Ubuntu 的 Eigen 头文件不在默认的/usr/include/Eigen,而在/usr/include/eigen3/Eigen。如果你自己写 Makefile 或者编译命令时,没有加-I/usr/include/eigen3,就会踩这个坑。
一个常用但我不太推荐的解法是做个软链接:
sudo ln -s /usr/include/eigen3/Eigen /usr/include/Eigen这样#include <Eigen/Core>就能找到,省事是省事,但容易掩盖真实的路径逻辑,以后换机器又会踩一脚。我更推荐搞清楚你自己的 CMake 工程里怎么通过find_package(Eigen3 REQUIRED)拿到Eigen3_INCLUDE_DIRS变量,然后正确使用。这才是长期可维护的写法。
4.3 C++ 标准版本与编译参数对齐
Sophus 的 a621ff 版本需要 C++11 以上的标准支持,Ubuntu 20.04 的 GCC 9.3 默认使用了gnu++14标准,完全满足要求,所以这一块正常情况下不会报错。但如果你在自己的项目里用了旧标准,比如 CMake 里写了set(CMAKE_CXX_STANDARD 98),编译引用 Sophus 头文件时就会看到一些模板相关的语法错误,报错信息五花八门,很容易让人误以为是库本身问题。
如果你用的是新版 Sophus,必须把标准升到 C++14 或更高,a621ff 这个过渡版本要求宽松一点,但保险起见,我自己写 CMake 工程时通常会这么设置:
set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON)还有一类报错和编译警告相关。老版本代码在新编译器下容易生成警告,比如类型转换、符号扩展这类。默认情况下 GCC 只是警告,不会中断编译。但如果你自己项目的 CMake 里开了-Werror,警告就会直接升级成错误,编译 Sophus 时就会看到一大堆error: ...。这时候不要慌,看看是不是-Werror导致的,单独编译 Sophus 时关掉它就行。
5. 安装后的验证与项目集成
5.1 用最小的程序验证安装
安装结束后,赶紧写一个最小测试程序验证一下,不要急着丢进项目里。这个验证能区分常见的不兼容问题,也能给你后续排查提供基线。最简单的验证程序只需要包含 Sophus 头文件,创建几个基本类型,然后编译运行。
在任意目录下创建test_sophus.cpp,内容如下:
#include <iostream> #include <Sophus/se3.hpp> #include <Eigen/Core> int main() { Sophus::SE3d T = Sophus::SE3d(); Eigen::Vector3d t(1.0, 2.0, 3.0); std::cout << "Sophus version test passed, translation = " << t.transpose() << std::endl; return 0; }编译命令取决于你的安装路径。装在默认/usr/local的情况下:
g++ -std=c++14 test_sophus.cpp -o test_sophus -I/usr/local/include -L/usr/local/lib -lSophus -lfmt如果编译通过、运行成功,说明 Sophus 本身安装没有问题。如果这里就报错,问题基本锁定在安装环节,而不是你的项目配置。我之前就在这一步排查出不少机器上 fmt 链接不上的问题,定位速度比直接跑去项目里翻报错快得多。
这里有一个容易被忽略的点:链接顺序。-lSophus必须放在源文件之后,-lfmt必须放在-lSophus之后。GCC 在处理依赖时是顺序敏感的,如果顺序写反,某些老版本库会出现undefined reference错误。现代链接器允许一定程度的循环依赖,但别把希望完全寄托在这个上面,老老实实按照依赖关系从后往前排列最稳。
5.2 在 CMake 工程里正确引用 Sophus
接下来是大家真正关心的部分:怎么在自己的 CMake 项目里引用 Sophus。最直接的办法是使用find_package,但前提是你安装的 Sophus 带有 CMake 配置文件。如果你用sudo make install安装,通常能看到/usr/local/lib/cmake/Sophus/这样的目录,里面就是配套的 CMake 配置。
一个标准写法如下:
cmake_minimum_required(VERSION 3.10) project(MySlamApp) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Sophus REQUIRED) include_directories(${Sophus_INCLUDE_DIRS}) add_executable(my_slam_app main.cpp) target_link_libraries(my_slam_app ${Sophus_LIBRARIES})关键点是别忘记链接 fmt。Sophus 的库本身依赖于 fmt 的符号,如果你的项目只链接了libSophus.so,不链接 fmt,运行时或者链接阶段会出现 fmt 相关报错。Sophus 的 CMake config 文件有时会把 fmt 作为公共依赖传递出去,但不同版本的配置行为不一致,保险起见,你可以在自己的工程里加一行:
find_package(fmt REQUIRED) target_link_libraries(my_slam_app ${Sophus_LIBRARIES} fmt::fmt)如果你的项目不使用 CMake,而是直接写 Makefile 或者 IDE 里配置,那就直接按照验证程序里的 include 和 link 参数来配置。包括路径/usr/local/include,库路径/usr/local/lib,库名Sophus和fmt。
5.3 集成后的常见动态库加载问题
有些时候程序编译链接都成功,运行时却报找不到共享库,例如cannot open shared object file: libSophus.so。这个问题的原因是 ldconfig 缓存里没有包含/usr/local/lib。解决办法有两种,各取所需。
一是把/usr/local/lib加到 ldconfig 配置里,然后刷新缓存:
echo "/usr/local/lib" | sudo tee /etc/ld.so.conf.d/sophus.conf sudo ldconfig二是不改系统配置,在运行程序前手动指定库路径:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH第一种属于一劳永逸,第二种适合临时测试。我一般选第一种,因为后续还有多个程序会用到 Sophus,让系统知道这个路径能省很多事。改完 ldconfig 后重新运行程序,一般就正常了。如果还不行,用ldd查看可执行文件的依赖情况:
ldd ./my_slam_appldd会列出所有依赖库和对应的路径,没有找到的会显示not found,定位问题非常直观。
6. 一些值得注意的实操心得
6.1 不要随便升级依赖环境
装完之后,我特别想强调一点:这个组合的系统依赖固定在 Ubuntu 20.04 自带的版本上,是最舒服的状态。不要因为看到新版 Eigen 或者新版 fmt 就顺手升级,升级带来的收益远小于破坏这个已经被验证过的组合的风险。
我自己吃过一个亏:某次为了另一个项目装了一个更新的 fmt 到/usr/local,结果再把 Sophus 相关的老程序拉出来编译,链接阶段直接报了一堆 fmt 符号错误,因为/usr/local/lib的优先级高于/usr/lib,系统默认选了新版动态库。排查了很久才意识到是版本优先级的问题。如果确实要装新版 fmt,尽量用 conda 或者虚拟环境隔离,别污染全局路径。
如果你有多个项目,一个用 Sophus 老接口,一个用新接口,最稳妥的办法是分别构建在隔离的环境里,或者把两个版本的 Sophus 装到不同的前缀路径。比如老版本装到/usr/local,新版本装到/opt/sophus-new,然后在各自的 CMake 工程里通过CMAKE_PREFIX_PATH指定目标路径。这个方案一开始配置起来要花点心思,但能免掉之后反复切换环境的痛苦。
6.2 锁版本不等于锁死环境
很多人看到项目里锁定了 a621ff,就以为环境完全固定了,其实不是。版本锁定了源码接口,但系统依赖、编译器版本、构建工具链都还在动态变化。Ubuntu 20.04 的 apt 源会持续更新,即便大版本不跳,小版本变化也可能影响行为。所以团队协作时,除了锁定 Sophus 的 commit,最好把依赖包版本和安装命令都写进文档或 Dockerfile。
我习惯的规范做法是:在一个项目根目录放一个setup.md或者部署脚本,把以下内容固定下来:
Ubuntu 20.04 gcc 9 cmake 3.16 eigen3 3.3.7 fmt 6.1.2 Sophus commit a621ff这样任何新加入的人照着流程走,至少能在同一套依赖体系下复现,不会因为环境差异产生莫名其妙的 bug。我们实验室的模拟项目X就是这么干的,人员流动之后接手的人也不用靠口头流传的经验来配环境。
6.3 如果这个坑你还想深挖
如果你装完 a621ff 之后有精力,我建议把 Sophus 源码再读一遍。它的so3.hpp和se3.hpp实现得很清晰,既有公式又有代码,是理解李群李代数在工程中落地的绝佳窗口。你不需要把每一行都看透,重点看它的类型定义、构造函数、对数映射这三个部分,就能理解为什么这个库在设计上要套一层模板。
另外,如果你未来要集成别的依赖包,注意 Sophus 上游代码已经迭代很多年,a621ff 在新系统上的兼容性会逐渐变差。遇到这种情况,第一时间想到的不是硬改源码,而是看能不能用 Docker 或者容器把旧环境固定下来。我身边已经有同事转向容器化开发,所有旧依赖直接封装成一个镜像,省去了每换一台机器就要重排一遍环境的精力。
最后再分享一个小技巧:如果你的环境变量或者 CMake 配置实在排查不动了,直接看 Sophus 构建目录下的 CMakeCache.txt,里面记录了那次 cmake 配置使用的所有路径、编译器、选项。你对照着这个文件,就能发现它实际找到的 fmt 和 Eigen 到底在哪,路径冲突的问题基本都能从这里找到蛛丝马迹。按这个思路排查,很少有没有头绪的情况。