138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Create the Stage 1 ROI validation report.
|
|
|
|
The first ROI target is the generated Cocos capture. If no Creator capture is
|
|
available yet, the report records the exact next solution instead of silently
|
|
passing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageChops
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE_PREVIEW = ROOT / "artifacts" / "canvas_preview" / "stage1_world_camera_source_preview.png"
|
|
CREATOR_CAPTURE = ROOT / "artifacts" / "creator_capture" / "stage1_creator_capture.png"
|
|
OUT_DIR = ROOT / "artifacts" / "visual_compare"
|
|
REPORT_PATH = OUT_DIR / "stage1_roi_report.json"
|
|
|
|
|
|
GLOBAL_ROIS = {
|
|
"world_static": (0, 0, 720, 1280),
|
|
"sky_background": (0, 0, 720, 430),
|
|
"land_area": (0, 560, 720, 1070),
|
|
"world_lower": (0, 430, 720, 850),
|
|
}
|
|
|
|
TOP_HUD_ROIS = {
|
|
"top_hud_all_masked_dynamic_text": (0, 70, 720, 180),
|
|
"top_left_hud_masked_dynamic_text": (20, 80, 375, 170),
|
|
"top_right_currency_masked_dynamic_text": (535, 75, 720, 175),
|
|
"close_button": (20, 85, 105, 175),
|
|
}
|
|
|
|
TOP_HUD_DYNAMIC_TEXT_MASKS = [
|
|
(210, 85, 335, 130),
|
|
(168, 110, 224, 170),
|
|
(210, 108, 360, 170),
|
|
(592, 74, 690, 130),
|
|
(592, 120, 690, 176),
|
|
]
|
|
|
|
|
|
def roi_similarity(
|
|
a: Image.Image,
|
|
b: Image.Image,
|
|
box: tuple[int, int, int, int],
|
|
masks: list[tuple[int, int, int, int]] | None = None,
|
|
) -> float:
|
|
crop_a = a.crop(box).convert("RGBA")
|
|
crop_b = b.crop(box).convert("RGBA")
|
|
if masks:
|
|
from PIL import ImageDraw
|
|
|
|
draw_a = ImageDraw.Draw(crop_a)
|
|
draw_b = ImageDraw.Draw(crop_b)
|
|
for mask in masks:
|
|
mask_box = (
|
|
max(mask[0], box[0]) - box[0],
|
|
max(mask[1], box[1]) - box[1],
|
|
min(mask[2], box[2]) - box[0],
|
|
min(mask[3], box[3]) - box[1],
|
|
)
|
|
if mask_box[0] < mask_box[2] and mask_box[1] < mask_box[3]:
|
|
draw_a.rectangle(mask_box, fill=(0, 0, 0, 0))
|
|
draw_b.rectangle(mask_box, fill=(0, 0, 0, 0))
|
|
diff = ImageChops.difference(crop_a, crop_b)
|
|
histogram = diff.convert("L").histogram()
|
|
total = sum(value * count for value, count in enumerate(histogram))
|
|
max_total = 255 * crop_a.width * crop_a.height
|
|
return 1.0 - (total / max_total if max_total else 0.0)
|
|
|
|
|
|
def main() -> int:
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
if not SOURCE_PREVIEW.exists():
|
|
report = {"status": "needs_solution", "reason": "missing_source_canvas_preview", "path": str(SOURCE_PREVIEW)}
|
|
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
return 1
|
|
if not CREATOR_CAPTURE.exists():
|
|
report = {
|
|
"status": "needs_solution",
|
|
"reason": "missing_creator_capture",
|
|
"sourcePreview": str(SOURCE_PREVIEW),
|
|
"expectedCreatorCapture": str(CREATOR_CAPTURE),
|
|
"note": "Creator screenshot must come from the generated Cocos Creator 3.8 project.",
|
|
}
|
|
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
return 1
|
|
|
|
with Image.open(SOURCE_PREVIEW) as source, Image.open(CREATOR_CAPTURE) as creator:
|
|
if source.size != creator.size:
|
|
report = {
|
|
"status": "needs_solution",
|
|
"reason": "size_mismatch",
|
|
"sourceSize": source.size,
|
|
"creatorSize": creator.size,
|
|
}
|
|
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
return 1
|
|
global_results = {name: roi_similarity(source, creator, box) for name, box in GLOBAL_ROIS.items()}
|
|
top_hud_results = {
|
|
name: roi_similarity(
|
|
source,
|
|
creator,
|
|
box,
|
|
TOP_HUD_DYNAMIC_TEXT_MASKS if "masked_dynamic_text" in name else None,
|
|
)
|
|
for name, box in TOP_HUD_ROIS.items()
|
|
}
|
|
global_status = "pass" if all(value >= 0.99 for value in global_results.values()) else "fail"
|
|
top_hud_status = "pass" if all(value >= 0.99 for value in top_hud_results.values()) else "fail"
|
|
report = {
|
|
"status": top_hud_status,
|
|
"sourcePreview": str(SOURCE_PREVIEW),
|
|
"creatorCapture": str(CREATOR_CAPTURE),
|
|
"threshold": 0.99,
|
|
"topHudStatus": top_hud_status,
|
|
"topHudDynamicTextMasks": TOP_HUD_DYNAMIC_TEXT_MASKS,
|
|
"topHudRoiSimilarity": top_hud_results,
|
|
"globalStaticStatus": global_status,
|
|
"globalStaticRoiSimilarity": global_results,
|
|
"globalStaticNote": "Whole-scene ROIs still include older world/decor differences and diagnostic PIL-vs-Creator rendering differences. Current pass uses top HUD module ROIs with dynamic text masked per project rules.",
|
|
}
|
|
REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
return 0 if report["status"] == "pass" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|