| |
| """DreamZero RoboTwin Dataset - reads qpos+videos+metas, outputs DreamTransform format.""" |
| import os, random, json, logging |
| from pathlib import Path |
| import numpy as np |
| import torch |
| import torch.utils.data as data |
| import cv2 |
|
|
| from groot.vla.model.dreamzero.transform.dreamzero_cotrain import DreamTransform |
| from groot.vla.data.schema.lerobot import ( |
| DatasetMetadata, DatasetStatistics, DatasetStatisticalValues, |
| DatasetModalities, VideoMetadata, StateActionMetadata, |
| ) |
| from groot.vla.data.schema.embodiment_tags import EmbodimentTag |
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| ACTION_Q01 = np.array([-0.002055, -0.003258, -0.002083, -0.002576, -0.002143, -0.004001, -0.006708, 0.0, -0.006258, -0.010985, -0.009753, -0.008991, -0.006935, -0.011197], dtype=np.float32) |
| ACTION_Q99 = np.array([0.004110, 0.004289, 0.002321, 0.006814, 0.002750, 0.002597, 0.006232, 0.0, 0.006063, 0.014186, 0.010926, 0.010506, 0.006669, 0.010019], dtype=np.float32) |
| STATE_Q01 = np.array([-0.2]*7 + [0.0]*7, dtype=np.float32) |
| STATE_Q99 = np.array([0.2]*7 + [0.0]*7, dtype=np.float32) |
|
|
| def _normalize_q99(x, q01, q99, eps=1e-8): |
| return np.clip(2.0 * (x - q01) / (q99 - q01 + eps) - 1.0, -1.0, 1.0) |
|
|
|
|
| class RobotWinDataset(data.Dataset): |
| """RoboTwin dataset for DreamZero. Reads converted qpos+videos+metas format.""" |
|
|
| def __init__(self, dataset_dir, num_frames=12, action_horizon=12, |
| state_horizon=1, num_views=1, video_height=160, video_width=320, |
| max_episodes=None, seed=42, **kwargs): |
| self.dataset_dir = Path(dataset_dir) |
| self.num_frames = num_frames |
| self.action_horizon = action_horizon |
| self.state_horizon = state_horizon |
| self.num_views = num_views |
| self.video_height = video_height |
| self.video_width = video_width |
| self.seed = seed |
| self.max_episodes = max_episodes |
| self.rng = random.Random(seed) |
|
|
| |
| self.tasks = sorted([d for d in self.dataset_dir.iterdir() if d.is_dir()]) |
| if not self.tasks: |
| raise FileNotFoundError("No task dirs in " + str(dataset_dir)) |
|
|
| |
| self.episodes = [] |
| for task_dir in self.tasks: |
| video_dir = task_dir / "videos" |
| qpos_dir = task_dir / "qpos" |
| meta_dir = task_dir / "metas" |
| if not video_dir.exists() or not qpos_dir.exists(): |
| continue |
|
|
| video_files = sorted(video_dir.glob("episode*.mp4")) |
| qpos_files = sorted(qpos_dir.glob("episode*.pt")) |
|
|
| for vf in video_files: |
| ep_name = vf.stem |
| qf = qpos_dir / (ep_name + ".pt") |
| if not qf.exists(): |
| continue |
| |
| cap = cv2.VideoCapture(str(vf)) |
| n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) |
| cap.release() |
| if n_frames < self.num_frames + self.action_horizon: |
| continue |
| |
| qpos_data = torch.load(str(qf)) |
| n_state = qpos_data.shape[0] |
| if n_state < self.num_frames + self.action_horizon: |
| continue |
| self.episodes.append((str(task_dir), str(vf), str(qf), n_frames, n_state, ep_name)) |
|
|
| if not self.episodes: |
| raise FileNotFoundError("No valid episodes found (need >= {} frames)".format( |
| self.num_frames + self.action_horizon)) |
|
|
| if self.max_episodes: |
| self.rng.shuffle(self.episodes) |
| self.episodes = self.episodes[:self.max_episodes] |
|
|
| self._build_metadata() |
|
|
| self.transform = DreamTransform( |
| default_instruction="Perform the default behavior.", |
| language_dropout_prob=0.0, always_use_default_instruction=False, |
| max_state_dim=44, max_action_dim=32, max_length=512, |
| state_horizon=self.state_horizon, action_horizon=self.action_horizon, |
| num_views=self.num_views, |
| embodiment_tag_mapping={"oxe_droid": 17}, |
| tokenizer_path=kwargs.get("tokenizer_path", "/root/autodl-tmp/checkpoints/umt5-xxl"), |
| ) |
| self.transform.set_metadata(self.merged_metadata["oxe_droid"]) |
| self.transform.train() |
|
|
| n_ep = len(self.episodes) |
| logger.info("RobotWinDataset: {} episodes across {} tasks".format(n_ep, len(self.tasks))) |
|
|
| def _build_metadata(self): |
| ds = DatasetStatisticalValues( |
| max=np.ones(1), min=np.zeros(1), mean=np.zeros(1), |
| std=np.ones(1), q01=np.zeros(1), q99=np.ones(1)) |
| self.merged_metadata = { |
| "oxe_droid": DatasetMetadata( |
| statistics=DatasetStatistics( |
| state={"joint_position": ds}, action={"joint_position": ds}), |
| modalities=DatasetModalities( |
| video={"image": VideoMetadata( |
| resolution=(self.video_width, self.video_height), channels=3, fps=30)}, |
| state={"joint_position": StateActionMetadata( |
| absolute=True, shape=(14,), continuous=True)}, |
| action={"joint_position": StateActionMetadata( |
| absolute=True, shape=(14,), continuous=True)}, |
| ), |
| embodiment_tag=EmbodimentTag.OXE_DROID, |
| ) |
| } |
|
|
| def reset_seed(self, new_seed): |
| self.seed = new_seed |
| self.rng = random.Random(new_seed) |
|
|
| def __len__(self): |
| return max(len(self.episodes) * 10, 1) |
|
|
| def _load_task_desc(self, task_dir, ep_name): |
| """Load task description from metas directory.""" |
| meta_dir = task_dir / "metas" |
| if not meta_dir.exists(): |
| return "Manipulate the object on the table" |
| meta_files = sorted(meta_dir.glob("*.txt")) |
| if not meta_files: |
| return "Manipulate the object on the table" |
| |
| ep_idx = int(ep_name.replace("episode", "")) |
| |
| if ep_idx < len(meta_files): |
| with open(meta_files[ep_idx]) as f: |
| return f.read().strip() |
| return "Manipulate the object on the table" |
|
|
| def _compute_actions(self, qpos_data, t, video_fps=30): |
| """Compute actions from state differences, matching video frame rate.""" |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| actions = np.zeros((self.action_horizon, 14), dtype=np.float32) |
| for i in range(self.action_horizon): |
| fi = min(t + i + 1, qpos_data.shape[0] - 1) |
| si = min(t + i, qpos_data.shape[0] - 1) |
| actions[i] = qpos_data[fi] - qpos_data[si] |
| return actions |
|
|
| def __getitem__(self, idx): |
| task_dir, video_path, qpos_path, n_video_frames, n_qpos_frames, ep_name = \ |
| self.rng.choice(self.episodes) |
|
|
| |
| max_start = max(0, n_video_frames - self.num_frames - self.action_horizon) |
| t = self.rng.randint(0, max_start) if max_start > 0 else 0 |
|
|
| |
| cap = cv2.VideoCapture(video_path) |
| frames = [] |
| for i in range(self.num_frames): |
| fi = min(t + i, n_video_frames - 1) |
| cap.set(cv2.CAP_PROP_POS_FRAMES, fi) |
| ret, frame = cap.read() |
| if not ret: |
| frame = frames[-1] if frames else np.zeros((240, 320, 3), dtype=np.uint8) |
| frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) |
| frames.append(frame) |
| cap.release() |
|
|
| video = np.stack(frames, axis=0) |
| video = video[:, np.newaxis, :, :, :] |
|
|
| |
| if video.shape[2] != self.video_height or video.shape[3] != self.video_width: |
| resized = np.zeros((self.num_frames, 1, self.video_height, self.video_width, 3), dtype=np.uint8) |
| for ti in range(self.num_frames): |
| resized[ti, 0] = cv2.resize(video[ti, 0], (self.video_width, self.video_height)) |
| video = resized |
|
|
| |
| qpos_data = torch.load(qpos_path).numpy() |
|
|
| |
| |
| video_to_qpos_ratio = n_qpos_frames / max(n_video_frames, 1) |
| qpos_start = int(t * video_to_qpos_ratio) |
| qpos_start = min(qpos_start, n_qpos_frames - 13) |
|
|
| |
| state_val = qpos_data[min(qpos_start, n_qpos_frames - 1)] |
| state = np.tile(state_val, (self.state_horizon, 1)) |
|
|
| |
| actions = np.zeros((self.action_horizon, 14), dtype=np.float32) |
| for i in range(self.action_horizon): |
| fi = min(qpos_start + i + 1, n_qpos_frames - 1) |
| si = min(qpos_start + i, n_qpos_frames - 1) |
| actions[i] = qpos_data[fi] - qpos_data[si] |
|
|
| |
| task_name = self._load_task_desc(Path(task_dir), ep_name) |
|
|
| |
| |
| state = _normalize_q99(state, STATE_Q01, STATE_Q99) |
| actions = _normalize_q99(actions, ACTION_Q01, ACTION_Q99) |
|
|
| raw = { |
| "video": video, |
| "state": state, |
| "action": actions, |
| "annotation.human.action.task_description": task_name, |
| } |
| return dict(self.transform(raw)) |
|
|