ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

M2DGR多模态SLAM数据集:地面机器人退化场景评测与实战

2026/9/18 11:33:17 拓冰建站 浏览量
M2DGR多模态SLAM数据集:地面机器人退化场景评测与实战 去年年底接了个室内外混合场景的巡检机器人项目客户要求方案在电梯、走廊、地下车库、园区道路这几类环境里都能稳定定位我第一反应是找数据集做离线验证——毕竟真车跑一遍的成本太高出了问题还复现不了。翻了一圈发现自己手里的存货都不够用EuRoC 全是室内小场景没有室外和 GNSSKITTI 是车载视角跑不进电梯TUM VI 只有视觉惯性碰到地下车库这种无纹理又无卫星信号的地方直接歇菜。后来在社区里被人安利了M2DGR一个专门为地面机器人做的多模态 SLAM 数据集一口气把鱼眼相机、红外相机、RGB-D、事件相机、全景相机、16 线激光雷达、IMU 和 RTK 全塞在一台小车上还配了三条不同精度的真值链路。这篇就把我这两年用它做多模态融合、退化场景测试、算法对比评估的经验整个掏出来从传感器配置讲到下载解包、从算法接入讲到踩过的坑SLAM新手和做过几年工程的老手应该都能从里面捞到点东西。1. 先搞清楚这个数据集到底解决什么问题1.1 地面机器人和车载、无人机根本不是一个赛道很多人拿到数据集的第一反应是有没有 KITTI 那么全但地面服务机器人这个品类的约束条件非常特殊。轮式底盘离地高度通常在 20 到 40 厘米之间激光雷达装在这个高度上视野被桌椅腿、花盆、行人下半身切得七零八落有效点云比车载少一大截。更要命的是运动模式车载平台基本可以假设非完整约束、速度连续、不会原地打转而地面机器人要进电梯、要贴着墙走、要在狭窄走廊里原地转向掉头这种纯旋转和零速状态恰恰是视觉 SLAM 最容易漂移的时刻。M2DGR 的采集平台就是按这个逻辑设计的。它没有追求传感器越贵越好而是把地面机器人上真实会装的传感器凑齐了并且专门设计了七类场景去覆盖这些退化情况。我拿它做算法选型的时候最大的感受是在 KITTI 上 ATE 表现差不多的两个方案到了 M2DGR 的电梯序列上能差出一个数量级。原因很简单KITTI 里没有电梯也没有金属壁面 无 GNSS 纯垂直运动这种组合拳。所以判断一个数据集值不值得投入时间我的标准一直是两条场景是否覆盖目标落地环境退化情况是否有真值兜底。M2DGR 在这两点上做得比较扎实这也是我后来把它当成主力验证集的原因。1.2 七个场景的设计意图每个都对应一类失效模式数据集把序列按场景分了七组表面看只是换了个地方录实际上每一组都在针对性地打某一类算法的软肋。我按自己的理解梳理一下这七类场景分别想考什么。电梯场景是最变态的一个。轿厢是金属封闭空间激光雷达打上去会有明显的多路径反射和近距离过饱和点云里经常出现现实中不存在的鬼影视觉侧几乎没有稳定纹理轿厢内壁大面积同色GNSS 完全失效。同时机器人进出电梯时的加减速和垂直方向扰动会让 IMU 的预积分模型吃不消。做过电梯场景的人都知道这里最容易出现的不是慢慢漂而是突然跳变。走廊场景考的是长距离尺度漂移。笔直走廊里前后视角高度相似视觉特征重复度高回环检测容易误匹配到错误位置激光雷达在走廊里退化成一个二维问题沿走廊方向的可观测量基本为零。大厅场景考的是大空间和玻璃幕墙。玻璃对激光雷达来说几乎是透明的会直接穿过去打到后面导致地图上出现不存在的走廊。花园/草地场景考的是非结构化地形。植被是半透性介质激光点云在树冠和草丛上会形成一层模糊的噪声带地面起伏导致轮式里程计的平面假设失效。街道场景考的是动态物体和 GNSS 遮挡。车辆、行人、非机动车全是移动的干扰源同时楼宇遮挡会造成 GNSS 多路径和信号跳变。地下车库场景是室内外过渡的典型。完全没有 GNSS光照极低柱子、车位线、管道大量重复激光雷达的 scan matching 很容易在两根相似的柱子之间跳柱子。旋转场景是专门设计来测纯旋转退化的机器人原地转圈理论上位置不变、姿态连续变化这个过程中视觉的三角化约束几乎没有纯视觉方案的尺度会直接崩掉。挑选序列做实验时建议至少覆盖电梯、地下车库、街道三类分别代表多传感器全失效无 GNSS 低纹理室外动态GNSS 跳变这三个过了再谈泛化。1.3 和主流数据集摆在一起看它的位置在哪里我把常用的几个数据集按地面机器人的视角拉了个对比方便你判断要不要引入 M2DGR。数据集主要平台传感器丰富度室内场景室外 GNSS真值方式适合的验证目标EuRoC微型无人机双目IMU有无动捕/激光跟踪视觉惯性里程计KITTI乘用车双目激光GNSS无有RTK车载激光视觉TUM VI手持/车载双目IMU部分无动捕视觉惯性nuScenes乘用车多相机激光毫米波无有RTK地图自动驾驶感知M2DGR地面机器人多相机激光IMUGNSS有有动捕/全站仪/RTK地面机器人多模态融合与退化测试这张表最值得注意的一点是最后一列的真值方式。M2DGR 不是单一真值源而是按场景条件切换小范围室内用光学动捕大范围室内和部分室外用全站仪跟踪棱镜开阔室外用 RTK。这种分级真值的设计很务实因为没有任何一种设备能在所有场景下同时保证精度和覆盖范围。但它也带来一个副作用——不同序列的真值精度和输出频率不一致评估时必须区别对待这一点后面会专门展开。2. 传感器配置拆解多模态的多到底多在哪2.1 硬件清单与各自的职责边界先把配置摆清楚。根据论文和官方说明采集平台上的传感器大致包括传感器大致规格在这套系统里的主要作用鱼眼相机多路水平视场接近 200 度近距离大范围视觉观测弥补针孔相机视野不足红外相机单目红外暗光环境下的辅助观测车库场景有优势RGB-D 相机中等分辨率深度输出近距离稠密深度用于小范围精细建图事件相机异步事件流高动态、快速运动场景的低延迟观测全景相机360 度成像大范围视觉覆盖与回环候选三维激光雷达16 线机械式水平 360 度主要几何观测源尺度可靠IMUMEMS百赫兹量级输出高频运动先验、预积分、去畸变GNSS 接收机支持 RTK 差分室外全局约束抑制累积漂移这套组合的妙处在于冗余但不重复。鱼眼和全景在功能上有重叠但鱼眼畸变模型固定、更适合做特征跟踪全景更适合做场景级描述子RGB-D 和激光在几何上有重叠但 RGB-D 只在近距离有效正好补上激光在脚边 1 米内的盲区事件相机和红外相机都是为暗光和高速场景准备的备胎。做多模态融合研究的话这套配置能撑起相当多的组合实验比如鱼眼IMU激光IMU激光鱼眼IMU事件IMU等等。我自己的经验是别一上来就想着全模态融合。传感器越多标定误差、时间同步误差、数据量都会成倍放大最后往往是融合了一堆效果还不如激光IMU。正确的做法是先单模态打通再两两融合最后再加第三个模态并且每一级都要用真值量化增益。2.2 时间同步多模态数据集的第一道命门多传感器融合里时间同步的重要性怎么强调都不过分。假设机器人以 1 米/秒前进相机和激光雷达之间有 20 毫秒的相对延迟等效空间错位就是 2 厘米如果是在电梯里突然加速误差还会放大。更麻烦的是这种错误不会表现为明显的抖动而是让整个轨迹缓慢地歪掉你很难从可视化的结果里一眼看出来。M2DGR 在硬件层面做了同步触发相机和激光雷达由统一的触发信号驱动IMU 是自身高频采样并打上统一时间基准。这一点很关键因为纯软件时间戳对齐在多模态系统里经常出问题——ROS 的message_filters只能保证读到的时间戳接近没法保证物理曝光时刻接近而后者才是真正的物理量。实际用的时候我建议做两件事。第一把每个话题的时间戳序列单独导出来画个直方图看间隔是否稳定有没有异常跳变第二把 IMU 的角速度和相机图像序列做一个粗略的相关性检查机器人做匀速转向时图像特征的横向位移速度和 IMU 的角速度应该高度相关如果相关性很差说明同步或者外参有问题。# 快速查看各话题的消息数、频率和时间跨度 rosbag info your_sequence.bag # 单独抽某个话题的时间戳导成文本再画图 rostopic echo -b your_sequence.bag -p /imu/data imu.csv一个容易被忽略的点解包成图片时很多脚本会把bag 记录时间当成曝光时间写进文件名。这两者在硬件触发系统里通常很接近但如果你要做严格的视觉惯性标定还是尽量用原始消息头里的时间戳。2.3 标定文件怎么读外参怎么核对数据集一般会附带标定参数文件通常包含相机内参、畸变系数以及相机到激光雷达、相机到 IMU 的外参。这些东西看着是一堆数字但里面藏着不少坑。先看相机内参。鱼眼镜头必须用专门的畸变模型OpenCV 里对应cv::fisheye那一套等距投影模型四个畸变系数如果你错误地用普通布朗模型的cv::undistort去处理图像边缘会被撕裂得不成样子。判断方式很简单拿一张有明显直线的场景比如走廊踢脚线去畸变后看直线是否依然是直线如果弯了模型就用错了。再看外参。外参的本质是从一个坐标系到另一个坐标系的刚体变换包含平移和旋转。常见的坑有三个一是旋转的表示方式四元数、旋转矩阵、欧拉角没对齐四元数的实部虚部顺序搞反二是坐标系定义方向不一致有的是相机光学系Z 向前、X 向右、Y 向下有的是机器人体系X 向前、Y 向左、Z 向上三是外参的标定时间和数据采集时间差得比较久中间的机械形变没被考虑。我的核对办法比较笨但有效把一个已知位置的标定板或者反光柱分别在点云和图像里手动标注出来然后把点云里的点用外参投影到图像上看是否落在正确位置。误差在几个像素内算正常超过十几个像素基本可以判定外参有问题。# 伪代码把激光点云投影到图像上做外参粗检 import numpy as np def project_lidar_to_image(points_lidar, K, dist, T_cam_lidar): # points_lidar: N x 3 R T_cam_lidar[:3, :3] t T_cam_lidar[:3, 3] pts_cam (R points_lidar.T t.reshape(3, 1)).T # 只保留相机前方的点 mask pts_cam[:, 2] 0.1 pts_cam pts_cam[mask] uv K pts_cam.T uv (uv[:2] / uv[2]).T return uv, mask3. 从下载到解包把数据变成能用的素材3.1 下载策略别一上来就全下这个数据集的完整体积是 TB 量级的全量下载对硬盘和带宽都是折磨。我的建议是按实验目标分批下第一轮先选 3 到 5 个短序列覆盖室内、室外、退化三类把数据管线跑通。这一轮的目标不是出结果而是确认我能读到数据、能解包、能喂给算法、能算出误差。第二轮针对你关心的具体问题补充序列。比如你要做视觉惯性退化检测就把电梯和旋转序列都拉下来要做 GNSS 与激光融合就把街道序列拉齐。第三轮等实验基本成型再考虑全量跑一遍做统计对比。下载过程中常见的两个问题一是断点续传一定要用支持续传的工具不然一个几十 GB 的包下到 90% 断了会非常崩溃二是校验下完对一次哈希别等到解包报错才发现文件损坏。3.2 ROS 环境准备与播放数据基本是 ROS bag 格式所以环境准备主要是装 ROS。ROS1 和 ROS2 的 Python 接口差异比较大如果你只是想读数据做离线处理用rosbags这个库反而更省事它不依赖完整的 ROS 安装能直接读写 bag 里的消息。# 如果走完整 ROS 路线 sudo apt install ros-noetic-desktop-full # 视你的发行版而定 source /opt/ros/noetic/setup.bash # 只做离线解析的话安装 rosbags 就够了 pip install rosbags播放的时候有两个实用技巧。第一个是--clock参数配合use_sim_time让所有节点都使用 bag 内的时间基准避免和系统时间混在一起第二个是降速播放-r 0.5这类参数在处理大点云序列时很有用否则 RViz 会因为来不及渲染而丢帧你看到的画面和实际数据对不上。# 以 0.5 倍速播放并使用 bag 内部时钟 rosbag play -r 0.5 --clock your_sequence.bag # 只看话题列表先确认数据完整性 rosbag info your_sequence.bag | grep topics -A 403.3 把 bag 拆成图片、点云和 IMU 数据大多数开源 SLAM 算法不直接吃 bag而是读图片目录 时间戳文件所以解包这一步跑不掉。这里给一个用rosbags的示例不依赖 ROS 安装。from pathlib import Path from rosbags.rosbag1 import Reader from rosbags.typesys import Stores, get_typestore import cv2 import numpy as np typestore get_typestore(Stores.ROS1_NOETIC) bag_path Path(your_sequence.bag) img_dir Path(images); img_dir.mkdir(exist_okTrue) ts_file open(images/timestamps.txt, w) with Reader(bag_path) as reader: # 先列出所有话题确认相机话题名 for conn in reader.connections: print(conn.topic, conn.msgcount) for conn, timestamp, raw in reader.messages(): if conn.topic ! /camera/fisheye/image_raw: continue msg typestore.deserialize_ros1(raw, conn.msgtype) # 图像数据在 msg.data 里编码在 msg.encoding arr np.frombuffer(msg.data, dtypenp.uint8) h, w msg.height, msg.width img arr.reshape(h, w, -1) if msg.encoding bgr8 else arr.reshape(h, w) name f{timestamp}.png cv2.imwrite(str(img_dir / name), img) ts_file.write(f{timestamp} {name}\n) ts_file.close()IMU 数据建议直接导成文本格式上跟 EuRoC 的data.csv保持一致时间戳 角速度 加速度这样大多数视觉惯性算法都能直接复用现成的读取代码。# IMU 导出输出格式timestamp wx wy wz ax ay az with open(imu0/data.csv, w) as f: f.write(#timestamp [ns],w_x,w_y,w_z,a_x,a_y,a_z\n) # 遍历 bag 中的 imu 话题写入对应字段解包这一步看着无聊但它决定了后面所有实验的可复现性。我的习惯是给每次解包单独建一个目录写一份README记录用了哪个脚本、哪个版本、参数是什么。半年后回来看的时候你会感谢当时的自己。4. 跑通第一个 SLAM 实验从真值到误差曲线4.1 三条真值链路精度不同用法也不同真值是评估的基准但基准本身也有误差。M2DGR 按场景用了三种真值来源它们的特性差别很大用错了会得出完全错误的结论。光学动捕的原理是靠多个红外相机捕捉标记点精度可以到毫米级但它需要固定的相机阵列和足够大的空间只能在有限区域内工作而且标记点被遮挡就会丢帧。全站仪是靠跟踪棱镜来测距测角覆盖范围比动捕大得多适合大范围室内和楼宇周边精度在厘米级但输出频率通常比动捕低遇到遮挡也会中断。RTK 靠差分修正把卫星定位精度压到分米甚至厘米级覆盖范围最广但在楼宇密集区和树下会有多路径和失锁。这就带来一个很实际的问题不同序列的真值精度不一样你不能拿电梯序列动捕真值的 ATE 数值去和街道序列RTK 真值的数值直接横向比较。前者反映的是算法真实的室内精度后者里混进了 RTK 本身的误差。我一般在报告里会明确标注每个序列的真值来源做跨场景统计的时候也倾向于同真值源内部比较。另外还有频率问题。真值频率通常低于 IMU 和相机评估时需要在时间上做对齐常见做法是对估计轨迹插值到真值时间戳上或者反过来。选择哪种取决于你要考察的是位置误差还是轨迹形状误差。4.2 用 evo 做评估的完整流程evo是目前最省事的轨迹评估工具支持 TUM 和 KITTI 格式。TUM 格式每行是时间戳、位置三轴、四元数四元顺序为 qx qy qz qw这里顺序很容易搞错写反了结果会离谱。# 安装 pip install evo --upgrade --no-binary evo # 绝对轨迹误差带对齐和可视化 evo_ape tum groundtruth.txt estimated.txt -va --plot --plot_mode xyz --align --correct_scale # 相对位姿误差考察局部漂移 evo_rpe tum groundtruth.txt estimated.txt -va --plot --delta 1 --delta_unit m--align做的是 Umeyama 对齐允许尺度、旋转、平移的自由度--correct_scale只修正尺度。判断用哪个有个简单原则如果你评估的是单目视觉 SLAM尺度是估计出来的必须开--correct_scale才有意义如果评估的是激光 SLAM 或者视觉惯性尺度天然可观测就别开尺度修正否则等于放水。还有一个参数值得留意--t_max_diff控制时间戳匹配的最大容差默认值比较宽松。真值频率低的时候可能没问题但如果估计轨迹时间戳有系统性偏移这个参数会让匹配结果变得很怪。我的做法是先在时间轴上画一下两条轨迹的时间范围确认重叠区间再把容差收紧一点试一次。指标反映的问题何时该重点看ATE 的 RMSE整体轨迹与真值的平均偏离报告主结论、横向比较方案ATE 的 max最坏情况偏离判断是否有跳变式失效ATE 的 std误差波动程度判断误差是系统性还是偶发RPE局部相对运动误差判断里程计短期精度和漂移速度我个人的习惯是三个数一起看RMSE 说明平均水平max 说明有没有翻车时刻std 说明稳定性。有些方案 RMSE 很漂亮但 max 大得离谱这种在真机上大概率会在某个瞬间炸掉不能只看平均值。4.3 几类主流算法的接入要点激光惯性类比如 LIO-SAM、FAST-LIO 系列接入相对简单主要是把点云话题、IMU 话题、外参配置改对。需要注意的是这些算法通常假设点云已经去畸变或者提供了去畸变所需的时间字段如果 bag 里的点云带time或ring字段直接就能用如果没有需要先自己做去畸变预处理否则快速运动段会出现明显的运动畸变。视觉惯性类VINS 系列、ORB-SLAM3 的视觉惯性模式要处理的是鱼眼模型。很多开源实现的鱼眼支持不完整或者畸变参数个数不匹配需要自己改代码。我的建议是先用单目针孔模式跑通一个最简单的序列把整个评估链路走通再逐步换成鱼眼。多模态融合类要额外处理的是标定和初始化。多模态系统的初始化非常关键初始阶段如果某个模态没有充分激励比如起步就是匀速直线IMU 的加速度计偏置不可观后面的结果会一塌糊涂。建议选序列开头有明显加减速或转向的片段做初始化。纯激光类比如 LIO-SAM 关掉 IMU、Cartographer在电梯和旋转序列上会明显退化这正好可以作为对照组用来量化加入 IMU 和视觉之后到底提升了多少。我做的对比实验里纯激光在车库序列的 ATE 大概是激光惯性方案的三倍以上这个数字放在方案汇报里非常有说服力。5. 常见问题与排查技巧实录5.1 踩过的坑整理成速查表现象可能的根因排查与解决轨迹整体偏移但形状正确外参错误或坐标系定义不一致用点云投影法核对相机-激光外参检查轴向约定起步阶段轨迹剧烈抖动初始化激励不足偏置不可观换一段带明显加减速的序列开头做初始化电梯序列突然跳变点云多路径反射、视觉无纹理、IMU 冲击引入运动约束屏蔽异常点云检查 IMU 饱和车库序列回环错位结构重复导致误匹配提高回环检测的描述子阈值加入几何验证评估结果好得不真实开了尺度修正或对齐过松关掉--correct_scale重跑收紧时间容差图像去畸变后边缘撕裂畸变模型用错鱼眼用等距模型别用普通径向畸变模型播放时 RViz 卡顿丢帧点云渲染开销过大降速播放关闭不必要的显示项5.2 时间戳和外参这两个坑几乎人人都会踩时间戳的坑有很多变种。最典型的是bag 记录时间和消息头时间不一致硬件触发系统里这两者通常很接近但并不是同一个量。有些算法读的是消息头时间有些读的是 bag 时间如果两者差了固定偏移评估时会表现为整体时间错位ATE 会被系统性放大。另一个变种是跨传感器的时间戳基准不统一。IMU 用纳秒相机用微秒转换时一不小心就差了三个数量级表现是轨迹完全对不上。我的做法是在数据准备阶段就把所有时间戳统一到同一单位和同一基准写进配置文件后面所有脚本都读这个配置。外参的坑更隐蔽。它不会让轨迹形状明显变化而是让误差在特定方向上系统性偏大。一个实用的检测方法把估计轨迹和真值做对齐后画出三个轴向的误差分量随时间的变化如果某个轴一直存在固定偏置八成是外参问题如果是随机分布那更可能是算法本身的噪声。5.3 让结果更可信的几个工程习惯第一个习惯是固定随机种子和配置版本。大多数 SLAM 算法里有随机采样环节RANSAC、特征提取不同次运行结果会有细微差异。报告结果时最好跑三次取中位数并且把配置文件和提交哈希记下来。第二个习惯是做消融对照。同一段序列分别跑纯激光、纯视觉、激光惯性、激光视觉惯性用同一套评估脚本出结果。这样得出的提升幅度才站得住脚而不是我看图感觉好了很多。第三个习惯是关注失败序列而不是平均分。平均 ATE 好看不代表方案能用真正决定落地成败的是最差的那几个序列。我通常会把所有序列按 ATE 排序重点分析最差的三条看看失效模式是什么是能靠工程手段规避比如加个传感器遮挡检测还是算法本身的能力边界。关于数据量拆包出来的图片如果全存 PNG几百 GB 很快就被吃掉。我在本地是转成无损压缩的 JPEG 或者用质量 95 的 JPEG视觉 SLAM 的特征提取对这种程度的压缩基本不敏感但硬盘压力小很多。关键实验再回头用原图复现一次即可。6. 拿它做研究还能往上延伸哪些方向6.1 几个我觉得比较有空间的选题退化检测与自适应融合。M2DGR 的电梯、旋转、车库这几个序列天然就是退化样本而且带真值可以直接用来训练或验证退化判据。比如做视觉特征数量、激光点云几何可观测性把点云协方差做特征值分解看最小特征值是否接近零的联合判断一旦检测到退化就动态调整各模态权重。这类工作对真值依赖度不高主要看趋势对比很适合拿来练手。跨模态时空一致性。多模态融合里最常见的失败不是单模态坏掉而是模态之间打架。用这个数据集可以构造一个任务给定同一时刻的相机图像和激光点云判断两者的几何一致性是否被破坏。做这个问题需要处理标定、时间对齐、遮挡工程含量高但结论很实用。事件相机与暗光场景。车库序列和部分夜间序列给了事件相机发挥作用的机会。事件相机不吃绝对亮度只对亮度变化敏感理论上在暗光下比普通相机有优势。可以对比普通相机IMU和事件相机IMU在暗光序列上的表现差异这个方向的公开工作还不算特别饱和。动态物体剔除。街道序列里的车辆行人都有标注价值。可以用时序一致性或者语义分割先验把动态点云和动态特征剔除掉再跑 SLAM看ATE 改善多少。这类实验做起来直观结论也容易讲清楚。多模态时序融合方法。热词里经常出现的多模态时序数据融合在这个数据集上很容易落地把多个模态的特征按时间轴对齐做一个轻量的时序融合网络输出位姿增量。好处是数据本身已经做了硬件同步省掉了最难的对齐环节。6.2 和其他数据集组合使用的小技巧单靠一个数据集下结论容易被质疑泛化性我的做法是主力 交叉验证的组合主实验在 M2DGR 上做因为它场景覆盖最全交叉验证挑一到两个特性互补的数据集比如用 EuRoC 验证视觉惯性部分的精度上限用 KITTI 验证室外大范围表现。两边的结论方向一致说服力就上来了。组合使用的时候要注意坐标系和评估口径的统一。不同数据集的真值格式、频率、坐标系定义都不一样最好写一层统一的适配代码把各族数据都转成图像目录 IMU 文本 真值文本三件套。这层适配代码写一次后面换数据集基本不用改算法侧的东西效率提升非常明显。我这两年最大的体会是数据集的真正价值不在于有多少 GB而在于它能不能让你在离线阶段就复现真实落地时会遇到的问题。M2DGR 在这件事上做得比较到位尤其是电梯和地下车库那几段几乎每次跑新算法都会在那里翻车翻车的次数多了方案也就慢慢磨出来了。真要给一条建议就是别急着跑全量先把一个电梯序列和一个车库序列啃透把时间戳、外参、真值对齐这三件事彻底搞明白后面所有的实验都会顺很多。