KITTI Raw Data转TUM格式:位姿基准、坐标变换与SLAM评估避坑指南

📅 发布时间:2026/10/14 11:36:41
KITTI Raw Data转TUM格式:位姿基准、坐标变换与SLAM评估避坑指南
简介面向运行VINS-Fusion等SLAM算法时进行精度比对的开发者这份资源集中整理了KITTI raw data中00-10序列的groundtruth位姿与时间戳并给出了可直接使用的TUM格式位姿轨迹。包内还包含各序列对应的轨迹图、数据集sequence说明以及一个转换脚本将原始poses和times转换为TUM格式省去自行编写解析程序的麻烦。压缩包共47个文件以txt文本为主配合png/jpg轨迹图和py脚本整体仅3.54MB结构清晰、按序列组织。目前已有3747人学习下载。对于刚接触KITTI数据集格式或需要快速对照评估结果的研究者来说这套整理好的基准数据能显著减少数据预处理时间让算法调试与误差对比更加高效。1. 很多人第一次用KITTI raw data做SLAM评估都在位姿基准上翻了车做视觉SLAM或里程计评估的同学迟早会碰到KITTI数据集raw data。跟Odometry benchmark里已经处理好的groundtruth不同raw data里没有现成的位姿文件——只有oxts文件夹里一堆以时间戳命名的文本文件记录的是GPS/IMU融合后的经纬度、高度和姿态角。很多人直接把这些值当平移向量用结果轨迹飞出了银河系。真正的问题是KITTI raw data的groundtruth不是视觉坐标系下的相对位姿而是全局经纬度坐标必须经过坐标变换和时间戳对齐才能转成TUM格式的位姿序列喂给evo这类评估工具。这篇文章就把整条链路拆开讲清楚groundtruth的数学定义、时间戳怎么读、TUM格式怎么转以及最容易被忽略的四个坑。2. KITTI raw data里的groundtruth到底长什么样2.1 oxts文件夹的数据组织方式KITTI raw data的每个序列比如2011_09_26_drive_0005_sync解压后sync目录下有image_00、image_01、oxts等子目录。oxts文件夹每个时间戳一个txt文件命名跟image_00里同名。每个文件里只有一行共30个数值前17个是GPS/IMU原始解算值后13个是位姿相关参数。这30个数值分三块位置lat、lon、alt、姿态roll、pitch、yaw和速度/角速度最后还有一列position accuracy一般是0。真正转groundtruth位姿用到的是前6个值加最后3个矫正参数。oxts文件的命名时间戳本身就是跟图像帧对齐的线索。我一般先把一个序列里所有oxts文件名读出来按字典序排好后就是跟图像帧一一对应的顺序。2.2 groundtruth位姿的数学基准为什么不能拿经纬度直接用KITTI官方提供的raw data中oxts里记录的是WGS84坐标系下的经纬度和海拔外加IMU测出来的roll、pitch、yaw。这不叫groundtruth位姿这叫原始传感器输出。要做SLAM评估得先把经度纬度转成平面坐标再以第一帧为原点算出每一帧相对第一帧的旋转和平移才是轨迹评估用的groundtruth。转成平面坐标的常见做法是UTM投影。KITTI官方文档里给的是Mercator投影近似实际工程里直接用pyproj做UTM投影误差在厘米级足够SLAM评估用。核心步骤是from pyproj import Proj def latlon_to_utm(lat, lon): # WGS84 - UTM 50NKITTI采集区域在德国卡尔斯鲁厄附近 proj Proj(projutm, zone50, ellpsWGS84, datumWGS84, unitsm) x, y proj(lon, lat) # 注意pyproj的API是proj(lon, lat) return x, y逻辑说明UTM投影把经纬度转成以米为单位的平面坐标x、y。KITTI序列采集位置都在UTM 50N区带内所以zone固定填50即可。需要注意pyproj的接口签名是proj(lon, lat)而不是proj(lat, lon)很多人在这一步就把x、y写反了。有了UTM坐标再加上IMU的roll、pitch、yaw就可以构造每一帧的4x4位姿矩阵。但这样构造出来的矩阵第一帧不是单位阵而是某个全局坐标系下的位姿。要变成评估用的groundtruth轨迹需要把第一帧的位姿矩阵求逆然后左乘到所有帧上。这一步缺失所有评估指标APE、RPE算出来都是错的。3. 时间戳raw data的时间同步与TUM格式的时间基准3.1 timestamp.txt的结构与坑KITTI raw data的sync目录下有一个timestamp.txt每行一个浮点数单位是秒是GPS时间戳。这个文件的行数跟oxts文件夹下的文件数、image_00下的图片数完全一致一一对应。但这里有个坑KITTI原始数据采集时相机帧率和IMU输出频率不完全一致sync目录下的数据其实已经被官方用插值对齐过了所以timestamp.txt里每帧的间隔并不严格等于标称帧率比如10Hz而是有微小抖动。做法是把timestamp.txt读进来作为绝对时间基准后续转TUM格式的时间戳直接用它不要自己按帧率去补时间。timestamps [] with open(timestamp.txt, r) as f: for line in f: timestamps.append(float(line.strip()))逻辑说明这样得到的时间戳序列跟图像、oxts一一对应。KITTI的timestamp是GPS秒起点不是0TUM格式本身不要求时间从0开始所以可以直接用。但如果后面要用evo做时间对齐建议把时间戳减去第一帧的值变成相对时间数值更小对齐时不容易出现浮点精度问题。3.2 时间戳对齐的一个隐蔽问题raw data里除了sync目录还有raw目录未同步版本。raw目录下的oxts文件数量跟图像不一致因为IMU是100Hz、相机是10Hz。很多教程直接让你解压raw data后去raw目录里找数据然后按文件名配对结果文件数对不上时间戳也对不上。正确做法是只使用sync目录那里已经做过时间同步。另外注意timestamp.txt里的浮点数精度是微秒级但KITTI官方同步时本身有约1ms的误差。做SLAM评估时这1ms对视觉里程计影响不大但如果你的系统是IMU融合的高帧率里程计1ms的误差在高速运动下会带来可见的漂移。这时评估结果里会看到APE偏高不是你的算法问题是groundtruth本身的时间精度天花板。4. 把KITTI raw data的groundtruth转成TUM格式位姿完整实现4.1 位姿矩阵构造与相对化处理TUM格式是RGB-D SLAM Benchmark定义的轨迹格式每行7个数时间戳、tx、ty、tz、qx、qy、qz、qw注意顺序是最后才是qw。时间戳单位秒位置单位米四元数必须是单位四元数。核心转换流程分成四步读oxts参数、算UTM坐标、构造全局位姿、相对化到第一帧。import numpy as np from pyproj import Proj import os, glob def read_oxts(oxts_path): # 读取单个oxts文件返回位置和姿态 with open(oxts_path, r) as f: values [float(v) for v in f.readline().strip().split()] lat, lon, alt values[0], values[1], values[2] roll, pitch, yaw values[3], values[4], values[5] return lat, lon, alt, roll, pitch, yaw def oxts_to_pose(lat, lon, alt, roll, pitch, yaw, proj): # UTM坐标转米制平面坐标 x, y proj(lon, lat) z alt # 由欧拉角构造旋转矩阵KITTI的roll/pitch/yaw是绕x/y/z轴的旋转 rx np.array([[1, 0, 0], [0, np.cos(roll), -np.sin(roll)], [0, np.sin(roll), np.cos(roll)]]) ry np.array([[np.cos(pitch), 0, np.sin(pitch)], [0, 1, 0], [-np.sin(pitch), 0, np.cos(pitch)]]) rz np.array([[np.cos(yaw), -np.sin(yaw), 0], [np.sin(yaw), np.cos(yaw), 0], [0, 0, 1]]) R rz ry rx T np.eye(4) T[:3, :3] R T[:3, 3] [x, y, z] return T def traj_to_tum(timestamps, poses): # 转成TUM格式的Nx8数组 tum [] for t, T in zip(timestamps, poses): tx, ty, tz T[:3, 3] qx, qy, qz, qw rotmat_to_quat(T[:3, :3]) tum.append([t, tx, ty, tz, qx, qy, qz, qw]) return np.array(tum)逻辑说明read_oxts读出来的roll、pitch、yaw单位是弧度。KITTI的惯性坐标系是x-forward、y-left、z-up而视觉SLAM常用坐标系是x-right、y-down、z-forward所以如果直接把KITTI的R拿去做比较坐标系不一致会导致APE巨大。处理办法有两种一是构造位姿时把坐标轴交换一下二是在评估时将轨迹转换到同一个坐标系下。我一般在转换时就做坐标变换让输出轨迹的坐标系跟TUM的视觉坐标系一致。4.2 坐标系变换的一个关键步骤常规做法是KITTI的IMU坐标系定义是x向前、y向左、z向上而TUM格式来自RGB-D相机通常是x向右、y向下、z向前。直接使用会导致轨迹看起来旋转了90度。处理方式是在构造旋转矩阵后乘一个固定变换矩阵# KITTI IMU坐标系到视觉坐标系x右 y下 z前的映射 C np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]], dtypefloat) R_cam C R逻辑说明这个变换矩阵把KITTI的x-forward映射到视觉的z-forwardy-left映射到x-rightz-up映射到y-down。不做的结果是轨迹形态对但方向不对evo画图出来轨迹绕了90度后续跟自己的VO轨迹比直接GG。4.3 第一帧相对化groundtruth基准的核心做完坐标变换后把第一帧位姿求逆左乘所有帧得到以第一帧为原点、朝向为基准的相对轨迹。这一步是raw data转TUM和Odometry benchmark自带groundtruth的本质区别。T0_inv np.linalg.inv(poses[0]) poses_rel [T0_inv T for T in poses]逻辑说明poses是全局UTM坐标系下的位姿。左乘T0_inv后第一帧的位姿变成单位阵整个轨迹变成从原点出发。这样得到的groundtruth才跟视觉SLAM的输出在同一参考系下可以直接用evo的evo_ape tum gt.txt est.txt做评估。如果不做这一步全局坐标下的绝对位置误差会被UTM投影原点处的偏移量污染APE数值巨大且没有意义。4.4 完整转换脚本把上面所有步骤串起来一个最小可用脚本如下import numpy as np from pyproj import Proj import os, glob def rotmat_to_quat(R): # 旋转矩阵转四元数返回[qx, qy, qz, qw] tr np.trace(R) if tr 0: S np.sqrt(tr 1.0) * 2 qw 0.25 * S qx (R[2,1] - R[1,2]) / S qy (R[0,2] - R[2,0]) / S qz (R[1,0] - R[0,1]) / S else: # 处理对角线为负的情况KITTI数据一般用不到 d np.diag(R) i np.argmax(d) if i 0: S np.sqrt(1 R[0,0] - R[1,1] - R[2,2]) * 2 qw (R[2,1] - R[1,2]) / S qx 0.25 * S qy (R[0,1] R[1,0]) / S qz (R[0,2] R[2,0]) / S elif i 1: S np.sqrt(1 R[1,1] - R[0,0] - R[2,2]) * 2 qw (R[0,2] - R[2,0]) / S qx (R[0,1] R[1,0]) / S qy 0.25 * S qz (R[1,2] R[2,1]) / S else: S np.sqrt(1 R[2,2] - R[0,0] - R[1,1]) * 2 qw (R[1,0] - R[0,1]) / S qx (R[0,2] R[2,0]) / S qy (R[1,2] R[2,1]) / S qz 0.25 * S return qx, qy, qz, qw def convert_kitti_raw_to_tum(seq_path, out_path): oxts_dir os.path.join(seq_path, oxts) ts_path os.path.join(seq_path, timestamp.txt) # 按文件名排序保证oxts与图像帧对齐 oxts_files sorted(glob.glob(os.path.join(oxts_dir, *.txt))) timestamps [] with open(ts_path, r) as f: for line in f: timestamps.append(float(line.strip())) assert len(oxts_files) len(timestamps), ( foxts与timestamp数量不一致: {len(oxts_files)} vs {len(timestamps)} ) proj Proj(projutm, zone50, ellpsWGS84, datumWGS84, unitsm) # 坐标系变换KITTI IMU坐标 - 视觉坐标 C np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]], dtypefloat) poses_global [] for f in oxts_files: lat, lon, alt, roll, pitch, yaw read_oxts(f) T oxts_to_pose(lat, lon, alt, roll, pitch, yaw, proj) T[:3, :3] C T[:3, :3] # 坐标系变换 poses_global.append(T) # 相对化以第一帧为原点 T0_inv np.linalg.inv(poses_global[0]) poses_rel [T0_inv T for T in poses_global] # 输出TUM with open(out_path, w) as f: for t, T in zip(timestamps, poses_rel): qx, qy, qz, qw rotmat_to_quat(T[:3, :3]) tx, ty, tz T[:3, 3] f.write(f{t:.6f} {tx:.6f} {ty:.6f} {tz:.6f} f{qx:.6f} {qy:.6f} {qz:.6f} {qw:.6f}\n)逻辑说明脚本按文件名排序读取oxts保证与timestamp.txt一一对应。UTM投影固定用zone 50KITTI采集范围都在这个区带内跨区带的情况用KITTI数据不会遇到。坐标系变换矩阵C在每次构造位姿后立即作用这样后续的相对化、四元数提取都在视觉坐标系下完成。输出时时间戳保留6位小数微秒级位置和四元数保留6位小数足够evo评估使用。参数说明zone50是KITTI采集地所在UTM区带如果你的数据来自其他区域需要改成对应区带否则投影误差会达到数百米。unitsm指定输出单位为米。四元数转换时tr 0是常规分支这个脚本里也覆盖了对角线特殊分支实际KITTI数据里基本走不到但写全了避免碰到特殊姿态角时翻车。5. raw data转TUM的5个必踩坑与排查方法5.1 直接用lat/lon当平移向量轨迹完全错乱现象转出来的TUM轨迹看着像一团乱线位置数值非常大几十万米量级。 原因经纬度是角度单位不是米。直接写进TUM的tx/ty等于把角度当长度用。 解决必须做UTM投影把经纬度转成米制平面坐标。投影后x、y的量级在几十万米是正常的因为UTM原点在赤道和中央经线附近KITTI的数据距离原点很远。相对化后这个偏移会被消除最终轨迹的坐标量级在几十米到几百米。5.2 没有做第一帧相对化APE计算结果全错现象evo的APE结果巨大几十米到上百米但轨迹形态看着是正常的。 原因groundtruth还是全局坐标系下的绝对位姿而你的VO轨迹是从原点出发的相对轨迹。两者之间差了一个固定变换APE算的是绝对位置误差这个固定偏移直接计入误差。 解决在构造完全局位姿后用T0_inv T把所有位姿相对化到第一帧。验证方法转换完成后检查第一行的tx/ty/tz是否接近0、四元数是否接近(0,0,0,1)。5.3 pyproj里lon/lat传参顺序反了现象转出来的轨迹形状像是旋转了90度或镜像翻转UTM坐标数值跟预期差很远。 原因pyproj的Proj对象调用时签名是proj(lon, lat)不是proj(lat, lon)。习惯写经纬度的人很容易传反。 解决在代码里写清楚注释或者用一个包装函数固定传入顺序。检查方法转出第一个点的UTM坐标后用在线工具反算一下经纬度看是不是跟原始值一致。不一致就先查参数顺序。5.4 四元数顺序写成qw qx qy qz现象evo读取轨迹后旋转部分完全乱了画出来的轨迹朝向全是错的。 原因TUM格式规定四元数的顺序是qx qy qz qw最后一个才是实部。很多人习惯numpy/scipy的[w, x, y, z]顺序写输出时直接复制过来了。 解决输出前先打印一行的四个四元数值确认最后一个数的绝对值最大因为KITTI数据的姿态变化不会很大qw一般在0.7以上。如果qw不是最大值基本就是顺序搞反了。输入evo时它不会帮你检查顺序错了就全错。5.5 raw目录和sync目录混用文件数量对不上现象oxts文件数和图像帧数差很多或者代码报list index out of range。 原因raw目录下存的是未同步的原始数据IMU有100Hz相机只有10Hz文件数天然不相等。sync目录才是对齐后的版本。 解决只用sync目录里的oxts和timestamp.txt。解压raw data后进去看如果有sync和raw两个目录一律处理sync。另外注意sync目录里也可能有几个文件损坏KITTI数据集下载不完整时常见先检查文件数是否一致再跑转换脚本。6. 用evo验证转换结果的三个技巧6.1 先画轨迹再算指标转换得到TUM格式的groundtruth后第一件事不是算APE而是用evo画轨迹图看看形状对不对。命令是evo_traj tum gt.txt --plot_modexz逻辑说明KITTI的车辆主要在地平面运动xz平面的投影能直观看出轨迹形状是否像一个正常的驾驶路线。如果轨迹突然跳到远处再跳回来或者形状像锯齿大概率是四元数或时间戳出问题了。轨迹形态正常再进入下一步指标计算。6.2 用Odometry benchmark的groundtruth做交叉验证KITTI Odometry benchmark里有现成的groundtruth位姿00-10序列格式是KITTI格式的12位姿序列。如果手头有对应序列的Odometry数据可以把raw data转出来的TUM和Odometry的groundtruth做个对比evo_traj tum raw2tum.txt kitti odom_gt.txt --ref raw2tum.txt -a逻辑说明KITTI Odometry的groundtruth已经做过相对化处理坐标系定义是相机坐标系跟raw data转出来的轨迹经过坐标变换后应当一致。用-a做轨迹对齐后如果两条轨迹的APE均值在0.1米以内说明转换正确。如果误差有几米检查坐标系变换矩阵C是否写对了。6.3 时间戳对齐的验证TUM格式评估时evo默认会做时间戳对齐--align相关参数。如果两条轨迹时间基准不一致对齐会引入额外误差。验证方法是检查转换文件的时间戳是否与图像时间戳严格对应import numpy as np tum np.loadtxt(gt.txt) ts np.loadtxt(timestamp.txt) assert np.allclose(tum[:, 0], ts, atol1e-4)逻辑说明这个断言确保TUM输出文件里的时间戳跟原始timestamp.txt逐行一致。如果时间戳有错位evo在做时间对齐时会插值或丢弃帧造成评估时轨迹长度不一致甚至报错。我在每次转换后都跑一遍这个检查成本不到一秒能避免后面排错一小时。一个血泪经验KITTI raw data转TUM的坑绝大多数不在数学上而在坐标系定义和时间戳对齐这种细节上。每次拿到新的序列我都会先转换一帧打印出第一帧的平移和四元数确认接近单位阵后再全量跑。希望帮到你。本文还有配套的精品资源点击获取