112 lines
4.5 KiB
Python
112 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Remove dynamic text/actors from the approved RSK-001 coordinate master."""
|
|
|
|
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-001-risk-board/approved/v001/master.png"
|
|
DEFAULT_OUTPUT = ROOT / "runtime/assets/entry/rsk_001_risk_board/risk-board-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 fill_slot(image: np.ndarray, x0: float, y0: float, x1: float, y1: float, sample: tuple[float, float, float]) -> None:
|
|
"""Cover a dynamic slot with restrained parchment texture, keeping no glyph edges."""
|
|
height, width = image.shape[:2]
|
|
left, top = round(x0 * width), round(y0 * height)
|
|
right, bottom = round(x1 * width), round(y1 * height)
|
|
colour = np.array(sample, dtype=np.float32)
|
|
# Keep the warm paper variation without preserving source lettering.
|
|
rng = np.random.default_rng(round((x0 + y0) * 100000))
|
|
noise = rng.normal(0.0, 1.4, (bottom - top, right - left, 1)).astype(np.float32)
|
|
slot = np.clip(colour.reshape(1, 1, 3) + noise, 0, 255).astype(np.uint8)
|
|
# Feather the edge so the slot reads as part of the paper rather than a
|
|
# hard rectangular sticker.
|
|
feather = 0
|
|
alpha = np.ones((bottom - top, right - left, 1), dtype=np.float32)
|
|
image[top:bottom, left:right] = (image[top:bottom, left:right] * (1.0 - alpha) + slot * alpha).astype(np.uint8)
|
|
|
|
|
|
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)
|
|
|
|
# Header: preserve the plaque/frame, replace all baked title, actors,
|
|
# position labels and version badge with a single clean engine-owned slot.
|
|
fill_slot(image, 0.255, 0.022, 0.985, 0.275, (224, 217, 201))
|
|
rect(mask, 0.255, 0.022, 0.985, 0.275)
|
|
# The left plaque is retained as ornament, but its two baked title lines
|
|
# are replaced so the localized Runtime title appears exactly once.
|
|
fill_slot(image, 0.070, 0.028, 0.270, 0.112, (52, 39, 25))
|
|
fill_slot(image, 0.070, 0.078, 0.270, 0.132, (52, 39, 25))
|
|
rect(mask, 0.070, 0.028, 0.270, 0.132)
|
|
|
|
# Option columns: preserve diamond letters, semantic icon circles and
|
|
# dividers; all labels and values are rendered by Runtime.
|
|
columns = [(0.085, 0.35), (0.365, 0.635), (0.65, 0.92)]
|
|
for left, right in columns:
|
|
fill_slot(image, left + 0.012, 0.285, right - 0.012, 0.805, (232, 224, 207))
|
|
rect(mask, left + 0.012, 0.285, right - 0.012, 0.805)
|
|
|
|
# Bottom rail: preserve the authored plates and icons while clearing text.
|
|
for x0, y0, x1, y1 in [
|
|
(0.015, 0.835, 0.28, 0.965),
|
|
(0.72, 0.835, 0.985, 0.965),
|
|
(0.32, 0.835, 0.68, 0.98),
|
|
]:
|
|
fill_slot(image, x0, y0, x1, y1, (57, 38, 22))
|
|
rect(mask, x0, y0, x1, y1)
|
|
|
|
cleaned = image
|
|
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")
|
|
cv2.imwrite(str(mask_path), mask)
|
|
report = output.with_name("static-master-report.md")
|
|
report.write_text(
|
|
"# RSK-001 Clean Static Master\n\n"
|
|
f"- Source: `{source}`\n"
|
|
f"- Output: `{output}`\n"
|
|
f"- Dynamic-region mask: `{mask_path}`\n"
|
|
"- Removed: generated title/party copy, baked header actors and positions, "
|
|
"option labels/values, and bottom command labels.\n"
|
|
"- Preserved: painted world, panel frames, semantic icon ornaments and rail geometry.\n"
|
|
"- Runtime policy: all values, labels, actors, selection 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()
|