具身智能数据处理的工程化实践

具身智能数据处理的工程化实践:从原始数据到 Model-Ready 的全流程工具链(v0.1.1)

写在前面:具身智能(Embodied AI)被广泛认为是 AGI 的终局形态,而"私有化数据"被业内共识为护城河。但很少有人从工程角度讲清楚:原始数据到底长什么样?如何处理才能喂给模型训练?本文记录了我从 0 到 1 搭建一套具身智能数据处理工具链的完整实践,包含 8 个核心工具、3 个场景配置、4 个真实客户场景测试,所有代码开源可复现。

版本 : v0.1.1(2026-07-24 优化版,补充算法细节、代码片段、踩坑复盘)

阅读时长 : 约 25 分钟

适合人群: 具身智能从业者、数据工程师、机器人算法工程师、想切入具身数据赛道的创业者

源码: https://download.csdn.net/download/m0_64997730/93160021

一、背景:为什么需要一套具身数据处理工具?

1.1 行业现状:数据才是真正的护城河

梁文锋在闭门会上提到 AGI 演进的五个阶段:

复制代码
思维链(CoT) → 智能体(Agent) → 持续学习 → 自我进化 → 具身智能(终局)

而具身智能的数据与大语言模型有本质区别:

维度 大语言模型 具身智能
数据来源 互联网公开文本/视频 真实物理世界采集
可获取性 可爬取 不可爬取,只能自己采
数据格式 纯文本 多模态时序(RGB+action+state+depth)
单条成本 近乎零 250元/小时(真机遥操)
数据特征 离散、静态 连续、动态、时间同步

这意味着:具身智能数据天然带私有化属性

1.2 市场规模与机会

根据公开数据:

  • 量子位统计国内已有 97 家具身数据玩家,70 家做采集,27 家做基础设施
  • 过去一年 15 家纯做数据的独立服务商 融资 44.7 亿元
  • 光轮智能 2026 Q1 单季新增订单 5.5 亿元,估值 150 亿
  • 帕西尼估值百亿,与比亚迪/京东/吉利绑定
  • 京东宿迁基地计划发动 60 万人众包采集,目标两年 1000 万小时

光轮、帕西尼卖的"Model-Ready Data"流水线实际只做 5 步:

  1. 数据清洗(对齐 + 异常 + 模糊 + 去重)
  2. 语言指令标注(VLM 生成 + 人工校)
  3. 阶段分割(自动预标注 + 人工校)
  4. 成功失败标记(启发式 + 人工校)
  5. 格式转换(按客户要求转)

我的工具链覆盖了同样的流程,且自动化程度更高(500 episode 4 秒跑完) 。这不是替代头部公司,而是证明了这套流程可以被工程化、自动化------这正是中小数据服务商的生存空间。

1.3 原始数据到底长什么样?

具身智能的原始数据不是文本,而是多模态时序信号流。以 DROID 数据集为例:

数据集目录结构(LeRobot v3.0 格式)
复制代码
lerobot_droid_100/
├── data/chunk-000/file-000.parquet          # 主数据(action/state/...)
├── meta/
│   ├── info.json                             # 数据集元信息
│   ├── tasks.parquet                         # 任务列表(47个任务)
│   └── episodes/chunk-000/file-000.parquet   # episode 元信息
└── videos/
    ├── observation.images.exterior_image_1_left/chunk-000/file-000.mp4
    ├── observation.images.exterior_image_2_left/chunk-000/file-000.mp4
    └── observation.images.wrist_image_left/chunk-000/file-000.mp4
info.json 关键字段
json 复制代码
{
  "codebase_version": "v3.0",
  "fps": 15,
  "total_episodes": 100,
  "total_frames": 32212,
  "features": {
    "action": {"dtype": "float32", "shape": [7], "names": ["dx","dy","dz","drx","dry","drz","gripper"]},
    "observation.state": {"dtype": "float32", "shape": [7]},
    "observation.images.exterior_image_1_left": {"dtype": "video"},
    "observation.images.wrist_image_left": {"dtype": "video"}
  }
}
每帧 action 的 7 维向量
python 复制代码
action = [dx, dy, dz, drx, dry, drz, gripper]
#         └── 末端速度(前后/左右/上下) ─┘  └─ 旋转 ─┘  夹爪
# action norm = √(dx²+dy²+dz²+drx²+dry²+drz²)  ← 用于判断机器人在不在动
一条样本的时间同步要求
复制代码
timestamp | RGB(多相机) | 深度图 | 点云 | 关节角 | 末端位姿 | 力/触觉 | 语言指令
    ↑          ↑                                    ↑
    └── 必须时间对齐 ──────────────────────────────┘

如果时间对齐错了,后面所有标注都是错的------这是具身数据标注的核心痛点。

1.4 痛点:数据不能直接喂模型

原始数据 80% 有以下问题:

问题类型 具体表现 影响
多模态时间不同步 视频和 action 序列错位 训练学到错误关联
缺少语义标注 没有成功/失败标记 无法剔除失败集
缺少语言指令 没有 task 描述 无法训练语言条件 policy
缺少阶段分割 没有 init/grasp/release 无法做精细 policy
质量问题 模糊帧、重复数据、传感器跳变 浪费算力
格式不统一 RLDS/HDF5/LeRobot/MCAP 客户训练框架不匹配

