ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

VINS-Mono地图保存与重载:从位姿图到evo精度评估全流程

VINS-Mono地图保存与重载:从位姿图到evo精度评估全流程 vins-mono 跑完一遍手里通常只有几个 CSV 位姿文件仓库里那个/pose_graph/save_maptopic 看着挺诱人但真要把地图存下来、下次重载接着用、再拿 evo 工具给精度打分中间的问题是一串接一串。这篇是我把整条链路完整走通之后的记录从触发保存到落盘文件结构从重载后日志怎么读到用 evo 算 APE/RPE 并对比闭环效果。适合已经能跑通 vins-mono、但还没搞清楚“地图”到底存了什么以及如何客观评估定位效果的同学。1. 先把“地图”这两个字在VINS-Mono里掰开揉碎1.1 VINS-Mono的“地图”不是点云而是位姿图加描述子库很多刚从 ORB-SLAM 或者 Cartographer 转过来的人第一反应是“保存地图不就是保存点云吗”。这个理解放在 VINS-Mono 上会直接跑偏。VINS-Mono 核心是一个单目视觉惯性紧耦合系统它通过滑窗优化估计的是当前机体IMU 坐标系的位姿维护的是滑动窗口内的关键帧和路标点而不是一份供导航或可视化用的稠密点云地图。代码仓库里的 PoseGraph 模块维护的所谓“地图”实际由两部分构成一部分是关键帧的位姿节点和相邻帧约束、回环约束组成的位姿图另一部分是这些关键帧上提取的 BRIEF 描述子以及对应的 DBoW2 词袋数据库。回环检测靠的就是“当前帧 BRIEF 描述子”在数据库里检索历史关键帧找到匹配后再在位姿图里加一条回环边做 4 自由度优化。所以VINS-Mono 保存地图等价于保存关键帧索引和位姿每个关键帧的 BRIEF 描述子用于回环检索的 DBoW2 数据库已经建立好的位姿图节点和边。它没有保存任何可用于渲染和导航的稠密地图也不像 FAST-LIO 那样直接导出 PCD 点云。搞清楚这一点之后后面所有操作都不会再被“地图”这个词带偏。1.2 保存和重载地图到底解决什么问题在单次跑通的基础上保存地图这件事的价值主要体现在三个场景里。第一个是大型场景分段建图。真机跑 vins-mono不可能像仿真一样一口气把整栋楼飞完。往往要先飞第一段回到起点附近换电池再飞第二段。如果每次都从零开始初始化累积漂移会越来越大但如果把第一段保存下来的位姿图重载第二段跑到重复区域时回环检测能直接把当前轨迹拉回历史轨迹避免误差滚雪球。第二个是算法参数对比实验。调特征点数量、IMU 权重、回环阈值这些参数时如果场景和轨迹每次都不同对比结果没有说服力。把地图固定下来只改前端或后端参数重载同一份地图才能在相同基准下比较效果。第三个是精度回测。保存地图时vins-mono 同时会输出vins_result_loop.csv和vins_result_no_loop.csv两条轨迹分别对应闭环开启前后的里程计结果。这两份文件配合 evo 工具能直观量化“回环到底把精度提升到了什么水平”。其实从工程交付角度看能保存、能重载、能量化才意味着这套系统不是只能在 Rviz 里看个热闹而是真正可以复用和验收的。2. 保存地图一条rostopic命令到磁盘文件的完整链路2.1 保存入口/pose_graph/save_mapVINS-Mono 官方把保存地图做成了一个 ROS topic而不是启动参数。在pose_graph_node.cpp里有一个订阅ros::Subscriber sub_save_map n.subscribe(/pose_graph/save_map, 1, save_map_callback);回调里做的事情也很直接调用posegraph.savePoseGraph()把当前内存里的关键帧、描述子、位姿图全部写盘。所以触发保存的命令只有一行rostopic pub /pose_graph/save_map std_msgs/Bool data: true --once用--once是为了避免按了 Tab 自动补全后命令变成一个持续发布的循环。否则你可能每隔几秒就触发一次保存磁盘里会多出一堆时间戳混乱的半成品文件。之所以设计成 topic 而不是配置项我的理解是它把“保存地图”变成了一种运行期可控行为。数据采集跑到一半或者回环优化刚结束外部脚本可以随时发一条消息触发保存不需要重启节点这在真机外场调试时非常有用。2.2 落盘文件到底长什么样默认保存路径是~/.ros/pose_graph/。如果你在 config 文件里改过output_path那保存位置也会跟着变。执行保存后这个目录下会多出几个文件文件内容作用keyframe.json关键帧索引、时间戳、位姿四元数等元信息重载时先读它知道有多少关键帧brief_kf.dat所有关键帧的 BRIEF 描述子重载后重建关键帧检索结构brief_loop.dat参与回环的关键帧描述子回环匹配时使用brief_db.datDBoW2 词袋数据库文件当前帧快速检索历史帧的核心pose_graph.txt位姿图节点、边、回环信息重载后恢复图优化结构这里有个很容易忽略的细节为什么描述子要分成多个.dat文件存而不是直接一个文件装完因为回环检索时系统需要区分“普通关键帧”和“已经产生回环约束的关键帧”。重载时如果混在一起会对所有历史帧做一遍无差别检索在线维护成本会明显增加。分开保存后加载逻辑可以按需重建这也是 VINS 系工程里比较典型的空间换时间做法。保存完成后你还可以用文件大小初步判断结果是否合理ls -lh ~/.ros/pose_graph/如果brief_db.dat只有几百字节说明关键帧数量少得可怜或者你触发保存时系统几乎没怎么运动。这种情况重载地图的意义不大。2.3 保存前要注意的三件事第一回环优化生效之后再保存。如果保存时系统还没建立任何回环那保存下来的位姿图基本就是一段裸的里程计轨迹重载后价值很低。建议先跑完一个包含回环的完整序列再触发保存。第二确认路径可写。savePoseGraph这个函数在写文件失败时往往不会有特别显眼的报错最多在终端里打一行路径信息。如果pose_graph目录不存在或者权限不对你以为保存成功了实际盘里什么都没有。最简单的方法是在触发保存前后各执行一次ls -lh ~/.ros/pose_graph/对比文件变化。第三保存地图和输出轨迹 CSV 是两件事。很多人以为保存地图后vins_result_loop.csv这些轨迹文件也会跟着生成。实际上轨迹 CSV 是 estimator 节点在运行过程中持续写的和 PoseGraph 节点的保存动作没有直接关系。两者是独立产物评估精度时用的是 CSV重载复用用的是 pose_graph 目录。3. 重载地图从启动日志到回环验证的排查路径3.1 启动时到底怎么加载的VINS-Mono 的重载逻辑写在 PoseGraph 构造函数里。节点启动后会去pose_graph_load_path指定的路径下找pose_graph目录如果有就依次读取前面的几个文件如果没有就从空图开始跑。实际启动时你会在终端看到类似这样的日志load pose graph from /home/xxx/.ros/pose_graph/ load keyframe: 342这里的关键信息是load keyframe: 342。如果数字是 0或者日志里提示找不到路径那就说明重载没有生效后面跑的还是全新的一张图。还有一种情况很容易误导人加载动作本身成功了但加载进去的关键帧太少后续回环匹配几乎不可能命中。比如你保存时只飞了几十米关键帧可能就一两百个重载到新场景里当前帧的描述子很难和这些旧关键帧对上。这不是代码问题而是数据覆盖度不够。3.2 重载地图不等于重定位这是我在踩坑之后才彻底想明白的一件事。重载地图后系统并不会直接告诉你“你现在在地图里的哪个位置”。VINS-Mono 的重载机制只是把历史关键帧放回了回环检索库当相机重新看到和旧地图重叠的区域时当前帧会检索到历史关键帧建立回环边再把当前轨迹拉回去。换句话说重载提供的是“再相遇时的约束”而不是“启动瞬间的定位”。所以在验证重载是否成功时不要指望一启动 Rviz 里就会出现当前位姿和旧地图的完美叠加。正确做法是重新走一遍曾经走过的路线观察是否出现回环边。如果走到了重复区域Rviz 里能看到当前帧和历史关键帧之间的连线或者位姿图节点明显多出一批旧的节点这才是重载真正起到作用的时刻。3.3 重载成功与否的三个验证手段我常用的验证手段有三个按简单到复杂排序看启动日志里的关键帧数量和保存时是否一致。跑一遍重叠路线看 Rviz 里是否出现回环边或历史关键帧的连线。对比重载前后两条轨迹的漂移趋势。如果重载后跑同一段路线终点漂移明显更小说明历史地图参与约束后起了作用。第三个手段其实已经是定量验证了需要配合 evo 工具才能把“漂移更小”变成具体数字。这正好引出了这篇的另一个重点如何在跑完 vins-mono 之后用 evo 把精度测明白。4. EVO准备把vins_result轨迹喂给评测工具4.1 EVO是什么怎么装EVO 是 SLAM 领域非常常用的轨迹评估工具不是某个算法而是一个 Python 写的评测框架。它支持 TUM、EuRoC、KITTI 等常见数据格式核心功能包括轨迹对齐、尺度修正、AP E 和 RPE 计算以及各种可视化。安装非常直接pip install evo --upgrade --no-binary evo如果系统里同时存在 ROS 的 Python 环境建议加--user装到当前用户目录避免污染系统环境python3 -m pip install evo --user装完验证一下evo pkg能打印出版本号就算成功。这里多说一句很多人会在同时装了很多 Python 包的环境里遇到evo命令找不到的情况。多半是 pip 装的位置和 PATH 不一致。查看which evo的输出如果指向了一个不存在的路径用python3 -m evo的方式调用往往能绕过去。4.2 把 VINS 的 CSV 转成 TUM 格式vins-mono 的轨迹输出在~/.ros/目录下文件名是vins_result_loop.csv和vins_result_no_loop.csv。列顺序是时间戳, x, y, z, qx, qy, qz, qwTUM 格式的顺序刚好也是时间戳 x y z qx qy qz qw所以转换非常无脑一个 awk 就够了awk -F, NR1{print $1,$2,$3,$4,$5,$6,$7,$8} ~/.ros/vins_result_loop.csv vins_loop.tum-F,表示按逗号切分NR1跳过第一行表头。转换完看一眼文件头head -n 5 vins_loop.tum如果前几行是完整的数字串没有表头和注释就可以直接进 evo 了。你可能也会看到有人用evo_traj csv直接读 VINS 原始 CSV。这个方法有时候能成但前提是 evo 的 CSV 解析器能跳过 VINS 那种带#注释和空格的表头。为了排除版本差异带来的不确定性我更推荐先转成干净 TUM 再做后续操作这也是离线批量测试时可控性最高的做法。4.3 真值轨迹也要先统一格式EVO 的评估必须有一个参考轨迹也就是真值。用 EuRoC 数据集时groundtruth.csv本身就是 TUM 格式可以直接用。真机数据就麻烦一些动作捕捉系统导出的轨迹往往带有很多额外列或者四元数顺序不同需要先洗成标准的t x y z qx qy qz qw。这一步看起来简单但很容易埋雷。我之前整理真机数据时动捕导出的四元数顺序是w x y z直接拿给 evo 用算出来的 APE 大得离谱但轨迹图画出来又是重合的折腾了半天才发现是四元数顺序问题。建议转完格式后先用evo_traj跑一遍可视化确认轨迹形状和真值基本一致再进入正式误差计算。5. 用EVO量化闭环与漂移APE和RPE怎么选、怎么看5.1 先看轨迹重合程度正式计算误差之前我习惯先看两条轨迹在空间上是否重合。这个检查能暴露很多格式和坐标系问题evo_traj tum vins_loop.tum --ref GT.tum -a -s --plot_modexyz这里两个参数很关键-a用 SE(3) Umeyama 算法对两条轨迹做刚体变换对齐把坐标系差异消掉。-s估计尺度并修正。对单目 vins-mono 来说-s几乎是必须的。因为单目视觉惯性系统恢复的轨迹存在尺度不确定性即便加了 IMU尺度标定也不会绝对准确。不做尺度对齐直接算误差结果里会混入一大块“尺度误差”掩盖了算法本身在轨迹形态上的表现。这一步输出的图里能直观看到 vins 轨迹和真值是否贴合。如果两条轨迹整体形状一样但有个固定旋转偏差说明坐标系没对齐如果形状都对不上那后面 APE/RPE 算出来的数字没有任何参考价值。5.2 APE衡量全局绝对误差APE全称 Absolute Pose Error衡量的是每个时间戳上估计位姿和真实位姿之间的绝对偏差。它最能反映全局一致性也就是“整条轨迹离真值到底漂了多少”。命令evo_ape tum vins_loop.tum GT.tum -v -a --plot --plot_mode xyz运行结束后终端会打印一组统计量指标含义max最大误差mean平均误差median中位数误差rmse均方根误差std标准差sse误差平方和日常对比中我主要看rmse和max。rmse对大的离群误差更敏感能反映整体定位稳定性max则代表最坏情况无人机避障、路径规划这类场景非常看重这个数字。如果只想要最终数字不想看一堆中间输出可以去掉-v输出会更干净。5.3 RPE衡量局部相对误差RPERelative Pose Error衡量的是沿轨迹每隔一定距离或时间相邻位姿之间的相对误差。它更关注局部平滑性对里程计层面的飘移更敏感。命令示例evo_rpe tum vins_loop.tum GT.tum -r trans_part --delta 1 --delta_unit m -v -a --plot --plot_mode xyz-r trans_part表示只计算平移部分误差--delta 1 --delta_unit m表示每隔 1 米取一个相对位姿来比较。你也可以把--delta_unit m改成--delta_unit s按时间间隔计算。在实际项目里APE 和 RPE 要配合着看。APE 好了但 RPE 很差说明轨迹整体被回环或后处理拉回来了但局部运动估计仍然不平滑反过来 RPE 好但 APE 差说明里程计局部稳定但缺少闭环约束长距离整体漂移收不住。5.4 对比闭环前后这一步最有说服力vins-mono 之所以同时输出vins_result_loop.csv和vins_result_no_loop.csv就是为了让你对比闭环开启前后的差异。这是评估回环模块最直接的实验设计。先把两条轨迹都转成 TUMawk -F, NR1{print $1,$2,$3,$4,$5,$6,$7,$8} ~/.ros/vins_result_no_loop.csv vins_noloop.tum然后分别算 APEevo_ape tum vins_noloop.tum GT.tum -v -a --save_results noloop.zip evo_ape tum vins_loop.tum GT.tum -v -a --save_results loop.zip最后把两份结果放到一起比较evo_res noloop.zip loop.zip --plotevo_res会把多份结果并排展示表格里对rmse、mean等逐项对比。如果闭环真的有效loop的 rmse 应该明显低于noloop而且差距越大说明回环对这个场景的修正越显著。在我自己测过的 EuRoC 序列上闭环前后 rmse 能差出 3 到 10 倍不等。但也遇到过一个现象某个场景里回环明明建立了APE 却几乎没变化。后来定位到原因是回环检测虽然触发了但位姿图优化的权重设置或者外点剔除把回环边当成了异常约束相当于回环白建立了。所以“看到回环边”和“精度提升”不能直接画等号最终还是要靠 evo 的数字说话。6. 我踩过的三个隐性坑时间戳、尺度、坐标系6.1 时间戳不同步会让所有指标失真EVO 默认会按照时间戳把两条轨迹配对。但如果 VINS 轨迹和真值轨迹的时间戳起始时间差很多或者频率不一致配对结果就会很差。处理方式是加同步限制evo_ape tum vins_loop.tum GT.tum -v -a --sync --t_max_diff 0.01--sync让 evo 先做时间戳同步--t_max_diff 0.01表示只接受最大 10 毫秒的时间差。这个阈值要按你实际系统的帧率调整VINS 输出频率通常在 10Hz 到 20Hz 之间真值如果是 100Hz 的动捕数据10 毫秒的阈值是合理的。如果时间戳偏移是固定的比如真值从 0 开始而 VINS 从系统启动时间开始--sync不一定能完全解决。更好的做法是在数据采集时用同一个时间源的 topic 记录或者转 TUM 之前先做一次时间对齐。6.2 单目尺度问题不要被 -s 掩盖-s参数在做轨迹对齐时非常方便但它也有副作用它会把尺度误差从误差指标里“藏掉”。如果你用了-s算出来的 APE/RPE 代表的是“尺度对齐后的轨迹形态误差”而不是系统在真实物理尺度下的绝对精度。对于大多数 vins-mono 算法验证工作这没问题。但如果你最终要把它用到真实抓取、无人机降落这类对绝对尺度敏感的任务里一定要单独看尺度漂移。方法是不用-s直接跑一次 APE然后对比两次结果的差异差异越大说明尺度问题越严重。单目里的尺度恢复依赖 IMU 加速度积分激励不充分时尺度会慢慢漂。这也是为什么很多应用到最后都换成了双目 VINS 或者加了额外的测距传感器。6.3 坐标系定义不一致VINS 输出的是 IMU 本体坐标系下的位姿而真值轨迹通常是动捕系统定义的某个刚体坐标系甚至可能是相机坐标系。两者如果不一致即便做了刚体对齐也无法彻底消除误差。我自己遇到的情况是动捕系统贴的 marker 中心点和 IMU 中心点本身就有几厘米的杆臂误差这个误差会在飞机做旋转运动时放大。解决方式是把 marker 的外参标定好或者在评估之前把轨迹都换算到同一个参考点。对于直接用公开数据集的同学这个问题会小很多因为 EuRoC 的 groundtruth 和 VINS 输出已经定义了明确坐标关系。但如果跑的是自采数据这一步不能跳过。最后再分享一条我的个人习惯不要在跑完的第一时间就急着保存地图和测精度。先拿vins_result_loop.csv做一次快速evo_traj可视化确认轨迹整体收敛、没有明显的飞点再触发/pose_graph/save_map保存地图。这样保存下来的位姿图才有复用价值。每次重载地图前也记得把旧pose_graph目录备份一下否则一次失败的加载可能把原来那份好好的地图覆盖掉。这套流程走顺之后vins-mono 就不再是只能看看效果的 demo而是能给出量化指标、能复用地图、能被反复验证的定位方案了。
返回列表