| """ |
| Extract T-Rex 250-point tracks for LeRobot v2 episodes with SAM2 + CoTracker. |
| |
| Frame-0 masks: one SAM2 predict per hand/view (fixed prompts, no appearance |
| auto-repair). Then CoTracker tracks: |
| - head_left: 100 pts (left hand/arm 50 + right hand/arm 50) |
| - each wrist: 75 pts (fixed 5×5 background grid 25 + hand 50) |
| |
| Tune prompts in ``trex_track/sam2_prompt_hands.py``. |
| |
| Example:: |
| |
| CUDA_VISIBLE_DEVICES=4 python scripts/extract_track.py \\ |
| --dataset-root data/trex_small --episode-index 0 \\ |
| --cotracker-device cuda:0 --sam2-device cuda:0 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import os |
| import sys |
| import tempfile |
| from pathlib import Path |
| from typing import NamedTuple |
|
|
| import numpy as np |
|
|
| _SCRIPT_DIR = Path(__file__).resolve().parent |
| _DREAMZERO_ROOT = _SCRIPT_DIR.parent |
| if str(_SCRIPT_DIR) not in sys.path: |
| sys.path.insert(0, str(_SCRIPT_DIR)) |
|
|
| from trex_track.layout import ( |
| NUM_COMBINED_POINTS, |
| NUM_HEAD_LEFT, |
| NUM_HEAD_PER_HAND as NUM_HAND_POINTS, |
| NUM_HEAD_POINTS, |
| NUM_WRIST_BACKGROUND as NUM_WRIST_GRID, |
| NUM_WRIST_HAND, |
| NUM_WRIST_POINTS, |
| POINT_SLICES, |
| TRACK_LAYOUT_VERSION, |
| VIEW_ORDER, |
| identity_metadata, |
| ) |
|
|
| VIDEO_FOLDERS = { |
| "head_left": "observation.images.head_left", |
| "left_wrist": "observation.images.left_wrist", |
| "right_wrist": "observation.images.right_wrist", |
| } |
|
|
| DEFAULT_OPENPI_ROOT = Path("/scratch2/home/zhicao/openpi") |
| DEFAULT_CALIB = _DREAMZERO_ROOT / "assets" / "trex_camera_calib.json" |
| DEFAULT_SAM2_MODEL = os.environ.get("SAM2_MODEL", "facebook/sam2-hiera-large") |
| DEFAULT_SAM2_LIBS = os.environ.get("SAM2_LIBS", "/scratch1/home/zhicao/physctrl/libs") |
|
|
|
|
| class TrackingRuntime(NamedTuple): |
| """Lazily-created heavy models shared across an episode batch.""" |
|
|
| calib: dict |
| out_hw: tuple[int, int] |
| cotracker_model: object |
| cotracker_device: object |
| sam2_predictor: object |
|
|
|
|
| def _ensure_openpi_on_path(openpi_root: Path) -> Path: |
| root = openpi_root.expanduser().resolve() |
| droid = root / "droid" |
| if not droid.is_dir(): |
| raise FileNotFoundError(f"openpi droid package not found: {droid}") |
| p = str(droid) |
| if p not in sys.path: |
| sys.path.insert(0, p) |
| return droid |
|
|
|
|
| def _enable_cotracker_sdpa_attention(openpi_root: str | Path) -> None: |
| """Replace CoTracker's quadratic-memory attention with PyTorch SDPA. |
| |
| The original implementation materializes an ``[B,H,T,T]`` attention |
| matrix. Long T-Rex episodes can therefore require more than 80 GB even on |
| an otherwise empty H100. SDPA uses Flash Attention for CUDA BF16 inputs, |
| preserving full-sequence attention without materializing that matrix. |
| """ |
|
|
| import torch.nn.functional as F |
|
|
| cotracker_root = Path(openpi_root).expanduser().resolve() / "co-tracker" |
| cotracker_path = str(cotracker_root) |
| if not cotracker_root.is_dir(): |
| raise FileNotFoundError(f"CoTracker package not found: {cotracker_root}") |
| if cotracker_path not in sys.path: |
| sys.path.insert(0, cotracker_path) |
|
|
| from cotracker.models.core.cotracker.blocks import Attention |
|
|
| if bool(getattr(Attention, "_trex_sdpa_enabled", False)): |
| return |
|
|
| def _sdpa_forward(self, x, context=None, attn_bias=None): |
| batch, query_steps, _ = x.shape |
| heads = int(self.heads) |
| query = self.to_q(x) |
| inner_dim = int(query.shape[-1]) |
| head_dim = inner_dim // heads |
| query = query.reshape(batch, query_steps, heads, head_dim).transpose(1, 2) |
|
|
| context = x if context is None else context |
| key, value = self.to_kv(context).chunk(2, dim=-1) |
| context_steps = int(context.shape[1]) |
| key = key.reshape(batch, context_steps, heads, head_dim).transpose(1, 2) |
| value = value.reshape(batch, context_steps, heads, head_dim).transpose(1, 2) |
|
|
| attended = F.scaled_dot_product_attention( |
| query, |
| key, |
| value, |
| attn_mask=attn_bias, |
| dropout_p=0.0, |
| is_causal=False, |
| ) |
| attended = attended.transpose(1, 2).reshape(batch, query_steps, inner_dim) |
| return self.to_out(attended) |
|
|
| Attention.forward = _sdpa_forward |
| Attention._trex_sdpa_enabled = True |
|
|
|
|
| def normalize_tracks_xy(tracks: np.ndarray, img_w: int, img_h: int) -> np.ndarray: |
| out = np.asarray(tracks, dtype=np.float32).copy() |
| w, h = max(float(img_w), 1.0), max(float(img_h), 1.0) |
| out = np.nan_to_num(out, nan=0.0, posinf=0.0, neginf=0.0) |
| out[..., 0] = np.clip(out[..., 0] / w, 0.0, 1.0) |
| out[..., 1] = np.clip(out[..., 1] / h, 0.0, 1.0) |
| return out.astype(np.float32, copy=False) |
|
|
|
|
| def normalize_track_result( |
| tracks: np.ndarray, |
| visibility: np.ndarray, |
| img_w: int, |
| img_h: int, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| """Normalize XY and clear visibility for non-finite/out-of-frame points.""" |
|
|
| pixels = np.asarray(tracks, dtype=np.float32) |
| vis = np.asarray(visibility, dtype=np.float32) |
| if pixels.ndim != 3 or pixels.shape[-1] != 2: |
| raise ValueError(f"tracks must be (T,N,2), got {pixels.shape}") |
| if vis.shape != pixels.shape[:2]: |
| raise ValueError(f"visibility {vis.shape} does not match tracks {pixels.shape}") |
| finite = np.isfinite(pixels).all(axis=-1) |
| in_frame = ( |
| (pixels[..., 0] >= 0.0) |
| & (pixels[..., 0] < float(img_w)) |
| & (pixels[..., 1] >= 0.0) |
| & (pixels[..., 1] < float(img_h)) |
| ) |
| clean_vis = ((vis > 0.5) & finite & in_frame).astype(np.float32) |
| return normalize_tracks_xy(pixels, img_w, img_h), clean_vis |
|
|
|
|
| def load_episode_videos( |
| dataset_root: Path, |
| episode_index: int, |
| *, |
| out_hw: tuple[int, int], |
| ) -> dict[str, np.ndarray]: |
| import cv2 |
|
|
| chunk = episode_index // 1000 |
| frames_by_view: dict[str, list[np.ndarray]] = {k: [] for k in VIEW_ORDER} |
| for view in VIEW_ORDER: |
| rel = VIDEO_FOLDERS[view] |
| video_path = ( |
| dataset_root / "videos" / f"chunk-{chunk:03d}" / rel / f"episode_{episode_index:06d}.mp4" |
| ) |
| if not video_path.is_file(): |
| raise FileNotFoundError(video_path) |
| cap = cv2.VideoCapture(str(video_path)) |
| while True: |
| ok, bgr = cap.read() |
| if not ok: |
| break |
| rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) |
| if (rgb.shape[0], rgb.shape[1]) != out_hw: |
| rgb = cv2.resize(rgb, (out_hw[1], out_hw[0]), interpolation=cv2.INTER_LINEAR) |
| frames_by_view[view].append(rgb) |
| cap.release() |
| if not frames_by_view[view]: |
| raise RuntimeError(f"empty video: {video_path}") |
| return {v: np.stack(frames_by_view[v], axis=0) for v in VIEW_ORDER} |
|
|
|
|
| def load_episode_states(dataset_root: Path, episode_index: int) -> tuple[np.ndarray, str]: |
| import pandas as pd |
|
|
| chunk = episode_index // 1000 |
| pq_path = dataset_root / "data" / f"chunk-{chunk:03d}" / f"episode_{episode_index:06d}.parquet" |
| df = pd.read_parquet(pq_path) |
| states = np.stack([np.asarray(x, dtype=np.float64) for x in df["observation.state"].values], axis=0) |
| task = "" |
| if "annotation.task" in df.columns: |
| task = str(df["annotation.task"].iloc[0]) |
| return states, task |
|
|
|
|
| def _run_cotracker_window( |
| model, |
| video_hwc: np.ndarray, |
| query_xy: np.ndarray, |
| device: object, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| """Run one bounded CoTracker window and immediately release its GPU tensors.""" |
|
|
| import torch |
|
|
| video_np = np.asarray(video_hwc, dtype=np.uint8) |
| queries_np = np.zeros((int(query_xy.shape[0]), 3), dtype=np.float32) |
| queries_np[:, 1:] = np.asarray(query_xy, dtype=np.float32) |
| device_type = torch.device(device).type |
| video_dtype = torch.bfloat16 if device_type == "cuda" else torch.float32 |
| video = ( |
| torch.from_numpy(video_np) |
| .permute(0, 3, 1, 2) |
| .unsqueeze(0) |
| .to(device=device, dtype=video_dtype) |
| ) |
| queries = torch.from_numpy(queries_np).unsqueeze(0).to(device) |
| with torch.inference_mode(), torch.autocast( |
| device_type=device_type, |
| dtype=torch.bfloat16, |
| enabled=device_type == "cuda", |
| ): |
| pred_tracks, pred_vis = model( |
| video, |
| queries=queries, |
| backward_tracking=False, |
| ) |
| tracks = pred_tracks[0].detach().cpu().numpy().astype(np.float32) |
| visibility = pred_vis[0].detach().cpu().numpy() |
| visibility = (visibility > 0.5).astype(np.float32) |
| del video, queries, pred_tracks, pred_vis |
| if device_type == "cuda": |
| torch.cuda.empty_cache() |
| return tracks, visibility |
|
|
|
|
| def _run_cotracker(model, video_hwc: np.ndarray, query_xy: np.ndarray, device: object): |
| """Track frame-0 queries in bounded, overlapping temporal windows. |
| |
| ``CoTrackerPredictor`` only copies backward predictions into frames before |
| each query timestamp. Every query here starts at frame zero, so |
| ``backward_tracking=True`` cannot change the result and nearly doubles the |
| peak memory for long T-Rex episodes. |
| |
| Even with Flash Attention, CoTracker's feature/correlation tensors grow |
| linearly with the frame count. Each new window is initialized from the |
| previous trajectory, and overlapping predictions are blended to avoid a |
| discontinuity at the boundary. |
| """ |
|
|
| video_np = np.asarray(video_hwc, dtype=np.uint8) |
| total_frames = int(video_np.shape[0]) |
| num_queries = int(query_xy.shape[0]) |
| window_frames = int(os.environ.get("TREX_COTRACKER_WINDOW_FRAMES", "768")) |
| overlap_frames = int(os.environ.get("TREX_COTRACKER_WINDOW_OVERLAP", "64")) |
| if window_frames < 2: |
| raise ValueError("TREX_COTRACKER_WINDOW_FRAMES must be at least 2") |
| if overlap_frames < 1 or overlap_frames >= window_frames: |
| raise ValueError( |
| "TREX_COTRACKER_WINDOW_OVERLAP must be in [1, WINDOW_FRAMES)" |
| ) |
|
|
| if total_frames <= window_frames: |
| return _run_cotracker_window(model, video_np, query_xy, device) |
|
|
| step = window_frames - overlap_frames |
| num_windows = 1 + (total_frames - window_frames + step - 1) // step |
| print( |
| f" CoTracker windowing: {total_frames} frames -> {num_windows} " |
| f"window(s), max={window_frames}, overlap={overlap_frames}" |
| ) |
| tracks = np.empty((total_frames, num_queries, 2), dtype=np.float32) |
| visibility = np.empty((total_frames, num_queries), dtype=np.float32) |
|
|
| start = 0 |
| filled_end = 0 |
| while start < total_frames: |
| end = min(start + window_frames, total_frames) |
| seed_xy = np.asarray(query_xy if start == 0 else tracks[start], dtype=np.float32) |
| window_tracks, window_visibility = _run_cotracker_window( |
| model, |
| video_np[start:end], |
| seed_xy, |
| device, |
| ) |
|
|
| overlap_end = min(filled_end, end) |
| existing_frames = max(0, overlap_end - start) |
| if existing_frames > 0: |
| alpha = np.linspace( |
| 0.0, |
| 1.0, |
| existing_frames, |
| dtype=np.float32, |
| ) |
| tracks[start:overlap_end] = ( |
| tracks[start:overlap_end] * (1.0 - alpha[:, None, None]) |
| + window_tracks[:existing_frames] * alpha[:, None, None] |
| ) |
| use_new_visibility = alpha >= 0.5 |
| visibility[start:overlap_end] = np.where( |
| use_new_visibility[:, None], |
| window_visibility[:existing_frames], |
| visibility[start:overlap_end], |
| ) |
|
|
| tracks[overlap_end:end] = window_tracks[existing_frames:] |
| visibility[overlap_end:end] = window_visibility[existing_frames:] |
| filled_end = max(filled_end, end) |
| if end >= total_frames: |
| break |
| start = end - overlap_frames |
|
|
| return tracks, visibility |
|
|
|
|
| def _tracks_episode( |
| *, |
| view_images: dict[str, np.ndarray], |
| out_hw: tuple[int, int], |
| cotracker_model, |
| cotracker_device: object, |
| sam2_predictor=None, |
| sam2_seed: int | None = None, |
| ) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray], dict[str, np.ndarray | None], dict[str, str]]: |
| from trex_track.sam2_cotracker_hands import head_hands_50, wrist_hand_50 |
| from trex_track.trex_projection import make_image_grid |
|
|
| view_tracks_px: dict[str, np.ndarray] = {} |
| view_vis: dict[str, np.ndarray] = {} |
| masks: dict[str, np.ndarray | None] = {} |
| tags: dict[str, str] = {} |
|
|
| |
| head_q, left_m, right_m, head_tag = head_hands_50( |
| sam2_predictor, |
| view_images["head_left"][0], |
| n_points=NUM_HAND_POINTS, |
| seed=sam2_seed, |
| ) |
| if int(head_q.shape[0]) != NUM_HEAD_POINTS: |
| raise ValueError(f"head queries expect {NUM_HEAD_POINTS}, got {head_q.shape[0]}") |
| head_trk, head_vis = _run_cotracker( |
| cotracker_model, view_images["head_left"], head_q, cotracker_device |
| ) |
| view_tracks_px["head_left"] = head_trk |
| view_vis["head_left"] = head_vis |
| masks["head_left_hand"] = left_m |
| masks["head_right_hand"] = right_m |
| tags["head_left"] = head_tag |
|
|
| |
| for view in ("left_wrist", "right_wrist"): |
| hand_q, mask, tag = wrist_hand_50( |
| sam2_predictor, |
| view_images[view][0], |
| view, |
| n_points=NUM_WRIST_HAND, |
| seed=sam2_seed, |
| ) |
| if int(hand_q.shape[0]) != NUM_WRIST_HAND: |
| raise ValueError(f"{view} hand queries expect {NUM_WRIST_HAND}, got {hand_q.shape[0]}") |
| grid_q = make_image_grid(out_hw[0], out_hw[1], grid_size=5).astype(np.float32) |
| q = np.concatenate([grid_q, hand_q], axis=0) |
| if int(q.shape[0]) != NUM_WRIST_POINTS: |
| raise ValueError(f"{view} queries expect {NUM_WRIST_POINTS}, got {q.shape[0]}") |
| trk, vis = _run_cotracker(cotracker_model, view_images[view], q, cotracker_device) |
| view_tracks_px[view] = trk |
| view_vis[view] = vis |
| masks[view] = mask |
| tags[view] = f"grid25+{tag}" |
|
|
| view_tracks: dict[str, np.ndarray] = {} |
| for view in VIEW_ORDER: |
| view_tracks[view], view_vis[view] = normalize_track_result( |
| view_tracks_px[view], |
| view_vis[view], |
| out_hw[1], |
| out_hw[0], |
| ) |
| return view_tracks, view_vis, masks, tags |
|
|
|
|
| def _save_seed_overlay( |
| out_dir: Path, |
| episode_index: int, |
| view_images: dict[str, np.ndarray], |
| masks: dict[str, np.ndarray | None], |
| view_tracks: dict[str, np.ndarray], |
| out_hw: tuple[int, int], |
| ) -> None: |
| """Save frame-0 overlays with mask + query points for debugging.""" |
| import cv2 |
|
|
| out_dir.mkdir(parents=True, exist_ok=True) |
| h, w = out_hw |
|
|
| def _draw(view: str, mask: np.ndarray | None, tracks_norm: np.ndarray, color_bgr): |
| rgb = view_images[view][0] |
| bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) |
| if mask is not None and np.asarray(mask).any(): |
| m = np.asarray(mask, dtype=bool) |
| bgr[m] = (bgr[m] * 0.35 + np.asarray(color_bgr, dtype=np.float32) * 0.65).astype(np.uint8) |
| pts = tracks_norm[0] * np.array([w, h], dtype=np.float32) |
| for p in pts.astype(int): |
| cv2.circle(bgr, (int(p[0]), int(p[1])), 3, (0, 255, 255), -1, cv2.LINE_AA) |
| return bgr |
|
|
| |
| head = cv2.cvtColor(view_images["head_left"][0], cv2.COLOR_RGB2BGR) |
| lm, rm = masks.get("head_left_hand"), masks.get("head_right_hand") |
| if lm is not None and np.asarray(lm).any(): |
| m = np.asarray(lm, dtype=bool) |
| head[m] = (head[m] * 0.35 + np.array([0, 255, 0], dtype=np.float32) * 0.65).astype(np.uint8) |
| if rm is not None and np.asarray(rm).any(): |
| m = np.asarray(rm, dtype=bool) |
| head[m] = (head[m] * 0.35 + np.array([0, 165, 255], dtype=np.float32) * 0.65).astype(np.uint8) |
| pts = view_tracks["head_left"][0] * np.array([w, h], dtype=np.float32) |
| for i, p in enumerate(pts.astype(int)): |
| col = (0, 255, 255) if i < NUM_HEAD_LEFT else (255, 255, 0) |
| cv2.circle(head, (int(p[0]), int(p[1])), 3, col, -1, cv2.LINE_AA) |
| cv2.imwrite(str(out_dir / f"episode_{episode_index:06d}_head_left_seeds.png"), head) |
|
|
| for view in ("left_wrist", "right_wrist"): |
| rgb = view_images[view][0] |
| bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR) |
| mask = masks.get(view) |
| if mask is not None and np.asarray(mask).any(): |
| m = np.asarray(mask, dtype=bool) |
| bgr[m] = (bgr[m] * 0.40 + np.array([0, 0, 220], dtype=np.float32) * 0.60).astype(np.uint8) |
| pts = view_tracks[view][0] * np.array([w, h], dtype=np.float32) |
| for i, p in enumerate(pts.astype(int)): |
| |
| col = (0, 255, 255) if i < NUM_WRIST_GRID else (255, 0, 255) |
| cv2.drawMarker( |
| bgr, |
| (int(p[0]), int(p[1])), |
| col, |
| markerType=cv2.MARKER_STAR, |
| markerSize=8, |
| thickness=1, |
| line_type=cv2.LINE_AA, |
| ) |
| cv2.imwrite(str(out_dir / f"episode_{episode_index:06d}_{view}_seeds.png"), bgr) |
|
|
|
|
| def _save_prompt_overlays( |
| out_dir: Path, |
| episode_index: int, |
| view_images: dict[str, np.ndarray], |
| masks: dict[str, np.ndarray | None], |
| ) -> None: |
| """Save SAM2 prompts (pos/neg/box) + mask overlays; reuse track masks (no 2nd SAM2).""" |
| import cv2 |
|
|
| from trex_track.sam2_prompt_hands import ( |
| build_head_prompts, |
| build_wrist_prompts, |
| draw_prompt_overlay, |
| ) |
|
|
| out_dir.mkdir(parents=True, exist_ok=True) |
| panels: list[np.ndarray] = [] |
| meta_dump: dict[str, object] = {} |
|
|
| def _one( |
| key: str, |
| rgb: np.ndarray, |
| mask: np.ndarray | None, |
| coords: np.ndarray, |
| labels: np.ndarray, |
| box: np.ndarray, |
| mask_bgr: tuple[int, int, int], |
| ) -> np.ndarray: |
| vis = draw_prompt_overlay( |
| rgb, |
| mask=mask, |
| coords=coords, |
| labels=labels, |
| box=box, |
| mask_bgr=mask_bgr, |
| ) |
| |
| n_pos = int((labels == 1).sum()) |
| n_neg = int((labels == 0).sum()) |
| area = int(np.asarray(mask).sum()) if mask is not None else 0 |
| cv2.putText( |
| vis, |
| f"{key} +pos={n_pos} -neg={n_neg} mask_px={area}", |
| (6, 14), |
| cv2.FONT_HERSHEY_SIMPLEX, |
| 0.40, |
| (255, 255, 255), |
| 1, |
| cv2.LINE_AA, |
| ) |
| path = out_dir / f"episode_{episode_index:06d}_{key}_prompt.png" |
| cv2.imwrite(str(path), vis) |
| print(f"Wrote {path}") |
| meta_dump[f"{key}_coords"] = np.asarray(coords, dtype=np.float32) |
| meta_dump[f"{key}_labels"] = np.asarray(labels, dtype=np.int32) |
| meta_dump[f"{key}_box"] = np.asarray(box, dtype=np.float32) |
| if mask is not None: |
| meta_dump[f"{key}_mask"] = np.asarray(mask, dtype=bool) |
| return vis |
|
|
| |
| for view, col in (("left_wrist", (0, 0, 220)), ("right_wrist", (0, 0, 220))): |
| rgb = view_images[view][0] |
| h, w = rgb.shape[:2] |
| coords, labels, box = build_wrist_prompts(h, w, view) |
| panels.append(_one(view, rgb, masks.get(view), coords, labels, box, col)) |
|
|
| |
| rgb = view_images["head_left"][0] |
| h, w = rgb.shape[:2] |
| for side, col, mkey in ( |
| ("left", (0, 255, 0), "head_left_hand"), |
| ("right", (0, 165, 255), "head_right_hand"), |
| ): |
| coords, labels, box = build_head_prompts(h, w, side) |
| panels.append(_one(f"head_{side}", rgb, masks.get(mkey), coords, labels, box, col)) |
|
|
| |
| if len(panels) == 4: |
| top = np.concatenate(panels[2:4], axis=1) |
| bot = np.concatenate(panels[0:2], axis=1) |
| |
| if top.shape[1] != bot.shape[1]: |
| tw = max(top.shape[1], bot.shape[1]) |
| top = cv2.resize(top, (tw, top.shape[0])) |
| bot = cv2.resize(bot, (tw, bot.shape[0])) |
| grid = np.concatenate([top, bot], axis=0) |
| grid_path = out_dir / f"episode_{episode_index:06d}_all_prompts.png" |
| cv2.imwrite(str(grid_path), grid) |
| print(f"Wrote {grid_path}") |
|
|
| meta_path = out_dir / f"episode_{episode_index:06d}_prompts.npz" |
| np.savez_compressed(meta_path, **meta_dump) |
| print(f"Wrote {meta_path}") |
|
|
|
|
| def _atomic_savez(path: Path, **arrays: object) -> None: |
| """Write an NPZ in the destination directory, then atomically replace.""" |
|
|
| path.parent.mkdir(parents=True, exist_ok=True) |
| fd, tmp_name = tempfile.mkstemp( |
| prefix=f".{path.name}.", |
| suffix=".tmp", |
| dir=path.parent, |
| ) |
| try: |
| with os.fdopen(fd, "wb") as file: |
| np.savez_compressed(file, **arrays) |
| file.flush() |
| os.fsync(file.fileno()) |
| os.replace(tmp_name, path) |
| except BaseException: |
| try: |
| os.unlink(tmp_name) |
| except FileNotFoundError: |
| pass |
| raise |
|
|
|
|
| def process_episode( |
| *, |
| dataset_root: Path, |
| episode_index: int, |
| output_path: Path, |
| calib: dict | None, |
| out_hw: tuple[int, int], |
| cotracker_model, |
| cotracker_device: object, |
| save_viz: bool, |
| viz_out_dir: Path, |
| viz_fps: int, |
| viz_trail: int, |
| sam2_predictor=None, |
| sam2_seed: int | None = None, |
| save_sam2_masks_flag: bool = True, |
| sam2_masks_dir: Path | None = None, |
| ) -> Path: |
| from trex_track.sam2_wrist_hand import save_sam2_masks |
| from trex_track.trex_viz_tracks import render_three_view_combined_video |
|
|
| del calib |
| states, task = load_episode_states(dataset_root, episode_index) |
| view_images = load_episode_videos(dataset_root, episode_index, out_hw=out_hw) |
| frame_counts = {"parquet": int(states.shape[0])} |
| frame_counts.update({view: int(view_images[view].shape[0]) for view in VIEW_ORDER}) |
| if len(set(frame_counts.values())) != 1: |
| raise ValueError( |
| f"episode {episode_index}: frame-count mismatch; refusing to truncate: " |
| f"{frame_counts}" |
| ) |
| t_len = int(states.shape[0]) |
|
|
| view_tracks, view_vis, masks, tags = _tracks_episode( |
| view_images=view_images, |
| out_hw=out_hw, |
| cotracker_model=cotracker_model, |
| cotracker_device=cotracker_device, |
| sam2_predictor=sam2_predictor, |
| sam2_seed=sam2_seed, |
| ) |
|
|
| tracks_combined = np.concatenate([view_tracks[v] for v in VIEW_ORDER], axis=1) |
| vis_combined = np.concatenate([view_vis[v] for v in VIEW_ORDER], axis=1) |
| if tracks_combined.shape != (t_len, NUM_COMBINED_POINTS, 2): |
| raise ValueError( |
| f"episode {episode_index}: combined tracks have {tracks_combined.shape}, " |
| f"expected {(t_len, NUM_COMBINED_POINTS, 2)}" |
| ) |
| if vis_combined.shape != (t_len, NUM_COMBINED_POINTS): |
| raise ValueError( |
| f"episode {episode_index}: combined visibility has {vis_combined.shape}" |
| ) |
|
|
| out_npz = output_path / f"episode_{episode_index:06d}.npz" |
| identities = identity_metadata() |
| _atomic_savez( |
| out_npz, |
| tracks=tracks_combined, |
| vis=vis_combined, |
| tracks_head_left=view_tracks["head_left"], |
| tracks_left_wrist=view_tracks["left_wrist"], |
| tracks_right_wrist=view_tracks["right_wrist"], |
| vis_head_left=view_vis["head_left"], |
| vis_left_wrist=view_vis["left_wrist"], |
| vis_right_wrist=view_vis["right_wrist"], |
| language=np.array(task), |
| episode_index=np.array(episode_index, dtype=np.int32), |
| num_steps=np.array(t_len, dtype=np.int32), |
| point_slices=np.array(POINT_SLICES, dtype=np.int32), |
| point_view_ids=np.asarray(identities["view_ids"], dtype=np.int8), |
| point_hand_ids=np.asarray(identities["hand_ids"], dtype=np.int8), |
| point_role_ids=np.asarray(identities["role_ids"], dtype=np.int8), |
| point_local_ids=np.asarray(identities["local_ids"], dtype=np.int16), |
| point_global_ids=np.asarray(identities["global_ids"], dtype=np.int16), |
| point_names=np.asarray(identities["point_names"]), |
| points_per_hand=np.array(NUM_HAND_POINTS, dtype=np.int32), |
| wrist_grid_points=np.array(NUM_WRIST_GRID, dtype=np.int32), |
| wrist_hand_points=np.array(NUM_WRIST_HAND, dtype=np.int32), |
| head_query_source=np.array(tags.get("head_left", "sam2")), |
| left_wrist_query_source=np.array(tags.get("left_wrist", "sam2")), |
| right_wrist_query_source=np.array(tags.get("right_wrist", "sam2")), |
| track_source=np.array("sam2_once_prompt_cotracker"), |
| tracks_coord_space=np.array("normalized_div_wh"), |
| track_layout_version=np.array(TRACK_LAYOUT_VERSION), |
| proj_image_hw=np.array(out_hw, dtype=np.int32), |
| ) |
|
|
| masks_dir = sam2_masks_dir if sam2_masks_dir is not None else output_path / "sam2_masks" |
| if save_sam2_masks_flag: |
| |
| save_map = { |
| "left_wrist": masks.get("left_wrist"), |
| "right_wrist": masks.get("right_wrist"), |
| "head_left_hand": masks.get("head_left_hand"), |
| "head_right_hand": masks.get("head_right_hand"), |
| } |
| for p in save_sam2_masks(masks_dir, episode_index, save_map): |
| print(f"Wrote {p}") |
| _save_seed_overlay(masks_dir, episode_index, view_images, masks, view_tracks, out_hw) |
| _save_prompt_overlays(masks_dir, episode_index, view_images, masks) |
|
|
| if save_viz: |
| render_three_view_combined_video( |
| view_images=view_images, |
| view_tracks=view_tracks, |
| view_vis=view_vis, |
| out_path=viz_out_dir / f"episode_{episode_index:06d}.mp4", |
| fps=viz_fps, |
| draw_trail=viz_trail, |
| ) |
| print(f"Wrote {out_npz}") |
| if save_viz: |
| print(f"Wrote {viz_out_dir / f'episode_{episode_index:06d}.mp4'}") |
| print(f" tags: {tags}") |
| return out_npz |
|
|
|
|
| def create_tracking_runtime( |
| *, |
| calib_path: str | Path = DEFAULT_CALIB, |
| openpi_root: str | Path = DEFAULT_OPENPI_ROOT, |
| cotracker_checkpoint: str | Path | None = None, |
| cotracker_device: str = "", |
| sam2_model: str = DEFAULT_SAM2_MODEL, |
| sam2_device: str = "", |
| sam2_libs: str | Path = DEFAULT_SAM2_LIBS, |
| image_height: int = 0, |
| image_width: int = 0, |
| ) -> TrackingRuntime: |
| """Load CoTracker and SAM2 once; safe to call from the batch builder.""" |
|
|
| import torch |
|
|
| from trex_track.sam2_wrist_hand import load_sam2_predictor |
| from trex_track.trex_projection import load_camera_calib |
|
|
| calib = load_camera_calib(calib_path) |
| if bool(image_height > 0) != bool(image_width > 0): |
| raise ValueError("image-height and image-width must be set together") |
| if image_height > 0: |
| out_hw = (int(image_height), int(image_width)) |
| else: |
| out_hw = tuple(int(x) for x in calib.get("video_hw", [180, 320])) |
|
|
| _ensure_openpi_on_path(Path(openpi_root)) |
| from utils.cotracker_wrist_grid import ( |
| default_cotracker_checkpoint, |
| load_cotracker_predictor, |
| ) |
| _enable_cotracker_sdpa_attention(openpi_root) |
|
|
| checkpoint_text = str(cotracker_checkpoint or "").strip() |
| checkpoint = ( |
| Path(checkpoint_text).expanduser().resolve() |
| if checkpoint_text |
| else default_cotracker_checkpoint() |
| ) |
| device_text = cotracker_device.strip() or ( |
| "cuda:0" if torch.cuda.is_available() else "cpu" |
| ) |
| device = torch.device(device_text) |
| print(f" CoTracker: {checkpoint} on {device}") |
| if device.type == "cuda": |
| print(" CoTracker attention: BF16 PyTorch SDPA (Flash-compatible)") |
| cotracker_model = load_cotracker_predictor(checkpoint, device) |
|
|
| sam_device = sam2_device.strip() or device_text |
| print(f" SAM2: {sam2_model} on {sam_device}") |
| sam2_predictor = load_sam2_predictor( |
| model_id=str(sam2_model), |
| device=sam_device, |
| sam2_libs=sam2_libs, |
| ) |
| return TrackingRuntime( |
| calib=calib, |
| out_hw=out_hw, |
| cotracker_model=cotracker_model, |
| cotracker_device=device, |
| sam2_predictor=sam2_predictor, |
| ) |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser( |
| description="Extract canonical T-Rex SAM2+CoTracker tracks (250 points)" |
| ) |
| parser.add_argument( |
| "--dataset-root", |
| type=str, |
| default=str(_DREAMZERO_ROOT / "data" / "trex_small"), |
| ) |
| parser.add_argument("--episode-index", type=int, default=0) |
| parser.add_argument( |
| "--output-path", |
| type=str, |
| default=str(_DREAMZERO_ROOT / "data" / "trex_small_tracks"), |
| ) |
| parser.add_argument("--calib-path", type=str, default=str(DEFAULT_CALIB)) |
| parser.add_argument("--openpi-root", type=str, default=str(DEFAULT_OPENPI_ROOT)) |
| parser.add_argument("--cotracker-checkpoint", type=str, default="") |
| parser.add_argument("--cotracker-device", type=str, default="") |
| parser.add_argument("--image-height", type=int, default=0) |
| parser.add_argument("--image-width", type=int, default=0) |
| parser.add_argument("--save-viz", action=argparse.BooleanOptionalAction, default=True) |
| parser.add_argument("--viz-out-dir", type=str, default="") |
| parser.add_argument("--viz-fps", type=int, default=10) |
| parser.add_argument("--viz-trail", type=int, default=15) |
| parser.add_argument( |
| "--sam2-model", |
| type=str, |
| default=DEFAULT_SAM2_MODEL, |
| ) |
| parser.add_argument("--sam2-device", type=str, default="") |
| parser.add_argument("--sam2-seed", type=int, default=0) |
| parser.add_argument("--save-sam2-masks", action=argparse.BooleanOptionalAction, default=True) |
| parser.add_argument("--sam2-masks-dir", type=str, default="") |
| parser.add_argument( |
| "--sam2-libs", |
| type=str, |
| default=DEFAULT_SAM2_LIBS, |
| ) |
| args = parser.parse_args() |
|
|
| dataset_root = Path(args.dataset_root).expanduser().resolve() |
| output_path = Path(args.output_path).expanduser().resolve() |
| runtime = create_tracking_runtime( |
| calib_path=args.calib_path, |
| openpi_root=args.openpi_root, |
| cotracker_checkpoint=args.cotracker_checkpoint, |
| cotracker_device=args.cotracker_device, |
| sam2_model=args.sam2_model, |
| sam2_device=args.sam2_device, |
| sam2_libs=args.sam2_libs, |
| image_height=args.image_height, |
| image_width=args.image_width, |
| ) |
|
|
| viz_out_dir = ( |
| Path(args.viz_out_dir).expanduser().resolve() |
| if args.viz_out_dir.strip() |
| else output_path / "viz_tracks" |
| ) |
| sam2_masks_dir = ( |
| Path(args.sam2_masks_dir).expanduser().resolve() |
| if args.sam2_masks_dir.strip() |
| else output_path / "sam2_masks" |
| ) |
|
|
| print("T-Rex SAM2+CoTracker extraction") |
| print(f" Dataset: {dataset_root}") |
| print(f" Episode: {args.episode_index}") |
| print(f" Output: {output_path}") |
| print(f" Image: {runtime.out_hw[1]}x{runtime.out_hw[0]}") |
| print( |
| f" Points: head={NUM_HEAD_POINTS} (50+50), " |
| f"wrist={NUM_WRIST_POINTS}x2 (grid{NUM_WRIST_GRID}+hand{NUM_WRIST_HAND}), " |
| f"total={NUM_COMBINED_POINTS}" |
| ) |
|
|
| process_episode( |
| dataset_root=dataset_root, |
| episode_index=int(args.episode_index), |
| output_path=output_path, |
| calib=runtime.calib, |
| out_hw=runtime.out_hw, |
| cotracker_model=runtime.cotracker_model, |
| cotracker_device=runtime.cotracker_device, |
| save_viz=bool(args.save_viz), |
| viz_out_dir=viz_out_dir, |
| viz_fps=int(args.viz_fps), |
| viz_trail=int(args.viz_trail), |
| sam2_predictor=runtime.sam2_predictor, |
| sam2_seed=int(args.sam2_seed), |
| save_sam2_masks_flag=bool(args.save_sam2_masks), |
| sam2_masks_dir=sam2_masks_dir, |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|