二、架构设计:工具 × 配置 = 按场景跑

2.1 设计理念:微工具架构

我没有选择做"大而全的标注平台",而是采用微工具架构:

复制代码
工具(每个功能1个 .py)
   ×
配置(每个场景1套参数)
   =
按场景跑

核心原则:

  1. 每个工具职责单一(Unix 哲学:Do one thing well)
  2. 工具与场景解耦(同一个工具服务多个场景,参数不同)
  3. CLI + HTML 报告(零运维,本地跑,不需要服务器)
  4. 自动化优先(把人工工时压到 30%)

2.2 为什么不用一个大系统?

14 个标注任务在交互模式上有本质差异,塞一个系统会变成大而全的怪物:

交互模式 任务 核心操作
看视频标全局 成功失败/语言指令 看完整段视频 → 填一个标签
看时序标区间 阶段分割/接触事件 看曲线+视频 → 拖时间轴标区间
看单帧画图形 边界框/关键点/分割 单帧图像 → 画框/点/多边形
看3D点云 3D框/点云标注 3D 旋转视角 → 标 3D 框
批量跑脚本 清洗/格式转换 无交互 → 跑完看报告

把这 5 种模式塞一个系统 = 5 套完全不同的 UI 逻辑,开发量爆炸。

2.3 工程目录结构

复制代码
embodied_data_toolkit/
├── core/                        # 核心库(3个模块)
│   ├── __init__.py              # 统一导出
│   ├── data_loader.py           # LeRobot 加载器(处理多模态对齐)
│   ├── reporters.py             # HTML/Console 报告生成
│   └── utils.py                 # pHash/Laplacian/日志/JSON 工具
│
├── tools/                       # 8 个数字员工(每个独立可跑)
│   ├── align_check.py           # L5.1 时序对齐校验
│   ├── anomaly_fix.py           # L5.2 异常值检测与修复
│   ├── blur_filter.py           # L5.3 模糊帧过滤
│   ├── dedup.py                 # L5.4 去重
│   ├── phase_segment.py         # L2.1 任务阶段分割
│   ├── success_predict.py       # L1.1 成功/失败预判
│   ├── lang_instruct.py         # L1.2 语言指令补充(VLM)
│   ├── format_convert.py        # L6  格式转换(LeRobot↔HDF5↔JSON)
│   └── long_video_split.py      # 长视频自动切片(场景A专用)
│
├── configs/                     # 3 个场景配置(YAML)
│   ├── internet_video.yaml      # 互联网视频(4工具)
│   ├── teleoperation.yaml       # 真机遥操(8工具全开,最核心)
│   └── simulation.yaml          # 仿真合成(2工具)
│
├── run_pipeline.py              # 流水线编排器(按场景串联多工具)
├── .env                         # API 配置(gitignore)
├── .gitignore
│
├── test_data/                   # 测试数据
│   ├── internet_video/          # 100 个 mp4(100MB)
│   ├── kitchen_egocentric/      # 50 个厨房第一人称视频
│   ├── long_video/              # 长视频+真值
│   └── desync_multi_camera/     # 多相机不同步数据
│
├── reports/                     # 所有 HTML 报告 + JSON 结果 + 图表
│   ├── charts/                  # 可视化图表
│   └── converted/               # 格式转换输出
│
├── logs/                        # 运行日志(按时间戳)
└── deliverables/                # 交付文档

2.4 模块依赖关系

复制代码
run_pipeline.py
    ├── 加载 configs/<scenario>.yaml
    ├── 按 pipeline 顺序调用 tools
    └── 汇总结果 → reports/

每个 tool:
    ├── import core.data_loader  (加载数据)
    ├── import core.utils        (工具函数)
    ├── 执行核心逻辑
    └── import core.reporters    (生成报告)

core.data_loader:
    ├── 读 info.json (元信息)
    ├── 读 tasks.parquet (任务列表)
    ├── 读 episodes parquet (episode 元信息)
    ├── 读 data parquet (主数据)
    └── 读 videos mp4 (视频流)

2.5 统一接口设计

每个工具都遵循统一接口:

python 复制代码
# 每个工具的 run() 函数签名
def run(input_path: str, toolkit_root: str = None, logger: logging.Logger = None, **kwargs) -> Dict[str, Any]:
    """
    统一入口
    输入:数据路径
    输出:{summary, details, report_path, json_path}
    """
    # 1. 加载数据
    loader = LeRobotLoader(input_path, logger=logger)
    
    # 2. 执行核心逻辑
    results = [...]
    
    # 3. 生成统计
    summary = {...}
    
    # 4. 控制台摘要
    ConsoleReporter.print_summary(title, summary)
    
    # 5. HTML 报告(给客户)
    reporter = HTMLReporter(toolkit_root)
    report_path = reporter.generate(...)
    
    # 6. JSON 结果(给下游工具)
    save_json({...}, json_path)
    
    return {"summary": summary, "details": details, "report_path": report_path}

三、核心模块实现

3.1 数据加载器(core/data_loader.py)

