138 lines
5.7 KiB
Python
138 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Remove dynamic actors, fact glyphs and command-state art from RSK-002."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_SOURCE = ROOT / "docs/design-authority/pages/RSK-002-risk-commitment/approved/v001/master.png"
|
|
DEFAULT_OUTPUT = ROOT / "runtime/assets/entry/rsk_002_risk_commitment/risk-commitment-static-master.png"
|
|
|
|
|
|
def rect(mask: np.ndarray, x0: float, y0: float, x1: float, y1: float) -> None:
|
|
height, width = mask.shape
|
|
cv2.rectangle(
|
|
mask,
|
|
(round(x0 * width), round(y0 * height)),
|
|
(round(x1 * width), round(y1 * height)),
|
|
255,
|
|
thickness=-1,
|
|
)
|
|
|
|
|
|
def grabcut_foreground(image: np.ndarray, mask: np.ndarray, bounds: tuple[float, float, float, float]) -> None:
|
|
"""Add a segmented actor silhouette without erasing the surrounding world."""
|
|
height, width = mask.shape
|
|
x0, y0, x1, y1 = [round(value * width) for value in (bounds[0], bounds[1], bounds[2], bounds[3])]
|
|
top, bottom = round(bounds[1] * height), round(bounds[3] * height)
|
|
left, right = round(bounds[0] * width), round(bounds[2] * 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)
|
|
margin_x = max(2, round(crop.shape[1] * 0.12))
|
|
margin_y = max(2, round(crop.shape[0] * 0.08))
|
|
gc[:margin_y, :] = cv2.GC_BGD
|
|
gc[-margin_y:, :] = cv2.GC_BGD
|
|
gc[:, :margin_x] = cv2.GC_BGD
|
|
gc[:, -margin_x:] = cv2.GC_BGD
|
|
# The central body/face corridor is a conservative foreground seed; the
|
|
# surrounding margin lets GrabCut preserve sky and stone between units.
|
|
seed_x0, seed_x1 = round(crop.shape[1] * 0.28), round(crop.shape[1] * 0.72)
|
|
seed_y0, seed_y1 = round(crop.shape[0] * 0.14), round(crop.shape[0] * 0.88)
|
|
gc[seed_y0:seed_y1, seed_x0:seed_x1] = 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, 6, cv2.GC_INIT_WITH_MASK)
|
|
foreground = (gc == cv2.GC_FGD) | (gc == cv2.GC_PR_FGD)
|
|
target = mask[top:bottom, left:right]
|
|
target[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)
|
|
|
|
# Actors, attached fact markers and identity bars are dynamic PTY-002
|
|
# entities. Segment each silhouette independently so the painted overlook
|
|
# and Party arc remain intact between units.
|
|
for bounds in [
|
|
(0.490, 0.255, 0.590, 0.675),
|
|
(0.585, 0.255, 0.705, 0.675),
|
|
(0.690, 0.225, 0.815, 0.685),
|
|
(0.795, 0.225, 0.940, 0.705),
|
|
]:
|
|
grabcut_foreground(image, mask, bounds)
|
|
# Small attached shield/coverage markers and identity bars sit outside
|
|
# the GrabCut silhouettes. Keep these masks narrow so the stone/sky between
|
|
# actors is not erased.
|
|
for center in [0.535, 0.635, 0.735, 0.845]:
|
|
rect(mask, center - 0.026, 0.350, center + 0.026, 0.405)
|
|
rect(mask, center - 0.026, 0.430, center + 0.028, 0.535)
|
|
rect(mask, center - 0.030, 0.575, center + 0.030, 0.625)
|
|
|
|
# The six consequence glyphs and value wells are runtime fact slots. Keep
|
|
# the outer parchment/ledger shell and separators, but remove source marks
|
|
# from each inner cell.
|
|
for left in [0.095, 0.235, 0.375, 0.515, 0.655, 0.795]:
|
|
rect(mask, left, 0.685, left + 0.055, 0.775)
|
|
|
|
# Command symbols are stateful (Cancel, acknowledgement, Begin Battle).
|
|
# Preserve the rail shell and neutralize the generated symbols; Engine
|
|
# draws the accepted semantic icons and disabled styling later.
|
|
for bounds in [
|
|
(0.070, 0.855, 0.145, 0.955),
|
|
(0.465, 0.845, 0.545, 0.955),
|
|
(0.820, 0.850, 0.915, 0.955),
|
|
]:
|
|
rect(mask, *bounds)
|
|
|
|
# Settings is a utility action, not a static state marker.
|
|
rect(mask, 0.925, 0.015, 0.990, 0.105)
|
|
|
|
# A moderate radius reconstructs stone/sky/paper texture without preserving
|
|
# glyph edges. The mask is intentionally retained for audit and later
|
|
# dynamic-layer checks.
|
|
cleaned = cv2.inpaint(image, mask, 3, cv2.INPAINT_NS)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
if not cv2.imwrite(str(output), cleaned):
|
|
raise SystemExit(f"could not write {output}")
|
|
|
|
mask_path = output.with_name("dynamic-regions.png")
|
|
if not cv2.imwrite(str(mask_path), mask):
|
|
raise SystemExit(f"could not write {mask_path}")
|
|
report = output.with_name("static-master-report.md")
|
|
report.write_text(
|
|
"# RSK-002 Clean Static Master\n\n"
|
|
f"- Source: `{source}`\n"
|
|
f"- Output: `{output}`\n"
|
|
f"- Dynamic-region mask: `{mask_path}`\n"
|
|
"- Removed: four generated actors, attached fact/identity markers, six consequence glyph/value wells, command symbols and Settings symbol.\n"
|
|
"- Preserved: painted overlook, flags, selected-risk relic shell, Party arc, consequence-band shell, command-rail shell and parchment separators.\n"
|
|
"- Runtime policy: Recruit identity, labels, values, semantic icons, selection, confirmation and hit areas 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()
|