Buckets:
| #!/usr/bin/env python | |
| """Contact sheet + side-by-side video for a zero-shot VACE run against its own window. | |
| Simpler than ``compare_wan_variants.py`` on purpose. That script exists because the | |
| A/B/C/D ablation compared generated clips against the *raw* 127-frame DROID capture, | |
| so it had to reproduce ``VaceVideoProcessor``'s own 81-of-127 frame selection and its | |
| resize+crop to line the two up. Here the ground truth is the window's own | |
| ``target.mp4``, which S8's exporter already wrote at exactly (81, 480, 832, 3) -- | |
| the same contract the generated clip comes out at -- so frame i corresponds to | |
| frame i and no matching step is needed or wanted. | |
| Metrics are deliberately secondary here. This project's own S8 note recorded that a | |
| photoreal *hallucinated human hand* outscores a correctly-rendered robot arm on | |
| PSNR/SSIM/LPIPS by matching the real clip's skin-tone texture and motion statistics | |
| -- so for any "is the agent right?" question the contact sheet is the evidence and | |
| the table is not. The numbers are printed anyway because they are cheap and do track | |
| the separate question of overall photorealism. | |
| Usage: | |
| PYTHONPATH=src python scripts/compare_zeroshot.py \\ | |
| --generated outputs/zeroshot/zs_ours_robotprompt.mp4 \\ | |
| --window outputs/datagen/<uuid>/<cam>/vace/window_00000_00081 \\ | |
| --out-dir outputs/zeroshot | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| import imageio.v3 as iio | |
| import numpy as np | |
| #: Frames sampled for the contact sheet. Same four the S8 note used, so its | |
| #: rows and this script's rows are visually comparable at a glance. | |
| SHEET_FRAMES = (5, 27, 50, 75) | |
| def read_video(path: Path) -> np.ndarray: | |
| v = np.asarray(iio.imread(path, plugin="pyav")) | |
| if v.ndim != 4 or v.shape[-1] != 3: | |
| raise ValueError(f"{path}: expected (T,H,W,3), got {v.shape}") | |
| return v | |
| def psnr(a: np.ndarray, b: np.ndarray) -> float: | |
| mse = np.mean((a.astype(np.float64) - b.astype(np.float64)) ** 2) | |
| return float("inf") if mse == 0 else 20.0 * np.log10(255.0 / np.sqrt(mse)) | |
| def temporal_activity(v: np.ndarray) -> float: | |
| """Mean abs frame-to-frame grayscale diff on a [0,1] scale. | |
| The S8/ablation notes' own "is the generation actually moving, or is it a | |
| near-static clip that flatters every similarity metric?" check. | |
| """ | |
| g = v.astype(np.float32).mean(axis=-1) / 255.0 | |
| return float(np.abs(np.diff(g, axis=0)).mean()) | |
| def label(img: np.ndarray, text: str) -> np.ndarray: | |
| """Burn a text label into the top-left of a frame copy (cv2, no font files).""" | |
| import cv2 | |
| out = img.copy() | |
| cv2.rectangle(out, (0, 0), (len(text) * 11 + 10, 26), (0, 0, 0), -1) | |
| cv2.putText(out, text, (5, 19), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA) | |
| return out | |
| def main() -> None: | |
| p = argparse.ArgumentParser(description=__doc__) | |
| p.add_argument("--generated", required=True, type=Path) | |
| p.add_argument("--window", required=True, type=Path) | |
| p.add_argument("--out-dir", required=True, type=Path) | |
| p.add_argument("--tag", default="zeroshot") | |
| args = p.parse_args() | |
| gen = read_video(args.generated) | |
| real = read_video(args.window / "target.mp4") | |
| ctrl = read_video(args.window / "control_normal.mp4") | |
| caption = (args.window / "caption.txt").read_text().strip() | |
| n = min(len(gen), len(real), len(ctrl)) | |
| if not (len(gen) == len(real) == len(ctrl)): | |
| print(f"WARNING: frame counts differ (gen={len(gen)} real={len(real)} " | |
| f"ctrl={len(ctrl)}); comparing the first {n}") | |
| gen, real, ctrl = gen[:n], real[:n], ctrl[:n] | |
| args.out_dir.mkdir(parents=True, exist_ok=True) | |
| # Contact sheet: rows = real / control / generated, cols = SHEET_FRAMES. | |
| frames = [f for f in SHEET_FRAMES if f < n] | |
| rows = [] | |
| for name, vid in (("real", real), ("control_normal", ctrl), ("generated", gen)): | |
| rows.append(np.concatenate([label(vid[f], f"{name} @{f}") for f in frames], axis=1)) | |
| sheet = np.concatenate(rows, axis=0) | |
| sheet_path = args.out_dir / f"{args.tag}_contact_sheet.png" | |
| iio.imwrite(sheet_path, sheet) | |
| # Side-by-side video, real on the left. | |
| sbs_path = args.out_dir / f"{args.tag}_side_by_side.mp4" | |
| iio.imwrite( | |
| sbs_path, | |
| np.concatenate([real, gen], axis=2), | |
| fps=16, | |
| codec="libx264", | |
| plugin="pyav", | |
| in_pixel_format="rgb24", | |
| ) | |
| metrics = { | |
| "caption": caption, | |
| "n_frames": int(n), | |
| "psnr_generated_vs_real": round(psnr(gen, real), 3), | |
| "temporal_activity": { | |
| "real": round(temporal_activity(real), 5), | |
| "generated": round(temporal_activity(gen), 5), | |
| "control": round(temporal_activity(ctrl), 5), | |
| }, | |
| "note": ( | |
| "PSNR here tracks overall photorealism only. Per this project's own S8 " | |
| "finding, a hallucinated human hand outscores a correct robot arm on " | |
| "PSNR/SSIM/LPIPS -- read the contact sheet for agent identity." | |
| ), | |
| } | |
| metrics["temporal_activity"]["generated_vs_real_ratio"] = round( | |
| metrics["temporal_activity"]["generated"] / metrics["temporal_activity"]["real"], 3 | |
| ) | |
| (args.out_dir / f"{args.tag}_metrics.json").write_text(json.dumps(metrics, indent=2)) | |
| print(json.dumps(metrics, indent=2)) | |
| print(f"\ncontact sheet: {sheet_path}") | |
| print(f"side by side: {sbs_path}") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 5.49 kB
- Xet hash:
- abd49c7adab18aa02415c505290776a775fbd20648c2152bb7bbf8bd5808d3bd
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.