Files
aetherbound-guild/tools/prepare_shp001_clean_master.py
T

118 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Remove generated Market actors, marks and values from the approved master."""
from __future__ import annotations
import argparse
from pathlib import Path
import cv2
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
PAGE = ROOT / "docs/design-authority/pages/SHP-001-market"
DEFAULT_SOURCE = PAGE / "approved/v002/master.png"
DEFAULT_OUTPUT = ROOT / "runtime/assets/entry/shp_001_market/market-static-master.png"
def rect(mask: np.ndarray, bounds: tuple[float, float, float, float]) -> None:
height, width = mask.shape
x0, y0, x1, y1 = bounds
cv2.rectangle(mask, (round(x0 * width), round(y0 * height)), (round(x1 * width), round(y1 * height)), 255, -1)
def segment_actor(image: np.ndarray, mask: np.ndarray, bounds: tuple[float, float, float, float]) -> None:
height, width = mask.shape
x0, y0, x1, y1 = [round(value * width) for value in bounds]
top, bottom = round(y0 * height), round(y1 * height)
left, right = round(x0 * width), round(x1 * width)
crop = image[top:bottom, left:right]
if crop.size == 0:
return
gc = np.full(crop.shape[:2], cv2.GC_PR_BGD, dtype=np.uint8)
mx = max(2, round(crop.shape[1] * 0.12))
my = max(2, round(crop.shape[0] * 0.08))
gc[:my, :] = cv2.GC_BGD
gc[-my:, :] = cv2.GC_BGD
gc[:, :mx] = cv2.GC_BGD
gc[:, -mx:] = cv2.GC_BGD
sx0, sx1 = round(crop.shape[1] * 0.28), round(crop.shape[1] * 0.72)
sy0, sy1 = round(crop.shape[0] * 0.12), round(crop.shape[0] * 0.92)
gc[sy0:sy1, sx0:sx1] = cv2.GC_PR_FGD
bgd = np.zeros((1, 65), dtype=np.float64)
fgd = np.zeros((1, 65), dtype=np.float64)
cv2.grabCut(crop, gc, None, bgd, fgd, 5, cv2.GC_INIT_WITH_MASK)
foreground = (gc == cv2.GC_FGD) | (gc == cv2.GC_PR_FGD)
mask[top:bottom, left:right][foreground] = 255
def build(source: Path, output: Path) -> None:
image = cv2.imread(str(source), cv2.IMREAD_COLOR)
if image is None:
raise SystemExit(f"could not read {source}")
if image.shape[1] != 2048 or image.shape[0] != 1152:
raise SystemExit(f"expected 2048x1152, got {image.shape[1]}x{image.shape[0]}")
mask = np.zeros(image.shape[:2], dtype=np.uint8)
# Company rail: remove the two generated figures and their baked markers,
# while preserving the authored rail shell and four capacity sockets.
for bounds in [(0.055, 0.045, 0.17, 0.175), (0.18, 0.045, 0.31, 0.175)]:
segment_actor(image, mask, bounds)
rect(mask, (0.045, 0.115, 0.34, 0.18))
# Three offer figures are dynamic Recruit entities. The counter/plates stay
# static; actor silhouettes, emblems and generated marks become overlays.
for bounds in [(0.18, 0.30, 0.30, 0.70), (0.35, 0.28, 0.49, 0.70), (0.56, 0.30, 0.69, 0.70)]:
segment_actor(image, mask, bounds)
for bounds in [
(0.18, 0.60, 0.30, 0.78), (0.35, 0.60, 0.49, 0.78), (0.56, 0.60, 0.69, 0.78),
(0.21, 0.40, 0.28, 0.54), (0.39, 0.40, 0.46, 0.54), (0.59, 0.40, 0.66, 0.54),
]:
rect(mask, bounds)
# Edge ledger is static framing, but all resource symbols, mode state,
# refresh/lock values and labels are engine-owned.
for bounds in [
(0.81, 0.08, 0.94, 0.18), (0.81, 0.20, 0.98, 0.31),
(0.81, 0.32, 0.98, 0.43), (0.81, 0.44, 0.98, 0.55),
]:
rect(mask, bounds)
# Bottom item objects and socket occupancy are sourced runtime inventory;
# clear the generated object drawings while retaining tray geometry.
for bounds in [(0.03, 0.875, 0.10, 0.985), (0.105, 0.875, 0.18, 0.985), (0.185, 0.875, 0.26, 0.985), (0.265, 0.875, 0.34, 0.985)]:
rect(mask, bounds)
cleaned = cv2.inpaint(image, mask, 4, cv2.INPAINT_NS)
output.parent.mkdir(parents=True, exist_ok=True)
if not cv2.imwrite(str(output), cleaned, [cv2.IMWRITE_PNG_COMPRESSION, 7]):
raise SystemExit(f"could not write {output}")
mask_path = output.with_name("dynamic-regions.png")
if not cv2.imwrite(str(mask_path), mask, [cv2.IMWRITE_PNG_COMPRESSION, 7]):
raise SystemExit(f"could not write {mask_path}")
report = output.with_name("static-master-report.md")
report.write_text(
"# SHP-001 Clean Static Master\n\n"
f"- Source: `{source}`\n"
f"- Output: `{output}`\n"
f"- Dynamic-region mask: `{mask_path}`\n"
"- Removed: generated Company/offer actors, offer marks, edge resource/mode symbols and generated inventory objects.\n"
"- Preserved: painted requisition world, counter, Company shell, ledger shell, review plate and equipment tray geometry.\n"
"- Runtime policy: Recruit identities, names, Profession/Traits, prices, Coin, availability, icons, selection and commands are engine-owned.\n",
encoding="utf-8",
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
args = parser.parse_args()
build(args.source, args.output)
if __name__ == "__main__":
main()