| |
| """Render uniformly sampled T-Rex track-cache episodes without rerunning models.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
| _SCRIPT_DIR = Path(__file__).resolve().parent |
| _REPO_ROOT = _SCRIPT_DIR.parents[1] |
| _SCRIPTS_DIR = _REPO_ROOT / "scripts" |
| if str(_SCRIPTS_DIR) not in sys.path: |
| sys.path.insert(0, str(_SCRIPTS_DIR)) |
|
|
| from extract_track import load_episode_videos |
| from trex_track.layout import VIEW_ORDER |
| from trex_track.trex_viz_tracks import render_three_view_combined_video |
|
|
|
|
| def _parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument( |
| "--dataset-root", |
| type=Path, |
| default=_REPO_ROOT / "data" / "trex_full_force", |
| ) |
| parser.add_argument("--track-cache", type=Path, default=None) |
| parser.add_argument( |
| "--output-dir", |
| type=Path, |
| default=_REPO_ROOT / "outputs" / "trex_track_samples", |
| ) |
| parser.add_argument("--num-samples", type=int, default=3) |
| parser.add_argument("--episodes", type=int, nargs="*", default=None) |
| parser.add_argument("--fps", type=int, default=0) |
| parser.add_argument("--trail", type=int, default=15) |
| return parser.parse_args() |
|
|
|
|
| def _uniform_episode_indices(total_episodes: int, count: int) -> list[int]: |
| if count < 1: |
| raise ValueError("--num-samples must be positive") |
| if count > total_episodes: |
| raise ValueError("--num-samples cannot exceed total episodes") |
| return [ |
| int(index) |
| for index in np.rint( |
| np.linspace(0, total_episodes - 1, count, dtype=np.float64) |
| ) |
| ] |
|
|
|
|
| def _load_tracks(path: Path) -> tuple[dict[str, np.ndarray], dict[str, np.ndarray]]: |
| if not path.is_file(): |
| raise FileNotFoundError(path) |
| with np.load(path, allow_pickle=False) as payload: |
| tracks = { |
| view: np.asarray(payload[f"tracks_{view}"], dtype=np.float32) |
| for view in VIEW_ORDER |
| } |
| visibility = { |
| view: np.asarray(payload[f"vis_{view}"], dtype=np.float32) |
| for view in VIEW_ORDER |
| } |
| for view in VIEW_ORDER: |
| if visibility[view].shape != tracks[view].shape[:2]: |
| raise ValueError( |
| f"{path}: {view} visibility {visibility[view].shape} " |
| f"does not match tracks {tracks[view].shape}" |
| ) |
| return tracks, visibility |
|
|
|
|
| def main() -> int: |
| args = _parse_args() |
| dataset_root = args.dataset_root.expanduser().resolve() |
| track_cache = ( |
| args.track_cache.expanduser().resolve() |
| if args.track_cache is not None |
| else dataset_root / "tracks_trex_track_force_v2" |
| ) |
| output_dir = args.output_dir.expanduser().resolve() |
| info = json.loads((dataset_root / "meta" / "info.json").read_text()) |
| total_episodes = int(info["total_episodes"]) |
| video_shape = info["features"]["observation.images.head_left"]["shape"] |
| out_hw = (int(video_shape[0]), int(video_shape[1])) |
| fps = int(args.fps) if args.fps > 0 else int(info["fps"]) |
|
|
| episode_indices = ( |
| [int(index) for index in args.episodes] |
| if args.episodes |
| else _uniform_episode_indices(total_episodes, int(args.num_samples)) |
| ) |
| if any(index < 0 or index >= total_episodes for index in episode_indices): |
| raise ValueError(f"episode indices must be in [0, {total_episodes})") |
|
|
| output_dir.mkdir(parents=True, exist_ok=True) |
| rendered: list[dict[str, object]] = [] |
| for episode_index in episode_indices: |
| cache_path = track_cache / f"episode_{episode_index:06d}.npz" |
| output_path = output_dir / f"episode_{episode_index:06d}_tracks.mp4" |
| print(f"Rendering episode {episode_index}: {output_path}", flush=True) |
| tracks, visibility = _load_tracks(cache_path) |
| videos = load_episode_videos( |
| dataset_root, |
| episode_index, |
| out_hw=out_hw, |
| ) |
| frame_counts = { |
| view: int(videos[view].shape[0]) |
| for view in VIEW_ORDER |
| } |
| for view in VIEW_ORDER: |
| if int(tracks[view].shape[0]) != frame_counts[view]: |
| raise ValueError( |
| f"episode {episode_index} {view}: " |
| f"{tracks[view].shape[0]} track frames != " |
| f"{frame_counts[view]} video frames" |
| ) |
| render_three_view_combined_video( |
| view_images=videos, |
| view_tracks=tracks, |
| view_vis=visibility, |
| out_path=output_path, |
| fps=fps, |
| draw_trail=int(args.trail), |
| dim_low_vis=True, |
| ) |
| rendered.append( |
| { |
| "episode_index": episode_index, |
| "frames": min(frame_counts.values()), |
| "fps": fps, |
| "track_cache": str(cache_path), |
| "video": str(output_path), |
| } |
| ) |
| del tracks, visibility, videos |
|
|
| summary_path = output_dir / "samples.json" |
| summary_path.write_text( |
| json.dumps( |
| { |
| "dataset_root": str(dataset_root), |
| "sampling": "uniform endpoints and midpoint", |
| "episodes": rendered, |
| }, |
| indent=2, |
| ) |
| + "\n" |
| ) |
| print(f"Wrote {summary_path}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|