118 lines
5.0 KiB
Python
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()
|