简介:这份资源面向SLAM、视觉里程计与自动驾驶方向的研究者和初学者,系统梳理了TUM RGB-D、KITTI与EuRoC三大经典公开数据集的构成与使用要点。压缩包共含1个docx文档,大小约1.93MB,适合快速查阅比对。TUM部分详细说明了彩色图与深度图的存储格式、比例缩放规则、groundtruth轨迹的txt字段含义,以及ATE和RPE两种评价准则的计算思路,并给出点云生成脚本的使用方法;KITTI部分介绍了基于大众帕萨特B6的采集平台,涵盖惯性导航系统、激光扫描仪与双目相机等传感器的数据组织方式;EuRoC部分则概述了微型无人机多传感器数据集的适用场景。已有9610人学习浏览,对于需要选取数据集、理解数据格式或开展量化评估的读者具有不错的参考价值。
1. 三大SLAM数据集对比:TUM、KITTI、EuRoC 到底该怎么选
做视觉SLAM和自动驾驶感知的人,迟早会撞上这三个数据集:TUM RGB-D、KITTI、EuRoC。TUM用Kinect相机采集室内桌面与走廊场景,带高精度真值和评估脚本,适合验证RGB-D SLAM的绝对轨迹误差(ATE)和相对位姿误差(RPE);KITTI是装在轿车上的多传感器套件,覆盖城市、住宅、校园道路,做自动驾驶的检测、跟踪、深度、里程计都得跟它打交道;EuRoC是双目相机加IMU的四旋翼数据,专门考验视觉惯性里程计在剧烈运动下的表现。这篇笔记不讲概念,直接拆文件格式、目录结构、评估脚本参数和混用时的踩坑点。新手可以照着跑通评估流程,熟手可以跳着查坐标系约定和参数细节。
2. TUM RGB-D 上手拆解:深度图缩放、轨迹格式与 ATE/RPE 评估脚本
2.1 文件格式先读对:彩色图、深度图与 5000 倍缩放系数
TUM 数据集的压缩包解压后,目录里基本就是 rgb/、depth/ 两个图像目录,加上 rgb.txt、depth.txt 两个时间戳清单,以及 groundtruth.txt。彩色图是 PNG,640x480 8位 RGB;深度图也是 PNG,640x480 16位单色。图像文件名就是采集时刻的时间戳,比如 1305031102.175304.png,这个设计在三个数据集里是一致的,后面会反复用到。
第一次跑的人经常翻车在深度图读取上:用 OpenCV 的默认参数读深度图,图像会直接被转成 8 位,近处物体全黑、远处全白,深度信息被截断得干干净净。正确做法是强制以 16 位无符号读入,然后按官方约定做缩放。
import cv2 import numpy as np depth_raw = cv2.imread("depth/1305031102.175304.png", cv2.IMREAD_UNCHANGED) depth_m = depth_raw.astype(np.float32) / 5000.0 depth_m[depth_raw == 0] = float("nan")这里第一个参数 IMREAD_UNCHANGED 表示保留原始位深,这是整个 TUM 读取流程里最容易错的一步,一旦默认读成 8 位,后续所有深度值都会变成 0 到 255 的近似等级,算出来的轨迹误差全是无效数据。第二行除以 5000,是因为官方约定深度像素值 5000 对应距相机 1 米,深度单位是米。第三行把像素值 0 的缺失区域置为 NaN,这类像素在 Kinect 上通常来自反光、太远或遮挡,属于无效测量,后续做点云和误差统计时必须排除,否则会把无效点当成相机近处的障碍物,直接带偏 ICP 或者建图结果。
拿到数据后我习惯先打印整张深度图的 min、max 和 NaN 比例,正常 TUM 序列里 NaN 占比一般在 30% 以内。如果一把跑出 NaN 比例过半,多半是图像时间戳和深度图对错了,这种问题在数据读取阶段解决,比进 SLAM 流程之后排查省时间得多。
提示:拿到任何数据集,第一件事是打印第一行时间戳和图像统计量,再决定后续流程。这一步能过滤掉一半以上的低级错误。
2.2 地面轨迹 txt:时间戳 + 平移 + 四元数的解析模板
TUM 的真值文件 groundtruth.txt 每行 8 个浮点数,格式为timestamp tx ty tz qx qy qz qw,全部由高精度运动捕捉系统给出。timestamp 是自 Unix 纪元以来的秒数;tx、ty、tz 是彩色相机光学中心在世界坐标系下的位置;qx、qy、qz、qw 是对应姿态的单位四元数。用四元数而不是欧拉角,是为了避免万向锁,也方便后续直接做刚体对齐和插值。
解析这段轨迹非常直接,numpy 一行就能读完:
import numpy as np data = np.loadtxt("groundtruth.txt") ts = data[:, 0] pos = data[:, 1:4] quat = data[:, 4:8] # 顺序:qx qy qz qw四元数顺序是 x y z w,这一点在把 TUM 轨迹转给其他框架时特别容易踩坑。有的框架内部用 w x y z 的顺序,有的库则要求单位四元数,直接喂进去姿态会乱成一片。另外,rgb.txt 和 depth.txt 里的时间戳并不是严格一一对应的,官方推荐的关联方式是最近邻匹配。我一般把时间差阈值设为 0.02 秒以内,超过阈值就丢帧,避免彩色图和深度图对错位置。这个关联操作在官方评估脚本里也有,但如果自己写数据预处理,需要单独实现一遍。
2.3 量化评估两条命令:evaluate_ate.py 与 evaluate_rpe.py 的参数语义
TUM 提供的官方评估脚本是整个数据集最有价值的部分。evaluate_ate.py 计算绝对轨迹误差(ATE),直接比较真值轨迹和估计轨迹的逐点位姿差;它先按时间戳关联,再用奇异值分解做刚体对齐,消除两个坐标系之间的旋转和平移偏差,最后输出均值、中值、标准差,也可以把两条轨迹画到 PNG 或 PDF 里。ATE 适合衡量整个 SLAM 系统在全局上有没有飘。
evaluate_rpe.py 计算相对位姿误差(RPE),比较的是时间戳对之间的相对运动误差,能反映视觉里程计的漂移速度,比如每秒的漂移量。默认情况下它会枚举估计轨迹里所有时间戳对,数量是轨迹长度的平方量级,所以必须限制采样,否则轨迹一长就直接卡死。
python evaluate_ate.py groundtruth.txt estimate.txt --plot ate.png --verbose python evaluate_rpe.py groundtruth.txt estimate.txt --fixed_delta 1 --delta_unit s --plot rpe.pngevaluate_rpe.py 里的关键参数是--fixed_delta和--delta_unit。固定时间窗口 1 秒、单位是秒,表示每间隔 1 秒的两个位姿算一次相对误差,这样算出来的 RPE 才能跟里程计的漂移对上。如果不用固定窗口,就要用--max_pairs限制随机抽样的对数,避免二次复杂度爆炸。
| 参数 | 作用 | 典型值 |
|---|---|---|
--plot | 输出轨迹对比图 png/pdf | ate.png / rpe.png |
--max_pairs | 限制随机时间戳对数量 | 10000 |
--fixed_delta | 固定相对运动窗口大小 | 1 |
--delta_unit | 窗口单位,s 为秒 | s |
评估前先确认 estimate.txt 的时间戳单位是秒,并且和 groundtruth 有时间交集。两段轨迹完全没有重叠时间戳时,脚本会直接报错;时间戳范围只交叠几帧时,评估结果也没有统计意义。我一般会先把两条轨迹用--plot画出来肉眼确认一遍,再记录数值指标,避免数值看着合理、轨迹实际飞了。
2.4 从彩色+深度图生成点云:PLY 输出与缺失值处理
官方提供的 generate_pointcloud.py 用彩色图和深度图生成 PLY 格式点云,可以直接用 meshlab 打开。相比直接调脚本,我更习惯自己写反投影,因为可以顺手把缺失值过滤掉。TUM 使用的 Kinect 相机内参比较固定,常见取值是 fx=fy≈525,cx≈319.5,cy≈239.5,但严谨一点应该先看官方提供的相机内参文件。
fx = fy = 525.0 cx, cy = 319.5, 239.5 h, w = depth_m.shape v, u = np.where(np.isfinite(depth_m)) z = depth_m[v, u] x = (u - cx) * z / fx y = (v - cy) * z / fy rgb = color[v, u, ::-1] # OpenCV 是 BGR,写 PLY 要转 RGB反投影公式就是针孔相机模型的逆过程:x=(u−cx)∗z/fx,y=(v−cy)∗z/fy。注意这里只选了有限深度值的像素参与计算,NaN 像素对应的行列已经被剔除。OpenCV 读进来的 color 是 BGR 顺序,写 PLY 时一定要转成 RGB,否则点云颜色会和真实场景左右互换。PLY 文件头里要声明 vertex 数量和属性类型,点云带颜色的话,属性要写 red、green、blue 三项。生成之后先在 meshlab 里看一眼,确认没有成片的空洞,再拿去跑配准或者做可视化。如果直接用 ROS,官方还提供了 bag 文件,里面打包了灰度图、深度图、点云和真值变换,rosbag play 之后可以直接在 rviz 里看实时点云,省去自己同步的工作。
3. KITTI 自动驾驶数据拆解:传感器同步、raw data 与 object/tracking 子集
3.1 采集平台与同步策略:10Hz 激光、双目相机与 1382x512 裁剪
KITTI 的采集车是一辆经过改装的帕萨特B6,车顶装了一台 64 线激光扫描仪,前挡风玻璃位置装了两台灰度相机和两台彩色相机,外加一套高精度 GPS/IMU 惯性导航系统。图像原始尺寸被裁剪为 1382x512,经过双目校正后还会进一步变小。所有传感器按激光雷达的 10Hz 节奏采样,相机快门在激光雷达朝向正前方时释放。
这个同步策略对自动驾驶数据至关重要。激光雷达是旋转扫描的,不同水平角度的点云实际采集时刻不同,如果相机快门在随机时刻触发,图像和点云之间会引入不可忽略的运动偏差。KITTI 用朝前触发的方式把这种偏差压到最小,后续做多传感器融合时能省掉一部分时间戳插值工作。选帕萨特B6这种普通轿车也很有讲究,采集的就是典型道路驾驶视角,而不是实验室里的小车视角,训练出来的模型直接面对真实路况。
| 传感器 | 关键参数 |
|---|---|
| 惯性导航系统(GPS/IMU) | 提供位置、姿态、速度 |
| 激光扫描仪 | 64线,10Hz,每帧十万点以上 |
| 灰度相机 x2 | 140万像素,10Hz |
| 彩色相机 x2 | 140万像素,10Hz |
| 镜头 | 4-8mm 变焦 |
3.2 raw data 目录组织:synced+rectified 与原始数据的取舍
raw data 按 Road、City、Residential、Campus、Person 分类,每个序列包含传感器原始数据、3D 目标跟踪标签和标定文件。解压后比较关键的是带 sync 的目录,也就是同步并校正过的版本,里面 image_00 到 image_03 分别对应左右灰度相机和左右彩色相机,velodyne_points 目录存 3D 激光点云,oxts 目录存 GPS/IMU 数据。所有传感器的数据都以 image_00 作为同步参考系,整体按 10Hz 对齐。
没有 sync 的目录保留的是原始带畸变图像,相机之间没有对齐,时间戳也没有和雷达严格同步,一般不直接拿来跑里程计或者立体匹配。我一般直接用 sync 版本,配套的标定文件 calib_cam_to_cam.txt 里能拿到投影矩阵。标定文件虽然多,解析逻辑却很简单,每行是 key: value 的格式,value 是一串浮点数,用一个通用函数就能读出来。
def read_kitti_calib(path): data = {} with open(path) as f: for line in f: if ":" in line: key, val = line.split(":", 1) data[key.strip()] = np.array(val.split(), dtype=np.float32) return data读取后重点关注 P_rect_02、P_rect_03 这类 3x4 投影矩阵,双目校正、图像反投影到雷达坐标系都要从它们出发。这里每一步都比直接调用现成库更能看清数据之间的关系。velodyne 点云是 .bin 文件,以 float32 矩阵存储,常见结构是每行四个数,前三维是 xyz,第四维是反射强度。读取这种二进制文件,重点是确认列数,有的子集是 Nx3,有的是 Nx4,读之前先算一下文件总字节数除以点数等于 12 还是 16,这个习惯能避免很诡异的解析错位。
对于做 SLAM 的人,KITTI 的 odometry 子集比 raw data 更顺手,它提供了连续序列的灰度图、点云和 groundtruth 位姿。odometry 真值文件每行 12 个数,按 3x4 矩阵排列,表示世界坐标系到相机坐标系的变换。这个格式和 TUM 的 8 位数格式完全不同,混用之前必须做转换,后面进阶章节会给出具体做法。
3.3 object 与 tracking:单帧检测与连续跟踪的标注差异
object 子集是给目标检测准备的,分 2D Object Detection、3D Object Detection 和 Bird's Eye View 三套评测,每套都有 training 和 testing 两个目录,training 里带标注文件。需要特别注意,object 数据不是严格连续采集的序列,而是从不同时刻抽出来的单帧结果,只能做检测,不能拿来做跟踪或里程计。标注内容包含目标类别、截断程度、遮挡等级、观测角度、2D 框、3D 框尺寸、位置和绕 Y 轴旋转角,类别覆盖汽车、卡车、火车、行人、骑车人。
tracking 子集则是连续序列,数据组织方式和 object 类似,区别在于帧与帧之间时间连续,标注以 XML tracklet 文件形式给出,记录每个目标跨帧的生命周期和状态变化。两个子集共用采集平台和时间戳体系,标定参数可以直接复用。不要因为标注文件长得像,就把 object 的单帧数据当序列喂给跟踪算法,那会让算法产生大量虚假的身份跳变,评估结果也会失真。下载时序数据前先想清楚自己要验什么:验检测模型选 object,验跟踪算法选 tracking,验里程计和建图选 odometry。
4. EuRoC MAV 双目惯性数据集:文件结构、坐标系约定与真值格式
4.1 四文件夹布局:cam0、cam1、imu0、leica0 各自装什么
EuRoC 是用一款四旋翼飞行器采集的,机载双目相机加 IMU,地面有运动捕捉系统提供毫米级真值。场景是室内大厅和机器大厅,飞行轨迹包括悬停、快速旋转、大机动,非常适合测试视觉惯性里程计在剧烈运动下的表现。序列解压后顶层是 cam0、cam1、imu0、leica0 四个传感器文件夹,外加一个 state_groundtruth_estimate0 文件夹。cam0 和 cam1 里各有一个 data 目录存同步后的图像序列,外加 sensor.yaml;imu0 存 IMU 测量数据;leica0 是激光测距数据。
文件结构如下:
MH_01_easy/ cam0/ data/ *.png sensor.yaml cam1/ data/ *.png sensor.yaml imu0/ data.csv sensor.yaml leica0/ data.csv sensor.yaml state_groundtruth_estimate0/ data.csv每个传感器文件夹下的 sensor.yaml 描述该传感器相对机体坐标系的安装位置和自身内参。很多人把它当摆设,实际上这里面的 T_BS 矩阵直接决定多传感器数据能不能对齐。cam0 和 cam1 的 sensor.yaml 还包含图像分辨率、相机模型、畸变系数,做去畸变和双目校正都要用,不要自己重新标定。
4.2 sensor.yaml 与坐标系变换:从 Sensor Frame 到 Body Frame
EuRoC 的坐标系约定比较绕,读错一次就能让整个轨迹翻车。它定义了 Sensor Frame、Body Frame、World Frame 三层。官方规定飞行器的 Body Frame 以 IMU 中心为原点,所以 imu0 的 Sensor Frame 就是 Body Frame。sensor.yaml 里的 T_BS 表示把 Sensor 坐标系下的向量变换到 Body Frame 坐标系下,写出来就是 p_B = T_BS * p_S。世界坐标系由运动捕捉系统定义,真值给的是世界坐标系下的位姿。
多个传感器统一到世界坐标时,把两条变换链拼起来就行:
import yaml import numpy as np cam0_cfg = yaml.safe_load(open("cam0/sensor.yaml")) T_BS = np.array(cam0_cfg["T_BS"]).reshape(4, 4) T_WB = load_groundtruth_pose() # 从真值解析 T_WS = T_WB @ T_BST_BS 的旋转部分下标要读清楚:R_BS 表示 B 系相对 S 系的姿态,也就是把 S 系坐标转到 B 系。如果 sensor.yaml 里给的是从 Body 到 Sensor 的矩阵而你没注意,双目图像投影到世界坐标后整体会绕着一个奇怪轴心旋转,ATE 直接飙到米级却找不到原因。我每次换序列的第一件事,是把 T_BS 和 T_WB 都打印出来,手动验证一下 T_BS 作用在 Sensor 原点上得到的偏移是否和机械安装图一致。
注意:打印 T_BS 后,用该矩阵乘以 Sensor 原点 [0,0,0,1],得到的位置应该与安装偏移吻合。这一步能提前拦住九成坐标系错误。
4.3 groundtruth 的数据格式:18 位时间戳与六维位姿真值
state_groundtruth_estimate0/data.csv 是真值文件,常见列结构是:timestamp、p_RS_R 位置、q_RS_R 姿态四元数、v_RS_R 速度、b_w 陀螺零偏、b_a 加速度计零偏。四元数顺序对视觉惯性融合很关键,官方给的是 x y z w,和 TUM 的 qx qy qz qw 一致,但和某些库默认的 w x y z 不同,做接口时统一转换一次最省心。
时间戳是纳秒级整数,官方说明里是 18 位时间戳,单位不是秒。这一点和 TUM 的秒级浮点时间戳差别很大,混用时最容易出事。读取真值我习惯这样写:
import pandas as pd gt = pd.read_csv("state_groundtruth_estimate0/data.csv") ts_s = gt["timestamp"].to_numpy() / 1e9 pos = gt[["p_RS_R_x", "p_RS_R_y", "p_RS_R_z"]].to_numpy() quat = gt[["q_RS_R_x", "q_RS_R_y", "q_RS_R_z", "q_RS_R_w"]].to_numpy()除以 1e9 之后时间戳才是秒,才能和相机图像时间戳统一匹配。如果直接把 18 位整数当秒用,估计轨迹会被放大十亿倍,任何评估指标都会失去意义。EuRoC 的图像文件名也是纳秒时间戳,所有传感器在采集阶段做了硬件同步,但不同传感器之间依然存在微小的时间偏移,严谨的做法是最近邻匹配时把阈值设在 1ms 量级,同时检查 cam0 和 cam1 的时间戳是否严格成对。运动捕捉系统提供的真值是单一坐标系下的 6D 位姿,帧率 100Hz,精度到毫米,配合 IMU 零偏真值,可以完整评估视觉惯性里程计的状态估计质量。
5. 避坑指南:三个数据集混用时的 5 个翻车点
5.1 数据读取期的三个坑:深度图、时间戳单位、未同步数据
坑一:深度图全黑或全白。现象是 TUM 深度图加载后一片黑,或者近处物体轮廓全糊。原因是用了 cv2.imread 的默认模式,深度 PNG 被转成 8 位,16 位深度值被截断。解决是 IMREAD_UNCHANGED 读入,转 float32 后除以 5000,并把 0 像素置为 NaN。这是我见过最多的翻车点,十个跑 TUM 的人里至少三个栽在这里。
坑二:ATE 数值大得离谱。现象是同一段数据,用 TUM 脚本评估,ATE 均值从厘米级突然变成几十米。原因是时间戳单位混用,常见于把 EuRoC 的 18 位纳秒时间戳当成秒,或者把 TUM 的秒级时间戳当成纳秒。解决是先打印数据第一行时间戳:秒级大概是 10 位带小数,纳秒级是 18 位整数,任何评估之前统一用秒,EuRoC 记得除 1e9。这个问题不会报错,只会让你对着一个毫无意义的数字怀疑自己的算法。
坑三:KITTI 选了没有 sync 的目录。现象是图片和点云内容对不上,同一个路口,图像里车辆已经在路口中间,点云里车还在路口后面。原因是拿了原始未同步数据,图像带畸变且时间没有对齐。解决是只下载带 sync 的版本,以 image_00 作为同步参考系,配套标定文件里用投影矩阵做校正。KITTI 的原始数据目录和同步目录并存,解压后别顺手把原始目录当成输入。
5.2 评估期的两个坑:RPE 复杂度爆炸、坐标系姿势搞反
坑四:RPE 脚本跑半天不出结果。现象是轨迹文件几千帧,evaluate_rpe.py 直接卡死。原因是脚本默认枚举所有时间戳对,数量是帧数的平方,5000 帧就是两千五百万对,还要逐对做差和姿态分解,计算量巨大。解决是加--max_pairs限制采样对数,或者用--fixed_delta固定窗口。窗口大小按数据集运动速度选,1 秒对里程计漂移评估比较常见,剧烈运动场景可以缩短到 0.1 秒。
坑五:位姿旋转方向装反。现象是 TUM 轨迹可视化后整体正常,但每个局部都在抖动;或者 EuRoC 双目点云叠加真值后像照镜子。原因是坐标系约定没统一,T_WS 写成了 T_WB @ T_SB,把从 Sensor 到 Body 的矩阵当成了从 Body 到 Sensor。解决是先把 T_BS、T_WB 打印出来验证,按第 4 章的方法把 Sensor 原点乘进去看偏移是否和安装一致。坐标系问题属于最典型的玄学问题,不对齐时所有指标都会异常,但异常模式又很随机,最有效的办法就是从一开始逐层打印变换,别等跑到最后才怀疑标定。
6. 进阶:把三套数据接进同一套评测流程
三套数据格式差异大,但评测流程可以统一。我现在所有数据集都是先转成 TUM 风格,也就是秒时间戳加 tx ty tz 加 qx qy qz qw,转完直接喂 evaluate_ate.py。转换器只需要三个入口函数,每种格式写一个静态方法就行:
class TrajectoryLoader: @staticmethod def load_tum(path): data = np.loadtxt(path) return data[:, 0], data[:, 1:4], data[:, 4:8] @staticmethod def load_kitti_odom(path): mats = np.loadtxt(path).reshape(-1, 3, 4) ts = np.arange(len(mats)) pos = mats[:, :, 3] quat = rotmat_to_quat(mats[:, :, :3]) return ts, pos, quat @staticmethod def load_eurco(path): df = pd.read_csv(path) ts = df["timestamp"].to_numpy() / 1e9 pos = df[["p_RS_R_x", "p_RS_R_y", "p_RS_R_z"]].to_numpy() quat = df[["q_RS_R_x", "q_RS_R_y", "q_RS_R_z", "q_RS_R_w"]].to_numpy() return ts, pos, quatKITTI 的 3x4 矩阵转成平移加四元数时,要先判断矩阵排布是 R|t 还是 t|R,odometry 子集一般是前者;EuRoC 真值里直接有四元数,但要确认 x y z w 顺序。转换完成后先画一次轨迹叠加图,肉眼确认两条轨迹基本重合,再跑论文里的误差指标,得到的数字才可信。从那以后,我每次接新数据集都强制走一遍这个流程:先统一时间戳,再统一位姿格式,最后可视化验证,任何一步对不上就先停。三个数据集轮流用的时候,这套骨架能省掉大量重复代码,也能防止把 KITTI 的矩阵格式当成 TUM 的四元数格式一路错到底。希望帮到你。
本文还有配套的精品资源,点击获取