做地面机器人 SLAM 的人大概都有过这种体验:激光雷达在空旷街道上跑得挺稳,一进楼道拐角就飘;视觉方案在纹理丰富的走廊里看着还行,遇到玻璃门和大面积白墙直接抓瞎;好不容易把两个传感器拼一起,又发现手头的数据集不是缺 IMU,就是没有可靠真值,标定参数还得自己猜。M2DGR 这个数据集,就是冲着这类尴尬局面来的。它全称是 Multi-sensor and Multi-scenario SLAM Dataset for Ground Robots,直译过来就是"面向地面机器人的多传感器多场景 SLAM 数据集",核心关键词就三个:多模态、地面机器人、SLAM 真值。它把全景鱼眼、红外、RGB、事件相机、深度相机、机械式多线激光雷达、IMU 和 RTK-GNSS 一股脑装在一台地面平台上,在门内门外、电梯、街道、广场这些真实场景里跑了一遍,还配了厘米级到分米级的轨迹真值。这篇文章不打算复述论文摘要,而是按我自己的使用顺序,把数据集选择、下载解压、时间戳解析、ROS 环境搭建、多模态融合跑通、轨迹评估、踩坑排查整条链路讲透。刚入门想找第一个多模态数据集的同学,和已经跑过 KITTI、正想换地面场景验证算法的老手,都能在里面找到能直接抄的东西。
1. 先搞清楚 M2DGR 到底解决了什么问题
1.1 地面机器人多模态 SLAM 的数据缺口
车载自动驾驶数据集发展得早,KITTI、nuScenes 这些基本成了行业标配,但它们的采集平台是汽车,运动模型和地面机器人差得远。汽车主要做平面运动,速度高、转弯半径大、悬挂过滤掉了大部分高频振动;地面机器人(尤其是室内外切换的服务机器人、巡检机器人、配送小车)速度低、转向灵活、经常原地打转,还会频繁上下坡、进出电梯、贴墙走。这些动作对 IMU 预积分和雷达点云去畸变都是实打实的考验。
更麻烦的是多模态。真正做多模态融合的人,需要的是同一时刻、同一场景、多路不同物理量测的传感器同时在线。市面上不少数据集名义上"多传感器",实际上是分时段采集,或者几个相机只是不同视角的同一模态,算不上真正的多模态。M2DGR 的设计思路是:把可见光、红外、事件、深度这四种成像模态放在同一平台上,再叠加激光雷达和 RTK,这样你才能认真地做可见光-红外融合、事件-帧融合、雷达-视觉-惯性紧耦合这些课题,而不是拿着两路 RGB 硬凑。
还有一个常被忽略的点是真值。很多数据集只给 GPS 轨迹或者干脆不给真值,导致你跑完算法只能靠肉眼看轨迹像不像。M2DGR 在室外用 RTK-GNSS 加后处理差分,室内用跟踪仪一类的设备做高精度位姿采集,两个来源拼起来覆盖了室内外全场景,这才让定量评估变得有意义。
1.2 M2DGR 的传感器配置与设计取舍
我把这套配置按"模态"和"用途"两条线拆开看,会更清楚作者为什么这么选。需要说明的是,下面这些是这类数据集常见配置的归纳,具体型号和参数请以你下载到的官方 README 为准。
| 传感器 | 数量 | 主要用途 | 选它的理由 |
|---|---|---|---|
| 鱼眼相机 | 6 路 | 360 度全景视觉、视觉 SLAM 前端 | 单目视野太窄,全景能覆盖机器人四周,且鱼眼畸变本身是研究点 |
| RGB 相机 | 1 路 | 常规视觉里程计、目标检测 | 无畸变前视图像,方便和成熟算法对齐 |
| 红外相机 | 1 路 | 暗光、热辐射场景 | 补可见光在弱光下失效的短板 |
| 事件相机 | 1 路 | 高动态、高速运动 | 输出异步事件流,研究事件视觉与帧图像融合 |
| 深度相机 | 1 路 | 近距离稠密深度 | 室内小范围建图、避障验证 |
| 机械式多线激光雷达 | 1 台 | 3D 建图、雷达里程计 | 点云稠密,360 度水平视场 |
| IMU | 1 个 | 惯性预积分、运动先验 | 高频输出,是紧耦合方案的骨架 |
| RTK-GNSS | 1 套 | 室外绝对定位、真值来源 | 厘米级绝对观测,长距离漂移可用它压住 |
这套组合的取舍逻辑其实很明确:作者没有追求"每个模态都上最贵的设备",而是追求"每个模态都要能被别的模态交叉验证"。比如深度相机和激光雷达在近距区域有重叠,你可以拿雷达点云去检查深度图的尺度;红外和可见光同场景采集,可以做跨模态特征匹配。这种"冗余设计"是评估多模态融合算法的重要前提,因为你要验证的恰恰是"某个模态失效时,其他模态能不能顶上来"。
平台本身是轮式地面机器人,前轮转向,运动学上接近阿克曼模型。这意味着你可以拿自行车模型做运动约束,也可以直接当差分驱动处理——两种假设在低速时误差都不大,但高速过弯时差异会显现,值得作为一个小实验去做。
1.3 和 KITTI、nuScenes 这类数据集的横向对照
很多人第一反应是"我 KITTI 都跑通了,为什么还要折腾这个"。直接说结论:KITTI 的强项是室外结构化道路和高速场景,它的激光雷达是 64 线,点云质量很好,但它是车载平台,没有室内场景,没有红外和事件模态,IMU 数据也不是它的卖点(早期版本甚至没有 IMU)。nuScenes 加了毫米波雷达和更丰富的标注,但同样偏车载。
M2DGR 的差异化在于三点:第一,室内外连续,你能拿到从楼里走到楼外、进电梯出电梯这种真实切换序列,这对依赖场景假设的算法是很好的压力测试;第二,成像模态丰富,红外和事件相机是很多数据集没有的,做多模态融合论文的同学会特别需要;第三,地面平台的低速、多转向特性,更接近服务机器人的真实工况。
至于 M2DGR-Plus,可以理解成同一个采集平台和场景体系的升级版,主要变化在激光雷达这类传感器上做了替换,并对部分时间同步问题做了修正。如果你的研究重点在非重复扫描雷达的 SLAM,Plus 版本会更对味;如果你做的是一般性的多模态融合,两个版本都值得上手,我个人的建议是先用原版把流程跑通,再拿 Plus 版做对比实验。
2. 36 个序列怎么挑:场景分组与难度评估
2.1 场景类别与典型挑战
M2DGR 的序列按场景组织,整体是几十条独立序列,官方通常用 seq 加编号的方式命名,覆盖了门内、门外、电梯、走廊、街道、广场、桥梁等类别。我把常见场景和它们各自"坑人"的地方列一下,方便你按研究目标对号入座。
门内场景指的是建筑物内部,核心挑战是几何退化。走廊里前后两面墙平行,激光雷达沿走廊方向几乎没有约束,扫描匹配会出现"沿轴向滑动"的现象;如果墙面是大白墙或者玻璃,视觉特征点数量会掉到很低。这类序列最适合验证你的算法有没有引入平面约束、有没有用 IMU 或轮速里程计来补方向观测。
门外和街道场景是另一个极端:特征丰富、GNSS 可用,但动态物体多。行人、车辆、自行车都会在点云和图像里留下痕迹,如果你的算法没有做动态点剔除,回环检测容易被错误的匹配带偏。这类序列更适合验证鲁棒性后端和动态物体处理。
电梯场景值得单独说。电梯间空间狭小,四面金属壁,激光雷达多路径反射明显,点云会出现重影;而且电梯门开关过程中视野被遮挡,前后帧重叠度骤降。这是极佳的失败案例素材,用来做退化检测和状态机的实验非常合适。
广场和桥梁这类开阔场景则考验 GNSS 与雷达的组合,长距离下累积漂移是否被绝对观测拉住,是这类序列的看点。
2.2 选序列的决策表
刚开始的时候我建议别一上来就挑最难的,容易把算法问题和数据问题混在一起,排查起来极其痛苦。下面这张表是我自己总结的选序列思路,你可以直接照着挑。
| 你的目标 | 建议先跑的序列类型 | 原因 |
|---|---|---|
| 验证雷达惯性里程计基本功能 | 门内走廊、门外平缓路段 | 运动平稳、特征稳定,便于判断是不是算法本身有问题 |
| 测试视觉惯性初始化 | 室外光照均匀、纹理丰富路段 | 特征点多,初始化成功率高 |
| 研究几何退化处理 | 长直走廊、狭长电梯间 | 退化明显,能直接体现算法差异 |
| 研究多模态融合增益 | 有红外和可见光同时可用的暗光段 | 能对比单模态与融合模态的差距 |
| 做回环检测 | 有重复往返路线的序列 | 同一地点多次经过,回环真值可查 |
| 长距离漂移评估 | 庭院绕行、广场大回环 | 里程长,漂移量容易量化 |
注意:不要用不同序列跑出来的绝对误差直接横向比较。不同序列的长度、速度、场景差异很大,绝对误差差距可能有数倍,只有在同一条序列上比不同算法才有意义。跨序列比较请用相对误差,比如误差除以轨迹长度。
2.3 真值来源与精度边界
真值的可信度决定了你评估结论的可信度,这一点必须掰开说。M2DGR 的室外真值主要靠 RTK-GNSS 采集后做后处理差分,好处是绝对精度高,不足是在高楼遮挡、树荫覆盖的地方会出现失锁或多路径,此时真值本身就会跳。室内真值一般靠跟踪仪一类的设备追踪机器人上的目标标志,精度能到厘米级甚至更好,但测量范围受限于设备通视条件,出了覆盖区域就断。
这意味着你在用真值之前,必须做两件事。第一,把真值轨迹画出来,人眼看一遍有没有突变点、有没有大段缺失。第二,把真值和你的估计轨迹做时间对齐,因为真值的时间基准和传感器时间戳不一定严格一致,如果直接算误差,会出现"明明轨迹形状一样但误差很大"的假象。
我一般会用下面这套流程先体检一遍真值:
import numpy as np import matplotlib.pyplot as plt def load_tum(path): data = np.loadtxt(path) return data # 每行: timestamp tx ty tz qx qy qz qw gt = load_tum('groundtruth.txt') t = gt[:, 0] xyz = gt[:, 1:4] dt = np.diff(t) print('总时长(s):', t[-1] - t[0]) print('平均采样间隔(s):', dt.mean()) print('最大采样间隔(s):', dt.max()) print('是否存在时间倒序:', np.any(dt <= 0)) step = np.linalg.norm(np.diff(xyz, axis=0), axis=1) print('最大单步位移(m):', step.max())如果最大采样间隔突然比平均间隔大一个数量级,说明中间有断档;如果最大单步位移大得离谱,说明有跳变。这两种情况都要在评估时把对应区间剔掉,否则算出来的 RMSE 没有参考价值。
3. 下载、目录结构与被忽略的元数据
3.1 下载渠道与体积规划
M2DGR 的体积不小,单条序列从几百 MB 到几 GB 不等,全量下下来对硬盘是实打实的考验。我的建议是先确定你的研究范围,只下和你目标相关的三到五条序列,跑通流程之后再决定要不要全量。
下载渠道上,官方一般会在论文主页或项目主页给出链接,社区里也常有整理好的镜像。实际经验是:大文件下载最怕中途断线,尤其是几十 GB 的压缩包。所以一定要用支持断点续传的工具,并且下完之后立刻校验文件完整性。如果官方提供了 MD5 或 SHA256 校验值,务必对一遍,损坏的压缩包解压时不一定报错,但会在中间某条序列里给你埋一个解不开的文件。
# 先看压缩包完整性,避免解压到一半失败 sha256sum M2DGR_seq01.zip # 解压到指定目录,保持原有结构 unzip -q M2DGR_seq01.zip -d ~/datasets/M2DGR/ # 查看解压后各子目录体积,心里有数 du -sh ~/datasets/M2DGR/seq01/*3.2 目录结构逐层拆解
解压之后的目录层级,是很多人第一次就懵的地方。典型结构大致是这样(不同版本可能有细微差异,以你手上的为准):
seq01/ ├── camera/ │ ├── fisheye_front/ # 鱼眼图像序列,按帧编号 │ ├── fisheye_left/ │ ├── rgb/ # 无畸变 RGB │ ├── infrared/ # 红外图像 │ └── depth/ # 深度图 ├── event/ # 事件流数据 ├── lidar/ # 点云,通常是 pcd 或 bin ├── imu/ # 惯性数据 ├── gnss/ # RTK 输出,含经纬高与状态 ├── timestamps/ # 各传感器时间戳文件 └── groundtruth/ # 轨迹真值第一次打开这种目录,很多人会犯一个错:以为文件名里的数字就是时间戳。实际上大多数这类数据集里,图像文件名只是序号(比如 000123.png),真正的时刻记录在单独的 timestamps 文件里,而且是纳秒或微秒为单位的整数。命名序号和时间戳是两套体系,这个区分非常重要,搞混了后面话题对齐会全线崩盘。
另外要注意每个传感器可能有自己的坐标系定义,点云是雷达系还是机体系,图像是光学系还是相机系,README 里一般会写。没写的话,你可以通过检查数据的数值范围反推,比如图像光心是否在中心、点云的 z 轴方向指向哪里。
3.3 时间戳与文件命名规范
时间戳是整个数据集里最容易埋雷的地方,我踩过的坑基本都集中在这一块。
第一,单位不统一。IMU 和雷达可能是纳秒,图像可能是微秒,GNSS 可能直接是秒级浮点。如果不做统一,你在 ROS 里发出去的话题时间会相差几个数量级,rviz 里什么都看不到,或者看到一堆乱跳的帧。
第二,起点不统一。有的传感器时间戳是从采集开始计时,有的是从系统开机计时,有的是 Unix 时间。跨传感器对齐时,你需要找出一个公共起点,通常做法是各自减去本条序列的最小时间戳,归一到从零开始。
第三,多相机同帧触发的问题。硬件触发理论上能让多路相机在同一时刻曝光,但实际读取和写入会有微秒级抖动。做全景拼接或者多相机融合时,这点抖动足以让特征匹配出现明显误差。我一般会先统计各路相机的时间戳差值分布,确认抖动量级再决定要不要做插值对齐。
import numpy as np def check_sync(ts_a, ts_b, name_a, name_b): # 把两路时间戳都归一到从零开始,并统一到秒 a = (ts_a - ts_a.min()) b = (ts_b - ts_b.min()) # 最近邻匹配,看两路之间的时间差分布 idx = np.searchsorted(b, a) idx = np.clip(idx, 1, len(b) - 1) d_left = np.abs(a - b[idx - 1]) d_right = np.abs(a - b[idx]) diff = np.minimum(d_left, d_right) print(f'{name_a} vs {name_b}: 中位差 {np.median(diff)*1e3:.3f} ms, ' f'最大差 {diff.max()*1e3:.3f} ms')跑完这个脚本,如果中位差在毫秒以内,说明同步质量不错;如果出现几十毫秒甚至更大的差值,那你在做视觉惯性紧耦合时就必须显式建模这个偏移,否则轨迹会有系统性偏差。
4. 从原始数据到可运行 ROS 环境
4.1 环境准备与依赖
我习惯用 ROS 加 PCL 这一套来做数据预处理,原因是生态成熟,几乎所有开源 SLAM 方案都能直接吃 ROS bag。如果你用的是 ROS2,思路完全一样,只是话题发布和 bag 录制命令换成对应版本。
基础依赖大概是这些:ROS 桌面完整版、PCL、OpenCV、Eigen、Ceres 或 GTSAM(看你后面要跑哪个后端)、evo(轨迹评估)。Python 侧主要是 numpy、scipy、open3d 和 matplotlib。如果你打算在边缘计算平台上部署,比如 RK3588 这类带 NPU 的板子,那前期在 x86 上把数据流程跑通、把算法精度确认下来,再考虑往板子上移植,顺序不要反。
sudo apt update sudo apt install -y ros-noetic-desktop-full ros-noetic-pcl-ros \ ros-noetic-cv-bridge ros-noetic-tf2-msgs \ libeigen3-dev libceres-dev libpcl-dev pip install numpy scipy open3d matplotlib evo版本号只是示意,请按你实际使用的 ROS 发行版替换。装完之后跑一个最小验证:用rosbag info看一下别人提供的样例 bag 能不能读,能读说明环境基本没问题。
4.2 图像话题的坑:鱼眼、深度图、事件流
鱼眼图像是这类数据集里最容易被低估的部分。很多人拿鱼眼图直接扔进 ORB-SLAM 这类基于针孔模型的框架,结果特征匹配一塌糊涂,然后开始怀疑数据质量。问题不在数据,在于模型不匹配。鱼眼有很强的径向畸变,必须用对应的畸变模型去校正,或者用支持大视场的相机模型(比如等距投影、Kannala-Brandt 模型)来建模。如果你只是想快速验证,可以先用 OpenCV 的鱼眼校正把图转成近似针孔图像,代价是边缘区域被裁掉。
import cv2 import numpy as np # K 和 D 从数据集标定文件里读,别自己猜 K = np.loadtxt('fisheye_front_intrinsics.txt') D = np.loadtxt('fisheye_front_distortion.txt') img = cv2.imread('000123.png') h, w = img.shape[:2] new_K = cv2.fisheye.estimateNewCameraMatrixForUndistortRectify( K, D, (w, h), np.eye(3), balance=0.0) map1, map2 = cv2.fisheye.initUndistortRectifyMap( K, D, np.eye(3), new_K, (w, h), cv2.CV_16SC2) undistorted = cv2.remap(img, map1, map2, interpolation=cv2.INTER_LINEAR)深度图要注意编码格式。有的数据集把深度存成 16 位 PNG,单位是毫米;有的存成 32 位浮点,单位是米。用 cv2.imread 读 16 位图时如果不加cv2.IMREAD_UNCHANGED,会被默认转成 8 位,深度信息直接丢失,图看起来"全是黑的"。这个坑我见太多人踩了,代码里加个参数就能避开。
事件流是另一种数据结构,它不是一个二维矩阵,而是一串 (x, y, timestamp, polarity) 四元组。直接当图像读会失败,必须用专门的解析库或者自己写读取逻辑。做事件-帧融合研究时,通常的做法是把一个时间窗口内的事件累积成事件帧,再和同一时刻的 RGB 或红外图配对。
4.3 GNSS/RTK 到局部坐标系
RTK 输出的是经纬度和海拔,而你的 SLAM 系统需要的是局部笛卡尔坐标。这中间的转换必须走一遍投影,常用的做法是选序列起点作为局部坐标原点,用等距圆柱或者横轴墨卡托投影把经纬度转成米。
import numpy as np from pyproj import Proj def lla_to_local(lla, origin=None): # lla: N x 3, 依次是 lat, lon, alt if origin is None: origin = lla[0] proj = Proj(proj='tmerc', lat_0=origin[0], lon_0=origin[1], k=1, x_0=0, y_0=0, ellps='WGS84', units='m') x, y = proj(lla[:, 1], lla[:, 0]) z = lla[:, 2] - origin[2] return np.stack([x, y, z], axis=1)这里有个容易忽略的点:RTK 数据里的定位质量标记。固定解、浮点解、单点解的精度差了好几个数量级,浮点解的误差可能有分米甚至米级。如果你把浮点解的点也当真值使用,评估结果会被严重污染。所以预处理阶段一定要按质量标记过滤,只保留固定解,中间缺失的部分做插值或直接挖掉。
另一个常见问题是坐标轴方向。经纬度转出来的 x 通常是东向,y 是北向,而大多数 SLAM 框架默认 x 向前、y 向左、z 向上。中间需要一个旋转矩阵来对齐,具体怎么转取决于你的机体坐标系定义,README 里如果没写,就靠数据反推:让机器人静止一段,看 IMU 的加速度主要落在哪个轴上。
4.4 自己写一个数据自检脚本
我强烈建议在写任何 SLAM 代码之前,先花半小时写一个自检脚本,把这条序列的家底摸清楚。脚本要输出:各传感器帧数、时间跨度、平均频率、时间戳单调性、是否有重复时间戳、图像分辨率一致性、点云点数分布、IMU 的角速度和加速度量级。
import numpy as np def summarize(name, ts, values=None): ts = np.asarray(ts, dtype=np.float64) dt = np.diff(ts) freq = 1.0 / np.median(dt) if len(dt) else 0.0 print(f'--- {name} ---') print(f'帧数: {len(ts)}') print(f'时长: {(ts[-1]-ts[0]):.2f} s') print(f'中位频率: {freq:.2f} Hz') print(f'单调递增: {np.all(dt > 0)}') print(f'重复时间戳数: {len(ts) - len(np.unique(ts))}') if values is not None: v = np.asarray(values, dtype=np.float64) print(f'数值范围: [{v.min():.4f}, {v.max():.4f}]')这份体检报告看起来朴素,但它能帮你在半小时内排除掉八成"算法跑不通"的假象。我就是靠这套东西发现过一条序列中某路相机有一段重复时间戳,导致建图时同一时刻出现两帧图像,特征匹配直接错乱。
5. 多模态融合跑通 SLAM 的三条路线
5.1 激光-惯性路线:先把地基打牢
如果只能先跑一条路线,我建议从激光惯性开始。原因很简单:地面机器人的运动以平移和偏航为主,激光雷达对平面结构的观测比较稳定,IMU 又能提供高频的姿态先验,两者的耦合逻辑清晰,调试起来反馈直接。
典型流程是:把点云和 IMU 数据喂给 LIO-SAM 或 FAST-LIO2 这一类紧耦合框架,先完成去畸变,再做点到面或点到面的残差优化,最后用回环检测修漂移。M2DGR 的机械式多线雷达点云质量不错,跑基础版本通常能出结果。
参数上有几个要注意的:IMU 频率必须高于雷达帧率一个数量级,否则预积分误差会明显增大;雷达和 IMU 的外参必须给准,尤其是平移部分,如果外参错了,在快速转向时会出现规律性的轨迹抖动,看起来像"画波浪"。
# LIO-SAM 参数片段示意,具体字段按你用的版本调整 imuTopic: "/imu/data" pointCloudTopic: "/velodyne_points" imuRate: 150.0 lidarMinRange: 1.0 lidarMaxRange: 100.0 extrinsicRot: [1, 0, 0, 0, 1, 0, 0, 0, 1] extrinsicRPY: [1, 0, 0, 0, 1, 0, 0, 0, 1]外参矩阵一开始不知道怎么办?如果你的标定文件缺失,可以先手动粗标:让机器人在平坦地面上做一次纯平移,观察点云在静止帧之间的偏移量,反推平移外参。这个方法精度不高,但足够让你把流程跑起来,后续再用标定工具精修。
5.2 视觉-惯性路线:初始化和光照是两道坎
视觉惯性路线在 M2DGR 上跑,主要卡在两个地方:初始化和光照变化。
初始化要求机器人先做一段有足够激励的运动,让加速度计和陀螺仪的观测能解出尺度和重力方向。如果你选的是一段几乎匀速直线的序列,初始化很容易失败或者解出错误的尺度。我的做法是先用门外场景里带明显转向和加减速的片段,初始化成功之后再切入目标序列做评估。
光照变化是另一个麻烦。室外阳光直射到阴影区域的过渡段,图像亮度可能瞬间变化好几倍,特征点数量剧烈波动,视觉前端会跟不上。这时红外模态的价值就体现出来了,红外图像不受可见光亮度影响,在极端光照条件下反而更稳。做多模态融合时,一个实用策略是根据当前可见光图像的质量动态调整红外特征的权重,而不是平均加权。
def compute_visual_quality(img_gray): # 用简单指标衡量当前帧的可用性 grad_x = np.abs(cv2.Sobel(img_gray, cv2.CV_32F, 1, 0)) grad_y = np.abs(cv2.Sobel(img_gray, cv2.CV_32F, 0, 1)) texture = (grad_x + grad_y).mean() contrast = img_gray.std() return 0.5 * texture + 0.5 * contrast这个指标不是学术级方案,但胜在可解释、可调,能直接嵌进你的权重调度逻辑里,做消融实验时也方便解释增益来源。
5.3 多传感器紧耦合:什么时候值得上
雷达加视觉加惯性的紧耦合框架,比如 R3LIVE 这类,确实能在复杂场景里给出更完整的建图结果,但它对数据质量的要求也更高。时间同步偏差、外参误差、标定不准确,这三种问题在松耦合里可能只是精度略降,在紧耦合里会直接导致发散。
我的建议是:先在激光惯性方案上把时间同步和外参问题彻底解决掉,确认单模态方案在这条序列上能达到可接受的精度,再往紧耦合上叠加。跳步骤的结果通常是分不清是传感器数据的问题还是融合逻辑的问题,白白浪费时间。
另外一点是关于模态数量的取舍。多模态不等于模态越多越好。红外和事件相机在特定场景下确实有优势,但在光照充足、纹理丰富的常规室外路段,它们带来的增益有限,反而增加了计算量和故障点。做研究时,与其把所有模态都堆上去,不如针对性地设计"哪个模态在什么条件下接管"的调度机制,这样的工作更有说服力,也更容易落地。
5.4 外参标定与时间同步处理
这两个是绕不开的基础工程,必须单独拿出来讲。
外参方面,很多数据集会提供传感器之间的标定文件,但如果你发现建图结果有规律性形变,第一嫌疑就是外参不准。我一般会做这样一个验证:在静态场景下采集几分钟数据,把点云按 IMU 姿态补偿后叠加,如果点云边缘出现明显的"模糊带",说明外参或者时间同步有问题;如果叠加后边界清晰,说明这两项至少没有大错。
时间同步方面,前面提过要统计两路时间戳的差值分布。如果发现存在恒定偏移(比如相机比雷达系统性地晚 30 ms),那你需要在发布话题时手动把时间戳平移回去。如果是随机抖动,那就只能通过插值和状态估计来处理,紧耦合框架里一般会显式估计这个偏移量。
注意:不要在没确认时间同步的情况下调 SLAM 参数。我见过太多次把时间偏移当成"算法调参问题",反复改后端权重、改噪声矩阵,最后发现只是相机时间戳差了 50 毫秒。
5.5 用 evo 做轨迹评估
评估环节用 evo 就够了,它支持 TUM 和 KITTI 两种轨迹格式,能做绝对位姿误差和相对位姿误差,出图也好看。
# 绝对位姿误差,带对齐,输出统计信息 evo_ape tum groundtruth.txt est.txt -va --align --plot --plot_mode xyz \ --save_results ape_seq01.zip # 相对位姿误差,评估局部漂移 evo_rpe tum groundtruth.txt est.txt -va --align --delta 1 --delta_unit m \ --plot --plot_mode xyz # 轨迹叠加对比 evo_traj tum groundtruth.txt est.txt --ref groundtruth.txt --align -p \ --plot_mode xyz几个实操要点:第一,一定要加--align,因为不同算法的初始位姿和坐标系原点不同,不对齐直接算误差毫无意义。第二,绝对误差反映全局一致性,相对误差反映局部漂移,两个都要看,只报一个容易得出片面结论。第三,评估前把真值和估计轨迹做时间对齐,evo 支持按时间戳匹配,如果两条轨迹时间戳体系不一致,需要手动插值到同一时间轴。
还有一个容易被忽略的细节:评估要分段看。一条序列里如果有几段明显的退化区域,全局 RMSE 会被平均掉,看不出问题。把误差曲线画出来,结合场景标注看哪一段误差突然变大,往往能定位到算法的具体缺陷,这对写论文和改算法都比一个孤立数字有用得多。
6. 踩坑实录与常见问题速查
6.1 典型报错与排查表
下面这张表基本覆盖了我在这套数据上遇到过的八成问题,按"现象-可能原因-排查方法"整理。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| rviz 里看不到图像 | 时间戳单位错误或话题未发布 | 用rosbag info看话题时间范围,检查时间戳量纲 |
| 点云帧数远少于图像帧数 | 传感器频率不同,正常现象 | 统计各传感器中位频率,确认是否符合预期 |
| 轨迹整体有固定偏移 | 外参平移错误 | 静止段叠加点云检查边缘清晰度 |
| 轨迹呈规律波浪形 | 时间同步偏移或外参旋转错误 | 统计两路时间戳差值分布,检查外参旋转矩阵 |
| 深度图全黑 | 16 位图被按 8 位读取 | 用cv2.IMREAD_UNCHANGED重新读取 |
| 视觉里程计初始化失败 | 运动激励不足或图像纹理差 | 换一段有转向和加减速的序列片段 |
| 建图在走廊处沿轴向拉伸 | 几何退化 | 引入平面约束或提高惯性权重 |
| 评估误差异常大但轨迹形状相似 | 未做时间对齐 | 检查两条轨迹的时间戳体系,做插值对齐 |
| 回环检测误匹配 | 动态物体或重复结构 | 加动态点剔除,或限制回环搜索半径 |
| 程序在读取事件数据时崩溃 | 事件流不是图像格式 | 用专门解析工具,按四元组格式读取 |
6.2 数据自身的坑
有些问题不在你的代码,而在数据本身,识别出来能省很多力气。
一是时间同步的历史问题。早期版本的部分序列,相机与其他传感器之间的时间同步曾被社区讨论过,这也是后续 Plus 版本做修正的动机之一。实际使用时,建议对每条序列都跑一遍前面那个同步检查脚本,做到心里有数,不要默认所有序列的同步质量都一样。
二是真值的覆盖盲区。室内跟踪仪受通视条件限制,某些位置可能没有真值,或者精度显著下降。你在评估时如果发现某一段误差突然暴增,先别急着改算法,去看看真值那一段是不是本身有问题。
三是压缩包损坏。大文件下载中途出问题很常见,损坏的文件可能在前半段序列解压正常,最后几条才报错。下完立刻校验哈希值,这是性价比最高的一个习惯。
6.3 我个人的几条经验
第一条:先把单模态跑通,再上多模态。我见过太多人一上来就想做三模态紧耦合,结果连基础里程计都没调稳,最后完全不知道问题出在哪。先用激光惯性跑通一条最简单的序列,出一张像样的建图结果,再去加视觉和红外,这个顺序能帮你节省大量时间。
第二条:真值体检和数据体检一样重要。不要默认真值就是绝对正确的,画出来看一眼,算一下采样间隔和跳变,成本几分钟,收益是避免你基于错误基准调一两周参数。
第三条:把每次实验的配置记下来。序列名、算法版本、参数文件、评估结果,都存一份。多模态融合实验的组合方式多,靠脑子记一定会乱。我自己是用一个简单的表格加日期目录来管理,虽然土,但比事后回忆靠谱得多。
第四条:评估结果要看曲线,不要只看数字。一条误差曲线能告诉你哪一段算法失效、失效持续多久、是否可恢复,这些信息比一个 RMSE 数字有价值得多,尤其是写论文做分析的时候。
第五条:如果想把这套东西往边缘平台移植,先在 PC 上把数据流程和算法精度完全确认好,再考虑算力裁剪。地面机器人场景里,数据预处理(去畸变、点云裁剪、时间对齐)往往占了不少算力,这些环节优化好了,移植难度会下降很多。
我自己在这套数据上折腾了挺长时间,最深的体会是:多模态 SLAM 的瓶颈往往不在融合算法本身,而在数据链路的基础工程。时间戳对齐、外参标定、真值质量这三件事做扎实了,后面无论跑哪种框架都会顺畅很多;这三件事糊弄过去,再花哨的融合策略也架不住底层数据的系统性偏差。所以如果你正准备用 M2DGR 做实验,不妨先花两天时间只做数据体检和预处理,把每条序列的脾气摸清楚,后面会省下好几倍的调试时间。