ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

ORB-SLAM2语义标注实战:从编译到多视图投票的完整记录

ORB-SLAM2语义标注实战:从编译到多视图投票的完整记录

跑通ORB-SLAM2并加上语义标注(labelling)这件事,听起来就是“编译、跑个demo、拷个点云”三步,但真正动手做的时候,才发现每一步都可能卡你两三天。我这次从零开始,把ORB-SLAM2完整编译、运行、改造,并在地图点上成功附加语义标签,前前后后折腾了将近两周,踩了一堆坑,也整理出了一套相对稳定的流程。这篇文章就是完整的实操记录,包括环境安装、代码改造思路、运行参数设置、常见问题排查,适合准备做语义SLAM方向的研究生,以及想给传统特征点SLAM加语义信息的工程师参考,帮你绕开那些文档里根本不会写的坑。

1. 项目整体设计与思路拆解

1.1 ORB-SLAM2主框架回顾

ORB-SLAM2是视觉SLAM领域一个典型得不能再典型的开源方案,它用ORB特征点来做前端跟踪和后端优化,整套代码分为三个并行线程:Tracking线程负责逐帧估计相机位姿;LocalMapping线程负责维护局部地图、筛选关键帧、做局部BA(Bundle Adjustment,光束法平差);LoopClosing线程负责检测回环并做全局优化。这种多线程架构非常经典,也决定了后续做语义标注时只能在哪些环节“插手”。

理解了这个框架,你才能明白labelling要加在哪里。地图点(MapPoint)和关键帧(KeyFrame)是整个系统中最核心的数据结构,地图点承载着空间坐标和描述子,关键帧承载着图像、位姿和观测关系。如果要给地图赋予语义信息,最自然的位置就是把标签附加到地图点上——因为这相当于给点云中的每个3D点打上“这是桌子”“这是椅子”的标签,生成语义地图。这个选择绕开了对SLAM主流程的侵入式改动,也方便后续做语义导航或者物体级地图重建。

1.2 labelling方案的选型思路

给SLAM加语义标签,业界主流做法有两条路线:一条是在线集成,比如在Tracking线程里实时跑一个语义分割网络,每帧都输出像素级别的标签,然后融合到地图点中。另一条是离线标注,先跑一遍SLAM保存关键帧和时间戳,再用分割网络逐帧做语义分割,最后把分割结果反向投影到地图点上。

实时方案听起来更酷,但实际工程难度大得多,需要GPU推理和SLAM线程同步,处理不好就会拖慢Tracking,导致位姿估计漂移。我这次选择的是离线方案,先把ORB-SLAM2跑完拿到关键帧和地图点,再用语义分割网络对每个关键帧做分割,最后把像素标签映射到地图点。这个方案的优点是完全不影响SLAM本身的性能和精度,调试起来也直观,哪个环节出了问题很快就能定位。缺点就是不能在线输出语义地图,但对于验证算法流程和后续研究来说,这个取舍非常划算。

2. 环境准备与依赖安装

2.1 系统与依赖版本的选择

环境配置是整个项目里最容易劝退新手的一步,我的经验是:版本锁死,不要用“最新版”。我这次用的是一套经过多轮验证的稳定组合,基本能避开90%的编译错误。

依赖项版本建议备注
操作系统某主流桌面版Linux发行版不要用太新的内核版本,部分旧版库会出兼容问题
编译器GCC/G++ 7及以下ORB-SLAM2源码较老,新编译器会在C++标准上报警
CMake3.10及以上太旧会找不到一些新库的模块
OpenCV3.x系列4.x也能编译但要把头文件路径和API适配一遍
Eigen3.3.x必须提前安装,g2o强依赖
Pangolin0.6版左右太新版和旧源码不兼容
DBoW2/g2o源码自带编译前确保依赖完整

注意:ORB-SLAM2自带的三方库(DBoW2和g2o)在编译时会自动链接Eigen和OpenCV,所以Eigen必须提前装好,而且路径要让CMake能找到,否则会报“Eigen3 not found”之类的问题。

2.2 编译主程序的完整操作

编译过程大致分三步,我建议严格按照这个顺序来,不走捷径。

