视觉SLAM评估避坑指南:TUM、KITTI、EuRoC真值格式与对齐机制详解
简介面向SLAM、视觉里程计与自动驾驶方向的学习者与研究者这份文档系统梳理了TUM RGB-D、KITTI与EuRoC三大主流数据集的采集平台、数据组织与评价方式帮助读者在选型、复现实验和量化评估时快速建立整体认知。压缩包共1个docx文件约1.93MB以图文条目形式分数据集展开TUM部分说明Kinect彩色图与深度图的存储规格、深度值缩放关系、groundtruth轨迹的位姿格式以及ATE与RPE两类评价准则和点云生成脚本KITTI部分介绍采集车传感器配置、raw data分类与标定、标签文件组织EuRoC部分概述微型无人机多传感器数据构成。已有9650人学习下载适合需要横向对比数据集差异、搭建评测流程或撰写论文实验章节的读者作为案头速查资料。1. 三个数据集三种“真值”为什么 SLAM 评估的坑从数据格式就开始了跑通一个视觉 SLAM 或 VIO 系统很多人第一反应是去下载 TUM、KITTI、EuRoC 这三个数据集然后照着 README 跑一遍评估脚本看到 ATE 数字降下来就觉得成了。但真正踩过坑的人会发现翻车往往不在算法本身而在“真值到底怎么定义、时间戳怎么对齐、坐标系怎么变换”这些看起来最不起眼的地方。TUM 用 Kinect 采集、深度图缩放 5000 倍、四元数顺序是 qx qy qz qwKITTI 用 64 线激光雷达和双目相机、所有传感器以 image_00 为同步参考系EuRoC 用双目加 IMU、真值来自动作捕捉系统、每个传感器都有自己的 sensor.yaml。三套数据、三种真值来源、三套坐标约定如果你不理解它们背后的设计动机评估脚本跑出来的数字很可能只是玄学。这篇就把这三个数据集的底层机制拆开讲清楚让你知道每一行数据为什么长这样。2. TUM RGB-D深度图为什么缩放 5000 倍ATE 又为什么必须先做 SVD 对齐TUM RGB-D 是理解 RGB-D SLAM 评估最好的入口因为它把“数据格式”和“评估准则”绑得很紧。你拿到的彩色图是 640x480 的 8 位 RGB PNG深度图是同样分辨率的 16 位单色 PNG但深度值不是米而是被放大了 5000 倍。这个设计不是随手定的它直接决定了你读深度时的第一行代码怎么写。2.1 深度图的 5000 倍缩放与 0 值语义16 位无符号整数能表示 0 到 65535如果直接用毫米做单位1 米就是 1000量程只到 65 米左右对于 Kinect 这种室内传感器够用但精度上每单位 1 毫米还是太粗。TUM 选择用 5000 作为比例因子意味着 1 米对应像素值 5000理论精度到 0.2 毫米同时量程覆盖到约 13 米正好匹配 Kinect 的有效深度范围。更关键的是像素值 0 被约定为“缺失值/没有数据”而不是“距离为 0”。这个语义如果搞错点云生成时会把大量无效点当成贴在相机上的点生成一堆噪声。常见的实现思路是读深度时先做类型转换再除以比例因子同时把 0 单独筛掉import numpy as np import cv2 # 读取 16 位单色深度图保持原始位深 depth_raw cv2.imread(depth/1305031102.175574.png, cv2.IMREAD_UNCHANGED) # 像素值 0 表示缺失必须先掩掉再缩放否则 0/5000 仍是 0 但语义错误 valid_mask depth_raw 0 depth_m depth_raw.astype(np.float32) / 5000.0 depth_m[~valid_mask] np.nan # 用 NaN 标记无效后续点云生成时过滤 print(depth_raw.dtype, depth_raw.shape) # uint16 (480, 640) print(np.nanmin(depth_m), np.nanmax(depth_m))这里IMREAD_UNCHANGED是关键参数默认的IMREAD_COLOR会把 16 位深度图强行转成 8 位三通道深度信息直接丢光这是血泪经验里最常见的一类翻车。valid_mask的作用是把缺失值和真实近距离区分开后面生成 PLY 点云时只对有效像素做反投影。2.2 groundtruth 的 txt 格式与四元数顺序TUM 的地面轨迹是纯文本每行八个字段timestamp tx ty tz qx qy qz qw。timestamp 是 Unix 纪元以来的秒数浮点tx ty tz 是彩色相机光心相对于运动捕捉系统世界原点的位置qx qy qz qw 是单位四元数表示相机光心的朝向。注意四元数的顺序是 x y z w而很多库比如某些 Eigen 接口或 ROS 的某些消息习惯 w 在前直接喂进去会导致旋转完全错乱轨迹画出来像被拧过的麻花。时间戳的语义也值得说清楚它是采集时刻的绝对时间不是相对第一帧的偏移。评估脚本用时间戳把估计轨迹和真值轨迹做关联如果你的估计轨迹时间戳是从 0 开始累加的相对时间关联就会全部错位ATE 会大得离谱。我一般会这么理解TUM 把时间戳当成“唯一对齐键”位置和姿态只是这个键上的负载键错了负载再准也没用。2.3 ATE 的 SVD 对齐为什么不能直接逐点相减绝对轨迹误差ATE衡量的是真实轨迹和估计轨迹之间的整体差异但两条轨迹通常不在同一个坐标系下——估计轨迹的起点是算法自己定的真值轨迹的起点是运动捕捉系统定的。所以评估前必须先做对齐。TUM 的脚本用时间戳关联配对后基于奇异值分解SVD求一个最优的刚体变换旋转加平移把估计轨迹对齐到真值轨迹上再计算每对位姿的差异输出均值、中值、标准差。这个 SVD 对齐的本质是求解一个最小二乘问题找旋转 R 和平移 t使对齐后的估计点与真值点的距离平方和最小。常见的实现思路是先去质心、算协方差矩阵、做 SVD、再根据行列式修正反射最后得到 R 和 t。如果不做这一步直接逐点相减你测到的其实是“两个坐标系原点的偏差”而不是算法精度。这也是为什么同一段估计轨迹对齐前后 ATE 可能差一个数量级。2.4 RPE 与漂移固定窗口为什么比全配对更合理相对位姿误差RPE测的是“相对运动”的误差适合评估视觉测距系统的漂移。TUM 的evaluate_rpe.py默认计算估计轨迹中所有时间戳对之间的相对运动误差但时间戳对的数量随轨迹长度是二次增长的长序列会爆炸。所以脚本提供了-max_pairs做下采样或者用-fixed_delta加-delta和-delta_unit指定固定窗口让每个位姿只和稍后的一个位姿关联。固定窗口的物理意义很直接它模拟“每秒漂移多少”这类指标。窗口太小噪声占主导窗口太大累积误差被平均掉看不出局部退化。实际用的时候室内小场景我一般取 1 秒左右的窗口长距离室外序列会放大到几秒具体看你想暴露的是高频抖动还是长期漂移。RPE 不关心全局对齐它只关心两帧之间的相对变换是否一致所以对坐标系原点不敏感这是它和 ATE 最本质的分工。3. KITTI64 线激光雷达和双目相机怎么在时间上“对齐”raw data 又为什么分同步与未同步KITTI 的复杂度比 TUM 高一个量级因为它不是单一传感器而是一整套采集平台1 个 GPS/IMU、1 个 Velodyne HDL-64E 激光扫描仪、2 个灰度相机、2 个彩色相机激光雷达 10 帧每秒相机也保持 10Hz并且在激光雷达朝向正前方时释放快门。这个“朝向正前方时释放快门”的细节就是整个同步机制的物理基础。3.1 以 image_00 为同步参考系的含义KITTI 明确说所有传感器的数据序列都以 image_00 作为同步参考系。这意味着时间对齐不是简单地把各传感器时间戳四舍五入到同一毫秒而是以参考相机的时间轴为基准把其他传感器的数据“归算”到这个轴上。激光雷达每帧 10 万多个点以.bin格式的 float 矩阵存储相机图像被裁减为 1382x512 像素校正后可能进一步变小。raw data 里同时提供“原始的没有同步和没有矫正的”和“处理过的同步和矫正过”两套双目图像前者带畸变且未同步后者消除畸变并做了双目矫正还和其他传感器对应好。一般我们用的是同步加矫正的数据因为只有这套数据的时间戳和图像内容才是严格对应的。如果你拿未同步的原始图去做视觉里程计再和激光雷达点云比对会发现每一帧都差那么几十毫秒高速运动时这个偏差足以让评估结果失真。这就是为什么 raw data 要分两套一套给你做标定和算法研究一套给你做公平评估。3.2 标定文件相机之间、相机与 IMU、相机与激光雷达KITTI 为每个序列提供标定文件txt 文本存储覆盖相机与相机之间、相机与 GPS/IMU 之间、相机与激光雷达之间的变换。这些变换不是可选项而是把不同传感器数据统一到同一坐标系的前提。比如你要把激光雷达点投影到图像上做目标检测就必须用相机与激光雷达之间的外参你要做视觉惯性里程计就必须用相机与 IMU 之间的外参和时间偏移。常见的实现思路是把标定文件读成 3x4 或 4x4 的变换矩阵注意 KITTI 的相机坐标系约定和很多 SLAM 库不同相机朝向是 z 轴向前、x 轴向右、y 轴向下而激光雷达坐标系通常是 x 向前、y 向左、z 向上。直接套用会导致点云投到图像上上下颠倒。这个坐标系约定的差异是跨数据集迁移时最容易翻车的地方之一。3.3 object 与 tracking单帧检测和连续序列的分工KITTI 按用途把数据分成 stereo、flow、sceneflow、depth、odometry、object、tracking、road、semantics 和 rawdata。其中 object 数据集用于目标检测包含 2D 检测框、3D 包围盒和鸟瞰图三部分每个下载包解压后有 training 和 testing 两个文件夹training 带标注。关键点是 object 的数据并不是严格连续采集的都是单帧结果只适合检测算法不适合跟踪。tracking 数据集组织方式类似但每个序列是连续的专门用于目标跟踪。这个分工背后是评估逻辑的差异检测看单帧的精度和召回跟踪看跨帧的 ID 保持和轨迹连续性。如果你拿 object 的单帧数据去训跟踪模型模型学不到帧间运动模式评估数字会很难看但这不是模型的问题是数据选错了。odometry 部分则提供连续序列和位姿真值用于里程计评估和 raw data 的区别在于它已经整理成适合位姿评估的形式。4. EuRoC双目加 IMU 的真值从哪来sensor.yaml 里的变换到底谁变谁EuRoC 是三个数据集里坐标系约定最讲究的一个因为它面向的是微型无人机MAV的视觉惯性里程计。数据来自装备双目相机和 IMU 的四旋翼真值由 Vicon 动作捕捉系统提供通过在飞行器上贴一组反射标志以 100Hz 帧率、毫米级精度给出单一坐标系下的 6D 位姿。4.1 四个传感器文件夹与 Body Frame 的定义EuRoC 的数据结构里有 cam0、cam1、imu0、leica0 四个文件夹分别对应两个相机、一个 IMU 和一个激光追踪器。prism 和 marker 共用一个坐标系而飞行器的 Body Frame 是以 IMU 的中心定义的。也就是说IMU 的传感器坐标系就是 Body Frame其他传感器都要通过各自的 sensor.yaml 变换到 Body Frame 下。每个传感器文件夹里配套一个 sensor.yaml描述该传感器相对于 Body 坐标系的坐标变换以及传感器内参。这个设计的动机很实际VIO 算法通常以 IMU 为核心做预积分把相机观测变换到 IMU 坐标系下融合。如果 Body Frame 不定义在 IMU 上每次融合都要多绕一层变换误差来源更多。把 IMU 定为 Body Frame等于把“最频繁使用的坐标系”设成原点这是工程上的合理取舍。4.2 旋转坐标系变换约定TBS 到底把谁变到谁EuRoC 文档里给了一个容易读反的约定设相机坐标系 pc 下的向量 (c1,c2,c3) 变换为世界坐标系 pw 下的向量 (w1,w2,w3)公式里的 RWC 意思是“将一个在 C 坐标系下表示的点的坐标变换成以 W 坐标系下表示的点的坐标”。所以 TBS 表示把所有 Sensor 坐标系下的向量变为以 Body Frame 坐标系下的向量。这个“下标从右往左读”的约定是很多人翻车的地方。如果你把 TBS 理解成“从 Body 变到 Sensor”所有外参都会用反VIO 初始化时相机和 IMU 的相对姿态直接错掉轨迹会缓慢发散而不是立刻崩排查起来非常痛苦。我一般会这么记下标右边是源坐标系左边是目标坐标系TBS 就是把 S 系下的量变到 B 系。写代码时先拿一个已知点验证一遍变换方向比盯着公式猜靠谱得多。4.3 groundtruth 的 18 位时间戳与对齐策略EuRoC 的 groundtruth 输出格式里时间戳是 18 位纳秒级整数。这个精度远高于 TUM 的浮点秒和 KITTI 的常规时间戳原因是 IMU 数据频率高通常 200HzVIO 评估需要亚毫秒级的时间对齐否则预积分和真值比对会有系统性偏差。18 位纳秒时间戳在 64 位整数范围内不会溢出同时保留了足够精度。对齐策略上EuRoC 的真值来自动作捕捉系统和相机、IMU 是两套独立时钟所以评估前必须做时间偏移估计。常见的做法是用互相关或者手动标定一个固定偏移再按最近邻关联。如果忽略这个偏移ATE 里会混入一个和运动速度成正比的误差项看起来像算法有恒定延迟其实是时钟没对齐。5. 避坑与排查跨数据集评估时最容易翻车的五个地方5.1 深度图读成 8 位点云全是噪声现象用 OpenCV 默认方式读 TUM 深度图生成的点云形状完全不对或者深度值全在 0 到 255 之间。 原因cv2.imread默认IMREAD_COLOR把 16 位单色图转成 8 位三通道高位信息被截断。 解决显式传cv2.IMREAD_UNCHANGED读完后检查dtype是否为uint16再除以 5000 并掩掉 0 值。5.2 四元数顺序搞反轨迹被拧成麻花现象TUM 轨迹可视化时旋转完全错乱位置对但姿态乱飞。 原因TUM 的 txt 是qx qy qz qw而某些库或消息格式期望qw qx qy qz直接按列取会错位。 解决读入后显式按字段名重组构造四元数前打印前几个值用单位四元数模长接近 1 做 sanity check。5.3 时间戳用相对时间ATE 大得离谱现象估计轨迹和真值轨迹形状相似但 ATE 数值异常大对齐后仍然很差。 原因估计轨迹时间戳从 0 开始累加真值是 Unix 绝对秒关联时全部错位。 解决统一到同一时间基准或者关联前先做时间偏移估计检查关联上的配对数量是否接近帧数。5.4 KITTI 相机与激光雷达坐标系约定混用现象激光雷达点投影到图像上上下颠倒或左右镜像。 原因KITTI 相机是 z 向前、y 向下激光雷达是 x 向前、z 向上外参矩阵的坐标系定义没对齐。 解决读标定文件后先用一个已知地面点验证投影方向确认旋转矩阵的列向量语义再批量处理。5.5 EuRoC 的 TBS 变换方向用反现象VIO 初始化后轨迹缓慢发散短期看着正常几十秒后明显漂移。 原因把 TBS 理解成 Body 到 Sensor外参用了逆变换相机和 IMU 的相对姿态错误。 解决按“右源左目标”的约定重读公式用一个已知向量手动验证变换方向再接入算法。6. 设计取舍为什么没有一套“通用真值格式”以及我现在的评估习惯三个数据集放在一起看会发现它们对“真值”的定义根本不同TUM 的真值来自运动捕捉系统是外部高精度基准所以它敢用 SVD 对齐后算 ATE因为真值本身可信KITTI 的真值一部分来自 GPS/IMU 融合一部分来自人工标注所以它把 raw data 分成同步与未同步两套把评估的公平性建立在“用哪套数据”的约定上EuRoC 的真值同样来自动作捕捉但它面向 VIO所以把 Body Frame 定在 IMU 上、用 18 位纳秒时间戳、用 sensor.yaml 显式描述每个传感器的外参。这套差异不是历史遗留的混乱而是各自评估目标的直接映射。TUM 要的是“RGB-D SLAM 的端到端精度”所以深度图缩放因子和 ATE 脚本绑死KITTI 要的是“自动驾驶多传感器融合的公平比较”所以同步参考系和标定文件必须显式EuRoC 要的是“视觉惯性里程计的漂移特性”所以时间戳精度和外参约定必须严格。想用一套通用格式统一它们反而会丢掉这些评估语义。维度TUM RGB-DKITTIEuRoC真值来源运动捕捉系统GPS/IMU 融合 人工标注动作捕捉系统核心传感器KinectRGB 深度双目 64 线激光雷达 GPS/IMU双目 IMU时间戳精度浮点秒常规时间戳18 位纳秒坐标约定要点四元数 qx qy qz qw相机 z 前 y 下雷达 x 前 z 上Body Frame 定在 IMUTBS 右源左目标主评估指标ATE RPE检测/跟踪/里程计多任务ATE RPEVIO 场景我现在的习惯是拿到任何一个数据集先不跑算法而是写一个最小验证脚本读一帧深度或一帧点云、打印时间戳范围、用单位向量验证一次外参变换方向、把真值轨迹画出来看一眼。这套动作花不了十分钟但能挡掉上面五类坑里的绝大多数。从那以后我每次跨数据集迁移评估流程都强制先走一遍这个最小验证再谈算法调参。希望帮到你。本文还有配套的精品资源点击获取