File size: 6,563 Bytes
ec0a9aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Run Tier-1 physics metrics (flow-vs-GT) over ctrlworld multiview episodes.

For each episode it reads ``pred_all_views.mp4`` (predicted views tiled
horizontally) and ``gt_all_views.mp4`` (GT views tiled), splits them into the
individual camera views, and computes optical-flow-vs-GT motion metrics per view
via SEA-RAFT. Results are written to ``physics_metrics.json`` per episode and a
``physics_summary.json`` per (method, split), plus a combined comparison JSON.

Example:
    CUDA_VISIBLE_DEVICES=2 python scripts/compute_physics_metrics_ctrlworld.py \
        --roots dense worldcache --split makovian --max-episodes 15
"""

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

import numpy as np
import imageio.v3 as iio

# repo root on path for `metrics` package import
_REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from metrics.dynamics.flow_backend import SeaRaftFlow
from metrics.dynamics.physics_metrics import flow_vs_gt_metrics, aggregate_view_metrics

CTRLWORLD_SUBPATH = "single_arm/output/multiview/ctrlworld"


def load_video(path: Path) -> np.ndarray:
    """Decode an mp4 to (T, H, W, 3) uint8 RGB."""
    frames = iio.imread(path, index=None)
    return np.asarray(frames)


def split_views(strip: np.ndarray, num_views: int) -> list[np.ndarray]:
    """Split a horizontally-tiled (T, H, W*num_views, 3) strip into views."""
    T, H, W, C = strip.shape
    assert W % num_views == 0, f"width {W} not divisible by num_views {num_views}"
    vw = W // num_views
    return [strip[:, :, v * vw : (v + 1) * vw, :] for v in range(num_views)]


def matched_episodes(roots: list[Path], split: str) -> list[str]:
    """Episodes present (with required mp4s) in ALL roots, sorted by index."""
    per_root_sets = []
    for r in roots:
        d = r / CTRLWORLD_SUBPATH / split
        eps = set()
        if d.is_dir():
            for ep in d.iterdir():
                if ep.is_dir() and ep.name.startswith("episode_"):
                    if (ep / "pred_all_views.mp4").exists() and (ep / "gt_all_views.mp4").exists():
                        eps.add(ep.name)
        per_root_sets.append(eps)
    common = set.intersection(*per_root_sets) if per_root_sets else set()
    return sorted(common)


def process_episode(ep_dir: Path, flow_model, num_views: int, overwrite: bool) -> dict:
    out_path = ep_dir / "physics_metrics.json"
    if out_path.exists() and not overwrite:
        with open(out_path) as f:
            return json.load(f)

    pred = load_video(ep_dir / "pred_all_views.mp4")
    gt = load_video(ep_dir / "gt_all_views.mp4")

    # align frame counts (safety; ctrlworld pairs are already equal)
    T = min(pred.shape[0], gt.shape[0])
    pred, gt = pred[:T], gt[:T]

    pred_views = split_views(pred, num_views)
    gt_views = split_views(gt, num_views)

    per_view = {}
    for v, (pv, gv) in enumerate(zip(pred_views, gt_views)):
        per_view[f"view_{v}"] = flow_vs_gt_metrics(pv, gv, flow_model)

    agg = aggregate_view_metrics(per_view)
    result = {**agg, "per_view": per_view, "num_frames": int(T), "num_views": num_views}

    with open(out_path, "w") as f:
        json.dump(result, f, indent=2)
    return result


def summarize(results: dict[str, dict]) -> dict:
    keys = ["flow_epe", "motion_mag_pred", "motion_mag_gt", "motion_mag_abs_err", "motion_mag_ratio"]
    summary = {"num_episodes": len(results)}
    for k in keys:
        vals = [r[k] for r in results.values() if k in r]
        if vals:
            summary[f"mean_{k}"] = float(np.mean(vals))
    return summary


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dataset-root", default=str(_REPO_ROOT / "sampling_dataset"))
    ap.add_argument("--roots", nargs="+", default=["dense", "worldcache"],
                    help="method dirs under sampling_dataset to evaluate")
    ap.add_argument("--split", default="makovian")
    ap.add_argument("--num-views", type=int, default=3)
    ap.add_argument("--max-episodes", type=int, default=15)
    ap.add_argument("--overwrite", action="store_true")
    ap.add_argument("--out", default=str(_REPO_ROOT / "sampling_dataset/physics_flow_trial.json"))
    args = ap.parse_args()

    ds_root = Path(args.dataset_root)
    roots = [ds_root / r for r in args.roots]

    episodes = matched_episodes(roots, args.split)
    if args.max_episodes:
        episodes = episodes[: args.max_episodes]
    print(f"[info] {len(episodes)} matched episodes across {args.roots} (split={args.split})")
    print(f"[info] episodes: {episodes}")

    flow_model = SeaRaftFlow(device="cuda")

    all_out = {}
    for method, root in zip(args.roots, roots):
        split_dir = root / CTRLWORLD_SUBPATH / args.split
        results = {}
        for i, ep in enumerate(episodes):
            ep_dir = split_dir / ep
            print(f"[{method}] ({i+1}/{len(episodes)}) {ep} ...", flush=True)
            try:
                r = process_episode(ep_dir, flow_model, args.num_views, args.overwrite)
                results[ep] = r
                print(
                    f"    epe={r.get('flow_epe'):.3f} "
                    f"mag_pred={r.get('motion_mag_pred'):.3f} mag_gt={r.get('motion_mag_gt'):.3f} "
                    f"ratio={r.get('motion_mag_ratio'):.3f} mag_abs_err={r.get('motion_mag_abs_err'):.3f}",
                    flush=True,
                )
            except Exception as e:  # keep going on a bad episode
                print(f"    ERROR: {e!r}", flush=True)

        summ = summarize(results)
        with open(split_dir / "physics_summary.json", "w") as f:
            json.dump(summ, f, indent=2)
        all_out[method] = {"summary": summ, "episodes": results}
        print(f"[{method}] summary: {json.dumps(summ, indent=2)}")

    with open(args.out, "w") as f:
        json.dump(all_out, f, indent=2)
    print(f"[info] wrote combined comparison -> {args.out}")

    # side-by-side print
    print("\n==================== COMPARISON ====================")
    metrics = ["mean_flow_epe", "mean_motion_mag_pred", "mean_motion_mag_gt",
               "mean_motion_mag_abs_err", "mean_motion_mag_ratio"]
    header = f"{'metric':<26}" + "".join(f"{m:>14}" for m in args.roots)
    print(header)
    for mk in metrics:
        row = f"{mk:<26}"
        for method in args.roots:
            v = all_out[method]["summary"].get(mk)
            row += f"{v:>14.4f}" if v is not None else f"{'-':>14}"
        print(row)


if __name__ == "__main__":
    main()