简介:面向运行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 50N,KITTI采集区域在德国卡尔斯鲁厄附近 proj = Proj(proj='utm', zone=50, ellps='WGS84', datum='WGS84', units='m') 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秒,起点不是0,TUM格式本身不要求时间从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]], dtype=float) R_cam = C @ R逻辑说明:这个变换矩阵把KITTI的x-forward映射到视觉的z-forward,y-left映射到x-right,z-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), ( f"oxts与timestamp数量不一致: {len(oxts_files)} vs {len(timestamps)}" ) proj = Proj(proj='utm', zone=50, ellps='WGS84', datum='WGS84', units='m') # 坐标系变换:KITTI IMU坐标 -> 视觉坐标 C = np.array([[0, -1, 0], [0, 0, -1], [1, 0, 0]], dtype=float) 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 50,KITTI采集范围都在这个区带内,跨区带的情况用KITTI数据不会遇到。坐标系变换矩阵C在每次构造位姿后立即作用,这样后续的相对化、四元数提取都在视觉坐标系下完成。输出时时间戳保留6位小数(微秒级),位置和四元数保留6位小数足够evo评估使用。
参数说明:zone=50是KITTI采集地所在UTM区带,如果你的数据来自其他区域,需要改成对应区带,否则投影误差会达到数百米。units='m'指定输出单位为米。四元数转换时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_mode=xz逻辑说明: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, atol=1e-4)逻辑说明:这个断言确保TUM输出文件里的时间戳跟原始timestamp.txt逐行一致。如果时间戳有错位,evo在做时间对齐时会插值或丢弃帧,造成评估时轨迹长度不一致甚至报错。我在每次转换后都跑一遍这个检查,成本不到一秒,能避免后面排错一小时。
一个血泪经验:KITTI raw data转TUM的坑绝大多数不在数学上,而在坐标系定义和时间戳对齐这种细节上。每次拿到新的序列,我都会先转换一帧,打印出第一帧的平移和四元数,确认接近单位阵后再全量跑。希望帮到你。
本文还有配套的精品资源,点击获取