| |
| """将 ManiFeel (zarr 格式) 转换为 HuggingFace datasets 格式。 |
| |
| 用法: |
| python scripts/data/convert_manifeel_to_hf.py \\ |
| --input /path/to/manifeel_zarr \\ |
| --output /path/to/hf_manifeel \\ |
| --num-workers 8 |
| |
| ManiFeel zarr 结构: |
| <task_dir>/ |
| ├── front/ # zarr 数组 [T, H, W, C] |
| ├── wrist/ |
| ├── side/ |
| ├── state/ # [T, 7] |
| ├── action/ # [T, 6] |
| └── meta/ |
| └── episode_ends # episode 边界索引 |
| """ |
|
|
| import os, json, argparse, logging |
| from pathlib import Path |
|
|
| import numpy as np |
| import pandas as pd |
| import pyarrow as pa |
| import pyarrow.parquet as pq |
| import zarr |
| import cv2 |
| from tqdm import tqdm |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
| |
| ACTION_HORIZON = 12 |
| MAX_STATE_DIM = 44 |
| MAX_ACTION_DIM = 32 |
| VIDEO_HEIGHT = 160 |
| VIDEO_WIDTH = 320 |
| FPS = 30 |
|
|
|
|
| def pad_state(state: np.ndarray) -> np.ndarray: |
| d = state.shape[-1] |
| padded = np.zeros((MAX_STATE_DIM,), dtype=np.float32) |
| padded[:d] = state.astype(np.float32) |
| return padded |
|
|
|
|
| def pad_action(action_chunk: np.ndarray) -> np.ndarray: |
| """action_chunk: [horizon, D]""" |
| d = action_chunk.shape[-1] |
| padded = np.zeros((*action_chunk.shape[:-1], MAX_ACTION_DIM), dtype=np.float32) |
| padded[..., :d] = action_chunk.astype(np.float32) |
| return padded |
|
|
|
|
| def process_task(args): |
| """处理一个 ManiFeel 任务目录,返回训练样本列表。""" |
| task_dir_str, output_dir = args |
| task_dir = Path(task_dir_str) |
| task_name = task_dir.name |
|
|
| try: |
| |
| front = zarr.open(str(task_dir / "front"), mode="r") |
| wrist = zarr.open(str(task_dir / "wrist"), mode="r") |
| side = zarr.open(str(task_dir / "side"), mode="r") |
| state_arr = zarr.open(str(task_dir / "state"), mode="r") |
| action_arr = zarr.open(str(task_dir / "action"), mode="r") |
| episode_ends = zarr.open(str(task_dir / "meta" / "episode_ends"), mode="r") |
| except Exception as e: |
| logger.warning(f"无法读取 {task_dir}: {e}") |
| return [] |
|
|
| |
| front_np = np.array(front) |
| wrist_np = np.array(wrist) |
| side_np = np.array(side) |
| state_np = np.array(state_arr) |
| action_np = np.array(action_arr) |
| ends = np.array(episode_ends) |
|
|
| records = [] |
| video_out_dir = output_dir / "videos" |
| |
| for view_idx, view_name in enumerate(["front", "wrist", "side"]): |
| (video_out_dir / f"view_{view_idx}").mkdir(parents=True, exist_ok=True) |
|
|
| prev_end = 0 |
| for ep_idx, end in enumerate(ends): |
| start = prev_end |
| prev_end = end |
| ep_len = end - start |
| if ep_len < ACTION_HORIZON + 1: |
| continue |
|
|
| |
| video_paths = [] |
| for view_idx, view_data in enumerate([front_np, wrist_np, side_np]): |
| ep_frames = view_data[start:end] |
| video_filename = f"episode_{task_name}_{ep_idx:04d}_view{view_idx}.mp4" |
| video_path = video_out_dir / f"view_{view_idx}" / video_filename |
|
|
| |
| h, w = ep_frames.shape[1], ep_frames.shape[2] |
| writer = cv2.VideoWriter( |
| str(video_path), |
| cv2.VideoWriter_fourcc(*"mp4v"), |
| FPS, (VIDEO_WIDTH, VIDEO_HEIGHT), |
| ) |
| for f_idx in range(ep_len): |
| frame = ep_frames[f_idx] |
| if h != VIDEO_HEIGHT or w != VIDEO_WIDTH: |
| frame = cv2.resize(frame, (VIDEO_WIDTH, VIDEO_HEIGHT)) |
| if frame.shape[-1] == 3: |
| frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) |
| writer.write(frame) |
| writer.release() |
| video_paths.append(f"videos/view_{view_idx}/{video_filename}") |
|
|
| |
| ep_state = state_np[start:end] |
| ep_action = action_np[start:end] |
|
|
| for t in range(ep_len - ACTION_HORIZON): |
| state = pad_state(ep_state[t]) |
| action_chunk = ep_action[t:t + ACTION_HORIZON] |
| action_chunk = pad_action(action_chunk) |
|
|
| |
| records.append({ |
| "task": task_name, |
| "episode_index": ep_idx, |
| "frame_index": t, |
| "state": state.tolist(), |
| "action": action_chunk.tolist(), |
| "action_mask": [True] * ACTION_HORIZON, |
| "text": task_name.replace("_", " ").replace("-", " "), |
| "video_path_0": video_paths[0], |
| "video_path_1": video_paths[1], |
| "video_path_2": video_paths[2], |
| }) |
|
|
| logger.info(f" {task_name}: {len(records)} samples") |
| return records |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="Convert ManiFeel zarr to HF datasets format") |
| parser.add_argument("--input", "-i", required=True, help="ManiFeel 数据根目录(包含任务子目录)") |
| parser.add_argument("--output", "-o", required=True, help="HF 数据集输出目录") |
| parser.add_argument("--num-workers", "-w", 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"找到 {len(task_dirs)} 个任务目录") |
|
|
| |
| all_records = [] |
| for task_dir in tqdm(task_dirs, desc="Processing tasks"): |
| records = process_task((str(task_dir), output_dir)) |
| all_records.extend(records) |
|
|
| logger.info(f"总共生成 {len(all_records)} 条训练样本") |
|
|
| if not all_records: |
| logger.error("未生成任何样本!") |
| return |
|
|
| |
| df = pd.DataFrame(all_records) |
| data_dir = output_dir / "data" |
| data_dir.mkdir(parents=True, exist_ok=True) |
|
|
| table = pa.Table.from_pandas(df) |
| pq.write_table(table, data_dir / "train-00000.parquet") |
|
|
| |
| dataset_info = { |
| "description": "ManiFeel benchmark dataset for DreamZero", |
| "features": { |
| "task": {"dtype": "string", "_type": "Value"}, |
| "episode_index": {"dtype": "int64", "_type": "Value"}, |
| "frame_index": {"dtype": "int64", "_type": "Value"}, |
| "state": {"dtype": "float32", "shape": [MAX_STATE_DIM], "_type": "Sequence"}, |
| "action": {"dtype": "float32", "shape": [ACTION_HORIZON, MAX_ACTION_DIM], "_type": "Sequence"}, |
| "action_mask": {"dtype": "bool", "shape": [ACTION_HORIZON], "_type": "Sequence"}, |
| "text": {"dtype": "string", "_type": "Value"}, |
| "video_path_0": {"dtype": "string", "_type": "Value"}, |
| "video_path_1": {"dtype": "string", "_type": "Value"}, |
| "video_path_2": {"dtype": "string", "_type": "Value"}, |
| }, |
| "num_views": 3, |
| "splits": {"train": {"num_examples": len(all_records)}}, |
| } |
| with open(output_dir / "dataset_info.json", "w") as f: |
| json.dump(dataset_info, f, indent=2) |
|
|
| |
| readme = f"""--- |
| license: cc-by-4.0 |
| --- |
| |
| # DreamZero - ManiFeel |
| |
| ## Description |
| ManiFeel benchmark dataset (3 views: front, wrist, side) converted for DreamZero. |
| |
| ## Schema |
| | Column | Type | Description | |
| |--------|------|-------------| |
| | video_path_0/1/2 | string | Front/wrist/side video files | |
| | state | float32[{MAX_STATE_DIM}] | Robot state (padded) | |
| | action | float32[{ACTION_HORIZON},{MAX_ACTION_DIM}] | Action chunks (padded) | |
| | text | string | Task description | |
| | episode_index | int64 | Episode ID | |
| |
| ## Statistics |
| - Total samples: {len(all_records)} |
| - Views: 3 (front, wrist, side) |
| - Video resolution: {VIDEO_WIDTH}x{VIDEO_HEIGHT} |
| """ |
| with open(output_dir / "README.md", "w") as f: |
| f.write(readme) |
|
|
| logger.info(f"转换完成!输出: {output_dir}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|