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

> Documentation Index
> Fetch the complete documentation index at: https://petwan.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# 机器人视频质检

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

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

这个专题分成两篇：

- **（一）帧级检测器的实现与标定** — 四层质检模型、一次解码七个指标、黑屏/冻结/模糊/曝光四个检测器，以及**阈值怎么定**。设计稿里的默认值在真实数据上精确率只有 0.71——本文给出实测标定值。
- **（二）跨模态摄像头配对校验** — 左右腕相机接反了，帧级质检完全看不出来。本文用腕部相机画面运动与关节运动的相关性矩阵检出它，并量化"缺失帧不能补零"这个陷阱的后果。

平台侧怎么把这些检查装进 DDL、版本化对象路径和 Airflow 调度，见 [Sparrow Data Platform：从 DDL 到真实具身数据质检](/dataplatform-build-and-real-qc/)。

## 数据来源

两篇文章用的是同一份数据：[allenai/MolmoAct2-BimanualYAM-Dataset](https://huggingface.co/datasets/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 个真实互换，且把负相关翻成正相关 |

## 代码

```text title="examples/robot-video-qc/"
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。

```bash
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/
```

## 参考

- 数据集主页（dataset card）：[allenai/MolmoAct2-BimanualYAM-Dataset](https://huggingface.co/datasets/allenai/MolmoAct2-BimanualYAM-Dataset)
- 数据集文件树（`meta/`、`data/`、`videos/`）：[Files and versions](https://huggingface.co/datasets/allenai/MolmoAct2-BimanualYAM-Dataset/tree/main)
- LeRobot v3.0 格式说明（`meta/episodes`、`from_timestamp`、任务映射等约定）：[LeRobotDataset v3.0](https://huggingface.co/docs/lerobot/en/lerobot-dataset-v3)
- 论文：[MolmoAct2: Action Reasoning Models for Real-world Deployment（arXiv:2605.02881）](https://arxiv.org/abs/2605.02881)

Source: https://petwan.tech/robot-video-qc/index.mdx
