3、轨迹表示与文件格式:TUM、KITTI、EuRoC数据集轨迹格式解析,时间戳对齐的重要性

做SLAM轨迹评估,第一步就是搞清楚轨迹长什么样。说白了,轨迹就是一系列带时间戳的位姿数据。但不同数据集,格式完全不同。我刚开始接触时,就被这些格式搞晕过。今天咱们一次性把它们理清楚。

3.1 轨迹的本质:时间戳 + 位姿

一条轨迹,本质上是一个序列。每个元素包含两个信息:

  • 时间戳:记录这个位姿是什么时候采集的
  • 位姿:通常是6自由度(位置x,y,z + 姿态四元数或旋转矩阵)

你想想看,如果时间戳对不上,那评估就毫无意义。我在项目中遇到过,有人直接用文件行号对齐,结果轨迹评估误差大得离谱——后来发现是时间戳没对齐。

核心原则:轨迹评估的前提是时间戳严格对齐。差之毫厘,谬以千里。

3.2 TUM格式:最直观的文本格式

TUM数据集(慕尼黑工业大学)的轨迹格式,我个人觉得是最容易理解的。每行一个位姿,格式如下:

# timestamp tx ty tz qx qy qz qw
1305031453.359375 1.234 2.345 3.456 0.123 0.234 0.345 0.789
1305031453.375000 1.235 2.346 3.457 0.124 0.235 0.346 0.788
...

字段说明:

字段 含义 单位
timestamp Unix时间戳(秒) 秒,浮点数
tx, ty, tz 相机位置(世界坐标系) 米
qx, qy, qz, qw 姿态四元数(w在最后) 无量纲

小技巧:TUM格式的四元数顺序是 qx, qy, qz, qw。很多库默认是 qw, qx, qy, qz。读取时一定要检查,我吃过这个亏。

3.3 KITTI格式:简洁但容易混淆

KITTI数据集(卡尔斯鲁厄理工学院)的轨迹格式,嗯,这里要注意——它没有时间戳!每行是一个3x4的变换矩阵,按行排列:

# 每行12个数字,对应3x4矩阵
1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0
0.999 0.012 -0.015 0.1 -0.012 0.999 0.008 0.2 0.015 -0.008 0.999 0.3
...

这个矩阵是:

[R | t]  其中R是3x3旋转矩阵,t是3x1平移向量

为什么没有时间戳?因为KITTI的轨迹文件通常对应图像序列的帧号。第i行就是第i帧的位姿。但这样做有个大坑——如果数据有丢帧,你的轨迹就全乱了。

警告:KITTI格式没有显式时间戳。如果你需要做时间对齐,必须从其他文件(如oxts数据)中获取时间信息。我曾经因为忽略这一点,评估结果差了30%。

3.4 EuRoC格式:带IMU的复杂格式

EuRoC数据集(苏黎世联邦理工学院)的轨迹格式稍微复杂一些。它包含两种数据:

1. 状态估计文件(state_groundtruth_estimate0/data.csv):

# timestamp, p_RS_R_x [m], p_RS_R_y [m], p_RS_R_z [m], q_RS_w [], q_RS_x [], q_RS_y [], q_RS_z [], v_RS_R_x [m s^-1], ...
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...
1.01, 0.01, 0.0, 0.0, 0.999, 0.005, 0.0, 0.0, 0.1, 0.0, 0.0, ...

2. IMU数据(imu0/data.csv):

# timestamp, omega_x, omega_y, omega_z, alpha_x, alpha_y, alpha_z
1.0, 0.01, 0.0, 0.0, 0.1, 0.0, 0.0
1.005, 0.011, 0.0, 0.0, 0.11, 0.0, 0.0

注意:EuRoC的四元数顺序是 qw, qx, qy, qz,和TUM正好相反。而且时间戳单位是秒,但精度很高(纳秒级)。

3.5 时间戳对齐:评估的灵魂

为什么时间戳对齐这么重要?我给你举个例子。假设你的SLAM算法输出轨迹和真值轨迹如下:

SLAM轨迹 真值轨迹
时间戳: 1.000, 位姿A 时间戳: 1.010, 位姿A'
时间戳: 2.000, 位姿B 时间戳: 2.010, 位姿B'
时间戳: 3.000, 位姿C 时间戳: 3.010, 位姿C'

如果直接按行对齐,你会把A和A'比较,B和B'比较。但实际时间差了10毫秒。对于快速运动的相机,10毫秒可能产生几厘米的位移误差。这个误差会直接算进你的ATE(绝对轨迹误差)里,让你误以为算法很差。

正确的做法:使用线性插值或最近邻插值,将SLAM轨迹和真值轨迹对齐到同一时间戳序列上。

我常用的对齐代码(Python):

import numpy as np
from scipy.interpolate import interp1d

def align_timestamps(slam_ts, slam_poses, gt_ts, gt_poses):
    """
    将真值轨迹插值到SLAM轨迹的时间戳上
    """
    # 对真值轨迹的每个维度进行插值
    aligned_gt = []
    for i in range(gt_poses.shape[1]):
        f = interp1d(gt_ts, gt_poses[:, i], kind='linear', 
                     bounds_error=False, fill_value='extrapolate')
        aligned_gt.append(f(slam_ts))
    
    return np.column_stack(aligned_gt)

避坑指南:我曾经直接用最近邻插值,结果在快速转弯处误差暴增。后来改用线性插值,效果好了很多。如果你的数据频率很高(如200Hz),最近邻也够用;但如果是10Hz以下,一定要用线性插值。

3.6 格式转换:实用工具

实际工作中,你经常需要在不同格式间转换。我整理了一个简单的对照表:

源格式 目标格式 注意事项
TUM → KITTI 去掉时间戳,四元数转旋转矩阵 注意四元数顺序
KITTI → TUM 添加虚拟时间戳,旋转矩阵转四元数 时间戳需要从其他来源获取
EuRoC → TUM 提取位姿部分,调整四元数顺序 EuRoC的qw在第一位

嗯,这里再分享一个我常用的转换函数片段:

def tum_to_kitti(tum_file, kitti_file):
    """将TUM格式转换为KITTI格式"""
    data = np.loadtxt(tum_file)
    timestamps = data[:, 0]
    positions = data[:, 1:4]
    quaternions = data[:, 4:8]  # qx, qy, qz, qw
    
    with open(kitti_file, 'w') as f:
        for i in range(len(timestamps)):
            # 四元数转旋转矩阵
            R = quaternion_to_rotation_matrix(quaternions[i])
            t = positions[i].reshape(3, 1)
            T = np.hstack([R, t])
            # 按行写入12个数字
            f.write(' '.join(map(str, T.flatten())) + '\n')

3.7 本章小结

轨迹格式看似简单,但细节决定成败。我见过太多人在评估时因为格式问题翻车。记住三点:

  • 时间戳是命根子——对齐不好,评估白做
  • 四元数顺序要确认——TUM和EuRoC正好相反
  • KITTI没有时间戳——需要额外处理

下一节我们会深入ATE和RPE的具体计算方法。但如果你连轨迹格式都没搞明白,那些指标算出来也是错的。先把基础打牢。

可可精品资料库