112 lines
4.2 KiB
Python
Executable file
112 lines
4.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Validate / normalize IdleSpectator dossier crops for agent visual checks.
|
|
|
|
Prefer Unity-written ``*-dossier.png`` files (exact UI rect via ReadPixels).
|
|
For player pastes or legacy full frames, produce a readable 2x nearest upscale.
|
|
|
|
Usage:
|
|
./scripts/crop-dossier-hud.py --strict IdleSpectator/.harness/hud-chronicle_smoke-dossier.png
|
|
./scripts/crop-dossier-hud.py IdleSpectator/.harness/hud-chronicle_smoke.png # warns: use -dossier.png
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
|
|
def analyze_nametag(im: Image.Image) -> tuple[bool, int]:
|
|
"""Heuristic: top ~28% of a dossier crop should contain bright nametag pixels."""
|
|
arr = np.array(im.convert("RGB"))
|
|
h, w, _ = arr.shape
|
|
band = arr[: max(1, h // 3), :]
|
|
white = (band[:, :, 0] > 165) & (band[:, :, 1] > 150) & (band[:, :, 2] > 110)
|
|
green = (band[:, :, 1] > 185) & (band[:, :, 0] < 130)
|
|
orange = (band[:, :, 0] > 190) & (band[:, :, 1] > 90) & (band[:, :, 1] < 180) & (band[:, :, 2] < 100)
|
|
bright = int(white.sum() + green.sum() + orange.sum())
|
|
# Tiny crops still need a handful of lit pixels; larger crops need more.
|
|
need = max(25, (w * h) // 400)
|
|
return bright >= need, bright
|
|
|
|
|
|
def normalize(path: Path, scale: int = 2) -> dict:
|
|
im = Image.open(path).convert("RGB")
|
|
w, h = im.size
|
|
is_fullframe = w >= 1600 or h >= 1000
|
|
stem = path.stem
|
|
if stem.endswith("-dossier"):
|
|
out = path
|
|
method = "unity_or_existing_crop"
|
|
elif is_fullframe:
|
|
# Do not pretend pixel heuristics can find the card reliably on 2880p.
|
|
out = path.with_name(path.stem + "-dossier.png")
|
|
# Top-left fallback only so agents have *something*; mark nametag unknown.
|
|
panel = im.crop((0, int(h * 0.08), min(w, int(w * 0.22)), int(h * 0.08) + min(280, h // 4)))
|
|
panel.resize((panel.width * scale, panel.height * scale), Image.NEAREST).save(out)
|
|
method = "fullframe_fallback_topleft"
|
|
else:
|
|
out = path.with_name(path.stem + "-dossier.png")
|
|
im.resize((w * scale, h * scale), Image.NEAREST).save(out)
|
|
method = "player_paste_upscale"
|
|
|
|
check_im = Image.open(out)
|
|
ok, bright = analyze_nametag(check_im)
|
|
if method == "fullframe_fallback_topleft":
|
|
ok = False # never trust full-frame fallback for PASS
|
|
|
|
report = {
|
|
"source": str(path),
|
|
"dossier": str(out),
|
|
"method": method,
|
|
"size": [check_im.width, check_im.height],
|
|
"nametag_bright_pixels": bright,
|
|
"nametag_ok": ok,
|
|
"is_fullframe_source": is_fullframe,
|
|
}
|
|
report_path = Path(str(out).replace(".png", ".json"))
|
|
if report_path.suffix != ".json":
|
|
report_path = out.with_suffix(".json")
|
|
# Prefer sibling *-dossier.json next to crop.
|
|
if out.name.endswith("-dossier.png"):
|
|
report_path = out.with_name(out.stem + ".json")
|
|
report_path.write_text(json.dumps(report, indent=2) + "\n")
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("images", nargs="+", type=Path)
|
|
ap.add_argument("--strict", action="store_true", help="Exit 1 if nametag band looks empty")
|
|
args = ap.parse_args()
|
|
|
|
worst = 0
|
|
for path in args.images:
|
|
if not path.is_file():
|
|
print(f"MISSING {path}", file=sys.stderr)
|
|
worst = 1
|
|
continue
|
|
report = normalize(path)
|
|
status = "OK" if report["nametag_ok"] else "WARN_NO_NAMETAG"
|
|
if report["method"] == "fullframe_fallback_topleft":
|
|
status = "WARN_FULLFRAME"
|
|
print(
|
|
f"{status} {report['dossier']} - prefer Unity '*-dossier.png' from harness screenshot action",
|
|
file=sys.stderr,
|
|
)
|
|
worst = max(worst, 1 if args.strict else 0)
|
|
else:
|
|
print(
|
|
f"{status} {report['dossier']} method={report['method']} "
|
|
f"bright={report['nametag_bright_pixels']} size={report['size']}"
|
|
)
|
|
if args.strict and not report["nametag_ok"]:
|
|
worst = 1
|
|
return worst
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|