mirror of
https://github.com/harry7557558/spirula-studio.git
synced 2026-10-02 02:44:54 +08:00
177 lines
6.5 KiB
Python
177 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Multi-scene benchmark driver.
|
|
|
|
Each scene runs as its own `spirula train` process, so the engine (world splats,
|
|
bilagrid/PPISP/background, optimizer moments, colour-space matrices, device
|
|
pool) starts clean. Several scenes in one process leak state between them and
|
|
silently degrade the later ones.
|
|
|
|
Each run writes its own metrics.json -- l1/psnr/ssim, their colour-corrected
|
|
`cc_` variants, `training_time` and `engine_vram`, all native. This script
|
|
reads those back and prints a table. With --lpips it also runs
|
|
eval_lpips.py per scene, which needs torch + torchmetrics; without it, this
|
|
script needs nothing beyond the standard library.
|
|
|
|
python3 reference/python/benchmark.py 360_v2 --data /path/to/360_v2
|
|
python3 reference/python/benchmark.py zipnerf --data /path/to/zipnerf \\
|
|
--preset academic-baseline --lpips
|
|
|
|
Extra arguments after `--` are appended to every `spirula train` command:
|
|
|
|
python3 reference/python/benchmark.py 360_v2 --data ... -- --cap-max 3000000
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# (scene, downscale factor). The rounding mode differs per scene because the
|
|
# published images_N folders were produced with different rounding.
|
|
BENCHMARKS: dict[str, dict] = {
|
|
"360_v2": {
|
|
"help": "7 permissively released scenes from Mip-NeRF 360",
|
|
"url": "http://storage.googleapis.com/gresearch/refraw360/360_v2.zip",
|
|
"scenes": [("bicycle", 4), ("garden", 4), ("stump", 4), ("bonsai", 2),
|
|
("counter", 2), ("kitchen", 2), ("room", 2)],
|
|
# garden's images_4 was rounded, the rest ceiled.
|
|
"rounding": lambda scene: "round" if scene == "garden" else "ceil",
|
|
"extra": lambda scene: [],
|
|
},
|
|
"zipnerf": {
|
|
"help": "4 fisheye scenes from Zip-NeRF",
|
|
"url": "https://smerf-3d.github.io/#data",
|
|
"scenes": [("alameda", 4), ("berlin", 4), ("london", 4), ("nyc", 4)],
|
|
"rounding": lambda scene: "ceil",
|
|
"extra": lambda scene: [],
|
|
},
|
|
}
|
|
|
|
|
|
def find_trainer() -> str:
|
|
exe = shutil.which("spirula")
|
|
if exe:
|
|
return exe
|
|
local = Path(__file__).resolve().parents[2] / "build" / "spirula"
|
|
if local.is_file():
|
|
return str(local)
|
|
raise SystemExit("`spirula` not found on PATH or in ./build -- build it first")
|
|
|
|
|
|
def run_scene(trainer: str, preset: str, data_dir: Path, out_prefix: Path,
|
|
out_name: str, downscale: int, rounding: str,
|
|
extra: list[str], want_lpips: bool) -> dict | None:
|
|
metrics_path = out_prefix / out_name / "metrics.json"
|
|
# A stale file from a previous run with the same name would masquerade as
|
|
# this run's result.
|
|
metrics_path.unlink(missing_ok=True)
|
|
|
|
cmd = [
|
|
trainer, "train", preset,
|
|
"--data", str(data_dir),
|
|
"--data-format", "colmap",
|
|
"--colmap-recon-dir", "sparse/0",
|
|
"--rescale-camera-to-fit", str(downscale),
|
|
"--image-dir", f"images_{downscale}",
|
|
"--downscale-rounding-mode", rounding,
|
|
"--eval-mode", "interval",
|
|
"--save-eval-images", "1",
|
|
"--load-depths", "0",
|
|
"--load-normals", "0",
|
|
"--steps-per-save", "0",
|
|
"--disable-viewer", "1",
|
|
"--keep-viewer-alive", "0",
|
|
"--output-dir-prefix", str(out_prefix),
|
|
"--output-dir-name", out_name,
|
|
] + extra
|
|
|
|
print(">>>", " ".join(cmd), flush=True)
|
|
if subprocess.call(cmd) != 0:
|
|
print(f"!!! {out_name} exited non-zero", flush=True)
|
|
return None
|
|
if not metrics_path.exists():
|
|
print(f"!!! {out_name} wrote no metrics.json", flush=True)
|
|
return None
|
|
|
|
if want_lpips:
|
|
lpips = Path(__file__).with_name("eval_lpips.py")
|
|
rc = subprocess.call([sys.executable, str(lpips),
|
|
str(out_prefix / out_name), "--write"])
|
|
if rc != 0:
|
|
print(f"!!! LPIPS failed for {out_name}; metrics are still valid "
|
|
f"without it", flush=True)
|
|
|
|
return json.loads(metrics_path.read_text())
|
|
|
|
|
|
def print_table(results: list[tuple[str, dict | None]]) -> None:
|
|
ok = [(name, m) for name, m in results if m is not None]
|
|
failed = [name for name, m in results if m is None]
|
|
if not ok:
|
|
print("\nEvery scene failed.")
|
|
return
|
|
|
|
# Scalar keys only; avg_* is what a scene summarizes to.
|
|
keys: list[str] = []
|
|
for _, m in ok:
|
|
for k, v in m.items():
|
|
if isinstance(v, list) or k in keys:
|
|
continue
|
|
keys.append(k)
|
|
|
|
w = max(len(k) for k in keys) + 2
|
|
print("\n" + "metric".ljust(w) + "".join(n[:14].rjust(16) for n, _ in ok))
|
|
for k in keys:
|
|
row = k[len("avg_"):] if k.startswith("avg_") else k
|
|
line = row.ljust(w)
|
|
for _, m in ok:
|
|
v = m.get(k)
|
|
line += ("" if v is None else f"{v:.5g}" if isinstance(v, float)
|
|
else str(v)).rjust(16)
|
|
print(line)
|
|
if failed:
|
|
print("\nfailed:", ", ".join(failed))
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(
|
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("benchmark", choices=sorted(BENCHMARKS))
|
|
ap.add_argument("--data", type=Path, required=True,
|
|
help="folder holding the benchmark's scenes")
|
|
ap.add_argument("--preset", default="academic-baseline",
|
|
help="spirula train preset (default: academic-baseline)")
|
|
ap.add_argument("--output-dir-prefix", type=Path, default=Path("outputs"))
|
|
ap.add_argument("--lpips", action="store_true",
|
|
help="also run eval_lpips.py per scene (needs torchmetrics)")
|
|
ap.add_argument("rest", nargs="*",
|
|
help="after `--`, extra flags for every spirula train call")
|
|
args = ap.parse_args()
|
|
|
|
spec = BENCHMARKS[args.benchmark]
|
|
if not args.data.is_dir():
|
|
raise SystemExit(f"dataset not found at {args.data}\n see {spec['url']}")
|
|
|
|
trainer = find_trainer()
|
|
tag = (f"{args.benchmark}-{args.preset}-"
|
|
f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}")
|
|
|
|
results = []
|
|
for scene, downscale in spec["scenes"]:
|
|
print(f"\n=== {scene} ===", flush=True)
|
|
results.append((scene, run_scene(
|
|
trainer, args.preset, args.data / scene, args.output_dir_prefix,
|
|
f"benchmark-{tag}-{scene}", downscale, spec["rounding"](scene),
|
|
spec["extra"](scene) + list(args.rest), args.lpips)))
|
|
|
|
print_table(results)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|