1. 问题来了:编译ORB-SLAM3时,Sophus库为何“人格分裂”?

最近在机器人上折腾ORB-SLAM3,环境是Ubuntu 18.04配上ROS Melodic,按理说这应该是个成熟稳定的搭配。代码是从GitHub上拉下来的最新版,在我自己的Ubuntu笔记本上编译得顺风顺水,可一到机器人的系统里,编译过程就卡在了Thirdparty的Sophus库这里,满屏飘红,错误信息看得人头大。

错误主要分两种,一种是“not declared in this scope”(在这个作用域内未声明),感觉像是编译器找不到某个函数或者类型;另一种就更直接了,是“redefinition”(重复定义),明明白白告诉你同一个东西被定义了两次。最让人困惑的是,单独去编译Sophus库本身,它是完全正常的,makemake install一气呵成,没有任何问题。这就很奇怪了,为什么在ORB-SLAM3的项目编译流程里,它就“犯病”了呢?

我一开始也以为是环境依赖没装全,或者CMake版本不对,反复检查了好几遍,甚至把整个Thirdparty文件夹删了重新git clone,问题依旧。上网搜了一圈,关于ORB-SLAM3的编译问题不少,但针对Sophus库这种“not declared”和“redefinition”混合双打的错误,相关的讨论却很少。这感觉就像走进了一个别人都没踩过的坑,只能自己摸着石头过河。

静下心来分析报错信息,特别是那些“redefinition”错误,它们往往指向一些模板类的重复定义。这给了我一个关键提示:编译器很可能从不止一个地方找到了Sophus库的头文件。想象一下,你告诉编译器要去A仓库拿一个零件,结果它发现B仓库也有一个一模一样的零件,它瞬间就懵了,不知道该用哪个,于是果断报错。在ROS环境中,这种“仓库”冲突特别常见,因为ROS本身作为一个庞大的生态系统,会安装和管理它自己的一套依赖库。

顺着这个思路,我检查了系统的头文件搜索路径。果然,发现了问题的根源:ORB-SLAM3的CMakeLists.txt里,明确指定了要使用自己项目目录下的Thirdparty/Sophus/。但同时,因为系统安装了ROS Melodic,在/opt/ros/melodic/include/这个标准路径下,也存在一个ROS官方维护的Sophus库。当编译器开始处理ORB-SLAM3的源代码时,它既会搜索项目指定的本地路径,也会搜索系统默认的包含路径(比如/usr/local/include, /opt/ros/melodic/include)。于是,同一个sophus/geometry.hpp文件,编译器从两个地方都找到了,重复定义的错误就此爆发。而“not declared”错误,有时则是这种路径混乱导致的后续连锁反应,编译器可能错误地链接了版本不一致的库,导致某些符号解析失败。

2. 深入排查:定位冲突的“罪魁祸首”

搞清楚是路径冲突导致的问题后,下一步就是精准定位冲突点。我们不能盲目修改,得拿出侦探精神,把线索一条条理清楚。

首先,我们需要亲眼看看编译器到底看到了什么。一个非常实用的方法是检查CMake生成的编译命令。在ORB-SLAM3的构建目录(通常是build文件夹)下,你可以找到一个叫CMakeCache.txt的文件,或者更直接地,在编译时使用make VERBOSE=1命令。这个命令会让make工具输出每一条具体的编译指令。你会看到类似下面这样的信息:

/usr/bin/c++ -DDISABLE_OPENNI2 -DDISABLE_PNG ... -I/home/yourname/ORB_SLAM3/Thirdparty/Sophus -I/opt/ros/melodic/include -I/usr/include/eigen3 ... -c /home/yourname/ORB_SLAM3/src/System.cc -o ...

注意看-I开头的参数,这些就是包含路径(Include Path)。你会发现,-I/home/yourname/ORB_SLAM3/Thirdparty/Sophus-I/opt/ros/melodic/include同时存在。而sophus的头文件就位于这两个路径下。当源代码写下#include “sophus/geometry.hpp”时,编译器会按顺序在这些-I指定的路径里查找。哪个路径在前,就可能先用到哪个版本的Sophus。如果两个路径下的头文件内容有细微差别,或者项目期望的是特定版本,混乱和错误就不可避免。

其次,要确认ROS系统里的Sophus版本。运行以下命令:

