#!/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())