ORB-SLAM2语义标注实战:从编译到多视图投票的完整记录
跑通ORB-SLAM2并加上语义标注labelling这件事听起来就是“编译、跑个demo、拷个点云”三步但真正动手做的时候才发现每一步都可能卡你两三天。我这次从零开始把ORB-SLAM2完整编译、运行、改造并在地图点上成功附加语义标签前前后后折腾了将近两周踩了一堆坑也整理出了一套相对稳定的流程。这篇文章就是完整的实操记录包括环境安装、代码改造思路、运行参数设置、常见问题排查适合准备做语义SLAM方向的研究生以及想给传统特征点SLAM加语义信息的工程师参考帮你绕开那些文档里根本不会写的坑。1. 项目整体设计与思路拆解1.1 ORB-SLAM2主框架回顾ORB-SLAM2是视觉SLAM领域一个典型得不能再典型的开源方案它用ORB特征点来做前端跟踪和后端优化整套代码分为三个并行线程Tracking线程负责逐帧估计相机位姿LocalMapping线程负责维护局部地图、筛选关键帧、做局部BABundle 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_TYPERelease make -j4 cd ../../g2o mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j4这里的-j4可以根据CPU核心数调整但不要盲目开高并发否则内存容易吃满我一开始用-j8直接导致编译中途崩溃后来改成-j4就稳定了。第三步编译主项目同样是构建一个单独的build目录cd ORB_SLAM2 mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease 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 3error: ‘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就是这样前期的坑多得让人想放弃但只要把流程理顺后面复用这套管线做其他实验就会越来越顺手。希望这份记录对你有用。