数据格式与质量导出
实时录制只写 LeRobot v2.1 原始布局;LeRobot v3.0、HDF5 和 MCAP 都是质量审查之后的导出产物。
先划清录制与导出的边界
EVA 的实时采集链路和离线格式转换是两个阶段。COLLECT 在 START 与 STOP 之间接收遥操作数据,保存 worker 对原始快照做对齐、校验和打包,然后写入 LeRobot v2.1 episode。录制过程中不会根据下拉框分支去写 v3.0、HDF5 或 MCAP;这些格式只在已有 v2.1 数据集上运行质量拆分和导出。这样可以让采集时序、回放方式和原始证据保持稳定,也让同一批数据能够在审查后重新选择产物格式。
| 阶段 | 输入 / 输出 | 可见入口 |
|---|---|---|
| 实时采集 | 机器人状态、下达动作、相机帧 → LeRobot v2.1 原始数据集 | COLLECT 的 START、STOP、后台保存队列 |
| 质量审查 | 自动质量标记,加上可选的 qc_verdict 与备注 | QUALITY CHECK / REPLAY 的 PASS、FAIL |
| 质量导出 | v2.1 原始数据集 → accepted 与 rejected 两个子集,按所选格式写出 | COLLECT 的 QUALITY EXPORT 面板 |
| 发布 / 上传 | 只将当前格式的 accepted 导出目录递归上传 | UPLOAD ACCEPTED(需配置上传后端) |
转换模块注册的格式共有四种:原生 lerobot_v21、lerobot_v3、hdf5 和 mcap。其中本页的新增重点是后三种。选择原生 v2.1 也会执行质量拆分并复制成 accepted/rejected,而不是把未审查的原始目录直接当作可发布目录。
export_dataset_by_quality 是供程序调用的 Python API,不是已声明的通用 CLI 命令;请勿据此编造 eva export 等命令。在 COLLECT 中完成一次数据生命周期
启动带有 collection 配置的 EVA 后,COLLECT 面板会把数据集名称和任务提示词分开显示。配置中的 collection.tasks 提供 DATASET NAME 与 TASK / PROMPT 的选择项;选中的数据集实际位于 <log_dir>/<数据集名>/raw/。空的 log_dir 会先按配置规则落到工作目录,再由同一规则解析路径。
| 顺序 | 操作 | 发生的事情 |
|---|---|---|
| 1 | 选择 DATASET NAME 与 TASK / PROMPT | 任务字符串写入 episode 元数据;不同数据集名称对应不同的 raw 目录。 |
| 2 | 打开 MOTION,按 START RECORD | 开始接收本次 episode。尚未完成遥操作跟踪的预热帧会跳过,不会被写成空动作。 |
| 3 | 按 END / SAVE(STOP) | 停止采集,把快照交给后台保存 worker;状态从 COLLECTING 变为 SAVING,保存结束后回到 IDLE。 |
| 4 | QUALITY CHECK | 选中已保存的 episode 回放,标记 PASS 或 FAIL,并可保存备注。自动检查结果也会写入元数据。 |
| 5 | QUALITY EXPORT | 选格式并按 EXPORT DATASET。面板轮询异步 job,显示 episode 进度与 accepted/rejected 数量。 |
| 6 | UPLOAD ACCEPTED | 只有当前所选格式已有完成的 accepted 导出,并且上传配置可用时才会启用;上传也异步报告文件和字节进度。 |
START 后采集与保存、图像编码分离运行;主机跟不上时队列会增长并记录告警。STOP 会校验、打包并排队保存,零帧 episode 不写入。重新启动同一配置会继续追加 episode,不覆盖原始数据。按 CANCEL 则清除当前尚未结束的快照;它不是一次失败的质量导出,也不会产生 accepted 或 rejected 目录。
QUEUE_FULL 时应等待 IDLE,必要时调整配置的 fps 或 save_queue_max。如何拆分 accepted 与 rejected
原始 v2.1 的 meta/episodes.jsonl 每行代表一个 episode。采集保存时,所有自动检查通过的行写入 quality: "green",任一检查失败则写入 quality: "red",并带有 quality_issues。QUALITY CHECK 的人工决定会以 qc_verdict: "pass" 或 qc_verdict: "fail" 写回同一行;只保存备注时可以没有 verdict。导出时,源码使用如下优先级:
| 元数据状态 | 导出归类 | 说明 |
|---|---|---|
qc_verdict == "pass" | accepted | 人工 PASS 优先,即使自动 quality 曾为 red 也接受;这表示操作者明确确认了该 episode。 |
qc_verdict == "fail" | rejected | 人工 FAIL 一定拒绝。 |
没有上述 verdict,且 quality == "red" | rejected | 自动质量问题未被人工 PASS 覆盖。 |
| 没有上述 verdict,且质量不是 red(通常为 green) | accepted | 导出器按源码的默认规则接受;若需要人工闭环,应在导出前完成回放与标记。 |
accepted 和 rejected 都是完整的数据集子目录:各自包含重新编号后的 episode、相应的视频或内嵌图像、任务表、统计、原始来源字段以及质量标记。拆分时不会把原始 episode 的质量原因抹掉;同一子集内部的 episode 会按该子集重新编号,Parquet 中的 episode_index 与全局 index 也会重写为连续值,以便下游读取。
阅读 quality_split.json
每个输出目录的 meta/quality_split.json 是来源和门控标记,不是可忽略的装饰文件。它至少包含以下四个关键字段:
| 字段 | 含义 | 使用方式 |
|---|---|---|
source_dir | 被拆分的 v2.1 原始数据集绝对路径 | 确认导出确实来自当前 COLLECT 数据集,而不是另一个目录。 |
subset | accepted 或 rejected | 标明目录可以代表哪一类质量结果;上传端只允许 accepted。 |
dataset_format | 实际写出的格式:lerobot_v21、lerobot_v3、hdf5 或 mcap | 阻止把一个格式的目录冒充为另一个格式。 |
source_episode_indices | 列表位置对应输出 episode,值是原始 v2.1 的 episode_index | 把重编号后的文件追溯回原始录制,并核对 accepted/rejected 覆盖范围。 |
例如,若 accepted 的列表是 [0, 3],那么 accepted 中的新 episode_index 0 与 1 分别来自原始 episode 0 与 3;它们不是原始编号 0 与 1 的简单别名。该列表也解释了为什么转换后仍能追溯原始 episode,即使每个子集都从零开始编号。
暂存、成对发布与回滚
质量导出不是边转换边把半成品暴露为最终目录。导出器先为 accepted 和 rejected 各建立临时 staging 根;非 v2.1 格式先在临时位置完成 v2.1 拆分,再分别写出目标格式。两个子集和各自的元数据都完成后,发布器才尝试把它们移动到目标路径。目标目录由当前原始目录和格式共同决定。
<log_dir>/<dataset-name>/raw/ └── ... # 实时采集的 v2.1 原始数据 <log_dir>/<dataset-name>/export/<format>/ ├── accepted/ # 当前格式、可发布子集 └── rejected/ # 当前格式、被拒绝子集
上面是目录名恰为 raw 时的路径。如果后端得到的源目录名称不是 raw,源码会采用 <source-name>_export/<format>/accepted 与 .../rejected;不要把这一规则硬编码成不存在的固定路径。Console 为当前数据集和格式计算路径,并把实际路径放入导出 job。
发布是 accepted/rejected 的一对逻辑结果:任一转换、路径检查或第二个目录发布失败,临时目录会清理,最终 accepted 与 rejected 不会留下半成品。若替换已有输出,发布器会先暂存旧目录;后续发布失败时删除已经发布的新目录并恢复旧目录。因此重新导出不会把一边换成新版本、另一边却保留旧版本。这个“原子发布”描述的是一对结果的一致性,不代表单个文件写入期间可以被外部读取。
原始 LeRobot v2.1 布局
理解三种导出格式前,先确认源数据的证据形态。一个选定数据集的实时录制目录以 meta/info.json 的 codebase_version: "v2.1" 为准;源读取器会拒绝 codebase_version 不是 v2.1,或显式 dataset_format 不是 lerobot_v21 的质量拆分输入(未写入该字段时按 v2.1 处理)。
<log_dir>/<dataset-name>/raw/
├── data/
│ └── chunk-000/
│ ├── episode_000000.parquet # 每帧一行的状态、动作和时间列
│ └── episode_000001.parquet
├── videos/
│ └── chunk-000/
│ └── <video-key>/
│ └── episode_000000.mp4 # 每个相机、每个 episode 一个 MP4
└── meta/
├── info.json # v2.1、fps、列与视频特征
├── episodes.jsonl # episode、任务、长度、质量与 QC
├── episodes_stats.jsonl # 可用时的逐 episode 统计
├── tasks.jsonl # task_index 与任务文本
└── stats.json # 数据集统计
原始 Parquet 的 timestamp 使用配置的目标 fps 生成等间隔时间;capture_time 保留采集时刻用于诊断。相机视频与数据表应逐帧对应。原始写入只会把采集得到的向量、图像和质量信息保存下来,格式转换不会重新推断机器人的动作或替换质量判定。
LeRobot v3.0:分片表与视频
选择 LeRobot v3 时,导出器读取每个 accepted 或 rejected 的 v2.1 episode,把数据表写成 v3 风格的 Parquet 文件,把各相机帧写成与分片对应的 MP4 文件。它仍然是离线导出,不改变原始 v2.1 目录,也不在采集时实时产生 v3 文件。
<export-root>/lerobot_v3/accepted/
├── data/
│ └── chunk-000/
│ ├── file-000.parquet # 一个数据分片可含多个 episode
│ └── file-001.parquet # 达到边界或 schema 改变时滚动
├── videos/
│ └── <video-key>/
│ └── chunk-000/
│ └── file-000.mp4 # 该分片内该相机的连续帧
└── meta/
├── episodes/
│ └── chunk-000/file-000.parquet # episode → 数据/视频分片索引与时间范围
├── tasks.parquet # 任务表的 Parquet 版本
├── info.json # dataset_format=v3、路径与特征
├── episodes.jsonl、tasks.jsonl # 保留的通用元数据
├── stats.json
└── quality_split.json
数据 writer 以 episode 为写入单位:在当前表大小加上本 episode 的 Arrow 表大小将超过约 100 MiB,或 schema 发生变化时关闭当前 Parquet/视频分片并滚动到下一个 file-NNN。meta/info.json 记录数据与视频文件的大小约定,以及 data_path 和 video_path 模板。一个 episode 的元数据会记录 data/chunk_index、data/file_index、dataset_from_index、dataset_to_index;有视频时还记录每个视频的分片索引、文件索引、from_timestamp 和 to_timestamp。
因此,使用 v3 时不要按 v2.1 的“每个 episode 一个 Parquet 与一个 MP4”假设读取;应先读取 meta/episodes/chunk-000/file-000.parquet 的定位列,再打开对应的 Parquet 与相机 MP4 分片。任务同时有 tasks.jsonl 和 writer 生成的 tasks.parquet,便于按任务索引恢复语义。任何视频帧数不等于该 episode 表的行数都会中止 writer,整对 accepted/rejected 都不会发布。
HDF5:每个 episode 一个文件
HDF5 导出把一个 episode 的非图像列、图像数组和 episode 元数据放入同一个文件。它不保留独立 MP4;源 v2.1 的视频会被解码为帧,并以内嵌的可追加 HDF5 数据集写入 images 组。导出后的 meta/info.json 将这些特征标为 dtype: "image",设置 embedded_images: true 和 total_videos: 0,并移除 video_path。
<export-root>/hdf5/accepted/
├── data/
│ └── chunk-000/
│ ├── episode_000000.hdf5
│ └── episode_000001.hdf5
└── meta/
├── info.json、episodes.jsonl、tasks.jsonl、stats.json
└── quality_split.json
episode_000000.hdf5
├── columns/ # 状态、动作等非图像列
│ └── item_000000/{value, attrs[key,kind]}
├── images/ # 每个图像键一个可追加 value
│ └── item_000000/{value, attrs[key,kind]}
└── metadata/ # episode 行 + fps
└── item_000000/{value, attrs[key,kind]}
三个顶层组的键值都用带 key 属性的 item_NNNNNN 表示。数值数组通常存为 kind: "array";字符串、bytes、None 和无法直接映射的容器会记录相应的 kind,容器可使用 MessagePack 字节保存,从而不丢失元数据结构。metadata 还写入该 episode 的原始元数据和源 fps。
每个图像 value 的第 0 轴是帧轴,数据集可追加;第一帧确定后续帧的形状。若同一图像键后续帧改变 H×W×C 形状,或最终观察帧数不是 Parquet 行数,HDF5 writer 报错。文件会停留在临时 staging,而不会作为最终 accepted 或 rejected 目录发布。
MCAP:EVA 消息与内嵌图像
MCAP 导出按 episode 生成一个 .mcap 文件。它适合已经有 MCAP 读取链路、并希望按消息主题和时间顺序消费数据的系统。当前实现不是 ROS bag,也没有使用 ROS message schema;不要把 topic 名称解释成 ROS 话题类型或把 payload 当成 ROS 消息。
<export-root>/mcap/accepted/
├── data/
│ └── chunk-000/
│ ├── episode_000000.mcap
│ └── episode_000001.mcap
└── meta/
├── info.json、episodes.jsonl、tasks.jsonl、stats.json
└── quality_split.json
MCAP records
├── topic: episode # 一个 episode 元消息
│ └── MessagePack({columns, images, metadata})
└── topic: episode/image/<video-key> # 每相机、每帧一个 MessagePack 图像
└── sequence=frame_index; shared timestamp per frame
writer 使用 MCAP profile eva-client 与 library eva-client。它注册 eva_client.episode 描述,随后以 message_encoding: "messagepack" 写入名为 episode 的元消息;该消息的内容包括非图像列、表中已有的内嵌图像和 {...episode row, fps} 元数据。这里的注册描述是当前实现的 MCAP 元数据,不是 ROS schema;实际 payload 编码是 MessagePack。
源视频不会以独立 MP4 搬到 MCAP 目录。每个相机注册 episode/image/<key> 主题,图像帧直接 MessagePack 编码。对 frame index 0、1、2……,所有相机都使用同一个按目标 fps 计算的时间戳(第一个时间戳为一个帧周期),并将该 index 作为 sequence;这样多相机消息可以按帧对齐。writer 还会检查每个相机没有提前结束,也没有多余尾帧;不一致即失败并阻止整对发布。meta/info.json 同样标记内嵌图像、total_videos: 0,并移除 video_path。
四种格式的差异
下表只比较源码实际写出的目录与内部结构,不把任何格式宣传成训练框架的通用标准,也不声称某种格式普遍优于其他格式。原生 v2.1 是录制的边界;后三种是从同一份源数据导出的不同消费形态。
| 格式 | 主数据布局 | 图像形式 | 元数据 / 适合检查的入口 |
|---|---|---|---|
lerobot_v21 | data/chunk-NNN/episode_NNNNNN.parquet,每 episode 一个表 | videos/chunk-NNN/<key>/episode_NNNNNN.mp4,每相机独立文件 | meta/info.json、episodes.jsonl、任务与统计;也是实时录制源 |
lerobot_v3 | data/chunk-000/file-NNN.parquet 分片;episode 通过 metadata 定位 | 按相机与分片写 MP4;与数据分片关联 | meta/episodes/chunk-000/file-000.parquet 记录索引和时间范围,另有 tasks.parquet |
hdf5 | data/chunk-NNN/episode_NNNNNN.hdf5,每 episode 一个文件 | 内嵌在 images 组的帧数组,无独立 MP4 | 一个 HDF5 文件内含 columns、images、metadata 三组 |
mcap | data/chunk-NNN/episode_NNNNNN.mcap,每 episode 一个消息文件 | 元消息内嵌图像列,视频图像作为 episode/image/<key> MessagePack 消息,无独立 MP4 | episode 元消息 + 按帧共享时间戳和 sequence;profile 为 eva-client |
所有导出结果都保留通用的 meta/info.json、任务和统计(源文件存在时复制),并写入 quality_split.json。对于 HDF5 与 MCAP,不能只检查一个文件名就假定有视频目录;应检查 embedded_images、total_videos 和 video_path 是否符合内嵌图像布局。对于 v3,应以 episode 定位表解释 Parquet/MP4 分片,而不是套用 v2.1 的单 episode 文件路径。
共同校验与核验办法
导出器首先把输入当作 v2.1 数据集验证:必须存在 meta/episodes.jsonl 与 meta/info.json,版本和格式必须匹配,episode 索引必须唯一;每个源 Parquet 与每个列出的相机文件都必须存在。源表需要包含 episode_index 和全局 index,任务索引必须能在 tasks.jsonl 中找到,统计列也必须能从逐 episode 统计或表列中得到。空 episode 会被 writer 拒绝。
| 校验层 | 检查内容 | 失败结果 |
|---|---|---|
| 采集保存 | episode_too_short、非单调时间戳、missing_camera、invalid_image_shape、配置字段缺失或维度错误、non_finite_value、frame_count_mismatch | episode 标红,原因持久化;缺失或非法向量可按固定长度补零,数据仍保持规整。 |
| 质量拆分 | v2.1 版本、文件路径、唯一索引、任务索引、统计与 accepted/rejected 规则 | 不开始发布;临时 staging 清理。 |
| LeRobot v3 | 非空表、每个视频解码帧数与表行数一致、分片 schema 一致 | 转换 job failed,成对输出不发布。 |
| HDF5 | 非空 episode、图像形状稳定、每个图像数据集帧数与表行数一致 | 转换 job failed,成对输出不发布。 |
| MCAP | 非空 episode、每个图像主题没有缺帧或尾帧,所有相机按相同 frame index 写入 | 转换 job failed,成对输出不发布。 |
核验一份导出时,先分别打开 accepted 与 rejected 的 meta/quality_split.json,检查 source_dir 是当前 raw 目录、subset 正确、dataset_format 与选择一致,再检查两个 source_episode_indices 是否按预期覆盖原始索引。接着将 info.json 的总 episode/帧数与实际文件对照:v2.1 对照每个 Parquet 行数和 MP4 帧数,v3 对照 episode 定位表、分片范围和视频帧数,HDF5 对照每个 images/*/value 第 0 轴,MCAP 对照每个图像 topic 的消息数量、sequence 与共享时间戳。
最后做一次真正的读取测试:用下游的 Parquet、HDF5 或 MCAP reader 读取一条状态、一条动作和一帧图像;不要只凭文件扩展名判断成功。MCAP reader 应看到一个 episode 元消息,以及每个相机的 episode/image/<key> 消息;它们的数据是 MessagePack,不是 ROS bag/schema。核验通过后,再执行上传门控。
为什么只能上传当前 accepted 导出
UPLOAD ACCEPTED 的按钮由当前选中的格式和已完成的导出 job 共同门控。前端切换格式时会清空已记录的 acceptedDir、导出与上传状态,并显示“需要先导出”;因此刚完成的 HDF5 accepted 不能直接当作 MCAP 上传。格式不同意味着目录布局、图像承载方式和 info.json 语义不同,重新导出是得到匹配产物的必要步骤,不是重复复制。
后端收到上传请求时还会检查:当前源目录与格式是否有最新的 completed export job;accepted 路径是否与该 job 一致;meta/quality_split.json 是否可读;subset 必须是 accepted;dataset_format 必须等于请求格式;source_dir 必须解析到当前源目录。任何 rejected 目录、旧格式、来源不符或未导出的目录都会被拒绝。没有配置上传后端时,接口也不会启动上传。
当前配置解析的公开上传后端是 collection.storage.sftp。它需要主机、端口、可选用户与 identity file,以及绝对、规范化的远端目录;Console 会递归统计文件和字节并异步上传。SFTP 先将文件写入同一父目录下的临时 staging 目录,再用远端移动发布;目标已经存在时不会覆盖旧目标,而会选择带时间戳的副本名称。上传失败会清理远端 staging,新目标不会被发布;已存在的旧目标不受这次失败影响。
故障处理
| 现象 | 原因与处理 |
|---|---|
| QUALITY EXPORT 立即失败 | 检查当前 raw 的 info.json 是否 v2.1、episodes.jsonl 是否存在且非空,以及数据表、相机文件、任务表和统计是否齐全。修复来源后重新点击 EXPORT DATASET。 |
| 某 episode 报视频帧数不一致 | 对照该 episode 的 Parquet 行数和每个相机的解码帧数。v3、HDF5、MCAP 都将整次成对发布判为失败;不要上传残留临时目录,应修复或重录再导出。 |
| accepted 有旧文件,rejected 也不确定 | 不要手动拼目录。Console 重新导出时会使用同一格式路径和 replace_existing=True,发布器负责替换与回滚;导出完成后重新检查两边 marker。 |
| UPLOAD ACCEPTED 是灰的或返回“先导出当前数据集” | 确认选择器格式等于最近完成的导出格式,accepted marker 的 subset、format、source_dir 均匹配,且 SFTP 配置存在。格式切换、导出失败或只有 rejected 都不能绕过门控。 |
| SFTP 上传失败 | 检查 OpenSSH sftp/ssh、身份文件、主机、端口和远端权限。源码会清除新 staging 并保留旧目标;网络或凭据恢复后可从 accepted 目录重试。 |
| MCAP 下游读不到 ROS 消息 | 这是消费假设错误:当前文件使用 eva-client profile 和 MessagePack,且不是 ROS bag/schema。使用能读取 MCAP、解 MessagePack 并理解 EVA topic 的适配器。 |
状态面板会显示 queued、running、completed 或 failed,以及 episode、文件和字节进度。failed job 的错误文本用于定位问题;转换失败时不要把“已创建临时文件”当成成功信号,最终可见目录才是发布结果。若人工 PASS 覆盖自动 red,应把该决定和备注一并保留,便于之后理解为什么它进入 accepted。
按消费方式选择格式
选择原则是下游真正需要的目录和读取方式,而不是格式名称听起来是否“标准”。同一 raw 数据可按不同需求多次导出;每次导出都应独立核验,并只上传当次 accepted。
- 需要 EVA 回放、保留采集原貌或仍在审查:先留在
lerobot_v21raw;它保留每 episode 的 Parquet、独立相机 MP4 与采集质量字段,是所有后续导出的来源。 - 下游读取分片 Parquet 和分片 MP4:选择
lerobot_v3,并让读取器使用 episodes 元数据中的文件索引、全局索引和时间范围。 - 下游以 episode 文件为边界,直接访问列、图像和元数据:选择
hdf5;读取三组并从图像数据集第 0 轴取帧,不要寻找 MP4。 - 下游是消息日志或事件流处理器:选择
mcap;按episode元消息和各相机 image topic 消费 MessagePack,并使用 frame index/共享时间戳做对齐。
不存在源码支持的“通用训练标准”“最优格式”或“格式越新越好”的保证。若下游同时能读取多种格式,比较其已实现的解码器、并发读取、图像内存和元数据需求;若下游只支持一种格式,就以该读取器的契约为准。无论选哪种格式,都把 raw 目录和两个质量子集的 marker 一并保留,避免日后无法回溯来源。
交付前清单
- 确认实时录制已回到
IDLE,raw 目录包含 v2.1 的info.json、episode 表、相机文件与质量元数据。 - 在 QUALITY CHECK 中回放并处理需要处理的 episode,理解 green、red、PASS、FAIL 的优先级。
- 选择一个格式并点击 EXPORT DATASET,等待 job completed;同时检查 accepted 与 rejected 的 episode/frame 汇总。
- 检查两个
quality_split.json的来源、subset、format 和 source episode 索引,检查目标格式的图像承载方式与目录。 - 用真实下游 reader 做最小读取和帧数核验。v3 检查分片定位,HDF5 检查三组和图像第 0 轴,MCAP 检查 MessagePack topic、sequence 与时间戳。
- 只有在格式未切换、当前 accepted marker 匹配、SFTP 已配置且导出 job 是 completed 时,才点击 UPLOAD ACCEPTED。