dpkg -l | grep sophus

或者直接去目录里查看:

cat /opt/ros/melodic/include/sophus/sophus.hpp | grep -i “version”

同时,也查看一下ORB-SLAM3自带的Sophus版本(通常在其Thirdparty/Sophus/sophus目录下的CMakeLists.txt或源码文件中会有版本信息)。对比这两个版本,如果它们一个是较新的模板库风格(例如基于Eigen的Sophus::SE3d),另一个是旧的宏定义风格,那么混合使用必然会导致大量的类型不匹配和“not declared”错误。

最后,也是最关键的一步,分析ORB-SLAM3的源代码,看它是如何引用Sophus的。用文本编辑器或grep命令在全项目范围内搜索:

grep -r “Thirdparty/Sophus” /path/to/ORB_SLAM3/src/

你会发现,原始的ORB-SLAM3代码中,大量使用了相对路径的方式来包含Sophus,例如:

#include “Thirdparty/Sophus/sophus/geometry.hpp”
#include “Thirdparty/Sophus/sophus/so3.hpp”

这种写法本意是确保使用项目自带的、经过测试的第三方库,避免因系统环境不同而导致的兼容性问题。这是一个好习惯。但在我们已经安装了ROS版Sophus的环境中,这种明确的相对路径,加上编译器搜索路径里存在的系统路径,共同构成了重复定义的直接原因。编译器在处理#include “Thirdparty/Sophus/sophus/geometry.hpp”时,确实只找到了项目本地的那一份。但是,这个本地头文件内部,可能又通过#include <sophus/…>的方式引用了其他文件,这时编译器就会去系统路径找,从而可能将两个不同来源的头文件混合在一起编译,引发难以预料的冲突。

3. 解决方案一:统一引用路径,拥抱系统库

经过上面的排查,解决方案已经比较清晰了。我们的目标是让编译器只从一个地方获取Sophus库。这里有两个主流思路,我们先说第一个:修改ORB-SLAM3的代码,让其直接使用ROS系统自带的Sophus库

这个方法的前提是,你确认ROS自带的Sophus库版本与ORB-SLAM3兼容。对于ORB-SLAM3来说,它通常需要较新版本的、支持模板的Sophus(如Sophus libeigen3-dev版本)。ROS Melodic默认安装的Sophus版本可能稍旧,但经过我的实测,大多数情况下是兼容的。这样做的好处是干净利落,减少了项目本身的依赖复杂度,也便于后续通过apt统一管理更新。

操作步骤如下:

  1. 备份原始代码:这是一个好习惯,在ORB_SLAM3目录外复制一份整个项目,或者至少用git提交当前状态。
  2. 全局修改包含语句:使用你喜欢的代码编辑器或命令行工具,将源代码中所有指向本地Sophus的包含语句替换为标准的系统包含语句。 例如,将所有的:
    #include “Thirdparty/Sophus/sophus/geometry.hpp”
    
    批量替换为:
    #include <sophus/geometry.hpp>
    
    注意,引号””变成了尖括号<>。在C++中,#include “”通常用于包含项目相对路径的头文件,而#include <>用于包含系统或编译器标准路径下的头文件。这个改动明确告诉编译器:“请从系统路径里找Sophus”。
  3. 修改CMakeLists.txt:接下来需要修改项目的构建配置。打开ORB_SLAM3/CMakeLists.txt文件。
    • 找到添加本地Sophus包含目录和链接库的地方。通常会有类似这样的语句:
      include_directories(${PROJECT_SOURCE_DIR}/Thirdparty/Sophus)
      # 或者
      add_subdirectory(${PROJECT_SOURCE_DIR}/Thirdparty/Sophus)
      
    • 将这些语句注释掉删除。因为我们不再使用本地的Sophus,所以不需要将它纳入编译过程。
    • 确保CMake能找到系统的Sophus。通常,只要你的系统已经通过apt安装了ros-melodic-sophuslibsophus-dev,CMake的find_package()就能自动定位。但ORB-SLAM3的CMakeLists可能没有显式查找Sophus。为了更稳健,你可以在CMakeLists.txt中适当位置(比如在查找Eigen3之后)添加:
      find_package(Sophus REQUIRED)
      include_directories(${Sophus_INCLUDE_DIRS})
      # 如果后续链接需要,也可以添加:
      # target_link_libraries(ORB_SLAM3 ${Sophus_LIBRARIES})
      
      不过,对于头文件库(Header-only Library)性质的Sophus,通常只需要包含路径即可。
  4. 清理并重新编译
    cd ORB_SLAM3
    rm -rf build/  # 彻底清理旧的构建
    mkdir build && cd build
    cmake .. -DCMAKE_BUILD_TYPE=Release
    make -j4  # 使用4个线程并行编译,数字可按CPU核心数调整
    

