Skip to content

l3/ — L3 数据处理层(独立 venv 子项目)

为什么独立 venv

LeRobot 的依赖(torch、torchcodec、pyav 等)跟 IsaacSim 6.0.0 锁定的 torch==2.10.0 cu128 不兼容(已踩过坑)。所以仓库拓扑上是 l2/(IsaacSim + IsaacLab)和 l3/(lerobot)两个对称的独立 venv,用 HDF5 文件作为边界。

装环境

bash
cd l3
uv sync

pyproject.toml 里 pin 了 lerobot==0.3.3(最后一个 default 写 dataset 格式 v2.1 的 PyPI 版本),CPU torch wheel。版本选择的完整推理见 LeRobot 版本研究

在 pipeline 里的位置

data/demos/<source>_<task>.hdf5  (上游:l2/scripted / l2/rl / l2/teleop 的产物)

        ▼ convert.py
data/lerobot/<dataset_name>/     (下游:GR00T N1.7 微调直接消费)
        ├── meta/
        │   ├── info.json              codebase_version="v2.1"(GR00T 不读,纯礼仪)
        │   ├── episodes.jsonl
        │   ├── tasks.jsonl
        │   ├── stats.json             ★ 全局 stats(GR00T 1.7 hard assert 要这个)
        │   └── modality.json          GR00T-specific:state/action 切片定义
        └── data/chunk-000/episode_NNNNNN.parquet

现阶段无 camera observation,所以也不写 videos/info.json["features"] 里的 video feature。加 cam 后还要补 mp4 编码 + observation.images.<cam> feature 声明 + modality.jsonvideo 顶层 key。

怎么跑

bash
# 单文件转换
uv run python convert.py ../data/demos/scripted_lift_cube.hdf5 ../data/lerobot/lift_cube_v0/

# 多来源合并(按文件名前缀自动打 source 标)
uv run python convert.py ../data/demos/ ../data/lerobot/lift_cube_v0_mixed/

# 端到端 verify:用 GR00T 1.7 自己的 LeRobotEpisodeLoader 实际加载(最强证据)
PYTHONPATH=../Isaac-GR00T uv run python -c "
import sys, types
# stub 掉 video / initial_actions 模块避免拉 av/cv2/torchvision(lift_cube 当前无 cam,不需要它们)
sys.modules['gr00t.utils.video_utils'] = types.ModuleType('gr00t.utils.video_utils')
sys.modules['gr00t.utils.video_utils'].get_frames_by_indices = lambda *a, **kw: None
stub2 = types.ModuleType('gr00t.utils.initial_actions')
stub2.INITIAL_ACTIONS_FILENAME = 'initial_actions.npz'
stub2.load_initial_actions = lambda *a, **kw: None
sys.modules['gr00t.utils.initial_actions'] = stub2

from gr00t.data.dataset.lerobot_episode_loader import LeRobotEpisodeLoader
from gr00t.data.types import ModalityConfig

loader = LeRobotEpisodeLoader(
    '../data/lerobot/lift_cube_v0',
    {
        'state': ModalityConfig(delta_indices=[0],
            modality_keys=['joint_pos','joint_vel','object_position','target_object_position','last_action']),
        'action': ModalityConfig(delta_indices=[0],
            modality_keys=['ee_pose','gripper']),
    },
)
print('episodes:', len(loader), 'lengths:', loader.episode_lengths)
df = loader._load_parquet_data(0)
print('episode 0 切片后列:', list(df.columns))
"

关键设计决策

  • dataset schema 对齐 GR00T N1.7 demo (Isaac-GR00T/demo_data/cube_to_bowl_5/),不是 lerobot 库的纯 v2.1 spec。源码层 verify 见 GR00T 1.7 Dataset 约束
  • codebase_version="v2.1" 是 magic version——GR00T 1.7 整个 _load_metadata 不读这个字段,写 "v2.0" 也能跑;但跟随 demo dataset 的 "v2.1" 表述。
  • stats 走全局 meta/stats.json,不是 lerobot v2.1 的 per-episode episodes_stats.jsonlIsaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:182-185 是硬 assert;schema 含 mean/std/min/max/q01/q99 6 字段(带 q01/q99,不带 count),对齐 gr00t/data/stats.py:87-94
  • dtype 全 float32:对齐 demo dataset。GR00T stats.py:84-85 算 stats 时也强转 float32,落盘对齐省一次拷贝。
  • 不写 next.done / next.reward:GR00T 1.7 整个仓库不引用,demo dataset 也没有。
  • modality.json 必须生成——GR00T loader 强依赖它把 1-D concat 的 observation.state / action 切回语义字段。
  • 多来源合并支持convert.py 接受多个 HDF5 输入,按文件名前缀(scripted_* / rl_* / teleop_*)给每条 episode 打 source 自定义字段,写到 episodes.jsonl。GR00T loader 不读它(只读 length),合并多来源 dataset 时供质量过滤 / 调试用。
  • 现阶段无 video feature:等 l2/scripted/env_cfg.py 加 cam 后再补 video 编码 + features 声明。补的时候用 h264 + yuv420p(对齐 NVIDIA 公开 dataset),用 IsaacLab convert_dataset.pymp4v
  • total_chunks 严格等于 ceil(N/chunks_size)——IsaacLab 自带的 convert_dataset.py 硬编码 0,本仓库不抄。
  • splits={"train": "0:N"} 用真实 episode 数——IsaacLab 那边硬编码 "0:100",本仓库不抄。

文件清单

文件用途
pyproject.toml独立 venv 的依赖定义(lerobot==0.3.3 + h5py,convert.py 实际只用 h5py + numpy + pandas
convert.py主入口:HDF5 → LeRobot dataset(GR00T 1.7 兼容)
tests/schema 测试(暂未写,端到端 verify 直接跑 GR00T loader 即可)

参考

  • L2 三种轨迹来源 §4
  • LeRobot 版本研究
  • IsaacLab 自带的转换器 IsaacLab/scripts/imitation_learning/locomanipulation_sdg/gr00t/convert_dataset.py(参考实现,注意它的几处偏离规范)