这是整个工具链的地基,处理 LeRobot 格式的多模态对齐问题。

关键数据结构
python 复制代码
@dataclass
class EpisodeInfo:
    """单个 episode 的元数据"""
    episode_index: int
    length: int                    # 帧数
    task_index: int
    task_name: str = ""
    video_offset: int = 0          # 在视频文件中的全局帧偏移(关键!)
    fps: int = 15

@dataclass
class VideoStream:
    """视频流信息"""
    key: str                       # 如 observation.images.exterior_image_1_left
    path: str
    total_frames: int = 0
多模态对齐的核心逻辑
python 复制代码
def read_video_frame(self, video_key: str, ep_idx: int, frame_in_ep: int) -> Optional[np.ndarray]:
    """
    读取视频帧(已处理对齐)
    关键:全局帧位置 = episode 偏移 + episode 内帧索引
    """
    ep = self.get_episode_info(ep_idx)
    
    # ⚠ 这是最关键的一行!
    # frame_index 是 episode 内索引(每集从0)
    # 但视频是全局连续拼接的,必须加偏移
    global_pos = ep.video_offset + frame_in_ep
    
    cap = cv2.VideoCapture(stream.path)
    cap.set(cv2.CAP_PROP_POS_FRAMES, global_pos)
    ret, frame = cap.read()
    return frame
episode 偏移表构建
python 复制代码
def _build_episodes(self, eps_df: pd.DataFrame):
    """从 episodes parquet 构建 EpisodeInfo 列表"""
    # 先收集所有 episode 的 length
    ep_lengths = {}
    for _, row in eps_df.iterrows():
        ep_idx = int(row["episode_index"])
        ep_lengths[ep_idx] = int(row["length"])
    
    # 计算每个 episode 的视频偏移
    for _, row in eps_df.iterrows():
        ep_idx = int(row["episode_index"])
        length = int(row["length"])
        
        # 偏移 = 前面所有 episode 的 length 之和
        offset = sum(ep_lengths[i] for i in range(ep_idx) if i in ep_lengths)
        
        self.episodes.append(EpisodeInfo(
            episode_index=ep_idx,
            length=length,
            video_offset=offset,  # ← 关键
        ))

3.2 工具函数(core/utils.py)

感知哈希(pHash)--- 用于去重
python 复制代码
def compute_phash(img: np.ndarray, hash_size: int = 8) -> np.ndarray:
    """
    感知哈希:将图像转为 8x8 二值哈希
    算法:缩放→灰度→均值二值化
    """
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    gray = cv2.resize(gray, (hash_size, hash_size))
    avg = gray.mean()
    return (gray > avg).flatten()

def phash_similarity(hash1: str, hash2: str) -> float:
    """两个 pHash 的相似度(0~1,1=完全相同)"""
    same = sum(a == b for a, b in zip(hash1, hash2))
    return same / len(hash1)
Laplacian 方差 --- 用于模糊检测
python 复制代码
def laplacian_variance(img: np.ndarray) -> float:
    """
    Laplacian 方差:衡量图像清晰度
    原理:清晰图像有高频边缘,Laplacian 响应大,方差大
    阈值参考:
      - < 100: 模糊
      - 100-500: 可接受
      - > 500: 清晰
    """
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    return float(cv2.Laplacian(gray, cv2.CV_64F).var())

3.3 HTML 报告生成器(core/reporters.py)

每个工具运行后自动生成 HTML 报告,给客户交付用:

python 复制代码
class HTMLReporter:
    def generate(
        self,
        tool_name: str,
        dataset_name: str,
        summary: Dict[str, Any],      # 摘要统计
        details: List[Dict[str, Any]], # 详情列表
        charts: Optional[List] = None, # 图表
        recommendations: Optional[List[str]] = None,  # 处理建议
    ) -> str:
        """生成 HTML 报告,返回文件路径"""
        # 包含:摘要表、详情表(前50行)、图表、处理建议
        # 样式:卡片式布局,问题行高亮红色

四、8 个核心工具详解

4.1 工具1:时序对齐校验(align_check.py)

这是所有标注的前置必做。对齐错了,后面全废。

校验 5 项
python 复制代码
def check_global_alignment(loader, logger):
    """检查1:全局对齐 - 视频帧数 vs 数据行数 vs episode 长度和"""
    total_data = loader.total_frames
    total_ep_len = sum(ep.length for ep in loader.episodes)
    
    # 三者必须相等(容差1帧,编码误差)
    if total_data != total_ep_len:
        result["issues"].append(f"数据行数({total_data}) != episode 长度和({total_ep_len})")
    
    for vk, stream in loader.video_streams.items():
        if abs(stream.total_frames - total_data) > 1:
            result["issues"].append(f"视频 {vk} 帧数({stream.total_frames}) != 数据行数({total_data})")

def check_frame_index_semantics(loader, logger):
    """检查2:frame_index 语义 - 是 episode 内索引还是全局索引"""
    # 判断:每个 episode 内 frame_index 是否从 0 开始
    for ep in loader.episodes:
        ep_data = df[df["episode_index"] == ep.episode_index]
        first_fi = int(ep_data["frame_index"].iloc[0])
        if first_fi == 0:
            result["semantic"] = "episode_local"  # ← LeRobot 默认
            result["recommendation"] = "视频读取需加 video_offset"
