SLAM数据集评测避坑指南:TUM、KITTI、EuRoC细节剖析
简介:面向SLAM、自动驾驶与微型无人机方向研究者的数据集总结文档,系统梳理了TUM RGB-D、KITTI、EuRoC三大公开数据集的采集设备、数据格式、真值标注与评估指标。压缩包内为单个docx文件,共1个文件,约1.93MB,便于随时查阅。目前已有9609人浏览学习,适合作为入门数据集认知与选型参考。文档内容较为详尽:TUM部分涵盖彩色图与深度图的PNG存储、比例缩放规则、groundtruth轨迹txt格式,以及ATE与RPE评价指标和点云生成方法;KITTI部分介绍了采集平台所搭载的惯性导航系统、激光扫描仪、灰度与彩色相机等传感器配置,以及raw data等数据组织方式;EuRoC部分则说明微型无人机多传感器数据特点。这些梳理能帮助读者在SLAM、自动驾驶、无人机等实验与论文复现中快速理解数据来源与评测方法。
1. 三个数据集不只是“录数据”,而是一套精心布设的评测装置
大多数开发者第一次接触 TUM、KITTI、EuRoC 时,第一反应都是“找一段 rosbag 跑一下 SLAM”。但真正把这些数据集用明白的人会告诉你:数据集的复杂度不在于传感器多贵、场景多真实,而在于它的“评测设计”——TUM 的深度图为什么要缩放 5000 倍?KITTI 为什么要拿激光雷达的正前方时刻当快门基准?EuRoC 为什么非要把 IMU 中心定义为机体坐标系原点?这些细节决定了你从数据集里读出的轨迹精度数字到底可不可信。
把三个数据集放在一起看,本质上是三种典型的 SLAM 传感器配置:RGB-D 单目加深度、多传感器自动驾驶平台、双目加 IMU 的无人机。它们共享同一个难题——如何把一次 SLAM 运行的轨迹误差变成一个可复现、可比较的量化指标。搞懂这套评测装置,比多跑几个序列更有价值。
2. TUM RGB-D:深度图缩放、四元数轨迹与 ATE/RPE 的数学本质
2.1 深度图为什么是 16 位、为什么偏偏缩放 5000 倍
TUM 的深度图以 PNG 格式存储为 640x480 的 16 位单色图像,彩色图则是 8 位 RGB。这里最反直觉的是深度图的编码方式:像素值 5000 对应 1 米,也就是深度值等于像素值除以 5000.0,而像素值 0 被规定为缺失值。
为什么不用浮点深度、也不用 8 位整数?16 位 PNG 在解码上比浮点深度的二进制格式省事得多,PNG 无损压缩对 16 位数据的压缩率也稳定,而且缩放 5000 倍后,1 毫米的深度变化对应 5 个像素值,量化精度对视觉 SLAM 足够用。常见的实现思路是把depth_px / 5000.0当成实际米制深度,但要注意一个坑:0 像素值必须被过滤,否则点云里会出现一堆位于相机光心处、距离为 0 的杂点。
import numpy as np import imageio.v2 as imageio depth = imageio.imread("depth.png").astype(np.float32) depth_m = depth / 5000.0 mask = depth > 0 # 0 是缺失值,不能用 points_z = depth_m[mask]深度误差在视觉 SLAM 里不是均匀分布的,同样的像素深度误差在远距离处会被放大,因为三角化误差与距离平方近似成正比。TUM 的室内场景密度大、物体近,能把这个误差控制在小范围内。理解 5000 倍缩放,本质上就理解了 Kinect 这类消费级深度传感器的量化噪声边界。
2.2 groundtruth 轨迹格式:为什么是 tx ty tz qx qy qz qw
TUM 的真值存放在 txt 里,每行格式为timestamp tx ty tz qx qy qz qw,timestamp 是 Unix 纪元以来的浮点秒数,位置是彩色相机光学中心相对于运动捕捉系统世界原点的坐标,姿态用单位四元数表示。
这里值得追问的是:为什么不用欧拉角,或者干脆用旋转矩阵?四元数只有 4 个分量、没有万向锁问题、插值方便,更重要的是 SVD 对齐时四元数转旋转矩阵是线性操作。欧拉角的俯仰角接近 ±90° 时,数据集的真值会突然出现跳变,这种跳变不是传感器误差,是表示方式的奇异性。用四元数存储,姿态的连续性在数学上是被保证的。
另一个容易忽略的细节:四元数给出的方向是彩色相机光学中心的朝向,而不是深度相机或 IMU 的朝向。这意味着你在评估自己的 SLAM 系统时,输出的估计姿态必须是“彩色相机坐标系下的姿态”,否则对齐后误差会带上一个固定旋转偏移。
# rgbd_dataset_freiburg1_xyz 某行真值结构 1341845746.621552 -0.055673 0.010236 -0.007318 0.999504 -0.001873 0.011427 -0.028243 # 时间戳 tx ty tz qx qy qz qw该行含义是:在 1341845746.62 秒时刻,彩色相机光心位于运动捕捉系原点附近的 (-0.056, 0.010, -0.007) 米处,朝向由最后一个四元数给出。注意位置和姿态都是相对于世界坐标系的,而不是相对于上一帧。
2.3 ATE 的 SVD 对齐到底在“消除”什么
绝对轨迹误差(ATE)的思路很直接:把估计轨迹和真值轨迹按时间戳关联后,先做一个刚体变换对齐,再计算逐点差异。但为什么要对齐?因为你的 SLAM 系统估计出来的轨迹通常不在真值坐标系里——单目 SLAM 初始化任意,RGB-D SLAM 的世界系也可能和运动捕捉系差一个旋转平移。
TUM 官方评估脚本用的正是 Kabsch 算法:对两组对应点求质心,用 SVD 分解协方差矩阵得到旋转矩阵,再算出平移。
import numpy as np def kabsch_align(P, Q): # P: 估计轨迹位置 (N,3), Q: 真值轨迹位置 (N,3) centroid_P = P.mean(axis=0) centroid_Q = Q.mean(axis=0) H = (P - centroid_P).T @ (Q - centroid_Q) U, _, Vt = np.linalg.svd(H) R = Vt.T @ U.T if np.linalg.det(R) < 0: # 保证右手坐标系,否则会出现镜像旋转 Vt[-1, :] *= -1 R = Vt.T @ U.T t = centroid_Q - R @ centroid_P aligned = (R @ P.T).T + t return aligned, np.linalg.norm(aligned - Q, axis=1).mean()SVD 求出来的 R 一定是一个正交矩阵,但正交矩阵包括镜像反射(行列式为 -1),所以代码里必须检查行列式。这个细节是很多自写评估脚本翻车的重灾区——如果不处理,对齐后的轨迹会被“镜像”过去,误差大得离谱但你看不出哪不对。
2.4 RPE 固定窗口与漂移测量的关系
相对姿势误差(RPE)不关心全局坐标系,它计算的是“一段时间间隔内相对运动的误差”。官方脚本默认计算所有时间戳对之间的误差,但这个数量是轨迹长度的平方级,所以必须用-max_pairs下采样,或者用-fixed_delta固定窗口。
RPE 测量的是视觉里程计的漂移率,比如每秒漂移多少米。注意 TUM 对 RPE 的定位是“测量视觉测距系统的漂移”,对 ATE 的定位是“测量可视 SLAM 系统的性能”。如果你的算法是带闭环的 SLAM,ATE 更合适;如果只是纯里程计,RPE 更能反映真实漂移。窗口太小,RPE 只反映短时噪声;窗口太大,漂移被平均掉。一般做法是先跑一个-delta_unit s -delta 1,再跑一个-delta 5对比,观察漂移是否随时间线性增长。
3. KITTI:激光雷达快门同步、synced+rectified 与自动驾驶数据的时间基准
3.1 为什么要等激光雷达朝向正前方才释放快门
KITTI 的采集平台上有两个灰度相机、两个彩色相机和一个 64 线激光扫描仪,所有传感器以 10Hz 采集。硬件设计里有个容易被忽略的细节:相机快门在激光雷达朝向正前方时释放。
原因在于激光雷达是旋转扫描的,一帧 360° 点云不是瞬时采集的,而是花 100ms 扫完一圈。如果相机快门和雷达扫描相位不对齐,图像时刻对应的车体姿态和雷达帧首尾时刻对应的姿态会差几十毫秒,这在车辆高速行驶时会导致目标在图像和点云里错位几个像素到几十像素。把快门对齐到雷达朝向正前方的时刻,等于给所有数据定了一个统一的“时间锚点”。
# 同步逻辑:以10Hz雷达为节拍器 # 雷达打到正前方 -> 触发相机曝光 -> 记录GPS/IMU姿态 -> 组成一帧这套硬件同步的替代方案是软件时间戳插值,但插值依赖传感器时钟的稳定性和驱动延迟,精度远不如快门硬件触发。KITTI 选择硬件同步,换来的好处是所有传感器在同一个时间语义下采样,评估时不需要再做复杂的时间对齐。
3.2 raw data 与 synced+rectified:两个词各自解决了什么
raw data 里包含两类数据:原始的(没有同步、没有矫正)和处理过的(同步且矫正)。原始数据保留了畸变和未对齐的时间戳,用于标定和传感器建模;synced+rectified 数据是经过消除畸变和双目矫正的、且和其他传感器序列对齐过的图像。
双止矫正的本质是把左右相机图像投影到同一个虚拟平面上,使极线水平对齐。这样做的直接收益是视差计算变成一维搜索,深度估计和特征匹配的计算量大幅下降。对 SLAM 评估而言,用 synced+rectified 数据可以省掉畸变矫正这一步,直接喂给特征提取器。
3.3 image_00 为什么是同步参考系
KITTI 的 raw data 每个序列都包含 image_00 到 image_03 四路相机,外加 velodyne 点云和 GPS/IMU 数据。官方文档明确指出:所有传感器数据序列以 image_00 作为同步参考系。也就是说,其他传感器的时间戳需要向 image_00 对齐。
这个设计动机很实际:图像帧率的抖动最小、曝光时间固定、并且彩色和灰度图像成对出现,以它为参考系可以保证“看到的就是同时刻的”。工程上的代价是你下载的每个数据包里,GPS/IMU 时间戳并非整数 10Hz,而是按图像帧打点。评估自己的融合算法时,要用插值把 IMU 采样对齐到 image_00 的时间轴上,不能直接拿原始时间戳硬比。
3.4 object 是单帧,tracking 是连续序列:数据集结构如何约束算法边界
KITTI 的 object 数据集按“2D 检测、3D 检测、鸟瞰图检测”三个任务组织,数据本身是单帧,训练集带标注、测试集不带。tracking 数据集则是一段连续序列,标注以 tracklets 形式给出。object 和 tracking 的差异不是文件组织方式不同,而是任务本质不同:检测算法不要求帧间关联,所以单帧采样即可;跟踪算法必须验证时间一致性,所以需要连续帧。
做多目标跟踪时如果只拿 object 数据训练、用 tracking 数据测试,大概率翻车——因为 object 数据里没有帧间遮挡和轨迹碎片这些跟踪特有的难题。数据集结构的边界就是任务定义的边界,这个边界在选数据时就要想清楚。
4. EuRoC:Body Frame 为什么以 IMU 为中心、T_BS 坐标变换约定的工程逻辑
4.1 为什么机体坐标系的原点放在 IMU 中心
EuRoC 的无人机飞行器上,四个传感器(双目相机两个 cam0/cam1、惯性测量单元 imu0、激光测距仪 leica0)各有各的坐标系,但飞行器的 Body Frame 被定义在 IMU 的中心。
这个选择背后的逻辑是:视觉惯性导航的状态估计里,IMU 是积分核心,角速度和加速度都定义在 IMU 坐标系下,把 Body Frame 定在 IMU 中心,就把“积分坐标系”和“状态变量坐标系”合成了同一个,避免在每一个 IMU 采样时刻都做一次从 Body 到 IMU 的外参旋转。对惯导系统而言,外参标定的误差会二次方地累积进位置,所以能省一次变换就省一次。
配套地,每个传感器文件夹里都有一个 sensor.yaml,描述该传感器坐标系到 Body Frame 的变换关系以及内参。这意味着 EuRoC 的数据不是“裸传感器流”,而是已经带上了完整的外参定义——用官方给的变换做初始化,比从零标定双目-IMU 外参的结果稳定得多。
4.2 R_WC 和 T_BS:旋转坐标系变换约定的读法
EuRoC 的文档里定义了一个容易读反的约定:R_WC 表示“将 C 坐标系下表示的向量变换成 W 坐标系下表示”,所以 R_WC 实际上是从 Camera 到 World 的旋转矩阵,而不是反过来。同理,T_BS 表示将所有 Sensor 坐标系下的向量变为 Body Frame 下的向量。
import numpy as np # 设 T_BS 是 sensor.yaml 给出的 Sensor->Body 变换 T_BS = np.loadtxt("cam0/sensor.yaml的变换部分") # 设 T_WB 是 Vicon 给出的 Body->World 位姿(groundtruth 提供) T_WB = load_vicon_pose("data.csv", row=100) # 链式变换:Sensor -> Body -> World T_WS = T_WB @ T_BS # 验证:旋转部分的行列式必须为 +1,否则坐标系约定读反了 assert np.linalg.det(T_WS[:3, :3]) > 0这里最常见的错误是把矩阵乘法的顺序写反。T_WB 表示先取 Body 系下的点变换到 World,T_BS 表示先取 Sensor 系下的点变换到 Body,所以从 Sensor 到 World 必须执行T_WS = T_WB @ T_BS,外面的变换矩阵在左边。如果写成T_BS @ T_WB,语义就变成了“先到 World 再到 Sensor”,结果轨迹会绕着一个奇怪的轴旋转。
4.3 Vicon 动作捕捉系统如何提供毫米级真值
EuRoC 的 groundtruth 由 Vicon 动作捕捉系统提供:在无人机上贴一组反射标志,多台高速红外相机从不同角度观测这些标志,重建出刚体在单一坐标系下的 6D 位姿,帧率 100Hz、精度毫米级。
这套系统的本质是外部基准,它的精度比无人机上任何传感器都高一个量级,所以能当“真值”用。但注意 groundtruth 给出的是 Vicon 坐标系下的位姿,不是你 SLAM 系统自己定义的世界系。所以在评估时,你的估计轨迹仍然要做一次类似 TUM ATE 的刚体对齐,才能和 Vicon 真值对比。EuRoC 数据里还包含 leica0(激光测距仪)提供的辅助测量,用于校核 Vicon 系统自身的误差。
4.4 三个数据集的互补关系:室内、室外与空中
把三个数据集串起来看,它们覆盖了三类典型场景:TUM 是室内手持 Kinect,运动速度慢、深度信息丰富,适合 RGB-D SLAM;KITTI 是室外车载,运动速度快、场景深度大,适合双目视觉和激光雷达融合的自动驾驶;EuRoC 是室内无人机,快速旋转加剧烈平移,恰好是视觉惯性导航最吃紧的运动模式。
这种互补决定了按数据集选算法的策略:如果你的系统在 TUM 上精度好但在 KITTI 上会飞点,多半是深度噪声建模和远距离三角化出了问题;如果在 EuRoC 上发散,多半是 IMU 与视觉的外参或时间戳对齐有偏差。一个数据集说明不了问题,三个数据集的失败模式放在一起才能定位瓶颈。
5. 评测避坑:时间戳单位、坐标系链式乘法与深度缺失值的五处翻车现场
5.1 Unix 浮点秒、KITTI 基准、EuRoC 整数时间戳被混用
现象:跑通流程后 ATE 误差在几十米量级,改哪个参数都没用。
原因:三个数据集的时间戳语义不同。TUM 的 timestamp 是 Unix 纪元以来的浮点秒;KITTI 的时间戳是相对于采集启动时刻的毫秒/微秒整数;EuRoC 的 groundtruth 是 18 位整数时间戳,单位是纳秒。直接拿两套时间戳做最近邻关联,结果自然是错位到相隔几分钟的帧上。
解决:统一换算成 double 秒。TUM 直接用浮点秒;KITTI 用start_time + delta / 1e6;EuRoC 用ts_ns / 1e9。关联时再设置一个最大时间差阈值(如 0.02 秒),超过阈值的配对直接丢掉。从那以后我拿到新数据集的第一件事都是写一个时间戳单位探测函数,先打印三行样本看量级,再进评估流程。
5.2 SVD 对齐被少数离群帧拉偏
现象:ATE 的均值看着不大,但轨迹里某一小段明显有鼓包,标准差超大。
原因:Kabsch 算法是最小二乘意义下的对齐,少数漂移严重的帧(比如快速旋转时特征丢失导致的跳变)会把整个刚体变换“拽”向它们,导致正常区间也被偏置。
解决:先做一次粗对齐,计算逐点误差后去掉超过 3 倍中位数的离群帧,用剩余帧重新做精细对齐。评估脚本里看到的均值/中位数/标准差三个数字要对照着看——均值和中位数差太远,就是有离群帧存在。
5.3 深度像素 0 值被当成“距离 0 米”生成点云
现象:点云在相机光心附近出现一团密集的黑色杂点,肉眼上像是传感器坏掉了。
原因:深度图里 0 是缺失值(没有深度测量),不是距离 0 米。直接把像素值除以 5000.0 再生成 PLY,就把缺失点全部堆到了相机原点。
解决:生成点云前先构建 mask(像素值大于 0),只保留有效深度像素;同时把彩色图像对应像素同步过滤,保证点云的颜色和位置一一对应。凡是基于深度图跑点云处理的算法,第一步都该是“过滤 0 值”,而不是先转成米制浮点。
5.4 EuRoC 的 T_BS 乘反了,轨迹像被绕了一个轴翻转
现象:视觉惯性里程计跑出来的轨迹和真值在形状上一致,但整体绕某个轴转了 180 度,ATE 算下来大得离谱。
原因:T_BS 的语义是 Sensor 系到 Body 系,链式变换必须写T_WB @ T_BS。把顺序写成T_BS @ T_WB后,几何上变成先到世界再回传感器,轨迹被一个错误旋转矩阵扭曲。
解决:用官方提供的位姿真值做一次单位检查——变换矩阵旋转部分行列式为 +1、且用一组已知坐标点做正向变换验证输出方向。建议在工程里开一个“坐标系沙盒”脚本,所有外参读取后先在静态数据上验证一遍,再做在线融合。
5.5 KITTI 的 synced 与 rectified 分开下载后文件名对不上
现象:下载了同步数据又下载了矫正数据,结果图像文件名和点云时间戳对不上,程序直接崩。
原因:KITTI 的 raw data 里 synced 数据和 rectified 数据的文件组织、时间戳基准可能不同。它把“同步”和“矫正”拆成两个下载包,两者之间并没有保证文件名一一对应,尤其是不同版本解压后目录结构有差异。
解决:统一用时间戳做关联,不要依赖文件名前缀。先读所有数据源的 timestamp 字段,按 10Hz 周期取公共时间节点,再在这个节点上做传感器数据拼接。以 image_00 时间戳为基准,其他传感器全部插值到该时间轴上。
6. 设计取舍:三套真值系统的共同哲学与“尺子”困境
三个数据集都用了同一个策略:外部运动捕捉系统提供真值,而不是用传感器自身的测量当基准。这背后的取舍是——真值系统的精度必须比被测系统高一个量级,否则你量出来的“误差”里混着基准自身的噪声。Vicon 能做到毫米级,所以它能刻 IMU 和相机;运动捕捉系统做不到室外场景,所以 KITTI 只能退而求其次,用高精度 GPS/IMU 组合导航做基准。真值精度和场景真实度之间,本身就是一对矛盾。
ATE 与 RPE 的分工也藏着同一个设计取舍:绝对精度和相对一致性是两种不同的系统能力。ATE 对齐后比较全局轨迹,适合闭环 SLAM;RPE 只关心局部窗口的相对运动,适合漂移分析。没有一个指标能同时回答“系统最终有多准”和“系统会不会随时间飘”,所以数据集才要同时提供两个评测维度、两份脚本、两种参数调法。这种设计不是冗余,而是把“SLAM 系统性能”这个模糊概念拆成了两个可独立优化的目标。
从那以后,我每到一个新学到的数据集,都会先问三个问题:真值坐标系的原点定在哪;时间戳的单位是什么;传感器外参的变换方向按什么约定书写。这三个问题答对了,评估脚本才有意义;答错了,后面所有精度数字都是虚假的安慰。希望帮到你,也祝你在 SLAM 这条路上少走我走过的弯路。
本文还有配套的精品资源,点击获取