| |
| """ |
| PSNR + LPIPS for aligned frame pairs. |
| |
| First version focuses on static consistency revisit: |
| - Compare first frame vs last frame of a video (revisit proxy). |
| - Optionally compare symmetric frames if indices provided. |
| |
| Dependencies: |
| - PSNR: numpy |
| - LPIPS: optional (pip install lpips). If missing, lpips will be reported as None. |
| """ |
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import os |
| from typing import Any, Dict, List, Tuple |
|
|
| import numpy as np |
|
|
| try: |
| import cv2 |
| HAS_CV2 = True |
| except ImportError: |
| HAS_CV2 = False |
|
|
|
|
| def _read_video_frames(path: str) -> List[np.ndarray]: |
| if not HAS_CV2: |
| raise RuntimeError("opencv-python required to read mp4") |
| cap = cv2.VideoCapture(path) |
| frames = [] |
| while True: |
| ret, bgr = cap.read() |
| if not ret: |
| break |
| rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) |
| frames.append(rgb) |
| cap.release() |
| return frames |
|
|
|
|
| def psnr(img1: np.ndarray, img2: np.ndarray) -> float: |
| if img1.shape != img2.shape and HAS_CV2: |
| img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]), interpolation=cv2.INTER_LINEAR) |
| mse = np.mean((img1.astype(np.float64) - img2.astype(np.float64)) ** 2) |
| if mse <= 0: |
| return 100.0 |
| return float(10.0 * np.log10((255.0 ** 2) / mse)) |
|
|
|
|
| def _lpips_model(device: str = "cuda"): |
| try: |
| import torch |
| import lpips |
| m = lpips.LPIPS(net="alex").to(device) |
| m.eval() |
| return m |
| except Exception: |
| return None |
|
|
|
|
| def lpips_distance(img1: np.ndarray, img2: np.ndarray, model, device: str = "cuda") -> float | None: |
| if model is None: |
| return None |
| try: |
| import torch |
| if img1.shape != img2.shape and HAS_CV2: |
| img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]), interpolation=cv2.INTER_LINEAR) |
| |
| t1 = torch.from_numpy(img1).permute(2, 0, 1).unsqueeze(0).float() / 127.5 - 1.0 |
| t2 = torch.from_numpy(img2).permute(2, 0, 1).unsqueeze(0).float() / 127.5 - 1.0 |
| t1 = t1.to(device) |
| t2 = t2.to(device) |
| with torch.no_grad(): |
| d = model(t1, t2) |
| return float(d.item()) |
| except Exception: |
| return None |
|
|
|
|
| def compute_revisit_metrics(video_path: str, device: str = "cuda") -> Dict[str, Any]: |
| frames = _read_video_frames(video_path) |
| if len(frames) < 2: |
| return {"num_frames": len(frames), "psnr": None, "lpips": None} |
| first = frames[0] |
| last = frames[-1] |
| p = psnr(first, last) |
| m = _lpips_model(device=device) |
| l = lpips_distance(first, last, m, device=device) |
| return {"num_frames": len(frames), "psnr": p, "lpips": l} |
|
|
|
|
| def main(): |
| p = argparse.ArgumentParser(description="PSNR+LPIPS for revisit (first vs last frame)") |
| p.add_argument("--video", required=True) |
| p.add_argument("--device", default="cuda") |
| p.add_argument("--output", default=None) |
| args = p.parse_args() |
|
|
| res = compute_revisit_metrics(args.video, device=args.device) |
| out = json.dumps(res, indent=2) |
| print(out) |
| if args.output: |
| with open(args.output, "w", encoding="utf-8") as f: |
| f.write(out) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|
|
|