File size: 7,365 Bytes
59630ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Validate complete benchmark coverage and pool saved frame metrics/FID features."""

import argparse
import json
from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).resolve().parent / "metric_vendor"))

import numpy as np
import torch
import torchmetrics
from torchmetrics.image.fid import _compute_fid

from protocol import case_key, case_seed, select_cases
from score_case import metric_ranges


def feature_statistics(case_dirs, name, start, end, device):
    count, total, product = 0, None, None
    for directory in case_dirs:
        feature = torch.from_numpy(np.load(directory / f"{name}_features.npy")[start:end]).to(device, torch.float64)
        if total is None:
            total = torch.zeros(feature.shape[1], dtype=torch.float64, device=device)
            product = torch.zeros((feature.shape[1], feature.shape[1]), dtype=torch.float64, device=device)
        total += feature.sum(0)
        product += feature.T @ feature
        count += len(feature)
    mean = total / count
    covariance = (product - total[:, None] @ total[None, :] / count) / (count - 1)
    return mean, covariance, count


def pooled_fid(case_dirs, start, end, device):
    real_mean, real_cov, count = feature_statistics(case_dirs, "real", start, end, device)
    fake_mean, fake_cov, _ = feature_statistics(case_dirs, "fake", start, end, device)
    # This is the same legacy computation called by TorchMetrics0.11.4.compute,
    # including unbiased covariance and final float32 output conversion.
    value = _compute_fid(real_mean, real_cov, fake_mean, fake_cov).float()
    return float(value.cpu()), count


def validate_coverage(output, rows, dataset, seed, frames):
    directories = [Path(output) / "cases" / case_key(row) for row in rows]
    actual = {path.name for path in (Path(output) / "cases").iterdir() if path.is_dir()}
    expected = {directory.name for directory in directories}
    errors, summaries = [], []
    if len(expected) != len(rows):
        errors.append("Duplicate case identities in the selected manifest")
    if actual != expected:
        errors.append(f"Case coverage mismatch: missing={sorted(expected - actual)}, extra={sorted(actual - expected)}")
    if torchmetrics.__version__ != "0.11.4":
        errors.append(f"Expected torchmetrics0.11.4, found {torchmetrics.__version__}")
    for row, directory in zip(rows, directories):
        required = [directory / name for name in ("metrics.json", "frame_metrics.npz", "real_features.npy", "fake_features.npy", "prediction.mp4", "reference.mp4")]
        missing = [str(path) for path in required if not path.is_file()]
        if missing:
            errors.append(f"Incomplete case {directory.name}: {missing}")
            continue
        summary = json.loads((directory / "metrics.json").read_text())
        summaries.append(summary)
        wanted = (dict(row), dataset, case_seed(row, seed), frames)
        found = (summary["case"], summary["dataset"], summary["sample_seed"], summary["frames"])
        if found != wanted:
            errors.append(f"Case identity/dataset/seed/horizon mismatch: {directory.name}")
        with np.load(directory / "frame_metrics.npz") as metrics:
            names = {"mse", "psnr", "lpips", "ssim"} if dataset == "re10k" else {"mse", "psnr", "lpips"}
            if set(metrics.files) != names or any(metrics[name].shape != (frames,) or not np.isfinite(metrics[name]).all() for name in metrics.files):
                errors.append(f"Invalid per-frame metric population: {directory.name}")
        for name in ("real", "fake"):
            feature = np.load(directory / f"{name}_features.npy", mmap_mode="r")
            if feature.shape != (frames, 2048) or not np.isfinite(feature).all():
                errors.append(f"Invalid {name} FID feature population: {directory.name}")
    contracts = [{key: summary[key] for key in ("metric_resolution", "torchmetrics", "lpips", "psnr_epsilon", "fid", "video")} for summary in summaries]
    if contracts and any(contract != contracts[0] for contract in contracts):
        errors.append("Metric contracts differ between cases")
    if errors:
        raise ValueError("\n".join(errors))
    return directories, contracts[0]


def aggregate(output, manifest, dataset, seed=0, scope=None, fid_manifest=None,
              expected_frames=None, limit=None, device="cuda"):
    frames = expected_frames or (300 if dataset == "re10k" else 500)
    rows = select_cases(manifest, scope, limit=limit)
    directories, contract = validate_coverage(output, rows, dataset, seed, frames)
    ranges = metric_ranges(dataset, frames)
    report = {"dataset": dataset, "seed": seed, "manifest": str(manifest), "scope": scope,
              "cases": len(rows), "generated_frames_per_case": frames,
              "complete_full_horizon": frames == (300 if dataset == "re10k" else 500),
              "limited_selection": limit is not None, "metric_contract": contract,
              "frame_metrics": {}, "fid": {}}
    all_values = {}
    for directory in directories:
        with np.load(directory / "frame_metrics.npz") as values:
            for name in values.files:
                all_values.setdefault(name, []).append(values[name])
    all_values = {name: np.stack(parts).astype(np.float64) for name, parts in all_values.items()}
    for name, start, end in ranges:
        report["frame_metrics"][name] = {metric: float(value[:, start:end].mean()) for metric, value in all_values.items()}
    populations = {"all_cases": directories}
    if fid_manifest is not None:
        subset = select_cases(fid_manifest)
        requested = {case_key(row) for row in subset}
        available = {directory.name for directory in directories}
        if not requested <= available:
            raise ValueError(f"FID manifest cases absent from evaluated population: {sorted(requested - available)}")
        populations["official_fid50"] = [Path(output) / "cases" / case_key(row) for row in subset]
    for population, selected in populations.items():
        report["fid"][population] = {}
        for name, start, end in ranges:
            value, count = pooled_fid(selected, start, end, torch.device(device))
            report["fid"][population][name] = {"fid": value, "frames": count, "cases": len(selected)}
    Path(output, "aggregate.json").write_text(json.dumps(report, indent=2) + "\n")
    return report


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--manifest", type=Path, required=True)
    parser.add_argument("--dataset", choices=["minecraft", "re10k"], required=True)
    parser.add_argument("--scope")
    parser.add_argument("--seed", type=int, default=0)
    parser.add_argument("--fid-manifest", type=Path)
    parser.add_argument("--expected-frames", type=int)
    parser.add_argument("--limit", type=int)
    parser.add_argument("--device", default="cuda")
    args = parser.parse_args()
    result = aggregate(args.output, args.manifest, args.dataset, args.seed, args.scope,
                       args.fid_manifest, args.expected_frames, args.limit, args.device)
    print(json.dumps({"dataset": result["dataset"], "cases": result["cases"],
                      "overall": result["frame_metrics"]["overall"]}), flush=True)


if __name__ == "__main__":
    main()