第一步,安装基础依赖。Eigen、OpenCV、Pangolin这三样是核心,安装方式在这里就不赘述了,但有一个细节我要反复强调:Pangolin一定要用源码编译安装,不要用发行版自带的旧包,旧包缺头文件会导致ORB-SLAM2编译时找不到pangolin/display/display.h。

第二步,编译ORB-SLAM2的第三方库。进入源码目录下的Thirdparty/DBoW2和Thirdparty/g2o,分别执行:

cd Thirdparty/DBoW2 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4 cd ../../g2o mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4

这里的-j4可以根据CPU核心数调整,但不要盲目开高并发,否则内存容易吃满,我一开始用-j8直接导致编译中途崩溃,后来改成-j4就稳定了。

第三步,编译主项目,同样是构建一个单独的build目录:

cd ORB_SLAM2 mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release make -j4

这一步正常来说会编译四五个可执行文件,包括单目、双目、RGB-D三个接口的例程,以及一些工具程序。如果到了这一步编译通过,恭喜你,ORB-SLAM2主框架已经跑通了,可以进入语义标注的改造阶段。

3. 核心细节解析与实操要点

3.1 语义标签的数据来源

要想给SLAM地图打标签,得先解决一个问题:语义标签从哪来。我用的是深度卷积分割网络对关键帧图像做逐像素分类,这里有一个非常关键的预处理步骤:不能对RGB图像直接分割,必须用原图尺寸或对齐后的图,否则投影坐标就对不上。

具体操作是:先用ORB-SLAM2跑完一次回放,把所有关键帧的索引和时间戳保存下来,然后逐帧加载关键帧图像,调用分割网络推理得到像素标签图,标签图是一个二维矩阵,每个像素位置存储一个类别ID,比如“0-背景、1-桌子、2-椅子、3-显示器、4-键盘”之类。推理过程中建议把分割网络输入分辨率设置成与原始图像接近,因为后续要把3D点投影到这张标签图上取标签,如果分辨率变了,投影坐标要跟着缩放,很容易出细节偏差。

实操心得:分割网络这一次推理很耗时,如果是几百个关键帧,单卡推理可能也要十几分钟。我建议把所有关键帧的标签图提前保存成npy或png格式的掩膜文件,后续调试阶段直接读文件,不用反复重跑推理,省下来的时间够你做三四轮实验了。

3.2 标签到地图点的映射原理

这是整个labelling流程的核心,也是网上资料最少的地方。ORB-SLAM2的地图点坐标是世界坐标系下的3D位置,关键帧有对应的相机位姿和相机内参,因此理论上可以把任何地图点投影到任何关键帧的像素坐标上,然后读取该像素的语义标签。

但问题在于,单个像素的标签并不可靠,尤其是物体边缘和纹理较少的区域,分割网络输出会发生抖动。所以不能只用一个关键帧的标签就定死,要采用多视图投票策略:对同一个地图点,找出所有能观测到它的关键帧,把地图点投影到这些关键帧上,收集每次投影位置的标签,然后统计频次,得票最高的类别作为该点的最终标签。同时还要设置一个“最低有效观测数”阈值,比如至少被3个关键帧观测到并且这3次的标签一致,才把这个标签写进去,否则标记为“未知”。

这一步的物理意义在于:一个稳定的地图点往往被多个关键帧同时观测到,多视图投票能够有效消除单帧分割噪声。实际效果比我预想的好很多,桌腿、椅背这些容易误分类的区域,投票后基本都能归到正确类别。

3.3 代码改造的关键位置

在ORB-SLAM2源码里加语义标签,改动量其实不大,核心就三个地方。

第一,在MapPoint.h中增加一个成员变量,例如int mnSemanticLabel,初始化为-1表示未知,再加一个置信度字段表示投票得分。第二,在Map.h或者单独写一个后处理工具类中,增加一个函数,输入关键帧集合和对应标签图,遍历地图点,执行投影和投票,更新标签字段。第三,在可视化部分修改点的颜色渲染逻辑,让不同标签的地图点显示不同颜色,这里要注意ORB-SLAM2自带的Pangolin显示代码只对点云坐标做了渲染,颜色是统一值,需要把颜色数组也同步更新。

