Shuyang-Yu-808
Add Robometer code + Robometer-4B weights
319eb16
Raw
History Blame Contribute Delete
13 kB
#!/usr/bin/env python3
"""
H2R dataset loader for the generic dataset converter for Robometer model training.
https://huggingface.co/datasets/dannyXSC/HumanAndRobot
Human2Robot: Learning Robot Actions from Paired Human-Robot Videos
This module contains H2R-specific logic for loading and processing HDF5 files.
Updated to support OXE-style streaming conversion: write videos and build
HF entries on the fly, and return a ready `datasets.Dataset` to be pushed
or saved by the caller.
"""
import os
from pathlib import Path
from typing import Any
import h5py
import numpy as np
from dataset_upload.helpers import (
create_hf_trajectory,
generate_unique_id,
load_sentence_transformer_model,
)
from tqdm import tqdm
from datasets import Dataset
class H2RFrameLoader:
"""Pickle-able loader that reads H2R frames from an HDF5 dataset on demand.
Stores only simple fields so it can be safely passed across processes.
"""
def __init__(self, hdf5_path: str, convert_to_rgb: bool = True):
self.hdf5_path = hdf5_path
self.convert_to_rgb = convert_to_rgb
def __call__(self) -> tuple[np.ndarray, np.ndarray]:
"""Load frames from HDF5 when called.
Returns:
np.ndarray of shape (T, H, W, 3), dtype uint8
"""
with h5py.File(self.hdf5_path, "r") as f:
human_frames = f["/cam_data/human_camera"][:]
robot_frames = f["/cam_data/robot_camera"][:]
if self.convert_to_rgb:
human_frames = human_frames[..., [2, 1, 0]]
robot_frames = robot_frames[..., [2, 1, 0]]
# Ensure shape and dtype sanity
if not isinstance(human_frames, np.ndarray) or human_frames.ndim != 4 or human_frames.shape[-1] != 3:
raise ValueError(f"Unexpected frames shape for {self.hdf5_path}: {getattr(human_frames, 'shape', None)}")
if not isinstance(robot_frames, np.ndarray) or robot_frames.ndim != 4 or robot_frames.shape[-1] != 3:
raise ValueError(f"Unexpected frames shape for {self.hdf5_path}: {getattr(robot_frames, 'shape', None)}")
# Ensure uint8
if human_frames.dtype != np.uint8:
human_frames = human_frames.astype(np.uint8, copy=False)
if robot_frames.dtype != np.uint8:
robot_frames = robot_frames.astype(np.uint8, copy=False)
return human_frames, robot_frames
# Task mapping from folder names to task descriptions
FOLDER_TO_TASK_NAME = {
"grab_both_cubes_v1": "pick up each cube individually and place them onto the plate",
"grab_cube2_v1": "pick up the cube and place it onto the plate",
"grab_cup_v1": "move the cup from left to right",
"grab_pencil1_v1": "pick up the marker and place it on the plate",
"grab_pencil2_v1": "pick up the marker and place it on the plate",
"grab_pencil_v1": "pick up the marker and place it on the plate",
"grab_two_cubes2_v1": "pick up the green cube and place it onto the plate",
"grab_to_plate1_and_back_v1": "put the red cube on the darker plate",
"grab_to_plate1_v1": "pick up the red cube and place it onto the darker plate",
"grab_to_plate2_v1": "pick up the red cube and place it onto the lighter plate",
"grab_to_plate2_and_back_v1": "put the red cube on the yellow plate",
"grab_to_plate2_and_pull_v1": "put the cube on the plate, then pull the plate from bottom to top",
"pull_plate_grab_cube": "pull the plate from bottom to top, then pick up the cube and place it onto the plate",
"pull_plate_v1": "pull the plate from bottom to top",
"push_box_common_v1": "push the box from left to right",
"push_box_random_v1": "push the box from left to right",
"push_box_two_v1": "push the tissues from left to right",
"push_plate_v1": "push the plate from top to bottom",
# "roll": "pick up the brush and write on the table", # skipped because it's weird
# "writing": "write aimlessly on the desk", # skipped because writing aimlessly is not helpful for reward modeling
}
def _get_task_name_from_folder(folder_name: str) -> str:
"""Convert folder name to task name using the mapping."""
# First try to find exact match
if folder_name in FOLDER_TO_TASK_NAME:
return FOLDER_TO_TASK_NAME[folder_name]
else:
return None
def _discover_h2r_files(dataset_path: Path) -> list[tuple[Path, str]]:
"""Discover all video files in the H2R dataset structure.
Expected structure:
dataset_path/
folder_name_1/
hdf5_file_1.hdf5
hdf5_file_2.hdf5
hdf5_file_3.hdf5
...
folder_name_2/
hdf5_file_1.hdf5
hdf5_file_2.hdf5
hdf5_file_3.hdf5
...
...
Returns:
List of tuples: (hdf5_file_path, task_name)
"""
trajectory_files: list[tuple[Path, str]] = []
for folder in dataset_path.iterdir():
if folder.is_dir():
for file in folder.glob("*.hdf5"):
trajectory_files.append((file, folder.name))
return trajectory_files
def _stable_shard_for_index(index: int, shard_modulus: int = 1000) -> str:
"""Deterministically bucket an index into a shard directory name.
Matches the naming style used in the OXE loader for consistent layout.
"""
try:
idx = int(index)
except Exception:
idx = abs(hash(str(index)))
shard_index = idx // shard_modulus
return f"shard_{shard_index:04d}"
def _build_h2r_video_paths(
output_dir: str,
dataset_label: str,
episode_idx: int,
role: str,
) -> tuple[str, str]:
shard_dir = _stable_shard_for_index(episode_idx)
episode_dir = os.path.join(output_dir, dataset_label.lower(), shard_dir, f"episode_{episode_idx:06d}")
os.makedirs(episode_dir, exist_ok=True)
filename = f"clip@{role}.mp4"
full_path = os.path.join(episode_dir, filename)
rel_path = os.path.join(dataset_label.lower(), shard_dir, f"episode_{episode_idx:06d}", filename)
return full_path, rel_path
def _process_single_h2r_file(args):
"""Worker to process a single H2R HDF5 file into up to two entries.
Returns a list of entries (human and/or robot), each with relative frame paths.
"""
(
file_path,
folder_name,
ep_idx,
dataset_name,
output_dir,
max_frames,
fps,
task,
lang_vec,
) = args
entries: list[dict[str, Any]] = []
# Load frames for this file (human and robot)
human_frames, robot_frames = H2RFrameLoader(str(file_path))()
# HUMAN entry
full_h_path, rel_h_path = _build_h2r_video_paths(
output_dir=output_dir,
dataset_label=dataset_name,
episode_idx=ep_idx,
role="human",
)
human_traj = {
"id": generate_unique_id(),
"frames": human_frames,
"task": task,
"is_robot": False,
"quality_label": "successful",
"preference_group_id": None,
"preference_rank": None,
}
human_entry = create_hf_trajectory(
traj_dict=human_traj,
video_path=full_h_path,
lang_vector=lang_vec,
max_frames=max_frames,
dataset_name=dataset_name,
use_video=True,
fps=fps,
)
if human_entry:
human_entry["frames"] = rel_h_path
entries.append(human_entry)
# ROBOT entry
full_r_path, rel_r_path = _build_h2r_video_paths(
output_dir=output_dir,
dataset_label=dataset_name,
episode_idx=ep_idx,
role="robot",
)
robot_traj = {
"id": generate_unique_id(),
"frames": robot_frames,
"task": task,
"is_robot": True,
"quality_label": "successful",
"preference_group_id": None,
"preference_rank": None,
}
robot_entry = create_hf_trajectory(
traj_dict=robot_traj,
video_path=full_r_path,
lang_vector=lang_vec,
max_frames=max_frames,
dataset_name=dataset_name,
use_video=True,
fps=fps,
)
if robot_entry:
robot_entry["frames"] = rel_r_path
entries.append(robot_entry)
return entries
def convert_h2r_dataset_to_hf(
dataset_path: str,
dataset_name: str,
output_dir: str,
max_trajectories: int | None = None,
max_frames: int = 64,
fps: int = 10,
num_workers: int = -1,
) -> Dataset:
"""Convert the H2R dataset to HF format by writing videos directly.
This mirrors the OXE loader's streaming approach: iterate files, write videos,
assemble entries, and return a Dataset at the end.
"""
if dataset_name is None:
raise ValueError("dataset_name is required")
base_path = Path(dataset_path)
if not base_path.exists():
raise FileNotFoundError(f"H2R dataset path not found: {base_path}")
discovered = _discover_h2r_files(base_path)
if len(discovered) == 0:
# Return an empty dataset with expected columns
return Dataset.from_dict({
"id": [],
"task": [],
"lang_vector": [],
"data_source": [],
"frames": [],
"is_robot": [],
"quality_label": [],
# keep schema compatible with helpers/create_hf_trajectory usage
"preference_group_id": [],
"preference_rank": [],
})
# Language model and cache (avoid recomputing for identical tasks)
lang_model = load_sentence_transformer_model()
lang_cache: dict[str, Any] = {}
# Determine workers and batching (match OXE approach to control memory)
if num_workers == -1:
try:
from multiprocessing import cpu_count as _cpu_count
num_workers = min(_cpu_count(), 8)
except Exception:
num_workers = 1
elif num_workers == 0:
num_workers = 1
batch_size = 64
entries: list[dict[str, Any]] = []
produced_pairs = 0 # count by file; each file can produce up to 2 entries
max_limit = float("inf") if (max_trajectories is None or max_trajectories == -1) else int(max_trajectories)
print(f"Found {len(discovered)} HDF5 files; processing in batches of {batch_size} with {num_workers} workers...")
# Process files in batches
file_batch: list[tuple[Path, str]] = []
info_batch: list[tuple[int, str, Any]] = [] # (ep_idx, task, lang_vec)
for ep_idx, (file_path, folder_name) in enumerate(tqdm(discovered, desc="Queuing H2R files")):
if produced_pairs >= max_limit:
break
task = _get_task_name_from_folder(folder_name)
if task is None:
print("Skipping file: ", file_path)
continue
if task not in lang_cache:
lang_cache[task] = lang_model.encode(task)
lang_vec = lang_cache[task]
file_batch.append((file_path, folder_name))
info_batch.append((ep_idx, task, lang_vec))
if len(file_batch) >= batch_size or ep_idx + 1 == len(discovered):
# Build worker args
worker_args = list(
zip(
[f for (f, _) in file_batch],
[fn for (_, fn) in file_batch],
[i for (i, _, _) in info_batch],
[dataset_name] * len(file_batch),
[output_dir] * len(file_batch),
[max_frames] * len(file_batch),
[fps] * len(file_batch),
[t for (_, t, _) in info_batch],
[lv for (_, _, lv) in info_batch],
strict=False,
)
)
if num_workers == 1:
# Sequential processing
for args in worker_args:
entries.extend(_process_single_h2r_file(args))
produced_pairs += 1
if produced_pairs >= max_limit:
break
else:
from multiprocessing import Pool
with Pool(processes=num_workers) as pool:
results = list(
tqdm(
pool.imap_unordered(_process_single_h2r_file, worker_args),
total=len(worker_args),
desc=f"Processing batch (workers={num_workers})",
)
)
for res in results:
entries.extend(res)
produced_pairs += 1
if produced_pairs >= max_limit:
break
# Clear batch
file_batch = []
info_batch = []
if not entries:
return Dataset.from_dict({
"id": [],
"task": [],
"lang_vector": [],
"data_source": [],
"frames": [],
"is_robot": [],
"quality_label": [],
"preference_group_id": [],
"preference_rank": [],
})
return Dataset.from_list(entries)