113 lines
4.9 KiB
Python
113 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify and document the PTY-001 clean static coordinate master."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_APPROVED = ROOT / "docs/design-authority/pages/PTY-001-party-line/approved/v001/master.png"
|
|
DEFAULT_REGIONS = ROOT / "docs/design-authority/pages/PTY-001-party-line/reconstruction/v001/static-master/dynamic-regions.json"
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def rect_crop(array: np.ndarray, bounds: list[int]) -> np.ndarray:
|
|
left, top, right, bottom = bounds
|
|
return array[top:bottom, left:right]
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--candidate", required=True, type=Path)
|
|
parser.add_argument("--approved", type=Path, default=DEFAULT_APPROVED)
|
|
parser.add_argument("--regions", type=Path, default=DEFAULT_REGIONS)
|
|
parser.add_argument("--report", required=True, type=Path)
|
|
parser.add_argument("--preview", type=Path)
|
|
parser.add_argument("--max-static-mae", type=float, default=24.0)
|
|
args = parser.parse_args()
|
|
|
|
authority = json.loads(args.regions.read_text(encoding="utf-8"))
|
|
expected_size = tuple(authority["design_size"])
|
|
approved_image = Image.open(args.approved).convert("RGB")
|
|
candidate_image = Image.open(args.candidate).convert("RGB")
|
|
if approved_image.size != expected_size:
|
|
raise SystemExit(f"approved size {approved_image.size} != {expected_size}")
|
|
if candidate_image.size != expected_size:
|
|
raise SystemExit(f"candidate size {candidate_image.size} != {expected_size}")
|
|
|
|
approved = np.asarray(approved_image, dtype=np.float32)
|
|
candidate = np.asarray(candidate_image, dtype=np.float32)
|
|
protected = []
|
|
for region in authority["static_protected_regions"]:
|
|
source = rect_crop(approved, region["bounds"])
|
|
edited = rect_crop(candidate, region["bounds"])
|
|
mae = float(np.abs(source - edited).mean())
|
|
protected.append({"id": region["id"], "bounds": region["bounds"], "mae": round(mae, 4)})
|
|
|
|
dynamic = []
|
|
for region in authority["regions"]:
|
|
source = rect_crop(approved, region["bounds"])
|
|
edited = rect_crop(candidate, region["bounds"])
|
|
change_mae = float(np.abs(source - edited).mean())
|
|
dynamic.append({"id": region["id"], "bounds": region["bounds"], "change_mae": round(change_mae, 4)})
|
|
|
|
max_static = max(item["mae"] for item in protected)
|
|
changed_dynamic = sum(item["change_mae"] >= 4.0 for item in dynamic)
|
|
ok = max_static <= args.max_static_mae and changed_dynamic == len(dynamic)
|
|
report = {
|
|
"schema_version": 1,
|
|
"page_id": "PTY-001",
|
|
"candidate": args.candidate.relative_to(ROOT).as_posix() if args.candidate.is_relative_to(ROOT) else str(args.candidate),
|
|
"approved": args.approved.relative_to(ROOT).as_posix() if args.approved.is_relative_to(ROOT) else str(args.approved),
|
|
"candidate_sha256": sha256(args.candidate),
|
|
"approved_sha256": sha256(args.approved),
|
|
"dimensions": list(candidate_image.size),
|
|
"mode": Image.open(args.candidate).mode,
|
|
"protected_static_regions": protected,
|
|
"dynamic_regions": dynamic,
|
|
"thresholds": {"max_static_mae": args.max_static_mae, "min_dynamic_change_mae": 4.0},
|
|
"summary": {
|
|
"ok": ok,
|
|
"max_static_mae": round(max_static, 4),
|
|
"changed_dynamic_regions": changed_dynamic,
|
|
"dynamic_region_count": len(dynamic),
|
|
},
|
|
"manual_review_required": [
|
|
"no characters, cord, position values, relation placeholders or selected-state effects remain",
|
|
"painted ground and walls are coherent behind removed content",
|
|
"HUD frames and fixed icons retain the approved line weight and geometry",
|
|
"all writable plates and parchment interiors contain no text or pseudo-writing",
|
|
],
|
|
}
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
|
|
if args.preview:
|
|
preview = candidate_image.copy()
|
|
draw = ImageDraw.Draw(preview, "RGBA")
|
|
for region in authority["regions"]:
|
|
draw.rectangle(region["bounds"], outline=(225, 79, 62, 230), width=4)
|
|
for region in authority["static_protected_regions"]:
|
|
draw.rectangle(region["bounds"], outline=(52, 172, 126, 220), width=3)
|
|
args.preview.parent.mkdir(parents=True, exist_ok=True)
|
|
preview.save(args.preview, optimize=True)
|
|
|
|
marker = "ABG_PTY001_CLEAN_MASTER_OK" if ok else "ABG_PTY001_CLEAN_MASTER_FAILED"
|
|
print(f"{marker} static_max_mae={max_static:.4f} dynamic={changed_dynamic}/{len(dynamic)} report={args.report}")
|
|
if not ok:
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|