VINS-Mono跑通一阵子之后,最先被问到的问题通常不是“怎么调精度”,而是:地图能不能保存下来,下次重新开机直接重定位?以及,怎么证明我改出来的轨迹比原版好?前者要用到VINS-Mono的位姿图保存与重载,后者几乎绕不开evo这套轨迹评测工具。这两个功能叠在一起,恰好构成视觉惯性导航开发里最常用的调试闭环。这篇文章不讲花哨的算法推导,直接围绕地图保存、重载和evo评估三个环节,把配置、命令和踩坑点一次说清。如果你是正在做VINS二次开发,或者刚入门SLAM想验证系统精度,可以直接按步骤抄作业。
1. 先认清:VINS-Mono里的“地图”到底是什么
1.1 地图的本质是位姿图,不是导航栅格
许多刚接触SLAM的人,一听到“地图保存”就会下意识想到栅格地图、八叉树地图或者点云地图。这个直觉用在Cartographer或者LOAM系列里是对的,但放到VINS-Mono身上就会跑偏。VINS-Mono的产品定位是视觉惯性里程计加上轻量姿态图,它保存的地图核心是稀疏关键帧的位姿集合,以及这些关键帧之间的共视关系和回环约束。换句话说,保存的是“机器人走过哪些位置、从哪些角度看到过什么特征”这一类拓扑和几何信息,而不是给路径规划用的二维占据栅格。
这个差异很重要。VINS-Mono在重载地图后,做的事情是通过词袋模型(DBoW2)检索当前帧的关键帧候选,然后用PnP求当前位姿,再把当前帧坐标系对齐到全局地图坐标系上。栅格地图做不到这件事,因为栅格丢掉了关键帧的描述子信息;而VINS-Mono的可复用地图恰恰是围绕关键帧组织和索引的。所以你在RVIZ里看到的地图是一串相机位姿和稀疏点,不是一片能直接做避障的栅格,这很正常。
1.2 保存重载解决的真实痛点
在建图场景重复性高的项目里,每次重启都从零初始化是一种浪费。单目VINS初始化需要足够的视差和IMU激励,如果机器人静止不动、或者场景纹理很弱,初始化可能要等很久,甚至在原地打转。保存地图以后,下一次开机只要VIO完成了局部初始化,系统就会尝试去匹配保存的关键帧,一旦匹配成功,位姿就会跳到全局地图里,相当于省掉了从零建立全局约束的过程。
这在实际产品里对应一堆硬需求:AGV回到充电桩要能精确对位、巡检机器人每天走固定路线要能快速恢复、无人机降落后重新起飞不希望重新构建一遍环境模型。即便你的项目不做多机器人协同,单会话内保存重载也能在程序崩溃恢复、长距离闭环优化这些场景里省下大量时间。需要注意,VINS-Mono原版更偏单机单会话,多机共享地图不是它的强项,但单机场景的“回桩”“重启恢复”已经能覆盖不少落地需求。
1.3 地图文件长什么样
VINS-Mono保存地图时,目录下通常会出现一个名为pose_graph_map.pkl的序列化文件。这个文件把关键帧位姿、全局位姿、回环边信息、词袋特征打包在一起。文件里除了回环检测要用的特征描述子外,也会记录关键帧之间的相对变换,所以在代码里可以看到保存函数会遍历优化过的位姿图,把节点和边逐个序列化写入。
除了这个主文件,工作目录下往往还有keyframe_path.txt、vins_result_loop.csv、vins_result_no_loop.csv这类输出。它们不是地图的必要组成部分,但在地图调试时特别好用:vins_result_loop.csv就是带回环优化的轨迹,vins_result_no_loop.csv是纯VIO轨迹,两者对比能看出回环到底修正了多少漂移。后面用evo做精度评估的时候,这两个文件可以直接拿来转TUM格式,完全不需要重新录包。
2. 保存与重载地图的完整配置方案
2.1 关键参数逐个说清
在VINS-Mono里,跟地图保存重载直接相关的配置主要在Estimator的yaml文件里,launch文件只是负责把配置文件路径传进去。通常你会在config/euroc/euroc_config.yaml一类文件里看到这样几行:
pose_graph_save_path: "/home/yourname/catkin_ws/src/VINS-Mono/output/pose_graph/" load_previous_pose_graph: 0pose_graph_save_path是地图文件的保存目录,保存时节点会往这个路径写pose_graph_map.pkl。这个路径必须是已经存在的目录,而且最好以斜杠结尾,否则拼接文件名时容易踩坑。load_previous_pose_graph置0表示不加载旧地图,置1表示启动阶段把上述路径里的地图读进来。
| 参数 | 建图时 | 重载时 | 作用 |
|---|---|---|---|
pose_graph_save_path | 相同 | 相同 | 地图读写路径 |
load_previous_pose_graph | 0 | 1 | 是否加载旧地图 |
vins_path | 工作空间路径 | 工作空间路径 | 资源文件路径基准 |
还有一个经常被忽略的点:launch文件里的vins_path参数。VINS会用它计算模型文件、词典文件等资源的相对路径,如果你改了工作空间目录,vins_path不跟着改,启动时就会报找不到文件,地图自然也就保存不了。所以改完yaml之后,先确认launch文件里的vins_path指向的是当前工作空间,再跑程序。
2.2 建图并保存地图的完整操作
先跑一次完整建图。以一个EuRoC数据集为例,通常分三步:
roslaunch vins_estimator euroc.launch rosbag play MH_01_easy.bag数据集播完后,另开一个终端,向保存话题发一个字符串消息:
rostopic pub /pose_graph/save_pose_graph std_msgs/String --once "data: save"pose_graph节点收到这个消息后就开始序列化位姿图。此时VINS终端通常会打印保存路径,你可以看到类似Save pose graph to ...的输出。保存的时机建议放在数据集播放完毕后,因为此时全局优化已经收敛,回环约束已经充分传递,保存出来的地图质量最高。
如果你的程序改了话题名或者使用了自定义launch,先确认话题是否存在:
rostopic list | grep save_pose_graph没有这个topic时,多半是pose_graph节点没起来,或者launch没有加载相应配置。我遇到过把vins_estimator和pose_graph分开launch的情况,只启动了Estimator,保存话题自然找不到。
2.3 重载地图的参数切换与验证方法
重载地图时,把yaml里的load_previous_pose_graph改成1,其他路径保持不变,重新启动VINS。启动后先别看odometry,先打开RVIZ、订阅/pose_graph和/path两个话题。如果地图加载成功,你会看到保存过的关键帧路径立刻显示在地图坐标系里。这时候再播放当前环境的数据包,VIO初始化完成后,算法会尝试做重定位,一旦匹配成功,odometry会出现从局部坐标系跳变到全局地图坐标系的过程,这是正常现象,不是故障。
重定位成功的关键前提有三个:当前场景和保存地图的场景要有足够的共视区域;当前帧能提取到足量稳定的特征点;IMU参数和保存地图时的标定结果没有大的偏差。如果这三个条件不满足,即使地图加载成功,重定位也会反复失败,轨迹会一直停留在局部坐标系里。判断重定位是否成功,除了看RVIZ里的跳变,还可以看终端日志里有没有出现类似“found match”或“relocalization”的记录,VINS-Mono在这些关键节点都会有调试输出。
2.4 视觉模式与视觉惯性模式的差异
VINS-Mono默认是视觉惯性融合,但也可以退化到纯视觉模式。地图保存重载在两种模式下行为不太一样。纯视觉模式保存的是纯粹的单目位姿图,重载后只需要靠特征匹配恢复位姿;视觉惯性模式则会额外把重力方向和IMU状态纳入全局优化。这意味着同一个场景的地图,最好在同一类型的模式之间复用,否则会出现初始化异常或者重定位阶段位姿跳变剧烈。
实际操作中我还遇到过一种情况:保存地图时机器人在地面运行,IMU的z轴加速度分布很固定;重载时把传感器拿到了手里晃动,IMU激励模式和原来差异巨大。这时候即使地图加载成功,VIO初始化的收敛也会很慢,甚至重定位成功后过几秒又被优化拉回错误位置。所以视觉惯性系统保存地图,尽量保证传感器安装方式、IMU激励水平与建图时一致。
2.5 保存频率与地图更新的策略
地图不是保存一次就一劳永逸。每次建图过程中累积误差不同,保存出的地图质量也有高低。我的习惯是每跑一次完整闭环就保存一份带时间戳的副本:
cp output/pose_graph/pose_graph_map.pkl output/pose_graph/pose_graph_map_$(date +%Y%m%d_%H%M%S).pkl这样出了问题还可以回退到之前的版本。如果你的场景经常变化,比如货架位置调整、家具搬动,建议定期重新建图覆盖旧地图,否则重定位时检索到的关键帧和当前环境差异会越来越大,定位精度肉眼可见地下降。地图维护这件事不复杂,但漏掉它往往会让后续的精度评估数据失真。
3. 用evo工具做轨迹精度测试
3.1 evo到底解决了什么问题
evo是SLAM领域最常用的轨迹评测命令行工具之一,它的核心价值是把“轨迹对比”这件原本很麻烦的事情变成几条命令。它支持TUM、EuRoC、KITTI格式,也能直接读取rosbag,还能做Umeyama轨迹对齐、时间戳匹配、ATE/RPE指标计算和结果绘图。对我来说,它不是锦上添花,而是判断“改完参数到底有没有变好”的唯一客观依据。
evo的命令分成几类:evo_traj用来查看、转换、对齐和绘制多条轨迹;evo_ape计算绝对位姿误差;evo_rpe计算相对位姿误差;evo_res汇总多个结果;evo_zip把多个评估结果打包。日常调试最常用的是前三者。安装方式也不复杂,Python环境下直接:
pip install evo如果遇到numpy版本冲突的老毛病,可以考虑用虚拟环境,或者指定pip install evo --upgrade --no-binary evo,实测能避开不少编译方面的坑。
3.2 把VINS-Mono轨迹导出成TUM格式
evo虽然支持直接读rosbag,但我更推荐先把VINS输出保存成TUM格式,因为TUM格式就是纯文本,方便检查和处理。TUM轨迹的每一行是:
timestamp tx ty tz qx qy qz qw其中四元数顺序是x、y、z、w,这一点特别容易弄反。如果你用rostopic echo直接抄数据,看到的是x y z w;一旦在转换脚本里写错顺序,评估结果就会完全失真。
从bag提取最简单的方式是:
evo_traj bag vins.bag /vins_estimator/odometry --save_as_tum运行之后会在当前目录生成vins_estimator_odometry.tum。如果你手头有VINS输出的vins_result_loop.csv,也可以用awk转。比如EuRoC真值文件groundtruth.csv的列顺序是时间戳、位置xyz、四元数wxyz,要转成TUM的顺序:
awk -F',' 'NR>1 {printf "%.9f %s %s %s %s %s %s %s\n", $1/1e9, $2, $3, $4, $6, $7, $8, $5}' groundtruth.csv > groundtruth.tum转完之后一定要用head命令看一眼时间戳顺序,确认是从小到大递增,并且和真值的时间戳单位一致。EuRoC的CSV时间戳通常是纳秒,VINS输出多半是秒,这个单位坑几乎人人都会踩一次。
3.3 ATE和RPE两条命令吃透
全局精度看ATE,局部稳定性看RPE,两条命令可以固定成模板。ATE命令:
evo_ape tum vins_loop.tum --ref groundtruth.tum -a -p-a是Umeyama相似变换对齐,这一步对单目VINS来说是必须的,因为单目轨迹的尺度是任意确定的,直接跟真值比绝对位置没有意义。-p会弹出误差随时间的曲线图。输出里的rmse、mean、median等指标中,我一般优先看rmse。
RPE命令:
evo_rpe tum vins_loop.tum --ref groundtruth.tum -a --delta 1 -p--delta 1表示每隔1秒取一个增量段来计算相对位姿误差。如果你更关心短时间抖动,可以改用0.5;如果关心建图稳定性,可以用2甚至5。RPE反映了相邻时刻之间的位姿增量误差,它不像ATE那样会被全局漂移主导,因此能暴露控制层面的高频抖动和VIO输出不平滑的问题。
| 指标 | 看什么 | 常见用途 |
|---|---|---|
| ATE | 全局轨迹与真值的差距 | 判断系统整体漂移、闭环效果 |
| RPE | 相邻位姿增量的误差 | 判断局部抖动、输出稳定性 |
3.4 参数调试时如何用evo做批量对比
调参时,一次只改一个变量,然后用同样数据跑多次,把每次结果打包起来对比。操作流程是:
evo_traj tum result_v1.tum result_v2.tum --ref groundtruth.tum -a -p evo_zip results_v1_v2.zip result_v1.zip result_v2.zip evo_res results_v1_v2.zip -p这样能在一张表里看到不同版本轨迹的APE和RPE统计值,省得自己誊数据。尤其在你怀疑某个参数影响很大的时候,这个对比能快速给出结论。要注意的是,时间戳匹配参数在批量对比时也要保持一致,否则不同文件的有效帧数不一样,指标没有可比性。
3.5 一条龙实操示例
把保存重载和evo评估串起来,一套命令大概是这样的:
# 1. 建图并保存 roslaunch vins_estimator euroc.launch rosbag play MH_01_easy.bag rostopic pub /pose_graph/save_pose_graph std_msgs/String --once "data: save" # 2. 修改 yaml,把 load_previous_pose_graph 设为 1,重新启动 # 播放同一段bag或者现场数据,记录 /vins_estimator/odometry rosbag record -O vins_reload.bag /vins_estimator/odometry /vins_estimator/keyframe_pose # 3. 评估 evo_traj bag vins_reload.bag /vins_estimator/odometry --save_as_tum evo_ape tum vins_estimator_odometry.tum --ref groundtruth.tum -a -p这套流程跑完,你能得到两个关键信息:重定位后的轨迹和建图轨迹够不够接近;重定位模式下的空间漂移是否在可接受范围内。如果两个候选版本分别评估,再用evo_res汇总,调参效率会明显提高。
4. 保存重载与evo评估的常见坑
4.1 地图文件生成了,但重载后关键帧数量为零
这种情况九成是路径不一致。保存时pose_graph_save_path指向A目录,重载时配置文件被改成了B目录,地图读不到,关键帧数量自然是0。还有一成是权限问题,尤其输出目录放在/home之外或者系统盘根目录时,程序可能没有写权限但不报错。排查时先确认两件事:yaml里保存路径和加载路径完全一致;启动终端对目标目录有读写权限。其次再看文件大小,正常的pose_graph_map.pkl即使场景很小也有几兆字节,如果只有几百字节,基本就是序列化失败。
4.2 重定位成功但轨迹突然跳变
重定位成功后odometry跳变是正常的,它会从局部坐标系跳到地图坐标系。但如果你发现跳变之后轨迹和地图路径的衔接不自然,比如出现明显断点,那往往是重定位所选的关键帧不准确。这时候把VINS终端的日志打开,搜索relocalization相关输出,看当前帧最终被匹配到的关键帧序号是否合理。另一种可能是全局优化在重定位后立刻运行,把之前VIO的局部轨迹强行拉偏,这种情况如果影响实验数据,可以用只记录重定位后的轨迹来评估,或者给VIO留出足够的收敛时间再开始记录。
4.3 evo报时间戳无法对齐
evo要求估计轨迹和真值轨迹的时间戳有交集,并且时间单位一致。报错的时候先检查是不是纳秒和秒混用了。很多数据集的真值时间戳是纳秒,VINS输出是秒,直接丢给evo会显示找不到匹配。处理办法是统一成秒,或者用evo_traj转换时把时间戳除以1e9:
awk '{printf "%.9f %s %s %s %s %s %s %s\n", $1/1e9, $2, $3, $4, $5, $6, $7, $8}' groundtruth_ns.tum > groundtruth_s.tum如果时间戳本身有微小偏移,可以在evo_ape里加--t_max_diff 0.01,允许最大0.01秒的时间差来匹配。但t_max_diff设得太大也会把不同时刻的位姿强行匹配,反而引入误差,所以建议从0.01开始尝试。
4.4 只评估重定位后的轨迹段
原始bag里可能包含初始化前的废数据,VIO还没收敛,轨迹在地图坐标附近乱飘。如果整段去评估,这部分垃圾数据会把指标拉得很差。这时候可以先在evo的绘图窗口里确认为什么很差,然后用时间戳裁剪出有效区段:
awk '$1 > start_time && $1 < end_time' vins_full.tum > vins_valid.tum裁剪后重新评估,才能看到重定位本身的精度。重要的是,裁剪真值和估计值时要保留同一时间窗口,否则评估结果没有意义。
4.5 坐标系方向不一致导致指标虚高或虚低
evo的-a选项能对齐尺度、旋转和平移,但它无法处理坐标系朝向定义完全不同、比如z轴取反这类情况。VINS-Mono在大多数数据集里世界坐标系的朝向与真值并不完全一致,但-a通常能够纠正。如果评估结果还是差得离谱,可以先用evo_traj画图看两条轨迹朝向是否一致,如果朝向在视觉上就差90度或180度,就先做坐标变换统一朝向,再进入ape/rpe环节。
4.6 地图膨胀与长期运行退化
保存的地图文件会随着关键帧数量增加而变大,但有一个很容易忽视的问题是:频繁保存重载后,地图里会积累大量冗余关键帧,导致启动时加载变慢,匹配耗时增加。VINS-Mono原版没有自动剔除关键帧的机制,所以长周期项目要定期重建地图,别指望一份地图能无限期用下去。地图文件出来后,也可以看一眼时间戳覆盖范围,如果出现了异常的时间空洞,说明建图过程里有丢帧,这种地图重定位时会出现大片盲区。
写在最后的个人体会
这套流程我反复跑过很多次,最深刻的体会是:保存重载和evo评估必须配合着用,不能只做一半。只保存地图不评估,你不知道重载后的定位到底准不准;只评估不保存,你调参优化的对象只是一个不复用的临时轨迹。我习惯在每次大改动之后都跑一遍“保存地图、重载地图、重定位、再用evo对比原轨迹和重定位轨迹”的闭环,这样谁改坏了谁改好了,一眼就能看清楚。
最后再分享一个实用技巧:重载地图后,如果时间充裕,等待VIO初始化完成后先别急着播测试数据,让系统静止几秒钟。这样IMU偏置和重力方向能快速收敛,后续重定位的成功率和稳定度会明显提升。别看这个细节简单,很多重定位失败其实就栽在启动阶段没有给IMU留够收敛时间上。