Download scripts/data/prepare_robotwin.py from poet70/vla-sft-code-dreamzero: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/poet70/vla-sft-code-dreamzero/resolve/refs%2Fpr%2F1/scripts/data/prepare_robotwin.py
- Command line
-
hf download hf://poet70/vla-sft-code-dreamzero@refs/pr/1/scripts/data/prepare_robotwin.py
-
curl -L -o prepare_robotwin.py https://huggingface.co/poet70/vla-sft-code-dreamzero/resolve/refs%2Fpr%2F1/scripts/data/prepare_robotwin.py
10.8 kB
| #!/usr/bin/env python3 | |
| """RoboTwin 数据准备脚本:将原始 ZIP 格式转换为 RobotWinDataset 期望的 qpos+videos+metas 格式。 | |
| 用法: | |
| python scripts/data/prepare_robotwin.py \\ | |
| --input /root/autol-tmp/data/robotwin3/dataset \\ | |
| --output /root/autol-tmp/data/robotwin_gear | |
| RoboTwin 原始结构 (ZIP): | |
| <task_dir>/ | |
| ├── franka_clean_50.zip | |
| │ └── franka_clean_50/ | |
| │ ├── scene_info.json | |
| │ ├── instructions/episodeN.json # {"seen": [...], "unseen": [...]} | |
| │ ├── video/episodeN.mp4 | |
| │ └── _traj_data/episodeN.pkl # {arm_key: [seg1, seg2, ...]} | |
| ├── aloha-agilex_clean_50.zip | |
| └── ... | |
| 输出结构: | |
| <output_dir>/ | |
| ├── <task>_<robot>/ | |
| │ ├── qpos/ | |
| │ │ ├── episode0.pt # [T_qpos, 14] float32 | |
| │ │ └── episode1.pt | |
| │ ├── videos/ | |
| │ │ ├── episode0.mp4 | |
| │ │ └── episode1.mp4 | |
| │ └── metas/ | |
| │ ├── task_0.txt | |
| │ └── task_1.txt | |
| └── ... | |
| """ | |
| import os, sys, json, argparse, logging, pickle, shutil | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| import zipfile | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| logger = logging.getLogger(__name__) | |
| # 14-dim layout: 前 7 = right arm, 后 7 = left arm | |
| # 单臂机器人:使用的手臂放前 N 位,其余补 0 | |
| ROBOT_CONFIG = { | |
| "franka": {"arm": "right", "dims": 7}, | |
| "aloha-agilex": {"arm": "left", "dims": 6}, | |
| "arx-x5": {"arm": "left", "dims": 6}, | |
| "ur5": {"arm": "left", "dims": 6}, | |
| "piper": {"arm": "left", "dims": 6}, | |
| } | |
| OUTPUT_STATE_DIM = 14 | |
| def get_robot_name(zip_path: Path) -> str | None: | |
| """从 ZIP 文件名提取机器人名称(去掉 _clean_50.zip 后缀)。""" | |
| name = zip_path.stem # e.g. franka_clean_50 | |
| # Remove _clean_50 or _50 suffix | |
| for suffix in ["_clean_50", "_50"]: | |
| if name.endswith(suffix): | |
| name = name[:-len(suffix)] | |
| break | |
| return name | |
| def get_arm_key(robot: str) -> str: | |
| """根据机器人类型返回 pkl 中的 arm key。""" | |
| cfg = ROBOT_CONFIG.get(robot) | |
| if cfg is None: | |
| logger.warning(f"Unknown robot {robot}, trying right_joint_path") | |
| return "right_joint_path" | |
| return f"{cfg['arm']}_joint_path" | |
| def merge_trajectory_segments(segments: list) -> np.ndarray: | |
| """合并多段轨迹为一个连续数组。 | |
| Args: | |
| segments: list of dict, each with "position" key [T_i, D] | |
| Returns: | |
| concatenated position array [sum(T_i), D] | |
| """ | |
| arrays = [seg["position"] for seg in segments if seg.get("position") is not None] | |
| if not arrays: | |
| return np.zeros((0, arrays[0].shape[1])) if arrays else np.zeros((0, 1)) | |
| return np.concatenate(arrays, axis=0) | |
| def pad_to_14dim(arr: np.ndarray, joint_dim: int) -> np.ndarray: | |
| """Pad joint positions to 14-dim。 | |
| 布局:前 7 = right arm, 后 7 = left arm。 | |
| - franka (7, right): 放在前 7 维 | |
| - aloha (6, left): 放在后 6 维(前补 0) | |
| """ | |
| if arr.ndim == 1: | |
| padded = np.zeros(OUTPUT_STATE_DIM, dtype=np.float32) | |
| if joint_dim == 7: | |
| padded[:joint_dim] = arr.astype(np.float32) | |
| else: # 6-dim → 后 6 维 | |
| padded[OUTPUT_STATE_DIM - joint_dim:] = arr.astype(np.float32) | |
| else: | |
| padded = np.zeros((arr.shape[0], OUTPUT_STATE_DIM), dtype=np.float32) | |
| if joint_dim == 7: | |
| padded[:, :joint_dim] = arr.astype(np.float32) | |
| else: # 6-dim → 后 6 维 | |
| padded[:, OUTPUT_STATE_DIM - joint_dim:] = arr.astype(np.float32) | |
| return padded | |
| def get_task_description(zf: zipfile.ZipFile, robot_prefix: str, ep_idx: int) -> str: | |
| """从 instruction JSON 读取任务描述。 | |
| Returns: | |
| 第一条 "seen" 指令,或 fallback 文本 | |
| """ | |
| try: | |
| inst_path = f"{robot_prefix}/instructions/episode{ep_idx}.json" | |
| with zf.open(inst_path) as f: | |
| inst = json.load(f) | |
| seen = inst.get("seen", []) | |
| if seen: | |
| return seen[0] | |
| unseen = inst.get("unseen", []) | |
| if unseen: | |
| return unseen[0] | |
| except Exception as e: | |
| logger.debug(f" Cannot read instruction: {e}") | |
| return "Manipulate the object on the table" | |
| def process_robot_zip( | |
| zip_path: Path, | |
| output_dir: Path, | |
| delete_after: bool = False, | |
| ) -> int: | |
| """处理一个 RoboTwin ZIP 文件。 | |
| Returns: | |
| 成功处理的 episode 数 | |
| """ | |
| robot = get_robot_name(zip_path) | |
| if robot is None: | |
| logger.warning(f"Cannot determine robot from {zip_path.name}, skipping") | |
| return 0 | |
| # 输出目录:{task}_{robot} | |
| task_name = zip_path.parent.name | |
| out_subdir = output_dir / f"{task_name}_{robot}" | |
| qpos_dir = out_subdir / "qpos" | |
| video_dir = out_subdir / "videos" | |
| meta_dir = out_subdir / "metas" | |
| qpos_dir.mkdir(parents=True, exist_ok=True) | |
| video_dir.mkdir(parents=True, exist_ok=True) | |
| meta_dir.mkdir(parents=True, exist_ok=True) | |
| cfg = ROBOT_CONFIG.get(robot) | |
| if cfg is None: | |
| logger.warning(f"Unknown robot {robot}, skipping {zip_path}") | |
| return 0 | |
| joint_dim = cfg["dims"] | |
| arm_key = get_arm_key(robot) | |
| robot_prefix = f"{robot}_clean_50" # ZIP 内部目录名 | |
| try: | |
| zf = zipfile.ZipFile(str(zip_path)) | |
| except Exception as e: | |
| logger.error(f"Cannot open {zip_path}: {e}") | |
| return 0 | |
| # 从 scene_info.json 获取 episode 列表 | |
| try: | |
| with zf.open(f"{robot_prefix}/scene_info.json") as f: | |
| scene_info = json.load(f) | |
| except Exception as e: | |
| logger.warning(f"No scene_info in {zip_path}: {e}") | |
| zf.close() | |
| return 0 | |
| # 列出所有 episode | |
| episode_keys = sorted([k for k in scene_info if k.startswith("episode_")], | |
| key=lambda x: int(x.split("_")[1])) | |
| if not episode_keys: | |
| logger.warning(f"No episodes in scene_info of {zip_path}") | |
| zf.close() | |
| return 0 | |
| success_count = 0 | |
| for ep_key in episode_keys: | |
| ep_idx = int(ep_key.split("_")[1]) | |
| ep_name = f"episode{ep_idx}" | |
| # 检查是否有轨迹数据 | |
| traj_path = f"{robot_prefix}/_traj_data/{ep_name}.pkl" | |
| video_path_in = f"{robot_prefix}/video/{ep_name}.mp4" | |
| if traj_path not in zf.namelist(): | |
| logger.debug(f" No traj data for {ep_name} in {zip_path.name}") | |
| continue | |
| if video_path_in not in zf.namelist(): | |
| logger.debug(f" No video for {ep_name} in {zip_path.name}") | |
| continue | |
| try: | |
| # --- 轨迹处理 --- | |
| with zf.open(traj_path) as f: | |
| traj_data = pickle.load(f) | |
| segments = traj_data.get(arm_key, []) | |
| if not segments: | |
| logger.debug(f" No {arm_key} segments for {ep_name}") | |
| continue | |
| # 合并多段轨迹 | |
| pos = merge_trajectory_segments(segments) # [T, joint_dim] | |
| if pos.shape[0] < 24: # 至少需要 num_frames + action_horizon = 24 | |
| logger.debug(f" Too few frames ({pos.shape[0]}) for {ep_name}") | |
| continue | |
| # Pad 到 14-dim | |
| pos_14 = pad_to_14dim(pos, joint_dim) # [T, 14] | |
| # 保存为 .pt | |
| qpos_path = qpos_dir / f"{ep_name}.pt" | |
| torch.save(torch.from_numpy(pos_14), qpos_path) | |
| # --- 视频提取 --- | |
| video_out_path = video_dir / f"{ep_name}.mp4" | |
| with zf.open(video_path_in) as src, open(video_out_path, "wb") as dst: | |
| shutil.copyfileobj(src, dst) | |
| # --- 任务描述 --- | |
| task_desc = get_task_description(zf, robot_prefix, ep_idx) | |
| meta_path = meta_dir / f"task_{ep_idx}.txt" | |
| with open(meta_path, "w") as f: | |
| f.write(task_desc) | |
| success_count += 1 | |
| except Exception as e: | |
| logger.warning(f" Error processing {ep_name} in {zip_path.name}: {e}") | |
| continue | |
| zf.close() | |
| # 删除 ZIP 释放空间 | |
| if delete_after and success_count > 0: | |
| try: | |
| zip_path.unlink() | |
| logger.info(f" Deleted {zip_path.name}") | |
| except Exception as e: | |
| logger.warning(f" Cannot delete {zip_path.name}: {e}") | |
| if success_count > 0: | |
| logger.info(f"{zip_path.name}: {success_count}/{len(episode_keys)} episodes extracted -> {out_subdir}") | |
| return success_count | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Prepare RoboTwin data for DreamZero training") | |
| parser.add_argument("--input", "-i", required=True, | |
| help="RoboTwin 数据目录 (含任务子目录)") | |
| parser.add_argument("--output", "-o", required=True, | |
| help="输出目录") | |
| parser.add_argument("--delete-zip", action="store_true", | |
| help="处理完成后删除 ZIP 文件(节省空间)") | |
| parser.add_argument("--max-tasks", type=int, default=None, | |
| help="最多处理前 N 个任务(用于测试)") | |
| parser.add_argument("--num-workers", type=int, default=4, | |
| help="并行处理数(暂未实现)") | |
| args = parser.parse_args() | |
| input_dir = Path(args.input) | |
| output_dir = Path(args.output) | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| # 扫描所有任务目录 | |
| task_dirs = sorted([d for d in input_dir.iterdir() if d.is_dir()]) | |
| logger.info(f"Found {len(task_dirs)} task directories") | |
| if args.max_tasks: | |
| task_dirs = task_dirs[:args.max_tasks] | |
| logger.info(f"Limited to {args.max_tasks} tasks") | |
| total_episodes = 0 | |
| total_zips = 0 | |
| for task_dir in task_dirs: | |
| # 找到所有 ZIP 文件 | |
| zip_files = sorted(task_dir.glob("*_clean_50.zip")) | |
| if not zip_files: | |
| logger.warning(f"No ZIP files in {task_dir}") | |
| continue | |
| for zip_path in zip_files: | |
| try: | |
| ep_count = process_robot_zip( | |
| zip_path, output_dir, delete_after=args.delete_zip | |
| ) | |
| total_episodes += ep_count | |
| total_zips += 1 | |
| except Exception as e: | |
| logger.error(f"Fatal error processing {zip_path}: {e}") | |
| continue | |
| logger.info(f"Done! Processed {total_zips} ZIPs, {total_episodes} episodes") | |
| logger.info(f"Output: {output_dir}") | |
| if __name__ == "__main__": | |
| main() | |