mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 22:27:01 +01:00
Adds the route-independent spine of the planned art-generation bake-off: a harness that decides whether generated art is well-formed, so "non-sloppy" becomes machine-checkable instead of a judgement call. Findings that shaped the design, all measured rather than assumed: - Integer-scale invariance separates true pixel art (impurity 0.0000) from interpolated fakes (1.0000) exactly -- but passes trivially at scale 1, so it must never be trusted alone on native-resolution output. - Gate ORDER is load-bearing. A soft-alpha image has no fully-opaque pixels, so an opaque-only colour count returns 0 and silently passes. ALPHA_NOT_BINARY now runs first and COLOR_COUNT fails closed on an empty opaque set. - Counting must be over opaque RGB only: transparent pixels retain arbitrary RGB and inflated a naive RGBA count, rejecting correct art at 18/16. - Declaring a gate was not enough to run it. hand_painted.toml shipped declaring SILHOUETTE_LOW_CONTRAST with no implementation, so it parsed, ran nothing, and reported a pass. load_spec now fails closed on unimplemented gates, result-only codes, and gates missing their config. Thresholds are calibrated against 250 genuine 32x32 CC0 icons, not guessed: p50=7 p90=13 p99=26 opaque colours; cap 32 rejects 0.0% of real art while raw generator output measured 34403 colours. An initial cap of 16 wrongly rejected a real DCSS icon -- re-derived from the corpus rather than nudged. Verified: 57 tests, 100% branch coverage, zero suppressions, all hooks pass. 58/60 (96.7%) of real CC0 icons pass the full stack; both failures confirmed true positives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
"""Silhouette contrast: does the subject read against light AND dark?
|
|
|
|
One of only three gates valid for every style, because it makes no assumption
|
|
about palette, resolution or alpha. An asset that vanishes into one background
|
|
is unusable regardless of how it was produced.
|
|
|
|
Both backgrounds are required. Checking a single background is how art that
|
|
looks fine on a dark UI mock disappears entirely on a light one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import numpy as np
|
|
|
|
from python_pkg.artgate.codes import Code
|
|
|
|
if TYPE_CHECKING: # pragma: no cover
|
|
from numpy.typing import NDArray
|
|
|
|
# Rec. 709 luma weights, matching how perceived brightness is normally modelled.
|
|
_LUMA = (0.2126, 0.7152, 0.0722)
|
|
|
|
# Minimum mean luma separation (0-255) between subject and background.
|
|
# Calibrated against controls: a mid-grey blob on white scores ~1 and must
|
|
# fail; a normal opaque icon scores far above this against at least one side.
|
|
DEFAULT_MIN_DELTA = 12.0
|
|
|
|
|
|
def _luma(rgb: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
"""Convert RGB to Rec. 709 luma.
|
|
|
|
Args:
|
|
rgb: An ``(n, 3)`` float array.
|
|
|
|
Returns:
|
|
An ``(n,)`` array of luma values.
|
|
"""
|
|
return rgb[:, 0] * _LUMA[0] + rgb[:, 1] * _LUMA[1] + rgb[:, 2] * _LUMA[2]
|
|
|
|
|
|
def silhouette_delta(rgba: NDArray[np.uint8], background: int) -> float:
|
|
"""Measure mean luma separation between the subject and a background.
|
|
|
|
Alpha-weighted, so a mostly-transparent asset is judged on the pixels it
|
|
actually paints rather than on its empty margin.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
background: Grey level of the backdrop, 0-255.
|
|
|
|
Returns:
|
|
The absolute luma difference; 0.0 if nothing is painted.
|
|
"""
|
|
flat = rgba.reshape(-1, 4).astype(np.float64)
|
|
weight = flat[:, 3] / 255.0
|
|
total = float(weight.sum())
|
|
if total == 0.0:
|
|
return 0.0
|
|
subject = float((_luma(flat[:, :3]) * weight).sum() / total)
|
|
return abs(subject - float(background))
|
|
|
|
|
|
def check_silhouette(
|
|
rgba: NDArray[np.uint8],
|
|
min_delta: float = DEFAULT_MIN_DELTA,
|
|
) -> list[Code]:
|
|
"""Verify the subject separates from both a light and a dark background.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
min_delta: Minimum required luma separation from each background.
|
|
|
|
Returns:
|
|
``[SILHOUETTE_LOW_CONTRAST]`` if either background is too close, or
|
|
``[EMPTY_CANVAS]`` if the image paints nothing at all.
|
|
"""
|
|
flat = rgba.reshape(-1, 4)
|
|
if float(flat[:, 3].sum()) == 0.0:
|
|
return [Code.EMPTY_CANVAS]
|
|
on_dark = silhouette_delta(rgba, 0)
|
|
on_light = silhouette_delta(rgba, 255)
|
|
if min(on_dark, on_light) < min_delta:
|
|
return [Code.SILHOUETTE_LOW_CONTRAST]
|
|
return []
|