主题
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 syncpyproject.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.json的video顶层 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-episodeepisodes_stats.jsonl:Isaac-GR00T/gr00t/data/dataset/lerobot_episode_loader.py:182-185是硬 assert;schema 含mean/std/min/max/q01/q996 字段(带 q01/q99,不带 count),对齐gr00t/data/stats.py:87-94。 - dtype 全
float32:对齐 demo dataset。GR00Tstats.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),不用 IsaacLabconvert_dataset.py的mp4v。 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(参考实现,注意它的几处偏离规范)