我建议不要在Tracking和LocalMapping线程里直接改这些字段,最好单独开一个后处理程序,跑完主流程后离线加载地图文件和关键帧标签文件,再生成带语义的地图。这样做的好处是整个SLAM线程完全不受污染,出bug也好排查。

4. 实操过程与核心环节实现

4.1 数据集准备和运行前检查

数据集的选用直接决定了你调试的难易程度。我用的是一个公开室内RGB-D数据集,包含RGB图、深度图和关联的时间戳文件。数据集格式和ORB-SLAM2默认支持的TUM格式比较接近,但建议先在官方标准格式上跑通一次,再换自制数据,否则你分不清是SLAM本身的问题还是数据的问题。

开始运行前,要做几件事:第一,修改ORB-SLAM2的配置文件,填入相机内参、深度图缩放系数、RGB和深度的对应关系。第二,确认图像的加载路径和时间戳文件路径正确,很多诡异的问题都是因为时间戳对齐错了,导致Tracking阶段跟踪丢失。第三,如果用的是RGB-D模式,确保深度图和RGB图的尺寸一致,否则ORB-SLAM2在构建点云时会计算出大量非法深度值。

4.2 运行主流程生成关键帧和地图

我这次用的是RGB-D模式,运行命令大致是:

./Examples/RGB-D/rgbd_tum path_to_vocabulary/ORBvoc.txt path_to_settings/settings.yaml path_to_sequence path_to_associations_file

运行过程中,终端会实时输出每一帧的跟踪状态、关键帧数量、局部地图点数量等信息。如果看到“lost”字样频繁出现,优先检查相机参数和时间戳关联文件,而不是怀疑代码。

跑完之后,程序会弹出Pangolin窗口显示地图,这时候不要急着关,按保存按钮或者通过修改代码把地图点云输出成文件。保存格式可以是ls文本格式,包含每个地图点的坐标、RGB颜色、语义标签,后续后处理会用到。

注意:ORB-SLAM2默认在退出时会自动清空所有地图数据,所以在Pangolin窗口里要手动触发保存,或者修改System::Shutdown()函数,在退出前把mpMap里的点云和关键帧信息全部导出,否则跑完直接关窗口,数据就没了,这一条是血泪教训。

4.3 离线语义分割和标签映射

关键帧图像提取出来后,我逐帧用分割网络推理,得到标签掩膜。这里分辨率和原始图像保持一致。然后把地图点逐个投影到关键帧,执行多视图投票。

代码逻辑不复杂,但两个细节决定了最终效果。第一,投影前要把地图点坐标变换到关键帧的相机坐标系,这一步必须用关键帧的GetPose()返回的位姿矩阵,不能直接拿Tracking阶段的中间结果。第二,投影后要进行深度检查,如果地图点和关键帧之间隔着其他物体,单靠像素坐标读取标签会出错,因此要用深度一致性做一个过滤,如果地图点在当前关键帧的深度与像素处深度值差异过大,就跳过这个关键帧的投票。这个细节我参考了一些语义SLAM论文里的做法,实测下来确实能显著提升标签准确率。

4.4 语义地图可视化

最后一步是把带标签的地图点渲染出来,我用Pangolin重写了一个简单的可视化界面,每种语义类别映射为固定颜色:墙面用灰色、地面用蓝色、桌椅用橙色、显示器用绿色,未知类别保持白色。颜色映射表写成一个数组,按类别ID取色。

可视化效果非常直观,整个地图区域被分割成了有语义含义的块状结构,原本杂乱的点云变成了“可读”的场景模型。这一步也验证了整个流程的正确性:如果标签映射代码有bug,可视化时会出现大片点云颜色错乱或者所有点全是白色,可以让调试目标非常明确。

5. 常见问题与排查技巧实录

5.1 编译阶段的经典报错和解决