如果一切顺利,编译应该会成功通过。这个方案的优点是“治本”,从根本上消除了两个Sophus实例并存的可能性。但潜在风险是,如果未来ROS系统更新了Sophus库,且新版本与ORB-SLAM3不兼容,可能会导致你的项目突然无法编译。不过在实际的机器人开发中,系统库的版本通常是相对稳定的。

4. 解决方案二:屏蔽系统干扰,坚持使用本地库

如果你希望ORB-SLAM3的编译完全独立于系统环境,确保在任何机器上都能获得完全一致的行为,那么第二个方案可能更适合你:强化项目本地Sophus的使用,并屏蔽系统Sophus的干扰

这个思路的核心是,告诉编译器:“我只要用我自己家里的这个Sophus,外面的那个你看都不要看”。这需要对编译器的搜索路径进行更精细的控制。

操作步骤如下:

  1. 优先保证本地Sophus的完整性:确保Thirdparty/Sophus/目录下的代码是最新且完整的。可以通过git submodule更新或重新克隆。
  2. 修改CMakeLists.txt,调整包含路径顺序:在CMakeLists.txt中,当我们添加包含目录时,顺序很重要。编译器会按照你添加的顺序搜索。我们需要把本地Sophus的路径放在最前面,并且尽量避免让系统路径干扰。
    • 找到添加包含目录的部分,确保本地Sophus路径被优先添加:
      # 先添加本地第三方库路径
      include_directories(
          ${PROJECT_SOURCE_DIR}
          ${PROJECT_SOURCE_DIR}/include
          ${PROJECT_SOURCE_DIR}/Thirdparty/Sophus  # 本地Sophus放在关键位置
          ${PROJECT_SOURCE_DIR}/Thirdparty/DBoW2
          ${PROJECT_SOURCE_DIR}/Thirdparty/g2o
      )
      # 然后再添加系统依赖,如Eigen、Pangolin等
      include_directories(${EIGEN3_INCLUDE_DIR})
      include_directories(${Pangolin_INCLUDE_DIRS})
      
    • 一个更彻底的方法是,在编译ORB-SLAM3时,临时移除系统Sophus的路径。但这通常比较麻烦,因为ROS环境变量设置得很深。一个取巧的办法是,在CMakeLists.txt中,在添加了所有必要的包含目录后,不主动去查找系统的Sophus包(即不使用find_package(Sophus))。这样,编译器在搜索#include <sophus/…>时,如果先在本地路径找到了,就不会继续去系统路径搜索。但前提是源代码中的包含语句要配合。
  3. 保持源代码中的相对路径引用:既然决定使用本地库,那么源代码中的包含语句就不需要修改,继续保持:
    #include “Thirdparty/Sophus/sophus/geometry.hpp”
    
    这种写法强制编译器在项目目录下寻找,是最直接的隔离方式。
  4. 处理潜在的内部包含冲突:难点在于,本地Sophus的头文件内部,有时也会使用#include <sophus/…>这样的写法。当编译器处理这些内部包含时,它可能又会跳转到系统路径。为了解决这个问题,你可能需要轻微地修改本地Sophus库的头文件。例如,在Thirdparty/Sophus/sophus/sophus.hpp文件的开头,将其内部包含的其他Sophus头文件的路径也改为相对路径。但这需要谨慎操作,并且可能会使本地库的维护变得复杂。
  5. 使用编译器标志进行隔离:一个更高级但更干净的方法是使用编译器的-isystem-I标志。在CMake中,你可以通过include_directories(SYSTEM …)来添加系统目录,这些目录会被视为“系统目录”,编译器对其中的头文件警告会更少,但更重要的是,在搜索顺序上,-I(通过include_directories添加)指定的目录通常比-isystem目录有更高的优先级?这里需要澄清:实际上,对于#include “file”#include <file>,搜索顺序规则不同,且编译器实现有差异。最保险的做法是,在CMake中只通过-I添加本地Sophus路径,并确保不通过任何方式添加系统Sophus路径。对于Eigen、Pangolin等其他系统依赖,则可以使用find_package并让CMake自动管理。

