stzhao's picture
download
raw
31.4 kB
from __future__ import annotations
import argparse
import copy
import importlib.util
import json
import sys
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Callable, Iterable
import matplotlib
import numpy as np
import torch
from tqdm import tqdm
matplotlib.use("Agg")
import matplotlib.pyplot as plt
REPO_ROOT = Path(__file__).resolve().parents[1]
RAE_SRC_ROOT = REPO_ROOT / "RAE" / "src"
if str(RAE_SRC_ROOT) not in sys.path:
sys.path.append(str(RAE_SRC_ROOT))
from disc.lpips import LPIPS # noqa: E402
PATH_VIDEO_PATH = RAE_SRC_ROOT / "stage2" / "transport" / "path_video.py"
PATH_VIDEO_SPEC = importlib.util.spec_from_file_location("path_video", PATH_VIDEO_PATH)
if PATH_VIDEO_SPEC is None or PATH_VIDEO_SPEC.loader is None:
raise ImportError(f"Failed to load TrajectoryPlan from {PATH_VIDEO_PATH}")
path_video = importlib.util.module_from_spec(PATH_VIDEO_SPEC)
PATH_VIDEO_SPEC.loader.exec_module(path_video)
TrajectoryPlan = path_video.TrajectoryPlan
try:
from .forward import ( # noqa: E402
BACKBONE_SPECS,
ResolvedRAEConfig,
batched_indices,
decode_latents,
encode_frames,
frames_to_tensor,
get_device,
load_rae,
load_video_frames,
resolve_rae_config,
)
from .forward_intrapolate import ( # noqa: E402
collect_video_entries,
get_interpolator,
resolve_interpolation_mode,
)
except ImportError:
from forward import ( # type: ignore # noqa: E402
BACKBONE_SPECS,
ResolvedRAEConfig,
batched_indices,
decode_latents,
encode_frames,
frames_to_tensor,
get_device,
load_rae,
load_video_frames,
resolve_rae_config,
)
from forward_intrapolate import ( # type: ignore # noqa: E402
collect_video_entries,
get_interpolator,
resolve_interpolation_mode,
)
@dataclass(frozen=True)
class EvalJob:
backbone: str
protocol: str
interp_mode: str
@dataclass(frozen=True)
class VideoEvaluation:
sample_values: np.ndarray
pair_count: int
gap_indices: np.ndarray
gap_sample_counts: np.ndarray
gap_ppl_mean: np.ndarray
gap_ppl_std: np.ndarray
gap_ppl_min: np.ndarray
gap_ppl_max: np.ndarray
@dataclass(frozen=True)
class PerGapCurve:
backbone: str
protocol: str
interp_mode_used: str
video_path: str
gap_indices: np.ndarray
gap_sample_counts: np.ndarray
gap_ppl_mean: np.ndarray
gap_ppl_std: np.ndarray
gap_ppl_min: np.ndarray
gap_ppl_max: np.ndarray
@dataclass(frozen=True)
class EvalResult:
backbone: str
protocol: str
interp_mode_used: str
videos_evaluated: int
videos_skipped: int
pairs_evaluated: int
samples_evaluated: int
ppl_mean_raw: float
ppl_mean_trimmed: float
ppl_median: float
ppl_std: float
ppl_min: float
ppl_max: float
trim_value_low: float
trim_value_high: float
eps: float
samples_per_gap: int
split_name: str | None
video_path: str | None
image_size: int
seed: int
@dataclass(frozen=True)
class JobEvaluation:
result: EvalResult
per_gap_curves: list[PerGapCurve]
PPL_INTERP_MODES = ("auto", "linear", "slerp", "smooth_continuous")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Compute StyleGAN-inspired perceptual path length (PPL) on video latent interpolation paths "
"for MAE / DINOv2 / SigLIP2 RAE backbones."
)
)
parser.add_argument(
"--config",
type=Path,
default=None,
help="Optional stage-1 or stage-2 YAML. Only supported when evaluating a single backbone.",
)
parser.add_argument(
"--rae-backbone",
type=str,
choices=sorted(BACKBONE_SPECS),
default="dinov2",
help="RAE encoder backbone used when --config is not supplied.",
)
parser.add_argument(
"--all-backbones",
action="store_true",
help="Evaluate dinov2, mae, and siglip2 sequentially.",
)
parser.add_argument(
"--rae-root",
type=Path,
default=Path("/mnt/posttrain/zhaoshitian/models/RAE-collections"),
help="Root directory containing RAE decoder and latent normalization weights.",
)
parser.add_argument(
"--encoder-path",
type=Path,
default=None,
help="Optional local encoder path override for a single selected backbone.",
)
parser.add_argument(
"--video-path",
type=Path,
default=None,
help="Optional single video to evaluate. When set, split traversal is skipped.",
)
parser.add_argument(
"--video-root",
type=Path,
default=Path("/mnt/posttrain/zhaoshitian/datasets/ucf101/OpenDataLab___UCF101/raw/data/UCF101"),
help="Root directory containing extracted UCF101 class folders.",
)
parser.add_argument(
"--split-zip",
type=Path,
default=Path(
"/mnt/posttrain/zhaoshitian/datasets/ucf101/OpenDataLab___UCF101/raw/data/UCF101TrainTestSplits-RecognitionTask.zip"
),
help="Path to the UCF101 recognition split zip.",
)
parser.add_argument(
"--split-name",
type=str,
default="trainlist01.txt",
help="Split file inside the recognition split zip, e.g. trainlist01.txt.",
)
parser.add_argument(
"--image-size",
type=int,
default=None,
help="Center-crop size before encoding. Defaults to the selected encoder input size.",
)
parser.add_argument("--encode-batch-size", type=int, default=16, help="Batch size used for latent extraction.")
parser.add_argument("--decode-batch-size", type=int, default=16, help="Batch size used for latent decoding.")
parser.add_argument("--lpips-batch-size", type=int, default=16, help="Batch size used for LPIPS computation.")
parser.add_argument("--device", type=str, default=None, help="Torch device. Defaults to cuda when available.")
parser.add_argument("--max-videos", type=int, default=None, help="Optional cap for debugging a subset of videos.")
parser.add_argument(
"--interp-mode",
type=str,
choices=PPL_INTERP_MODES,
default="auto",
help=(
"Interpolation geometry for the default protocol. "
"`smooth_continuous` is only implemented for PPL evaluation, while the controlled protocol always uses linear."
),
)
parser.add_argument(
"--comparison-protocol",
type=str,
choices=("default", "controlled", "both"),
default="both",
help=(
"How to compare backbones: default uses each backbone's requested/default mode, "
"controlled forces linear, both reports both."
),
)
parser.add_argument(
"--eps",
type=float,
default=1e-4,
help="Small path step used in PPL finite differences. alpha is sampled in [0, 1 - eps].",
)
parser.add_argument(
"--samples-per-gap",
type=int,
default=1,
help="How many random alpha samples to draw from each adjacent source-latent pair.",
)
parser.add_argument(
"--trim-percentile-low",
type=float,
default=1.0,
help="Lower percentile for trimmed PPL mean.",
)
parser.add_argument(
"--trim-percentile-high",
type=float,
default=99.0,
help="Upper percentile for trimmed PPL mean.",
)
parser.add_argument("--seed", type=int, default=0, help="Random seed for alpha sampling.")
parser.add_argument(
"--save-json",
type=Path,
default=None,
help="Optional JSON path to save the aggregated PPL results.",
)
parser.add_argument(
"--plot-per-gap-ppl",
action="store_true",
help="Plot per-gap PPL means for a single input video.",
)
parser.add_argument(
"--plot-output",
type=Path,
default=None,
help="Optional PNG path for the per-gap PPL plot. Requires --plot-per-gap-ppl.",
)
parser.add_argument(
"--plot-output-dir",
type=Path,
default=None,
help="Optional output directory for the per-gap PPL plot. Requires --plot-per-gap-ppl.",
)
return parser.parse_args()
def validate_args(args: argparse.Namespace) -> None:
if args.all_backbones and args.config is not None:
raise ValueError("--all-backbones cannot be combined with --config.")
if args.all_backbones and args.encoder_path is not None:
raise ValueError("--all-backbones cannot be combined with --encoder-path.")
if args.video_path is not None and args.max_videos is not None:
raise ValueError("--max-videos cannot be used together with --video-path.")
if args.eps <= 0.0 or args.eps >= 1.0:
raise ValueError("--eps must be strictly between 0 and 1.")
if args.samples_per_gap < 1:
raise ValueError("--samples-per-gap must be at least 1.")
if args.encode_batch_size < 1 or args.decode_batch_size < 1 or args.lpips_batch_size < 1:
raise ValueError("All batch sizes must be at least 1.")
if not (0.0 <= args.trim_percentile_low < args.trim_percentile_high <= 100.0):
raise ValueError("Trim percentiles must satisfy 0 <= low < high <= 100.")
if args.plot_output is not None and args.plot_output_dir is not None:
raise ValueError("--plot-output and --plot-output-dir cannot be used together.")
if (args.plot_output is not None or args.plot_output_dir is not None) and not args.plot_per_gap_ppl:
raise ValueError("--plot-output and --plot-output-dir require --plot-per-gap-ppl.")
if args.plot_per_gap_ppl and args.video_path is None:
raise ValueError("--plot-per-gap-ppl currently requires --video-path.")
def clone_args(args: argparse.Namespace) -> argparse.Namespace:
return argparse.Namespace(**copy.deepcopy(vars(args)))
def expand_runtime_paths(args: argparse.Namespace) -> None:
args.video_root = args.video_root.expanduser()
args.split_zip = args.split_zip.expanduser()
args.rae_root = args.rae_root.expanduser()
if args.video_path is not None:
args.video_path = args.video_path.expanduser()
if args.encoder_path is not None:
args.encoder_path = args.encoder_path.expanduser()
if args.config is not None:
args.config = args.config.expanduser()
if args.save_json is not None:
args.save_json = args.save_json.expanduser()
if args.plot_output is not None:
args.plot_output = args.plot_output.expanduser()
if args.plot_output_dir is not None:
args.plot_output_dir = args.plot_output_dir.expanduser()
def build_eval_jobs(args: argparse.Namespace) -> list[EvalJob]:
backbones = sorted(BACKBONE_SPECS) if args.all_backbones else [args.rae_backbone]
protocol_order = (
["default", "controlled-linear"]
if args.comparison_protocol == "both"
else ["default"] if args.comparison_protocol == "default" else ["controlled-linear"]
)
jobs: list[EvalJob] = []
for backbone in backbones:
for protocol in protocol_order:
if protocol == "default":
interp_mode = resolve_interpolation_mode(backbone, args.interp_mode)
else:
interp_mode = "linear"
jobs.append(EvalJob(backbone=backbone, protocol=protocol, interp_mode=interp_mode))
return jobs
def make_job_args(base_args: argparse.Namespace, job: EvalJob) -> argparse.Namespace:
job_args = clone_args(base_args)
job_args.rae_backbone = job.backbone
return job_args
@dataclass(frozen=True)
class PairwisePPLInterpolator:
trajectory: torch.Tensor
interpolate_pair: Callable[[torch.Tensor, torch.Tensor, float], torch.Tensor]
def interpolate_gap(self, gap_index: int, alpha: float) -> torch.Tensor:
return self.interpolate_pair(
self.trajectory[gap_index],
self.trajectory[gap_index + 1],
alpha,
)
class SmoothContinuousPPLInterpolator:
"""Video-aware interpolator that matches TrajectoryPlan smooth_continuous semantics."""
def __init__(self, trajectory: torch.Tensor) -> None:
if trajectory.shape[0] < 2:
raise ValueError("smooth_continuous interpolation requires at least 2 latent frames.")
self.trajectory = trajectory
self.plan = TrajectoryPlan(sampling_mode="smooth_continuous")
self.times = torch.linspace(
0.0,
1.0,
trajectory.shape[0],
device=trajectory.device,
dtype=trajectory.dtype,
)
self.gap_dt = (self.times[1] - self.times[0]).clamp_min(self.plan.eps)
self.frame_velocities = self.plan._estimate_frame_velocities(
trajectory.unsqueeze(0),
self.times,
)[0]
def interpolate_gap(self, gap_index: int, alpha: float) -> torch.Tensor:
if gap_index < 0 or gap_index >= self.trajectory.shape[0] - 1:
raise IndexError(f"Gap index {gap_index} is out of range for {self.trajectory.shape[0]} frames.")
if not 0.0 <= alpha <= 1.0:
raise ValueError(f"alpha must lie in [0, 1], got {alpha}.")
left_idx = gap_index
right_idx = gap_index + 1
query_t = self.times[left_idx] + self.gap_dt * float(alpha)
xt, _ = self.plan._hermite_segment(
query_t.unsqueeze(0),
self.times[left_idx].unsqueeze(0),
self.times[right_idx].unsqueeze(0),
self.trajectory[left_idx].unsqueeze(0),
self.trajectory[right_idx].unsqueeze(0),
self.frame_velocities[left_idx].unsqueeze(0),
self.frame_velocities[right_idx].unsqueeze(0),
)
return xt[0]
def build_ppl_interpolator(trajectory: torch.Tensor, interp_mode: str) -> PairwisePPLInterpolator | SmoothContinuousPPLInterpolator:
if interp_mode == "smooth_continuous":
return SmoothContinuousPPLInterpolator(trajectory)
return PairwisePPLInterpolator(trajectory=trajectory, interpolate_pair=get_interpolator(interp_mode))
def compute_lpips_scores(
lpips_model: LPIPS,
frames_a: torch.Tensor,
frames_b: torch.Tensor,
batch_size: int,
device: torch.device,
) -> np.ndarray:
scores: list[np.ndarray] = []
for start, end in batched_indices(frames_a.shape[0], batch_size):
batch_a = frames_a[start:end].to(device, non_blocking=True).mul(2.0).sub(1.0)
batch_b = frames_b[start:end].to(device, non_blocking=True).mul(2.0).sub(1.0)
batch_scores = lpips_model(batch_a, batch_b, reduction="none")
scores.append(batch_scores.reshape(-1).detach().cpu().numpy().astype(np.float64))
if not scores:
return np.empty((0,), dtype=np.float64)
return np.concatenate(scores, axis=0)
def empty_video_evaluation(pair_count: int = 0) -> VideoEvaluation:
empty_int = np.empty((0,), dtype=np.int32)
empty_float = np.empty((0,), dtype=np.float64)
return VideoEvaluation(
sample_values=empty_float,
pair_count=pair_count,
gap_indices=empty_int,
gap_sample_counts=empty_int,
gap_ppl_mean=empty_float,
gap_ppl_std=empty_float,
gap_ppl_min=empty_float,
gap_ppl_max=empty_float,
)
def summarize_gap_samples(gap_sample_lists: list[list[float]]) -> tuple[np.ndarray, ...]:
gap_indices: list[int] = []
gap_sample_counts: list[int] = []
gap_ppl_mean: list[float] = []
gap_ppl_std: list[float] = []
gap_ppl_min: list[float] = []
gap_ppl_max: list[float] = []
for gap_index, gap_samples in enumerate(gap_sample_lists):
if not gap_samples:
continue
gap_values = np.asarray(gap_samples, dtype=np.float64)
gap_indices.append(gap_index)
gap_sample_counts.append(int(gap_values.size))
gap_ppl_mean.append(float(gap_values.mean()))
gap_ppl_std.append(float(gap_values.std()))
gap_ppl_min.append(float(gap_values.min()))
gap_ppl_max.append(float(gap_values.max()))
return (
np.asarray(gap_indices, dtype=np.int32),
np.asarray(gap_sample_counts, dtype=np.int32),
np.asarray(gap_ppl_mean, dtype=np.float64),
np.asarray(gap_ppl_std, dtype=np.float64),
np.asarray(gap_ppl_min, dtype=np.float64),
np.asarray(gap_ppl_max, dtype=np.float64),
)
def evaluate_video_ppl(
*,
rae,
video_path: Path,
args: argparse.Namespace,
interp_mode: str,
device: torch.device,
lpips_model: LPIPS,
rng: np.random.Generator,
) -> VideoEvaluation:
frames_np, _ = load_video_frames(video_path, args.image_size)
frame_tensor = frames_to_tensor(frames_np)
source_latents = encode_frames(rae, frame_tensor, args.encode_batch_size, device)
if source_latents.shape[0] < 2:
return empty_video_evaluation(pair_count=0)
interpolator = build_ppl_interpolator(source_latents, interp_mode)
num_pairs = source_latents.shape[0] - 1
latent_batch_a: list[torch.Tensor] = []
latent_batch_b: list[torch.Tensor] = []
latent_batch_gap_indices: list[int] = []
ppl_samples: list[np.ndarray] = []
gap_sample_lists: list[list[float]] = [[] for _ in range(num_pairs)]
pair_count = 0
def flush_pending_latents() -> None:
if not latent_batch_a:
return
latents_a = torch.stack(latent_batch_a, dim=0)
latents_b = torch.stack(latent_batch_b, dim=0)
latent_batch_a.clear()
latent_batch_b.clear()
decoded_a = decode_latents(rae, latents_a, args.decode_batch_size, device)
decoded_b = decode_latents(rae, latents_b, args.decode_batch_size, device)
lpips_scores = compute_lpips_scores(lpips_model, decoded_a, decoded_b, args.lpips_batch_size, device)
ppl_values = lpips_scores / (args.eps ** 2)
if ppl_values.size != len(latent_batch_gap_indices):
raise RuntimeError("Internal mismatch between decoded PPL samples and gap indices.")
ppl_samples.append(ppl_values)
for gap_index, sample_value in zip(latent_batch_gap_indices, ppl_values.tolist()):
gap_sample_lists[gap_index].append(float(sample_value))
latent_batch_gap_indices.clear()
for idx in range(num_pairs):
pair_count += 1
alphas = rng.uniform(0.0, 1.0 - args.eps, size=args.samples_per_gap)
for alpha in alphas:
alpha_value = float(alpha)
latent_batch_a.append(interpolator.interpolate_gap(idx, alpha_value))
latent_batch_b.append(interpolator.interpolate_gap(idx, float(alpha_value + args.eps)))
latent_batch_gap_indices.append(idx)
if len(latent_batch_a) >= args.decode_batch_size:
flush_pending_latents()
flush_pending_latents()
if not ppl_samples:
return empty_video_evaluation(pair_count=pair_count)
(
gap_indices,
gap_sample_counts,
gap_ppl_mean,
gap_ppl_std,
gap_ppl_min,
gap_ppl_max,
) = summarize_gap_samples(gap_sample_lists)
return VideoEvaluation(
sample_values=np.concatenate(ppl_samples, axis=0),
pair_count=pair_count,
gap_indices=gap_indices,
gap_sample_counts=gap_sample_counts,
gap_ppl_mean=gap_ppl_mean,
gap_ppl_std=gap_ppl_std,
gap_ppl_min=gap_ppl_min,
gap_ppl_max=gap_ppl_max,
)
def summarize_samples(
*,
samples: np.ndarray,
resolved: ResolvedRAEConfig,
protocol: str,
interp_mode: str,
args: argparse.Namespace,
videos_evaluated: int,
videos_skipped: int,
pairs_evaluated: int,
) -> EvalResult:
if samples.size == 0:
raise ValueError(f"No valid PPL samples collected for backbone={resolved.backbone}, protocol={protocol}.")
low_value = float(np.percentile(samples, args.trim_percentile_low))
high_value = float(np.percentile(samples, args.trim_percentile_high))
trimmed_mask = (samples >= low_value) & (samples <= high_value)
trimmed_samples = samples[trimmed_mask]
if trimmed_samples.size == 0:
trimmed_samples = samples
return EvalResult(
backbone=resolved.backbone,
protocol=protocol,
interp_mode_used=interp_mode,
videos_evaluated=videos_evaluated,
videos_skipped=videos_skipped,
pairs_evaluated=pairs_evaluated,
samples_evaluated=int(samples.size),
ppl_mean_raw=float(samples.mean()),
ppl_mean_trimmed=float(trimmed_samples.mean()),
ppl_median=float(np.median(samples)),
ppl_std=float(samples.std()),
ppl_min=float(samples.min()),
ppl_max=float(samples.max()),
trim_value_low=low_value,
trim_value_high=high_value,
eps=float(args.eps),
samples_per_gap=int(args.samples_per_gap),
split_name=None if args.video_path is not None else args.split_name,
video_path=str(args.video_path) if args.video_path is not None else None,
image_size=int(args.image_size),
seed=int(args.seed),
)
def print_job_header(
*,
resolved: ResolvedRAEConfig,
protocol: str,
interp_mode: str,
args: argparse.Namespace,
device: torch.device,
video_entries: list[tuple[Path, Path]],
) -> None:
print("=" * 88)
print(f"Backbone: {resolved.backbone}")
print(f"Protocol: {protocol}")
print(f"Interpolation mode: {interp_mode}")
print(f"Encoder path/id: {resolved.encoder_reference}")
if resolved.decoder_path is not None:
print(f"Decoder checkpoint: {resolved.decoder_path}")
if resolved.stats_path is not None:
print(f"Normalization stats: {resolved.stats_path}")
print(f"Frame crop size: {args.image_size}")
print(f"Input videos: {len(video_entries)}")
if args.video_path is not None:
print(f"Input video: {args.video_path}")
else:
print(f"Input split: {args.split_name}")
print(f"Samples per gap: {args.samples_per_gap}")
print(f"Epsilon: {args.eps}")
print(f"Trim percentiles: [{args.trim_percentile_low}, {args.trim_percentile_high}]")
print(f"Device: {device}")
def resolve_plot_output_path(args: argparse.Namespace, video_path: Path) -> Path:
if args.plot_output is not None:
return args.plot_output
if args.plot_output_dir is not None:
return args.plot_output_dir / f"{video_path.stem}_per_gap_ppl.png"
return Path.cwd() / f"{video_path.stem}_per_gap_ppl.png"
def build_curve_label(curve: PerGapCurve) -> str:
return f"{curve.backbone} | {curve.protocol} | {curve.interp_mode_used}"
def plot_per_gap_curves(
curves: list[PerGapCurve],
output_path: Path,
video_path: Path,
args: argparse.Namespace,
) -> None:
if not curves:
raise ValueError("No per-gap PPL curves available to plot.")
output_path.parent.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(12, 6))
for curve in curves:
plt.plot(
curve.gap_indices,
curve.gap_ppl_mean,
linewidth=1.5,
marker="o",
markersize=2.5,
label=build_curve_label(curve),
)
plt.xlabel("frame index (left frame of gap)")
plt.ylabel("PPL")
plt.title(
f"Adjacent-frame PPL | {video_path.stem} | "
f"samples_per_gap={args.samples_per_gap} | eps={args.eps}"
)
plt.grid(True, alpha=0.3, linewidth=0.5)
plt.legend(loc="best")
plt.tight_layout()
plt.savefig(output_path, dpi=150)
plt.close()
print(f"Saved per-gap PPL plot to {output_path}")
def evaluate_job(
*,
base_args: argparse.Namespace,
job: EvalJob,
device: torch.device,
lpips_model: LPIPS,
) -> JobEvaluation:
args = make_job_args(base_args, job)
resolved = resolve_rae_config(args)
if args.image_size is None:
args.image_size = resolved.encoder_input_size
interp_mode = resolve_interpolation_mode(resolved.backbone, args.interp_mode) if job.protocol == "default" else "linear"
video_entries = collect_video_entries(args)
print_job_header(
resolved=resolved,
protocol=job.protocol,
interp_mode=interp_mode,
args=args,
device=device,
video_entries=video_entries,
)
rae = load_rae(resolved.config, device)
rng = np.random.default_rng(args.seed)
all_samples: list[np.ndarray] = []
videos_evaluated = 0
videos_skipped = 0
pairs_evaluated = 0
per_gap_curves: list[PerGapCurve] = []
progress = tqdm(video_entries, desc=f"PPL {resolved.backbone} {job.protocol}")
for relative_video_path, video_path in progress:
try:
video_eval = evaluate_video_ppl(
rae=rae,
video_path=video_path,
args=args,
interp_mode=interp_mode,
device=device,
lpips_model=lpips_model,
rng=rng,
)
except Exception as exc:
videos_skipped += 1
tqdm.write(f"[WARN] Skipping {video_path}: {exc}")
continue
if video_eval.sample_values.size == 0:
videos_skipped += 1
tqdm.write(f"[WARN] Skipping {video_path}: no valid adjacent latent pairs.")
continue
all_samples.append(video_eval.sample_values)
videos_evaluated += 1
pairs_evaluated += video_eval.pair_count
if args.plot_per_gap_ppl and video_eval.gap_indices.size > 0:
per_gap_curves.append(
PerGapCurve(
backbone=resolved.backbone,
protocol=job.protocol,
interp_mode_used=interp_mode,
video_path=str(video_path),
gap_indices=video_eval.gap_indices,
gap_sample_counts=video_eval.gap_sample_counts,
gap_ppl_mean=video_eval.gap_ppl_mean,
gap_ppl_std=video_eval.gap_ppl_std,
gap_ppl_min=video_eval.gap_ppl_min,
gap_ppl_max=video_eval.gap_ppl_max,
)
)
progress.set_postfix_str(relative_video_path.name)
del rae
if device.type == "cuda":
torch.cuda.empty_cache()
return JobEvaluation(
result=summarize_samples(
samples=np.concatenate(all_samples, axis=0) if all_samples else np.empty((0,), dtype=np.float64),
resolved=resolved,
protocol=job.protocol,
interp_mode=interp_mode,
args=args,
videos_evaluated=videos_evaluated,
videos_skipped=videos_skipped,
pairs_evaluated=pairs_evaluated,
),
per_gap_curves=per_gap_curves,
)
def format_float(value: float) -> str:
return f"{value:.6f}"
def print_results_table(results: list[EvalResult]) -> None:
headers = (
"backbone",
"protocol",
"mode",
"videos",
"pairs",
"samples",
"ppl_mean_raw",
"ppl_mean_trimmed",
"ppl_median",
"ppl_std",
)
rows = [
(
result.backbone,
result.protocol,
result.interp_mode_used,
str(result.videos_evaluated),
str(result.pairs_evaluated),
str(result.samples_evaluated),
format_float(result.ppl_mean_raw),
format_float(result.ppl_mean_trimmed),
format_float(result.ppl_median),
format_float(result.ppl_std),
)
for result in results
]
widths = [len(header) for header in headers]
for row in rows:
for idx, value in enumerate(row):
widths[idx] = max(widths[idx], len(value))
def join_row(values: Iterable[str]) -> str:
return " ".join(value.ljust(widths[idx]) for idx, value in enumerate(values))
print("=" * 88)
print(join_row(headers))
print(join_row("-" * width for width in widths))
for row in rows:
print(join_row(row))
def per_gap_curve_to_payload(curve: PerGapCurve) -> dict[str, object]:
return {
"backbone": curve.backbone,
"protocol": curve.protocol,
"interp_mode_used": curve.interp_mode_used,
"video_path": curve.video_path,
"gap_indices": curve.gap_indices.tolist(),
"gap_sample_counts": curve.gap_sample_counts.tolist(),
"gap_ppl_mean": curve.gap_ppl_mean.tolist(),
"gap_ppl_std": curve.gap_ppl_std.tolist(),
"gap_ppl_min": curve.gap_ppl_min.tolist(),
"gap_ppl_max": curve.gap_ppl_max.tolist(),
}
def maybe_save_results(args: argparse.Namespace, results: list[EvalResult], curves: list[PerGapCurve]) -> None:
if args.save_json is None:
return
args.save_json.parent.mkdir(parents=True, exist_ok=True)
payload = {
"results": [asdict(result) for result in results],
"config": {
"all_backbones": bool(args.all_backbones),
"comparison_protocol": args.comparison_protocol,
"requested_interp_mode": args.interp_mode,
"eps": float(args.eps),
"samples_per_gap": int(args.samples_per_gap),
"trim_percentile_low": float(args.trim_percentile_low),
"trim_percentile_high": float(args.trim_percentile_high),
"seed": int(args.seed),
"split_name": None if args.video_path is not None else args.split_name,
"video_path": str(args.video_path) if args.video_path is not None else None,
"max_videos": args.max_videos,
"device": str(get_device(args.device)),
"plot_per_gap_ppl": bool(args.plot_per_gap_ppl),
},
}
if curves:
payload["per_gap_curves"] = [per_gap_curve_to_payload(curve) for curve in curves]
args.save_json.write_text(json.dumps(payload, indent=2))
print(f"Saved JSON results to {args.save_json}")
def main() -> None:
args = parse_args()
validate_args(args)
expand_runtime_paths(args)
device = get_device(args.device)
jobs = build_eval_jobs(args)
print(f"Using device: {device}")
print("Loading LPIPS model (first run may download VGG/LPIPS weights)...")
lpips_model = LPIPS().eval().to(device)
results: list[EvalResult] = []
per_gap_curves: list[PerGapCurve] = []
for job in jobs:
job_evaluation = evaluate_job(base_args=args, job=job, device=device, lpips_model=lpips_model)
result = job_evaluation.result
print(
f"Finished {result.backbone} / {result.protocol}: "
f"raw={result.ppl_mean_raw:.6f}, trimmed={result.ppl_mean_trimmed:.6f}, "
f"samples={result.samples_evaluated}"
)
results.append(result)
per_gap_curves.extend(job_evaluation.per_gap_curves)
print_results_table(results)
if args.plot_per_gap_ppl:
plot_output = resolve_plot_output_path(args, args.video_path)
plot_per_gap_curves(per_gap_curves, plot_output, args.video_path, args)
maybe_save_results(args, results, per_gap_curves)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
31.4 kB
·
Xet hash:
2d5d337ae8128349a744fc3d374e40133a1cde2a07bcf41dc23115242fce3221

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.