报错现象产生原因解决方案
fatal error: pangolin/display/display.h: No such file or directoryPangolin版本过旧或安装路径未生效源码编译安装新一代Pangolin,确认/usr/local/include下存在头文件
OpenCV 4.x does not define CV_LOAD_IMAGE_UNCHANGED代码用了OpenCV 3的常量全局替换为cv::IMREAD_UNCHANGED,或者干脆退回OpenCV 3
error: ‘G2O_FUNCTION’ was not declared in this scopeg2o版本过新,API变更使用源码自带的g2o版本,不要自己装别的版本
链接时出现Eigen未定义符号Eigen版本不一致或多次安装了不同版本彻底卸载旧版本,只保留一个稳定版本,并把头文件路径写进CmakeLists.txt

编译问题大多数是版本错乱造成的,解决思路非常朴素:让源码自带的第三方库去匹配它自己舒服的依赖版本,不要“手动升级”,升级一时爽,改代码火葬场。

5.2 运行阶段容易忽视的细节问题

运行阶段最常遇到的问题有三个。

第一个是Tracking线程频繁丢帧,这个几乎都是数据关联的问题。我一开始用自己录的视频,结果发现视频文件本身有掉帧,时间戳不均匀,ORB-SLAM2的跟踪模块对这种输入非常敏感。建议先跑标准数据集的官方格式,能稳定跟跟踪了,再考虑换数据。

第二个是内存消耗异常。ORB-SLAM2在构建地图点时,如果关键帧数量特别多,内存会持续上涨。这其实不是bug,而是局部BA优化时保留了大量关键帧。解决方法是调低关键帧插入阈值,让系统不要那么频繁地插入关键帧,或者在配置文件中提高KeyFrame::nMinObserved之类的阈值。

第三个是语义标签映射出现“斑马纹”,就是同一个物体表面标签反反复复,颜色杂乱。这是因为深度不一致导致投影时读取了错误的像素标签。排查思路是过滤掉深度差异过大的观测,并把多视图投票的最低观测数适当提高,比如从3调到5。每调一次参数,都要重新跑一遍后处理,最好写一个小脚本来自动化这个迭代过程。

5.3 一个必须避开的性能陷阱

语义分割推理本身非常耗时。如果直接在SLAM主流程里面调用分割网络,哪怕用GPU,也会由于显存争抢和同步延迟导致SLAM线程不稳定。我的建议是:用离线方式跑,把分割结果做成文件缓存,这也是为什么我前面反复强调保存关键帧和标签图的原因。离线缓存之后,重复实验的边际成本几乎为零,而且可以随时换不同网络、不同参数重新生成标签,不用动SLAM代码。

另外,我遇到过一个很隐蔽的问题:Pangolin可视化线程占用了太多CPU,导致整个系统变慢。特别是点云数量到几十万级别时,如果不降低渲染刷新率,帧率会掉到惨不忍睹。解决办法是在可视化线程里加一个简单的时间间隔控制,比如每200毫秒更新一次,实测流畅度和CPU占用都有明显改善。

6. 踩坑记录与经验心得

这次完整跑通ORB-SLAM2 with labelling,我自己最大的感受是:真正困难的不是SLAM本身,而是数据流和时间戳对齐这层“表面功夫”。语义标注结果准不准,很大程度上取决于前端SLAM跑得稳不稳,而前端稳不稳,又取决于数据预处理是否细致。很多刚接触的人一上来就急着改代码,结果被各种环境问题劝退,回头才发现是自己连官方demo都没跑通。

如果你想复现这个流程,我建议第一个实验务必用标准数据格式,跑通之后再逐步替换成自己的数据。改造代码时多写注释,每一次参数调整和代码改动都记录下来,方便回溯。

另外一点小技巧:保存地图点坐标时,建议用浮点精度而不是默认的double转字符串,避免格式化导致坐标精度损失。我在做标签映射时曾经因为坐标精度不够,一排地图点投影到关键帧上集体偏移了一两个像素,导致桌面边缘的点全部被误标成椅子,排查了大半天,最后发现是保存文件时用了%f而不是%lf。

最终生成带语义标签的地图那刻,整个场景被不同颜色清晰分割,桌面、椅子、显示器一目了然,那种感觉还挺有成就感的。做语义SLAM就是这样,前期的坑多得让人想放弃,但只要把流程理顺,后面复用这套管线做其他实验就会越来越顺手。希望这份记录对你有用。

RELATED READING

延伸阅读

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