经过这样一番设置后,重新执行清理和编译步骤。这个方案相当于为ORB-SLAM3项目建立了一个“沙箱”,使其依赖与环境隔离,复现性极高。缺点是配置稍显复杂,且如果本地Sophus版本过旧,可能需要手动更新。

5. 避坑指南与进阶思考

解决了眼前的编译错误,我们不妨再深入一点,思考如何从根本上避免这类问题,以及遇到其他类似依赖冲突时该怎么办。

首先,理解依赖管理的最佳实践。 在现代C++项目中,依赖管理是个大学问。像ORB-SLAM3这样直接携带第三方库源码(Thirdparty)的方式,在科研和快速原型开发中很常见,优点是开箱即用。但对于需要长期维护、部署在多台设备上的项目,更推荐使用包管理器(如ROS的apt、C++的vcpkgconan)来统一管理依赖。这样,所有机器上的库版本都是一致的,从根本上杜绝了“我电脑上能跑,你机器上就报错”的尴尬。如果你在团队中开发,强烈建议将环境依赖(如ROS包、系统库版本)用Docker容器封装起来,实现开发环境的绝对统一。

其次,掌握CMake的查找策略。 CMake的find_package()命令非常强大,它遵循一套复杂的规则来查找库。你可以通过设置CMAKE_PREFIX_PATHSophus_DIR这样的CMake变量或环境变量,来精确指导CMake去你希望的地方找库。例如,如果你希望强制使用本地Thirdparty中的Sophus,可以在执行cmake时这样做:

cmake .. -DCMAKE_PREFIX_PATH=”/home/yourname/ORB_SLAM3/Thirdparty/Sophus” -DCMAKE_MODULE_PATH=”/home/yourname/ORB_SLAM3/Thirdparty/Sophus/cmake_modules”

这明确告诉CMake:“请优先在这个路径下寻找包”。这比在代码中硬编码路径要灵活和规范得多。

再者,学会阅读和分析编译错误。 这次遇到的“redefinition”和“not declared”错误,是C++编译中的经典问题。面对它们,一个高效的排查流程是:

  1. 看完整信息:不要只看最后一行错误,往上翻,找到第一个报错的地方,那往往是根源。
  2. 定位重复定义:对于“redefinition”,错误信息通常会给出重复定义的符号名和两个定义所在的位置(文件名和行号)。对比这两个位置,就能知道冲突双方是谁。
  3. 检查包含守卫:C++头文件应该使用#pragma once#ifndef … #define … #endif来防止被多次包含。检查出错的头文件是否缺少这些守卫。
  4. 使用预处理工具:对于复杂的包含关系,可以使用编译器的-E选项进行预处理,然后查看展开后的代码,确认宏定义和类型定义到底来自哪里。例如:g++ -E -I… problem.cpp -o problem.i,然后查看problem.i文件。

最后,关于ORB-SLAM3与Sophus的版本搭配。 虽然我们通过路径调整解决了编译问题,但还要留意运行时是否会有隐晦的bug。Sophus库从早期到现代版本,其核心类(如SO3SE3)的接口和内部实现有过变化。例如,有些旧代码使用Sophus::SE3,而新版本推荐使用Sophus::SE3d(模板化的双精度版本)。如果ORB-SLAM3的代码是按照较新版本的Sophus编写的,而你链接到了一个旧版本的系统库,即使编译通过,也可能在运行时因为ABI(应用二进制接口)不兼容而导致崩溃或计算结果错误。因此,在解决编译问题后,运行简单的数据集(如KITTI、EuRoC)进行测试,验证SLAM系统功能是否正常,是非常必要的一步。

编译问题就像程序世界的谜题,每一次解决都是一次对系统底层理解加深的过程。这次与Sophus库的“较量”,让我又一次体会到环境隔离和依赖清晰的重要性。在机器人开发这条路上,配好一个稳定、可复现的编译环境,往往比写出酷炫的算法代码更能提升整体的工作效率和团队协作的顺畅度。

更多推荐