踩坑复盘(重要!)

现象:End 帧场景和 Start/Mid 不一致

错误做法:

python 复制代码
# ❌ 直接用 episode 内索引 seek 视频
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_in_ep)  # 所有"首帧"都指向视频开头!

正确做法:

python 复制代码
# ✅ 全局帧位置 = episode 偏移 + episode 内帧索引
global_pos = ep.video_offset + frame_in_ep
cap.set(cv2.CAP_PROP_POS_FRAMES, global_pos)

验证方法:视频总帧数(32212) == 数据总行数(32212) == episode length 总和(32212)

测试结果:DROID-100 全部通过,5 项检查 0 issues。

4.2 工具2:异常值检测(anomaly_fix.py)

4 类异常检测
python 复制代码
def detect_static_segments(actions, zero_threshold=0.001, min_len=5):
    """检测静止段(action norm 接近 0 的连续帧)"""
    norms = np.linalg.norm(actions[:, :6], axis=1)  # 前6维的欧几里得长度
    is_static = norms < zero_threshold
    
    # 找连续静止段(≥5帧)
    segments = []
    start = None
    for i, s in enumerate(is_static):
        if s and start is None:
            start = i
        elif not s and start is not None:
            if i - start >= min_len:
                segments.append({"start": start, "end": i-1, "length": i-start})
            start = None

def detect_jumps(actions, threshold_factor=5.0):
    """检测跳变帧(相邻帧差分超过 mean + N*std)"""
    diffs = np.abs(np.diff(actions, axis=0))
    diff_norms = np.linalg.norm(diffs[:, :6], axis=1)
    
    mean_d = diff_norms.mean()
    std_d = diff_norms.std()
    threshold = mean_d + threshold_factor * std_d  # 5σ 才算跳变
    
    jumps = [i+1 for i, dn in enumerate(diff_norms) if dn > threshold]

def detect_range_anomalies(actions, action_range=(-10, 10)):
    """检测范围异常(action 超出物理合理范围)"""
    for i, a in enumerate(actions):
        if np.any(a < action_range[0]) or np.any(a > action_range[1]):
            anomalies.append({"frame": i, "values": a})

def detect_frame_gaps(ep_data):
    """检测丢帧(frame_index 不连续)"""
    fi = ep_data["frame_index"].values
    for i in range(1, len(fi)):
        if fi[i] - fi[i-1] != 1:
            gaps.append({"frame": i, "gap": fi[i] - fi[i-1] - 1})

测试结果:DROID-100 检出 538 异常(314静止段 + 224跳变),99% episode 有异常------这是真机数据的典型特征。

4.3 工具3:模糊帧过滤(blur_filter.py)

算法:Laplacian 方差
python 复制代码
def analyze_video_blur(video_path, threshold=100.0, sample_step=1):
    """分析视频的模糊帧"""
    cap = cv2.VideoCapture(video_path)
    
    while True:
        ret, frame = cap.read()
        if not ret: break
        
        score = laplacian_variance(frame)  # cv2.Laplacian(gray, CV_64F).var()
        if score < threshold:  # 默认 100,值越小越模糊
            blur_frames.append({"frame": frame_idx, "score": score})
阈值参考
阈值 用途 说明
50 互联网视频 质量参差,阈值放宽
100 真机遥操 质量较好,标准阈值
200 高质量要求 严格过滤

测试结果:DROID-100 平均清晰度 1165,0 模糊帧(源数据质量高)。

4.4 工具4:去重(dedup.py)

