File size: 8,766 Bytes
ac29381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/usr/bin/env python3
"""
Prepare LIBERO dataset for DreamZero GEAR pipeline.

Converts chunked LeRobot v2 format (multiple episodes per parquet, images as PNG bytes)
into individual-episode format (one parquet + one mp4 per episode) expected by
convert_lerobot_to_gear.py.

Usage:
    python3 prepare_libero_gear.py \
        --input-dir /root/autodl-tmp/data/libero \
        --output-dir /root/autodl-tmp/data/libero_gear
"""

import argparse
import json
import os
import io
import sys
import time
from pathlib import Path

import numpy as np
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
from PIL import Image
import cv2


def parse_args():
    parser = argparse.ArgumentParser(description="Prepare LIBERO data for DreamZero GEAR pipeline")
    parser.add_argument("--input-dir", required=True, help="Path to original LIBERO dataset")
    parser.add_argument("--output-dir", required=True, help="Path for output GEAR-ready dataset")
    parser.add_argument("--num-workers", type=int, default=4, help="Number of parallel workers")
    parser.add_argument("--skip-video", action="store_true", help="Skip video encoding (test only)")
    return parser.parse_args()


def load_info(info_path: Path) -> dict:
    with open(info_path) as f:
        return json.load(f)


def save_info(info: dict, output_path: Path, num_episodes: int, total_frames: int):
    """Update info.json for individual-episode format."""
    info["total_episodes"] = num_episodes
    info["total_frames"] = total_frames
    info["chunks_size"] = 2000  # All episodes in chunk-000
    info["data_path"] = "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet"
    info["video_path"] = "videos/{video_key}/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.mp4"

    # Remove meta/episodes path reference since we'll embed tasks directly
    info.pop("splits", None)

    with open(output_path / "meta" / "info.json", "w") as f:
        json.dump(info, f, indent=2)


def decode_png_to_rgb(png_bytes: bytes) -> np.ndarray:
    """Decode PNG bytes to RGB numpy array (H, W, 3) uint8."""
    img = Image.open(io.BytesIO(png_bytes))
    return np.array(img.convert("RGB"))


def extract_episode_metadata(input_dir: Path) -> tuple[pd.DataFrame, dict]:
    """Read tasks from the episodes metadata."""
    meta_dir = input_dir / "meta" / "episodes"
    if meta_dir.exists():
        ep_files = sorted(meta_dir.rglob("*.parquet"))
        if ep_files:
            df = pd.read_parquet(ep_files[0])
            tasks = {}
            for _, row in df.iterrows():
                ep_idx = row["episode_index"]
                tasks[ep_idx] = row["tasks"]
            return df, tasks

    # Fallback: scan parquet files for task_index
    return None, {}


def process_parquet_file(
    parquet_path: Path,
    output_data_dir: Path,
    output_video_dir: Path,
    fps: float,
    skip_video: bool = False,
) -> tuple[int, int]:
    """
    Process a single chunked parquet file.
    Returns (num_episodes_processed, num_frames_processed).
    """
    # Read the parquet file
    df = pd.read_parquet(parquet_path)

    # Group by episode_index
    episodes_processed = 0
    frames_processed = 0

    for ep_idx, group in df.groupby("episode_index"):
        ep_idx = int(ep_idx)
        group = group.reset_index(drop=True)
        n_frames = len(group)

        # Output parquet path
        ep_parquet_path = output_data_dir / f"episode_{ep_idx:06d}.parquet"

        # Drop the image columns for the parquet (they're in the video now)
        # But keep them for now — the official stats computation only uses numeric columns
        # We need to keep image columns as they might be needed by the dataset loader
        # Actually, for the GEAR format, images should ONLY be in videos.
        # Remove image columns to avoid confusion.
        parquet_cols = [c for c in group.columns
                        if not c.startswith("observation.images.")]
        df_out = group[parquet_cols].copy()

        # Write parquet
        table = pa.Table.from_pandas(df_out)
        pq.write_table(table, ep_parquet_path)

        if not skip_video:
            # Decode and write video for observation.images.image (first camera)
            frames = []
            for _, row in group.iterrows():
                img_bytes = row["observation.images.image"]["bytes"]
                frame = decode_png_to_rgb(img_bytes)
                frames.append(frame)

            # Write mp4 video
            ep_video_path = output_video_dir / f"episode_{ep_idx:06d}.mp4"
            height, width = frames[0].shape[:2]
            fourcc = cv2.VideoWriter_fourcc(*"mp4v")
            out = cv2.VideoWriter(
                str(ep_video_path), fourcc, fps, (width, height)
            )
            for frame in frames:
                # cv2 uses BGR order
                out.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
            out.release()

        episodes_processed += 1
        frames_processed += n_frames

    return episodes_processed, frames_processed


