做嵌入式视觉的同学,十有八九都跟 ORB-SLAM2 打过交道。这个框架代码结构清晰、运行稳定,在室内建图、路径规划和视觉定位里几乎是标配方案。可一旦落到 Ubuntu 18.04 上,很多人不是被源码难住,而是被依赖环境折腾到怀疑人生。我在好几台不同配置的机器上装过它,从纯净的 18.04 服务器到预装过 ROS 的台式机,踩过的坑基本都集齐了。这篇就把整个安装、编译、跑数据集的链路完整捋一遍,顺便把嵌入式平台上移植的经验也聊透。准备入坑或者正在卡住的朋友,照着这条路走,能少走很多弯路。
1. Ubuntu 18.04 上装 ORB-SLAM2 的常见翻车点
1.1 依赖版本错位是最大的隐形门槛
Ubuntu 18.04 仓库默认提供的 OpenCV 是 3.2,这个版本本身和 ORB-SLAM2 官方源码的接口基本能对上。理论上直接apt install libopencv-dev就能编译,但实际很多人翻车,往往是因为系统里不止一套 OpenCV:有人之前源码编译过 4.x,有人为了跑其他项目用 pip 装过高版本,还有人装了 ROS,ROS 自带一套 OpenCV 和相关依赖库。结果就是 CMake 在find_package(OpenCV)的时候,匹配到的版本和你以为的版本根本不是同一个。
ORB-SLAM2 的依赖链是 Pangolin(GUI 可视化)、OpenCV(图像处理)、Eigen(线性代数)、DBoW2(词袋回环检测)、g2o(图优化)。DBoW2 和 g2o 在 Thirdparty 目录里随主工程一起编译,省心;真正需要手动操心的是前三个。如果 OpenCV 版本跳到 4.x,源码里大量CV_LOAD_IMAGE_UNCHANGED、CV_BGR2GRAY这类宏会直接报编译错误。所以第一步不是急着编译,而是先搞清楚系统里到底有几套 OpenCV。
我建议的做法是:先跑pkg-config --modversion opencv或者opencv_version确认当前默认版本。再用ldconfig -p | grep opencv看实际链接库路径。如果发现多个版本混在一起,编译时就用-DOpenCV_DIR=...强制指定你要的那套配置路径,别让 CMake 自己猜。这一步花十分钟,后面能省两个小时。
1.2 环境变量对链接过程的干扰
另一个容易被忽视的问题是环境变量。Ubuntu 18.04 默认 g++ 7.5,编译器本身没问题,但如果装过 CUDA、Anaconda 或者 ROS,LD_LIBRARY_PATH里可能会混入其他版本的.so。我遇到过一次很诡异的现象:ORB-SLAM2 编译一次通过,运行时却直接段错误,用 gdb 看调用栈发现链接到了 CUDA 自带目录里的一个老版本 OpenCV,和编译时用的头文件根本不是同一套。
所以安装前把环境变量理一遍是很值的。重点看LD_LIBRARY_PATH,以及CMAKE_PREFIX_PATH。如果里面有可疑路径,可以先临时清掉再编译链接,避免“编译时用 A 头文件、运行时加载 B 库”这种状态。区分依赖版本和依赖路径,是排查这类问题最快的方法。
2. 依赖安装的顺序与版本选择
2.1 Pangolin:版本别乱追,够用就好
Pangolin 负责 ORB-SLAM2 的窗口、3D 显示和交互,是 Viewer 线程的地基。这里我强烈建议选 Pangolin 0.5,不要一上来就追最新版。0.6 之后对 CMake 和 OpenGL 的要求变高,在 18.04 上如果缺libglew-dev,cmake 阶段就会失败。0.5 在 18.04 上非常稳,功能也完全够用。
安装依赖可以一次性装齐:
sudo apt-get install libglew-dev libpython2.7-dev libboost-dev \ libboost-thread-dev libboost-filesystem-dev ffmpeg \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev \ libpng-dev libjpeg-dev libtiff-dev然后下载 Pangolin 0.5 源码,常规编译流程:
git clone --branch v0.5 https://github.com/stevenlovegrove/Pangolin.git cd Pangolin mkdir build && cd build cmake -D CMAKE_BUILD_TYPE=Release .. make -j4 sudo make install这里有个细节:如果板子内存不大,make -j4有可能被 OOM killer 干掉,报错信息里会看到Killed。此时降到make -j2就能继续。我习惯把编译类型固定为 Release,Debug 版本下 CPU 开销大,后面跑 SLAM 的时候跟踪线程容易掉帧。
2.2 OpenCV 与 Eigen:一个决定能不能编过,一个决定会不会崩
OpenCV 的选择有两条路线。第一条:直接用系统自带的 OpenCV 3.2,省事,但后续如果想用更现代的接口或硬件加速,扩展性差一些。第二条:源码编译 OpenCV 3.4.16,这是 3.4 系列的最后一个版本,兼容性和性能都更好,代价是编译时间较长。
源码编译前先装基础依赖:
sudo apt-get install build-essential cmake git pkg-config \ libgtk-3-dev libavcodec-dev libavformat-dev libswscale-dev \ libv4l-dev libxvidcore-dev libx264-dev libjpeg-dev \ libpng-dev libtiff-dev libatlas-base-dev gfortran python3-dev编译 OpenCV 时,我习惯开启 TBB 和 OpenMP,多核 CPU 上特征提取会有可感知的速度提升:
cmake -D CMAKE_BUILD_TYPE=Release \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D WITH_TBB=ON \ -D WITH_OPENMP=ON \ -D OPENCV_GENERATE_PKGCONFIG=ON .. make -j4 sudo make installEigen 直接sudo apt-get install libeigen3-dev就够了,18.04 上版本是 3.3.4,ORB-SLAM2 官方使用的版本范围完全覆盖。唯一要注意的是 Eigen 的头文件默认在/usr/include/eigen3,有些项目找不到,需要在 CMakeLists 里补一句include_directories(/usr/include/eigen3)。虽然 ORB-SLAM2 自带处理,但如果你自己写测试代码,很容易在这个地方卡住。
2.3 Thirdparty 库和 ROS 共存问题
ORB-SLAM2 的Thirdparty/DBoW2和Thirdparty/g2o不用单独下载,build.sh会把它们和主工程一起编译。这两个库相对独立,出问题大多是编译器标准不匹配。18.04 的 g++ 7.5 默认支持 C++14,ORB-SLAM2 源码里有些地方写的是 C++11 风格,都能编过,不用额外加-std选项。
比较麻烦的是 ROS。如果系统里装了 ROS Melodic,它自带一套 OpenCV 3.2 和 Pangolin,版本和系统仓库的几乎一样,但 include 路径和 lib 路径可能被 ROS 的环境变量覆盖。编译 ROS 版本时,要确保source /opt/ros/melodic/setup.bash之后再编译,让 CMake 能找到 ROS 的依赖。编译非 ROS 版本时,反而要小心不要带着 ROS 的环境变量,否则链接阶段可能把 ROS 的库混进来。我一般会把两种版本分开编译,分别用不同的 build 目录,避免 CMake 缓存互相污染。
3. 本体编译与高频报错排查
3.1 编译流程与 CMake 选项
进入 ORB-SLAM2 目录,直接执行:
./build.sh脚本会依次编 Thirdparty、然后编主工程,最终生成Examples/Monocular/mono_tum、mono_kitti、mono_euroc和Examples/Stereo/stereo_kitti、stereo_euroc,以及 RGB-D 目录下的rgbd_tum。整个过程在 i5 四核机器上大约 10 到 15 分钟,主要是 g2o 和 DBoW2 的编译。
如果依赖路径不在标准位置,手动 cmake 时我习惯这样写:
cd ORB_SLAM2 mkdir build && cd build cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DOpenCV_DIR=/usr/local/lib/cmake/opencv4 make -j4-DOpenCV_DIR一定要指到正确的路径。源码编译的 OpenCV 4.x,配置路径通常在/usr/local/lib/cmake/opencv4;apt 安装的 3.2 则在/usr/share/OpenCV(实际可能随发行版而变)。CMake 报Could not find OpenCV的时候,十有八九就是这里没指对。
3.2 三类高频编译错误
第一类,usleep未声明。这在 Ubuntu 18.04 的 g++ 7.5 上很常见,报错长这样:
error: ‘usleep’ was not declared in this scope原因是usleep不是标准 C++ 函数,编译时没有被<unistd.h>暴露。手动在报错文件里加上#include <unistd.h>是最快的修法。如果想彻底一点,把源码里的usleep(...)替换成std::this_thread::sleep_for(std::chrono::microseconds(...)),需要额外 include<thread>和<chrono>,也更符合现代 C++ 习惯。我图省事,前面那个办法就够了。
第二类,OpenCV 宏找不到。如果你系统实际链接的是 OpenCV 4.x,会看到:
error: ‘CV_LOAD_IMAGE_UNCHANGED’ was not declaredOpenCV 4 把这些 C 风格的宏删掉了,改成cv::IMREAD_UNCHANGED。解决办法是全局搜索替换:
sed -i 's/CV_LOAD_IMAGE_UNCHANGED/cv::IMREAD_UNCHANGED/g' src/*.cc include/*.h顺手把CV_BGR2GRAY也改成cv::COLOR_BGR2GRAY。DBoW2 那边可能还会出现CV_8UC1的类型断言之类的问题,这种时候直接看它报错的 detail 是哪个文件,通常都是直接改类型判断。但是说句实话,如果系统里已经有可用的 OpenCV 3.2,没必要升级到 4.x 再来适配,时间成本不划算。
第三类,Eigen 对齐错误。这类错误通常不是编译期,而是运行期才出现,报错里能看到static assertion failed: YOU_MIXED_DIFFERENT_NUMERIC_TYPES或者EIGEN_MUST_ALIGN_TO_16_BYTE。ORB-SLAM2 的 g2o 部分大量使用 Eigen 固定尺寸类型,如果你在 C++11 模式下手动 new 了包含 Eigen 成员的对象,就可能触发对齐问题。解决办法是给相关类加上EIGEN_MAKE_ALIGNED_OPERATOR_NEW宏,或者确保编译时不要用-march=native去强制启用某些不稳定的 SIMD 指令。很多人在嵌入式板上编译爱加-march=native,结果换一台机器跑就崩,就是这个原因。
3.3 运行时段错误的一般排查思路
编译通过只算成功了一半。运行时 segfault 的常见原因有几个。第一个是链接库版本不一致,前面已经说过,用 gdb 看堆栈是最直接的。第二个是词袋文件加载失败,ORB-SLAM2 启动时会加载ORBvoc.txt,如果路径给错或文件损坏,会在初始化时直接退出,表现不一定是段错误,但会卡在启动阶段。第三个是图形环境问题,Pangolin 创建窗口失败,在没有显示器的环境或者 SSH 登录时经常发生,程序会报 X11 相关的错误然后退出。嵌入式场景下,一般会去掉可视化,后面讲到。
报错现象 | 大概率原因 | 处理建议 --------------------------|------------------------|---------------------------- 编译时找不到 OpenCV | OpenCV_DIR 没指对 | 用 -DOpenCV_DIR 指定路径 CV_LOAD_IMAGE_UNCHANGED | 链接到 OpenCV 4.x | 全局替换为 IMREAD_UNCHANGED usleep 未声明 | 缺少 unistd.h | 对应文件补 include 运行时段错误 | 多版本库混用 | 清理 LD_LIBRARY_PATH 启动即退出 | 词袋文件路径错误 | 检查 ORBvoc.txt 相对路径 无显示器环境运行失败 | Pangolin 创建窗口失败 | 修改源码去掉 Viewer4. 用真实数据集验证安装结果
4.1 TUM 数据集:单目和 RGB-D 的验证流程
编译完成第一件事,就是跑一个官方示例验证环境。TUM 数据集是首选,因为它格式规范、场景小、运行时间短,适合快速验证。
先下载rgbd_dataset_freiburg1_room这种规模较小的包。里面包含rgb/和depth/目录,以及rgb.txt、depth.txt、groundtruth.txt几个时间戳文件。ORB-SLAM2 的示例程序需要一个关联文件,把 RGB 图和深度图按时间戳对齐。官方仓库里有associate.py脚本,用法是:
python associate.py rgb.txt depth.txt associations.txt生成associations.txt之后,就可以跑了。单目命令:
./Examples/Monocular/mono_tum Vocabulary/ORBvoc.txt \ Examples/Monocular/TUM1.yaml \ path/to/rgbd_dataset_freiburg1_room程序会打开两个窗口,一个是当前帧的跟踪画面,一个是相机轨迹和地图点的 3D 视图。如果启动后左边窗口能看到被跟踪的彩色点,右边能画出轨迹,就说明这套环境没问题。RGB-D 命令类似,只是多了关联文件路径参数:
./Examples/RGB-D/rgbd_tum Vocabulary/ORBvoc.txt \ Examples/RGB-D/TUM1.yaml \ path/to/rgbd_dataset_freiburg1_room \ path/to/associations.txt这里有个小提醒:如果程序启动后长时间停留在 “Loading Vocabulary” 那一步,不要急着 Ctrl+C。词袋文件加载确实需要十几秒甚至更久,这取决于磁盘性能。我在机械硬盘上见过接近一分钟的,实属正常。
4.2 KITTI 双目和 EuRoC 的额外注意事项
KITTI 数据集的验证更简单,因为它的图像已经按时间戳排好文件名了。下载 odometry 数据集的 sequence 00,目录结构里包含image_0和image_1两个灰度图像序列,以及times.txt。运行命令:
./Examples/Stereo/stereo_kitti Vocabulary/ORBvoc.txt \ Examples/Stereo/KITTI00-02.yaml \ path/to/sequence/00注意 KITTI 的相机内外参已经写死在KITTI00-02.yaml里,不需要你自己标定。运行时如果画面里特征点特别稀疏,检查一下times.txt是否存在,代码依赖这个文件生成时间戳,缺失会导致程序读取图像序列出错。
EuRoC 数据集是无人机数据集,目录里按mav0/cam0/data和mav0/cam0/data.csv组织。data.csv的格式是时间戳和文件名,程序读取这个文件来同步图像。运行单目 EuRoC 的例子:
./Examples/Monocular/mono_euroc Vocabulary/ORBvoc.txt \ Examples/Monocular/EuRoC.yaml \ path/to/mav0注意这里传的路径是包含mav0的父目录,不是cam0目录。很多第一次跑的人在这儿卡住,程序会报找不到data.csv。路径层级差一级就会出问题,也不用慌,看它 README 里的示例就明白了。
4.3 怎么判断运行结果是否正常
很多人看到画面在动就觉得“跑通了”,其实离正常还有距离。我一般看三个指标。
第一个是“当前跟踪点数”。在跟踪窗口左上角,ORB-SLAM2 会显示当前帧提取到的特征点数量,以及跟踪到的地图点数量。正常室内场景一般稳定在 300 到 800 之间,如果这个数字经常掉到 100 以下,说明相机运动太快或者场景纹理太弱,这不是环境问题,是算法本身的限制。
第二个是“轨迹连续性”。3D 视图里,轨迹应该是平滑延伸的,如果出现明显的跳变,说明跟踪丢失后重定位成功,在纯视觉 SLAM 中是正常现象,但频繁发生就说明图像质量不够好,或者配置参数和数据集不匹配。
第三个是“平均耗时”。终端运行日志里会打印每帧处理时间,单目在台式机上通常 20 到 40 毫秒,双目和 RGB-D 会更慢。如果时间超过 100 毫秒,就要考虑是不是 Debug 版本、CPU 被其他程序占满,或者词袋匹配环节出问题了。
5. 嵌入式平台移植要点
标题既然带了嵌入式应用,这部分才是重点。我在某 ARM 开发板上移植过 ORB-SLAM2,整体过程比 x86 上折腾,但只要抓住几个关键点,跑起来没问题。
5.1 编译策略:板端原生编译优先
嵌入式平台上最常见的两个选择:交叉编译或者板端原生编译。我的观点是,如果板子的 CPU 不是特别弱(比如四核 A72 以上),优先在板端直接编译。原因很简单:ORB-SLAM2 依赖的 Pangolin、OpenCV、Eigen 都需要针对目标平台编译,交叉编译意味着你要把整个依赖链条全部交叉编译一遍,任何一个库的 sysroot 不对,后面编译 ORB-SLAM2 都会出莫名奇妙的错误。板端原生编译虽然慢,至少依赖是一致的。
OpenCV 在 ARM 上编译时间很长,一个完整的 3.4.16 源码编译可能两三个小时。如果不想等,可以先看板子的系统仓库里有没有现成的 OpenCV 包。很多嵌入式 Linux 发行版提供。只要版本在 3.x 范围内,直接用系统包可以省很多时间。我上回就是顺手apt install libopencv-dev,版本刚好是 3.2,省了一个下午。
5.2 实时性和内存优化
嵌入式平台资源有限,ORB-SLAM2 默认参数是给桌面 CPU 调的,直接搬到 ARM 上,单目可能只能跑 5 到 10 帧每秒,双目更是吃力。自己实测下来,最先动刀的地方是Examples/Monocular/xxx.yaml文件里的 ORB 提取参数。
默认ORBextractor.nFeatures是 1000,嵌入式上我改成 600,特征提取时间能显著下降。金字塔层数nLevels从 8 降到 6,在分辨率不高的图像上几乎没有精度损失。FAST 角点阈值thFAST从 20 提到 25,能过滤掉更多弱纹理点,减少无效计算。这些参数不是越极端越好,特征太少会导致跟踪容易丢失,所以要边调边跑,找到匹配自己应用场景的平衡点。
另一个容易被忽略的点是local mapping和loop closing线程里的空转。ORB-SLAM2 在嵌入式上即使空跑,也会占用不少 CPU。我把两个线程循环里的usleep时间适当调大,减少空转频率,实测 idle CPU 占用能降下来不少,不影响建图效果。
词袋文件的加载对嵌入式内存影响也很大。ORB-SLAM2 默认的ORBvoc.txt解析起来很慢,内存占用高。实测可以改成加载二进制词袋,加载速度快很多,运行内存也有改善。不过要注意 DBoW2 库对二进制格式的支持取决于版本,如果编译时报函数未定义,说明你用的 DBoW2 版本不支持,这时候就别硬上,回退到文本格式,或者把词袋文件放到更快的存储设备上,比如 tmpfs,加载速度提升也很明显。
5.3 无显示环境怎么处理
嵌入式设备大多数没有显示器,也不一定接键盘鼠标。Pangolin 的 Viewer 线程在这种情况下会尝试创建 X11 窗口,报错退出。最快的处理方式是在System.cc里把初始化 Viewer 的代码注释掉,同时把mpViewer指针置空,并且在跟踪线程里跳过mpViewer->Run()相关的调用。这样 SLAM 主流程完全不受影响,只是看不到可视化界面。
如果你还需要可视化调试,有两个替代方案。第一个是在开发机上跑远程可视化,把板子上的轨迹数据通过自定义协议发到开发机显示,这对嵌入式项目来说是个常用套路。第二个是直接把关键帧和轨迹记录到文件,跑完一轮之后离线用 Pangolin 或者 Python 脚本回放。这两个方案都不需要板子有显示接口,工程上也更干净。
6. 一点个人习惯与后续扩展
折腾完这套安装流程之后,有几个小习惯很值得养成。第一个是记录依赖版本。我在每台机器上装完 ORB-SLAM2,都会把pkg-config --modversion opencv、dpkg -l | grep eigen、Pangolin 版本号写到一个文本文件里。别觉得多余,嵌入式项目往往几个月后再回来维护环境,有了记录能省掉大量重新排查的时间。
第二个是改配置前先备份。ORB-SLAM2 的 yaml 参数文件调整起来很灵活,但如果调了参数之后丢弃了原始配置,后面想比对就很麻烦。我会保留一份官方原版副本,自己改动的版本单独命名。这样做的好处是,遇到精度突然下降时可以快速回退,而不是靠记忆猜之前改了哪儿。
如果后续想在这个框架上继续深入,有几个方向可以考虑。一个是接入 IMU 做视觉惯性融合,可以弥补纯视觉在快速运动和弱纹理场景下的不足;另一个是替换词袋模型,用更新的回环检测方案,提高在大场景下的召回率;还有一个是适配鱼眼相机。原版 ORB-SLAM2 基于针孔模型,鱼眼镜头需要改 Camera 模型和畸变处理,改动量比较大,但很多嵌入式视觉实际场景还真就需要这个。我自己接下来打算先用板子把双目版本跑起来,看看嵌入式上的精度和实时性能不能同时兼顾。这套流程踩过一遍之后,剩下的就是慢慢调参数了。