Skip to content

机器人视频质检

具身智能数据集的视频质检实践:从帧级质量检测到跨模态摄像头配对校验。两篇文章,所有数值均来自真实机器人数据集上的实跑验证。

11 min read

采集机器人示教数据时,相机朝向是固定的,场景光照是可控的,机械臂的动作是重复的。这些“好”的性质让视频看起来天然可靠——直到你打开第 12,000 个 episode 的第 3 号相机文件,发现整整 4 秒画面全黑。

视频里没有 token 可以统计。 这是具身智能数据质检和 NLP 数据质检的根本区别:你不能靠采样发现一段黑屏,你必须解码每一帧。而一旦解码每一帧,就有了做更深的校验的可能——比如发现两路腕部相机接反了。

这个专题分成两篇:

平台侧怎么把这些检查装进 DDL、版本化对象路径和 Airflow 调度,见 Sparrow Data Platform:从 DDL 到真实具身数据质检。

数据来源

两篇文章用的是同一份数据:allenai/MolmoAct2-BimanualYAM-Dataset(HuggingFace 官方仓库)。它不是“随手拍的双臂视频合集”,而是一份按 LeRobot v3 格式打包、有明确字段定义和许可的研究数据集。先把“它是什么”说清楚,后面的质检才有对象。

这是什么数据集

  • 发布者与用途:Allen Institute for AI(Ai2)发布,作为 MolmoAct2(动作推理模型)的训练数据之一。dataset card 写的是 “more than 720 hours of training demonstrations spanning diverse tabletop manipulation tasks”;论文进一步说明这是 720 小时遥操作(teleoperated)YAM 轨迹,覆盖桌面与家居任务,并称其为最大的开源双臂数据集。
  • 平台形态:robot_type = bi_yam_follower,即一对 YAM follower 机械臂。字段名显示每臂 6 个关节 + 1 个夹爪(left_joint_0..5.pos 加 left_gripper.pos,右侧同理),合起来 14 维;典型桌面作业场景。
  • 相机:三路,observation.images.left / .right / .top。card 里只有键名,没有写安装位置;第二篇 3.3 节用跨模态相关性验证了 left / right 是腕部相机、top 是俯视相机。
  • 规模与 split:32,246 个 episode、76,046,658 帧、34 条任务描述、30 fps;只有一个 split,train: 0:32246(meta/info.json)。Hugging Face 页面标注仓库总大小约 2.35 TB。
  • 许可:Apache-2.0(dataset card 元数据)。
  • 采集方式:论文写的是遥操作轨迹;card 只说 “training demonstrations”,没有展开遥操作设备、操作员人数或采集流程。

实测规格(读自 meta/info.json)

项 值
Episode 数 32,246
总帧数 76,046,658
帧率 30 fps
相机 3 路 — observation.images.left / .right / .top
编码 AV1,640×360,yuv420p
状态向量 14 维(前 7 维左臂含夹爪,后 7 维右臂)
任务数 34

本文实际下载并解码了 4 个 mp4,覆盖 31,115 帧、1037.17 秒。这是 file-000.mp4(三路相机各一份),完整包含 episode 0–15。

所有数值都来自实际运行。 每一段代码都可以在 examples/robot-video-qc/ 下直接跑通;每一个阈值都附带标定时的精确率与召回率。没有任何一个数字是估计出来的。

一条 episode 由什么组成

一条 episode 不是一个 mp4 加一个 state 文件,而是同一段示教在四类文件里的切片:

组成 位置 粒度 关键字段
逐帧低维数据 data/chunk-XXX/file-XXX.parquet 一行 = 一帧 action、observation.state、timestamp、frame_index、episode_index、index、task_index
视频 videos/observation.images.*/chunk-XXX/file-XXX.mp4 一个 mp4 含多个 episode episode 的 from_timestamp / to_timestamp
episode 元数据 meta/episodes/chunk-000/file-000.parquet 一行 = 一个 episode length、tasks、dataset_from_index / dataset_to_index、data 与各相机视频的 chunk/file index、每路相机的 from_timestamp / to_timestamp、逐特征 stats/*
任务文本 meta/tasks.parquet、meta/tasks_annotated.parquet 任务级 / episode 级 task_index → task;annotated 每行一个 episode 的完整指令
全局声明 meta/info.json、meta/stats.json 数据集级 特征名/形状/fps/路径模板;全局统计量

逐项解释(以数据集实际文件结构为准):

  • observation.state:14 个 float32,前 7 维左臂(6 关节 + 夹爪),后 7 维右臂。card 没有给出单位——角度还是弧度、夹爪量程是多少,都未声明,只给了关节名与维度。
  • action:同样 14 维 float32,关节名与 state 完全一致。card 没有说明它是目标位置、遥操作指令还是别的量;能确定的是它与 state 同维度、同时间轴、每帧一条。
  • timestamp:float32,按 LeRobot v3 的时间轴以秒为单位,每个 episode 从 0 重新开始(实测 episode 0、1 的首帧都是 0.0)。实测相邻帧差为 0.0333 s,正好是 30 fps 的步长。
  • frame_index:每个 episode 从 0 开始的帧号;index 是跨 episode 连续的行号(episode 1 的首行是 1732);episode_index 是全局 episode id;task_index 指向 meta/tasks.parquet 里的任务字符串。
  • length:该 episode 的帧数。dataset_from_index / dataset_to_index 是它在全局行号上的区间,差值应等于 length。
  • 视频时间区间:videos/observation.images.*/from_timestamp 与 to_timestamp 是这条 episode 在该 mp4 里的秒偏移。episode 0 的 length = 1732,to_timestamp − from_timestamp = 57.733333,乘 30 正好是 1732——元数据、低维行数、视频时长三者必须闭合。
  • 采样与对齐:三路视频都是 30 fps;同一帧的 state / action / timestamp 在 parquet 的同一行里,天然对齐。相机与关节流的时间对应靠 episode 的 from/to 区间与帧序号。card 没有说明硬件级同步方式(是否硬触发、时间戳来自哪里)。

