4. 轨迹数据格式与存储:常见轨迹格式(TUM、KITTI、EuRoC)、ROS Bag文件、自定义数据格式、HDF5存储

做轨迹评估,第一关就是跟数据格式打交道。说实话,我早年刚入行时,被各种格式搞得晕头转向。TUM、KITTI、EuRoC,每个数据集都有自己的脾气。你想想看,如果连数据都读不对,后面的评估全是白搭。

这一章,我就带你把这些格式彻底捋清楚。咱们不光要认识它们,还要知道怎么存、怎么读、怎么转。

4.1 常见轨迹格式:三巨头

目前学术界和工业界,最常用的就是这三种格式。我按使用频率排个序,咱们一个个看。

4.1.1 TUM 格式

TUM 格式来自慕尼黑工业大学。它简单到令人发指,但也正因为简单,成了最通用的交换格式。

格式长这样:

# timestamp tx ty tz qx qy qz qw
1.234567 0.1 0.2 0.3 0.0 0.0 0.0 1.0
2.345678 0.2 0.3 0.4 0.1 0.0 0.0 0.99

每行8列:时间戳(秒)、平移xyz、四元数xyzw。注意,时间戳是浮点数,单位是秒。

我的习惯: 我一般用TUM格式做中间格式。不管从哪来的数据,先转成TUM,再统一处理。这样代码复用率极高。

读TUM格式的代码,我通常这样写:

def read_tum(file_path):
    timestamps = []
    positions = []
    quaternions = []
    with open(file_path, 'r') as f:
        for line in f:
            if line.startswith('#'):
                continue
            parts = line.strip().split()
            if len(parts) < 8:
                continue
            t = float(parts[0])
            tx, ty, tz = map(float, parts[1:4])
            qx, qy, qz, qw = map(float, parts[4:8])
            timestamps.append(t)
            positions.append([tx, ty, tz])
            quaternions.append([qx, qy, qz, qw])
    return timestamps, positions, quaternions

4.1.2 KITTI 格式

KITTI 格式就「硬核」多了。它不存四元数,直接存3x4的变换矩阵。每行12个数,对应矩阵的3行4列。

r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz

注意,KITTI 格式没有时间戳。它假设数据是按帧顺序排列的。帧率通常是10Hz。

踩坑提醒: 我曾经在KITTI格式上栽过跟头。它的矩阵是行主序存储的,但有些库默认列主序。读出来直接reshape成3x4,结果矩阵是转置的。评估出来的轨迹误差大得离谱,排查了整整一天。

正确的读法:

def read_kitti_pose(file_path):
    poses = []
    with open(file_path, 'r') as f:
        for line in f:
            parts = list(map(float, line.strip().split()))
            if len(parts) != 12:
                continue
            # 注意:这里是行主序
            matrix = np.array(parts).reshape(3, 4)
            poses.append(matrix)
    return poses

4.1.3 EuRoC 格式

EuRoC 格式跟TUM很像,但多了一个东西:它用四元数表示旋转,但顺序是 qw qx qy qz。别小看这个顺序,搞反了旋转方向就反了。

# 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 []
1.234567 0.1 0.2 0.3 0.999 0.0 0.0 0.0

EuRoC 的时间戳是纳秒级的整数。我一般先转成秒再处理。

核心对比:
格式时间戳位置旋转典型应用
TUM浮点秒xyzqx qy qz qwSLAM评估
KITTI无(按帧)3x4矩阵3x4矩阵自动驾驶
EuRoC纳秒整数xyzqw qx qy qzVIO评估

4.2 ROS Bag 文件

做机器人相关的工作,ROS Bag 是绕不开的。它本质上是一个容器,可以同时存多个topic的数据。轨迹数据通常放在 /odom、/tf 或 /gps 这些topic里。

我个人习惯用 rosbag 命令行工具先看看包里有什么:

rosbag info trajectory.bag

输出会告诉你有哪些topic、消息类型、消息数量。然后你可以用Python脚本提取:

import rosbag
from geometry_msgs.msg import PoseStamped

bag = rosbag.Bag('trajectory.bag')
timestamps = []
positions = []
for topic, msg, t in bag.read_messages(topics=['/odom']):
    if isinstance(msg, PoseStamped):
        timestamps.append(t.to_sec())
        positions.append([
            msg.pose.position.x,
            msg.pose.position.y,
            msg.pose.position.z
        ])
bag.close()
避坑指南: 我曾经处理过一个100GB的bag文件,直接读内存爆了。后来改用迭代器逐条处理,才搞定。大bag文件一定要流式读取,别一次性全加载。

4.3 自定义数据格式

很多时候,现成的格式满足不了需求。比如你要存轨迹+IMU+图像时间戳,或者要存协方差矩阵。这时候就得自己定义格式。

我推荐用JSON或YAML做自定义格式。可读性好,扩展方便。

{
  "format_version": "1.0",
  "sensor": "lidar",
  "trajectory": [
    {
      "timestamp": 1.234567,
      "position": [0.1, 0.2, 0.3],
      "orientation": [0.0, 0.0, 0.0, 1.0],
      "covariance": [
        [0.01, 0, 0],
        [0, 0.01, 0],
        [0, 0, 0.01]
      ]
    }
  ]
}

读起来也简单:

import json
with open('trajectory.json', 'r') as f:
    data = json.load(f)
poses = data['trajectory']
注意: JSON文件大了之后读写很慢。超过10万帧的轨迹,我建议用二进制格式。别问我怎么知道的,等过你就知道了。

4.4 HDF5 存储

HDF5 是我最推荐的存储方案,尤其是数据量大的时候。它支持分层结构、压缩、并行读写。一个文件里可以存轨迹、图像、点云,互不干扰。

写HDF5的代码:

import h5py
import numpy as np

with h5py.File('trajectory.h5', 'w') as f:
    # 创建数据集
    f.create_dataset('timestamps', data=np.array(timestamps))
    f.create_dataset('positions', data=np.array(positions))
    f.create_dataset('quaternions', data=np.array(quaternions))
    
    # 添加属性
    f.attrs['sensor'] = 'camera'
    f.attrs['date'] = '2024-01-01'
    
    # 分组存储
    grp = f.create_group('evaluation')
    grp.create_dataset('ate', data=ate_value)
    grp.create_dataset('rpe', data=rpe_value)

读HDF5:

with h5py.File('trajectory.h5', 'r') as f:
    timestamps = f['timestamps'][:]
    positions = f['positions'][:]
    sensor = f.attrs['sensor']
为什么我偏爱HDF5?
  • 支持部分读取:只读需要的帧,不用全加载
  • 自带压缩:无损压缩能省50%-70%空间
  • 跨平台:Windows、Linux、macOS通用
  • 支持并行:多个进程同时读写不同区域

4.5 格式转换实战

实际项目中,你经常需要在不同格式间转换。我画了一张流程图,帮你理清思路:

轨迹格式转换流程 原始数据 统一中间格式(推荐TUM) KITTI格式 HDF5存储 ROS Bag 建议:所有格式先转TUM,再转目标格式,减少转换逻辑复杂度

转换的核心思路就一条:先统一成中间格式,再转目标格式。这样你只需要写N个转换器,而不是N×N个。

我的经验: 我一般把TUM格式作为中间格式。因为它最简单,解析起来不容易出错。而且几乎所有评估工具都支持TUM格式。

好了,这一章的内容就到这里。数据格式是轨迹评估的地基,地基打不牢,后面全是空中楼阁。下一章咱们会深入轨迹对齐和评估指标,到时候这些格式知识都会用上。