算法:三帧签名 pHash
python 复制代码
def compute_episode_signature(loader, ep_idx, video_key):
    """计算 episode 的三帧签名(首/中/尾)"""
    frames = {}
    for label, fi in [("first", 0), ("mid", ep.length // 2), ("last", ep.length - 1)]:
        frame = loader.read_video_frame(video_key, ep_idx, fi)
        frames[label] = frame_to_phash(frame)  # 8x8 = 64bit
    
    # 综合签名 = 三帧拼接(192bit)
    combined = frames["first"] + frames["mid"] + frames["last"]
    return combined

def find_duplicates(signatures, threshold=0.95):
    """O(n²) 两两比较,相似度 > 阈值判定为重复"""
    for i in range(n):
        for j in range(i+1, n):
            sim = phash_similarity(signatures[i]["combined"], signatures[j]["combined"])
            if sim >= threshold:
                duplicates.append((i, j, sim))

为什么用三帧? 单帧容易误判(不同 episode 的首帧可能都是静止的桌面),三帧能区分完整操作序列。

测试结果:100 个视频 0 重复,最高相似度 0.849(低于 0.85 阈值,数据多样性良好)。

4.5 工具5:任务阶段分割(phase_segment.py)

算法:action norm + gripper 变化点检测
python 复制代码
def segment_episode(actions, zero_threshold=0.01):
    motion = np.linalg.norm(actions[:, :6], axis=1)  # 末端运动
    gripper = actions[:, 6]  # 夹爪开合
    
    # 1. 检测 gripper 变化点(0→1 释放, 1→0 抓取)
    gripper_changes = []
    for i in range(1, len(gripper)):
        if abs(gripper[i] - gripper[i-1]) > 0.3:
            gripper_changes.append({
                "frame": i,
                "type": "release" if gripper[i] > gripper[i-1] else "grasp"
            })
    
    # 2. 基于 motion + gripper 状态切分阶段
    phases = []
    # init: 开头静止段(motion ≈ 0)
    # approach: 从开始运动到抓取
    # grasp: 夹爪闭合瞬间(单帧)
    # transport: 抓取后到释放
    # release: 夹爪张开瞬间(单帧)
    # retreat: 释放后剩余
可视化

生成时序图:上图是 action norm(带阶段着色),下图是 gripper 信号(标注抓取/释放时刻)。

测试结果:DROID-100 平均 2.13 阶段/episode。检测到 5 个 grasp 事件(DROID 的 gripper 编码方式特殊,需调优)。

4.6 工具6:成功/失败预判(success_predict.py)

启发式 5 维评分
python 复制代码
def predict_success(actions):
    scores = {}
    
    # 1. 运动能量(太低=没动,太高=异常)
    avg_energy = motion.mean()
    if avg_energy < 0.01: scores["energy"] = 0.1      # 没动
    elif avg_energy > 2.0: scores["energy"] = 0.3     # 异常
    else: scores["energy"] = 0.8                       # 正常
    
    # 2. 夹爪循环(开→合→开=完整操作)
    if gripper_changes >= 2: scores["gripper_cycle"] = 0.9  # 完整循环
    elif gripper_changes == 1: scores["gripper_cycle"] = 0.5
    else: scores["gripper_cycle"] = 0.3
    
    # 3. 末尾静止段(任务完成停顿)
    if last_static >= 5: scores["end_pause"] = 0.8
    
    # 4. 开头静止段(任务准备)
    if first_static >= 2: scores["start_pause"] = 0.7
    
    # 5. 运动连续性(无明显跳变)
    if max_diff < 0.5: scores["continuity"] = 0.8
    
    # 加权综合
    weights = {"energy": 0.15, "gripper_cycle": 0.35, "end_pause": 0.20,
               "start_pause": 0.10, "continuity": 0.20}
    confidence = sum(scores[k] * weights[k] for k in weights)
    success = confidence >= 0.6

为什么 gripper_cycle 权重最高(0.35)? 因为夹爪的"开→合→开"循环是任务成功执行的最强信号------抓了东西又放了,说明操作完整。

测试结果:DROID-100 预判 36% 成功率,符合真机数据典型值(真机遥操成功率通常 30-50%)。

4.7 工具7:语言指令补充(lang_instruct.py)

VLM 调用(OpenAI 兼容接口)
python 复制代码
def call_vlm(frames, prompt):
    """调用 MiniMax-Text-01 VLM"""
    client = OpenAI(api_key=os.environ["LLM_API_KEY"],
                    base_url=os.environ.get("LLM_BASE_URL", "https://api.minimaxi.com/v1"))
    
    # 三帧 base64 编码
    content = [{"type": "text", "text": prompt}]
    for frame in frames:
        b64 = frame_to_base64(frame)
        content.append({
            "type": "image_url",
            "image_url": {"url": f"data:image/jpeg;base64,{b64}"}
        })
    
    response = client.chat.completions.create(
        model="MiniMax-Text-01",
        messages=[{"role": "user", "content": content}],
        max_tokens=100,
        temperature=0.3,  # 低温度保证一致性
    )
    return response.choices[0].message.content.strip()
提示词工程
python 复制代码
prompt = """This is a wrist-camera view of a robot performing a manipulation task.
Describe the task in one English sentence starting with a verb.
Focus on what object is being manipulated and what action is performed."""
降级策略
python 复制代码
def predict_instruction(loader, ep_idx, use_vlm=True):
    if use_vlm and os.environ.get("LLM_API_KEY"):
        try:
            return call_vlm(frames, prompt)  # VLM 模式
        except Exception:
            pass  # 失败则降级
    
    return generate_instruction_fallback(task_name)  # 模板模式

测试结果:

  • VLM 模式:100% 调用成功(20/20)
  • 动作类别识别准确(placing/picking)
  • 物体识别受分辨率限制(320x180 下 pot→bowl)

4.8 工具8:格式转换(format_convert.py)

LeRobot → HDF5
python 复制代码
def lerobot_to_hdf5(loader, output_path):
    with h5py.File(output_path, "w") as hf:
        for ep in loader.episodes:
            ep_group = hf.create_group(f"episode_{ep.episode_index:04d}")
            
            # actions
            actions = loader.get_actions(ep.episode_index)
            ep_group.create_dataset("actions", data=actions)
            
            # states
            states = loader.get_states(ep.episode_index)
            ep_group.create_dataset("states", data=states)
            
            # 视频帧(只记录路径和偏移,不提取)
            ep_group.attrs["video_path"] = os.path.basename(stream.path)
            ep_group.attrs["video_offset"] = ep.video_offset
            
            # 元数据
            ep_group.attrs["task"] = ep.task_name
            ep_group.attrs["fps"] = loader.fps

测试结果:100 episode 转 HDF5,文件大小 1.95MB(不含视频帧)。


五、流水线编排:3 个场景配置

5.1 场景配置(YAML)

yaml 复制代码
# teleoperation.yaml(真机遥操,最核心)
scenario_name: "真机遥操数据"
description: "多模态完整的真机数据,需对齐+异常修复+语义标注"

tools:
  align_check:
    enabled: true
  
  anomaly_fix:
    enabled: true
    zero_threshold: 0.001
    jump_threshold: 5.0
  
  blur_filter:
    enabled: true
    threshold: 100.0
    sample_step: 3
  
  phase_segment:
    enabled: true
    visualize_count: 10
  
  success_predict:
    enabled: true
    threshold: 0.6
  
  lang_instruct:
    enabled: true
    use_vlm: false  # 真机通常有 task_name,优先模板
  
  format_convert:
    enabled: true
    format: "hdf5"

# 运行顺序(对齐 → 清洗 → 标注 → 转换)
pipeline:
  - align_check        # 1. 时序对齐校验(前置必做)
  - anomaly_fix        # 2. 异常值检测修复
  - blur_filter        # 3. 模糊帧过滤
  - dedup              # 4. 去重
  - phase_segment      # 5. 阶段分割预标注
  - success_predict    # 6. 成功预判
  - lang_instruct      # 7. 语言指令补充
  - format_convert     # 8. 格式转换交付

5.2 编排器实现

python 复制代码
def run_pipeline(input_path, scenario, toolkit_root):
    config = load_scenario(scenario)  # 加载 YAML
    
    for step_name in config["pipeline"]:
        tool_cfg = config["tools"].get(step_name, {})
        if not tool_cfg.get("enabled", False):
            continue  # 跳过未启用的工具
        
        # 提取参数
        params = {k: v for k, v in tool_cfg.items() if k != "enabled"}
        
        # 动态导入工具模块并执行
        module = importlib.import_module(TOOL_MODULE_MAP[step_name])
        result = module.run(input_path, toolkit_root=toolkit_root, **params)

5.3 一键运行

bash 复制代码
# 真机遥操场景(8个工具全开)
python run_pipeline.py --input datasets/lerobot_droid_100 --scenario teleoperation

# 互联网视频场景(4个工具)
python run_pipeline.py --input test_data/internet_video --scenario internet_video

# 仿真合成场景(2个工具)
python run_pipeline.py --input sim_data --scenario simulation

六、真实客户场景测试

6.1 场景A:长视频自动切片

客户画像:给一个 9.26 分钟未切片长视频(8333帧),需自动识别 episode 边界。

算法:帧间差分
python 复制代码
def compute_frame_diff(prev, curr):
    """两帧差异得分(0=相同,1=完全不同)"""
    # 1. 直方图相关性(场景切换时急剧下降)
    h1 = cv2.calcHist([prev_gray], [0], None, [64], [0, 256])
    h2 = cv2.calcHist([curr_gray], [0], None, [64], [0, 256])
    hist_corr = cv2.compareHist(h1, h2, cv2.HISTCMP_CORREL)
    
    # 2. 像素均值差(场景切换时大幅变化)
    mean_diff = abs(prev_gray.mean() - curr_gray.mean()) / 255.0
    
    # 综合得分
    score = (1.0 - hist_corr) * 0.7 + mean_diff * 0.3
    return score
测试结果
指标
视频总帧数 8333(9.26分钟)
检测到切换点 29 个
切分出 episode 30 个
真值 episode 30 个
准确率 100%
耗时 7 秒

6.2 场景B:厨房第一人称 VLM 指令

客户画像:50 个 wrist 腕部相机视频,无语言指令。

VLM vs Ground Truth 对比
Episode VLM 生成 Ground Truth 重叠率
EP000 The robot is placing a marker into a metal bowl Put the marker in the pot 0.167
EP027 The robot is placing slices of bread into a toaster Put the bread in the toaster 0.250
EP021 The robot is placing an orange object into a container Put the orange rubber duck into the pot 0.200
统计
指标
VLM 调用成功率 100%(20/20)
指令平均长度 12.4 词
与 GT 词汇重叠率 0.133(Jaccard)
平均耗时 2.7 秒/视频

分析:VLM 能稳定识别动作类别(placing/picking),但 320x180 低分辨率下物体识别有限(pot→bowl, rubber duck→orange object)。

6.3 场景C:多相机不同步检测

客户画像:三路相机时钟错位,帧数不一致。

测试数据
相机 帧数 模拟问题
exterior_1 32212 基准
exterior_2 32162 删除前 50 帧模拟时钟偏移
wrist 32182 删除后 30 帧模拟丢帧
检测结果

align_check 精准检测到 5 个问题:

  1. 全局对齐失败: data=32212 vs ep_sum=32182
  2. exterior_2 帧数不匹配: 32162 vs 32212(差 50 帧)
  3. wrist 帧数不匹配: 32182 vs 32212(差 30 帧)
  4. 多相机不同步: 三路帧数互不一致
  5. frame_index 语义: episode_local(正常)

结论 :能准确定位哪路相机偏移多少帧,这是修复的基础。

6.4 场景D:500 条批量性能

测试数据:DROID-500,500 episode,161060 帧,468MB。

性能基准
工具 100ep 500ep 扩展性
align_check 0.3s <1s 线性 O(n)
anomaly_fix 0.3s 1s 线性 O(n)
phase_segment 3.9s 1s* 线性 O(n)
success_predict 0.3s 1s 线性 O(n)
blur_filter 390s ~1950s 线性(慢)
dedup 22s ~110s O(n²)

*phase_segment 500ep 比 100ep 快是因可视化数量减少

处理结果
  • 500 episode 4 秒跑完(不含 blur_filter)
  • 处理速度 125 episode/秒
  • 检出 2690 个异常(1570 静止段 + 1120 跳变)
  • 预判成功率 36%
线性扩展预测
数据量 预计耗时
1,000 episode ~8 秒
10,000 episode ~80 秒
100,000 episode ~13 分钟

七、踩坑总结(重要!)

7.1 frame_index 语义陷阱

现象:End 帧场景和 Start/Mid 不一致。

根因 :frame_index 是 episode 内索引(每集从 0),但视频是全局连续拼接的。

错误做法:

python 复制代码
# ❌ 所有"首帧"都指向视频开头
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_in_ep)

正确做法:

python 复制代码
# ✅ 全局帧位置 = episode 偏移 + episode 内帧索引
global_pos = ep.video_offset + frame_in_ep
cap.set(cv2.CAP_PROP_POS_FRAMES, global_pos)

验证:视频总帧数(32212) == 数据总行数(32212) == episode length 总和(32212)

教训:这是具身数据标注的核心痛点。客户给的原始数据 80% 有这类对齐问题,你能提供"对齐校验+修复"服务,就是差异化竞争力。

7.2 blur_filter 性能瓶颈

问题:逐帧解码视频太慢(100ep 要 390 秒)。

优化方案:

bash 复制代码
# 方案1:采样加速(牺牲精度,10倍速)
python tools/blur_filter.py --input data --sample-step 10

# 方案2:用 ffmpeg 抽帧代替 opencv(后续优化)
# ffmpeg -i input.mp4 -vf "select=not(mod(n\,10))" -vsync vfn frame_%04d.jpg

7.3 VLM 低分辨率限制

问题:320x180 分辨率下,MiniMax-Text-01 把 pot 识别成 bowl,把 rubber duck 识别成 orange object。

原因:腕部相机视角 + 低分辨率,物体细节丢失。

解决方案:

  • 客户提供 640x360 以上分辨率
  • 或换 GPT-4V / Claude 3.5 Sonnet(视觉能力更强)
  • 或提高提取帧的分辨率(插值)

7.4 DROID 的 gripper 编码

问题:phase_segment 的 grasp 检出率低(100 episode 只检测到 5 个 grasp)。

原因:DROID 的 gripper 信号不是简单的 0/1,编码方式特殊。

解决:需要针对不同数据集调整阈值,或用差分检测代替绝对值检测。

7.5 模拟数据的视频对齐问题

问题:DROID-500(复制5份模拟)的 align_check 报视频帧数不匹配(32212 vs 161060)。

原因:模拟数据把 parquet 复制5份(161060帧),但视频只有1份(32212帧)。

这是真实场景:客户的多模态数据也可能帧数不一致,align_check 能检测出来。


八、商业思考

8.1 这套工具值多少钱?

光轮智能 Q1 单季订单 5.5 亿元,卖的就是这 5 步:

  1. 数据清洗(对齐 + 异常 + 模糊 + 去重)
  2. 语言指令标注(VLM + 人工校)
  3. 阶段分割(自动预标注 + 人工校)
  4. 成功失败标记(启发式 + 人工校)
  5. 格式转换(按客户要求)

我的工具链覆盖了同样的 8 个任务,且自动化程度更高(500ep 4秒)。

8.2 自动化难度与赚钱路径

复制代码
自动化难度    工具                          策略
⭐ 极易       align_check / blur_filter     全自动,卖工具
             dedup / format_convert
⭐⭐ 简单     anomaly_fix / phase_segment   自动预标注+人工校准
             success_predict
⭐⭐⭐ 中     lang_instruct(VLM)            VLM辅助+人工
⭐⭐⭐⭐ 高   视觉标注(边界框/分割)         主要靠人工(但具身智能用不到)

赚钱路径:先做 ⭐~⭐⭐ 的(全自动/半自动,毛利高),用现金流养 ⭐⭐⭐ 的(高单价)。

8.3 入口产品:"数据体检"免费

align_check 作为免费入口产品,给客户输出"数据体检报告",建立信任后再谈标注大单。

这是 SaaS 的经典 PLG(产品驱动增长)打法:

  1. 免费体检 → 发现问题
  2. 收费清洗 → 解决问题
  3. 收费标注 → 增值服务
  4. 长期合作 → 持续接单

8.4 定价策略

服务项目 计费方式 参考价格
数据体检 免费 ¥0(入口产品)
时序对齐校验 按小时 ¥50-150/小时
阶段分割 按episode ¥3-10/episode
语言指令(VLM) 按episode ¥0.3-0.8/episode
完整流水线(打包) 按episode ¥10-30/episode

批量优惠:500ep 9折,2000ep 8折,5000ep 7折。


九、开源与复现

9.1 环境配置

bash 复制代码
pip install numpy pandas opencv-python matplotlib pyyaml h5py pyarrow python-dotenv openai

9.2 数据准备

bash 复制代码
# 下载 DROID-100(HuggingFace)
# https://huggingface.co/datasets/lerobot/droid_100

# 目录结构
datasets/lerobot_droid_100/
├── data/chunk-000/file-000.parquet
├── meta/info.json
└── videos/...

9.3 快速开始

bash 复制代码
# 1. 跑真机遥操场景全流程
python run_pipeline.py --input datasets/lerobot_droid_100 --scenario teleoperation

# 2. 单工具跑
python tools/align_check.py --input datasets/lerobot_droid_100
python tools/phase_segment.py --input datasets/lerobot_droid_100 --visualize 10
python tools/success_predict.py --input datasets/lerobot_droid_100

# 3. 长视频切片
python tools/long_video_split.py --input raw_long_video.mp4 --threshold 0.25

# 4. VLM 指令补充(需配置 .env)
# .env 内容:
# LLM_API_KEY=你的key
# LLM_BASE_URL=https://api.minimaxi.com/v1
# LLM_MODEL=MiniMax-Text-01
python tools/lang_instruct.py --input datasets/lerobot_droid_100 --sample 100

# 5. 格式转换
python tools/format_convert.py --input datasets/lerobot_droid_100 --format hdf5

9.4 输出示例

每个工具运行后产出 3 类文件:

复制代码
reports/
├── 20260724_140319_align_check_lerobot_droid_100.html    # HTML报告(给客户)
├── 20260724_140319_align_check_lerobot_droid_100.json    # JSON结果(给下游)
└── charts/
    └── phase_segment_lerobot_droid_100/
        └── ep_000.png                                      # 可视化图

十、后续迭代方向

优先级 方向 说明
P0 blur_filter 性能优化 用 ffmpeg 抽帧代替 opencv,加速 10 倍
P0 VLM 升级 接入 GPT-4V/Claude 提升指令准确率
P1 RLDS 适配器 加 RLDS→LeRobot 转换(tensorflow_datasets)
P1 并行化 当前串行,加多进程加速
P2 CVAT 集成 对接 CVAT 做人工校准平台
P2 垂直场景扩展 工业装配、医疗手术、农业采摘
P3 GUI 界面 阶段分割的时间轴校准 UI

十一、总结

本文记录了从 0 到 1 搭建具身智能数据处理工具链的完整实践:

核心成果

  • 8 个核心工具:覆盖时序对齐、异常检测、模糊过滤、去重、阶段分割、成功预判、语言指令、格式转换
  • 3 个场景配置:互联网视频、真机遥操、仿真合成
  • 4 个客户场景测试 :
    • 长视频切片(100% 准确率)
    • VLM 指令(100% API 成功率)
    • 多相机检测(5/5 问题全部检测)
    • 500 条批量(4 秒完成,125ep/秒)
  • 工程化实践:模块化设计、配置驱动、统一接口、HTML 报告、完整日志

核心价值

把光轮/帕西尼卖几亿元的"Model-Ready Data"流水线,用开源工具+自动化脚本复现,个人本地即可运行。

这不是替代头部公司,而是证明了这套流程可以被工程化、自动化------这正是中小数据服务商的生存空间。

行业启示

  1. 数据清洗比对标注更赚钱:L5 清洗自动化程度最高,毛利最高
  2. "数据体检"是最佳入口产品:免费体检 → 发现问题 → 收费处理
  3. 具身智能不需要视觉标注:端到端 policy 直接吃原始图像,L3/L4 可不做
  4. 性能不是瓶颈:500ep 4 秒,万级数据分钟级完成

工具版本 : v0.1.1

测试日期 : 2026-07-24

转载请注明出处


Tags : 具身智能 Embodied AI 数据处理 LeRobot DROID VLM 数据标注 自动化 Python 机器人 Model-Ready Data 光轮智能 具身数据

相关推荐
dadaobusi1 小时前
Sniper
人工智能
AI人工智能集结号1 小时前
AI回答采集:基于PostgreSQL JSONB的原始数据存储与可追溯性方案
数据库·人工智能·postgresql
WeilinerL1 小时前
CodeGraph:让AI理解代码仓库的神器
前端·人工智能
DFT计算杂谈2 小时前
交错磁研究进展材料物性与交叉应用
数据库·人工智能·python·opencv·算法
CCYe、2 小时前
企业 AI 网关采购需求清单:一份集团级 RFP 的能力映射
人工智能
xfan_me2 小时前
汽车维保记录精准版 API 快速接入指南
人工智能·汽车
IvorySQL2 小时前
PG 日报|查询优化器重大修复,支持更多复杂 SQL 优化
数据库·人工智能·postgresql·开源
AI视觉网奇2 小时前
大模型多轮对话技巧
人工智能
qq_454245032 小时前
engine prompt
人工智能·prompt