另外,meta/episodes 的每一行还带 stats/ 下的逐特征统计量(count、min / max / mean / std 与 q01 / q10 / q50 / q90 / q99 分位数),是 episode 级的分布摘要;meta/stats.json 则是全局的。这两层统计量可以直接用来做粗筛。

这些结构对质检意味着什么

字段和文件结构决定了两类检查:一类可以写成确定性规则,另一类必须交给人工或模型。

确定性检查 用哪些字段 失败意味着
行数与帧数一致 length ↔ data 行数 ↔ dataset_to_index − dataset_from_index 截断、丢帧、元数据写错
时间轴单调 timestamp:首帧 0、步长 1/30、末帧约为 (length−1)/30 时钟漂移、掉帧、拼文件出错
视频区间闭合 to_timestamp − from_timestamp 乘 fps 约等于 length,且解码帧数等于 length 视频被截断、episode 边界错位
维度与数值合法 action / state 固定 14 维、无 NaN/Inf 录制中断、序列化损坏
任务可解析 task_index 命中 tasks.parquet;tasks_annotated 每个 episode 恰好一行 指令缺失或错配
三路相机齐全 每个 episode 在每个 videos/* 下都有 file index 与区间 某路相机漏录

要注意:length、行数、dataset_to_index 三者一致只是必要条件,不是充分条件。如果三路视频里有一路少了 5 帧,低维数据仍然可以完全自洽——视频帧数与 length 的比对必须真的解码视频才能做。

必须人工或模型判断的部分:

  • 指令语义。task_index 只能证明“有指令”,不能证明“指令描述了画面里实际发生的事”。
  • 任务是否成功。动作是否合理、夹爪是否真的夹住物体,这些不在字段里。
  • 相机装法是否正确。三路相机的安装位置 card 里都没写,需要跨模态证据来验证(第二篇 3.3 节)。
  • 文本质量。34 条任务描述里存在近重复和不一致:Clothes Folding 与 Folding CLothes,Sort waste ... dishwasher rack 与只差一个句号的版本。规则只能标出候选,取舍要人看。
  • 遮挡、标定漂移、光照变化。画面可能每一帧都“正常”,但语义已经不可用。

和质检流程怎么对接

一条 episode 送进质检工具时,平台至少要提供:三路视频(或可解码的帧)、逐帧 parquet 行、episode 元数据、任务文本。工具按成本从低到高跑:

  1. 结构校验(不解码):维度、NaN、length / index / timestamp 闭合、任务映射。
  2. 帧级指标(解码,最贵):三路相机逐帧提取指标,产出 frame_metrics.parquet。
  3. 画面检测(微秒级):黑屏、冻结、模糊、曝光,产出 quality_summary.json。
  4. 跨模态校验:两路腕相机与两臂的相关性,产出 ALIGNED / CROSSED / UNKNOWN。

产出至少要回答三个问题:这条 episode 能不能用(PASS / WARN / FAIL / UNKNOWN / INCOMPLETE)、哪几路相机哪个时间段坏了(半开区间)、左右腕相机有没有接反(配对判定)。

如果平台只能给出一个 mp4 和一个 state 数组、没有 episode 元数据,工具必须先重建 episode 边界(用 length / timestamp / fps 对齐),并把重建结果写进输出。否则 episode 一旦切错,后面的区间统计和相关性校验都建立在错误的帧上。

核心结论速览

结论 证据
设计稿的默认阈值在真实数据上不成立 lap_var = 30.0 精确率 0.71,mad = 0.20 精确率 0.40
标定后的四个检测器精确命中真值区间 五个样本上全部命中 [300, 419],零误报零漏报
评测框架必须是无损的 有损编码把冻结帧的 MAD 从 0.0 变成 0.1088
腕部相机看得见自己的手臂 16 个 episode 的对角相关全为正(+0.62~+0.86),非对角全为负(−0.66~−0.14)
交换相机后判定必须翻转 16/16 全部翻转
缺失帧不能补零 40% 停摆时漏掉 2/16 个真实互换,且把负相关翻成正相关

代码

examples/robot-video-qc/text
robot_video_qc/
├── metrics.py      # 指标提取(stage 1,唯一昂贵的一段)
├── detectors.py    # 四个检测器 + 所有权仲裁
├── intervals.py    # 半开纳秒区间
├── checks.py       # L1 文件检查、L2 时间检查
├── summary.py      # L4 可用性判定
├── contract.py     # Parquet / JSON 契约
├── calibrate.py    # 阈值扫描
├── inject.py       # 缺陷注入(无损 FFV1)
├── pairing.py      # 跨模态配对
└── pipeline.py     # 三段式流水线
run_qc.py           # 命令行入口

依赖:opencv-python、pyarrow、numpy、matplotlib。实测环境 Python 3.x / OpenCV 4.10.0 / PyArrow 22.0.0 / NumPy 2.2.6 / Matplotlib 3.9.2。

pip install opencv-python pyarrow numpy matplotlib

cd examples/robot-video-qc
python run_qc.py path/to/video.mp4 --camera left --episode 7 --out out/

参考

Type to search…

↑↓ navigate↵ selectEsc close