def main():
    args = parse_args()
    input_dir = Path(args.input_dir)
    output_dir = Path(args.output_dir)

    # Create output structure
    output_data_dir = output_dir / "data" / "chunk-000"
    output_video_dir = output_dir / "videos" / "observation.images.image" / "chunk-000"
    output_meta_dir = output_dir / "meta"

    output_data_dir.mkdir(parents=True, exist_ok=True)
    output_video_dir.mkdir(parents=True, exist_ok=True)
    output_meta_dir.mkdir(parents=True, exist_ok=True)

    # Load original info.json
    info = load_info(input_dir / "meta" / "info.json")
    fps = info.get("fps", 10.0)

    # Find all data parquet files
    data_dir = input_dir / "data" / "chunk-000"
    parquet_files = sorted(data_dir.glob("file-*.parquet"))
    print(f"Found {len(parquet_files)} parquet files")

    # Process each file
    total_episodes = 0
    total_frames = 0
    start_time = time.time()

    for i, pf in enumerate(parquet_files):
        n_eps, n_frames = process_parquet_file(
            pf, output_data_dir, output_video_dir, fps,
            skip_video=args.skip_video,
        )
        total_episodes += n_eps
        total_frames += n_frames

        elapsed = time.time() - start_time
        rate = (i + 1) / elapsed if elapsed > 0 else 0
        eta = (len(parquet_files) - i - 1) / rate if rate > 0 else 0
        print(
            f"  [{i+1}/{len(parquet_files)}] {pf.name}: "
            f"{n_eps} eps, {n_frames} frames "
            f"({rate:.1f} files/min, ETA {eta/60:.0f}min)"
        )

    # Write info.json
    save_info(info, output_dir, total_episodes, total_frames)

    # Copy tasks metadata if available
    meta_ep_dir = input_dir / "meta" / "episodes" / "chunk-000"
    if meta_ep_dir.exists():
        ep_files = sorted(meta_ep_dir.glob("*.parquet"))
        if ep_files:
            ep_meta_df = pd.read_parquet(ep_files[0])
            # Extract task_index → task mapping
            tasks = {}
            for _, row in ep_meta_df.iterrows():
                ep_idx = int(row["episode_index"])
                task_text = row["tasks"]
                if isinstance(task_text, np.ndarray):
                    task_text = task_text.item() if task_text.size > 0 else ""
                elif isinstance(task_text, bytes):
                    task_text = task_text.decode("utf-8")
                tasks[ep_idx] = str(task_text)

            # Write tasks.jsonl for GEAR format
            unique_tasks = sorted(set(tasks.values()))
            with open(output_meta_dir / "tasks.jsonl", "w") as f:
                for ti, task in enumerate(unique_tasks):
                    f.write(json.dumps({"task_index": ti, "task": task}) + "\n")

            # Write episodes.jsonl
            with open(output_meta_dir / "episodes.jsonl", "w") as f:
                for _, row in ep_meta_df.iterrows():
                    ep_idx = int(row["episode_index"])
                    length = int(row["length"])
                    task_text = tasks.get(ep_idx, "")
                    task_index = unique_tasks.index(task_text) if task_text in unique_tasks else -1
                    f.write(json.dumps({
                        "episode_index": ep_idx,
                        "length": length,
                        "task_index": task_index,
                    }) + "\n")

            print(f"Wrote {len(unique_tasks)} tasks and {total_episodes} episode entries")

    print(f"\nDone! {total_episodes} episodes, {total_frames} frames")
    print(f"Output: {output_dir}")
    print(f"Time: {(time.time() - start_time)/60:.1f} minutes")
    print(f"\nNext step: run convert_lerobot_to_gear.py on the output dir")


if __name__ == "__main__":
    main()