mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:47: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
170 lines
5.2 KiB
Python
170 lines
5.2 KiB
Python
"""Pixel-level gates: alpha binarity, palette conformance, colour count, scale.
|
|
|
|
Ordering is load-bearing and was established empirically, not by taste:
|
|
``ALPHA_NOT_BINARY`` must be evaluated before ``TOO_MANY_COLORS``. A soft-alpha
|
|
image has *no* fully-opaque pixels, so an opaque-only colour count returns 0
|
|
and silently passes -- two individually correct gates composing into a hole.
|
|
``COLOR_COUNT`` therefore also fails closed on an empty opaque set.
|
|
"""
|
|
|
|
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
|
|
|
|
# Alpha value denoting a fully opaque pixel.
|
|
OPAQUE = 255
|
|
|
|
# Alpha value denoting a fully transparent pixel.
|
|
TRANSPARENT = 0
|
|
|
|
|
|
def opaque_rgb(rgba: NDArray[np.uint8]) -> NDArray[np.uint8]:
|
|
"""Select the RGB values of fully-opaque pixels.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
|
|
Returns:
|
|
An ``(n, 3)`` array of RGB triples; may be empty.
|
|
"""
|
|
flat = rgba.reshape(-1, 4)
|
|
return flat[flat[:, 3] == OPAQUE][:, :3]
|
|
|
|
|
|
def check_alpha_binary(rgba: NDArray[np.uint8]) -> list[Code]:
|
|
"""Verify alpha is exactly ``{0, 255}``.
|
|
|
|
Semi-transparent edges are the clearest signature of a generator that has
|
|
not been through a pixel-art finishing stage.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
|
|
Returns:
|
|
``[ALPHA_NOT_BINARY]`` if any other alpha value occurs, else ``[]``.
|
|
"""
|
|
values = set(np.unique(rgba[:, :, 3]).tolist())
|
|
if values <= {TRANSPARENT, OPAQUE}:
|
|
return []
|
|
return [Code.ALPHA_NOT_BINARY]
|
|
|
|
|
|
def check_color_count(rgba: NDArray[np.uint8], max_colors: int) -> list[Code]:
|
|
"""Verify the opaque palette size, failing closed when nothing is opaque.
|
|
|
|
Counting opaque RGB only is required because fully-transparent pixels
|
|
retain arbitrary RGB values, which inflate a naive RGBA count above the
|
|
cap and reject correct art.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
max_colors: Maximum permitted distinct opaque RGB values.
|
|
|
|
Returns:
|
|
The failing codes, empty if the image conforms.
|
|
"""
|
|
opaque = opaque_rgb(rgba)
|
|
if opaque.size == 0:
|
|
return [Code.NO_OPAQUE_PIXELS]
|
|
distinct = np.unique(opaque, axis=0).shape[0]
|
|
if distinct > max_colors:
|
|
return [Code.TOO_MANY_COLORS]
|
|
return []
|
|
|
|
|
|
def check_palette(
|
|
rgba: NDArray[np.uint8],
|
|
palette: frozenset[tuple[int, int, int]],
|
|
) -> list[Code]:
|
|
"""Verify every opaque pixel uses an allowed colour.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
palette: The allowed opaque RGB triples.
|
|
|
|
Returns:
|
|
``[OFF_PALETTE]`` if any opaque pixel is outside the palette.
|
|
"""
|
|
opaque = opaque_rgb(rgba)
|
|
if opaque.size == 0:
|
|
return [Code.NO_OPAQUE_PIXELS]
|
|
used = {tuple(int(c) for c in row) for row in np.unique(opaque, axis=0)}
|
|
if used <= palette:
|
|
return []
|
|
return [Code.OFF_PALETTE]
|
|
|
|
|
|
def block_impurity(rgba: NDArray[np.uint8], scale: int) -> float:
|
|
"""Measure the fraction of ``scale``-sized blocks that are not uniform.
|
|
|
|
True pixel art delivered at an integer upscale has perfectly uniform
|
|
blocks; anything resampled with interpolation does not.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
scale: The block size to test.
|
|
|
|
Returns:
|
|
0.0 for perfectly blocky input, up to 1.0 for fully smooth input.
|
|
"""
|
|
height, width = rgba.shape[:2]
|
|
height -= height % scale
|
|
width -= width % scale
|
|
cropped = rgba[:height, :width]
|
|
blocks = cropped.reshape(
|
|
height // scale, scale, width // scale, scale, 4
|
|
).transpose(0, 2, 1, 3, 4)
|
|
flat = blocks.reshape(-1, scale * scale, 4)
|
|
impure = (flat != flat[:, :1, :]).any(axis=(1, 2))
|
|
return float(impure.mean())
|
|
|
|
|
|
def detect_scale(rgba: NDArray[np.uint8]) -> int:
|
|
"""Find the largest integer upscale factor the image is consistent with.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
|
|
Returns:
|
|
The largest factor dividing both dimensions with uniform blocks; 1 if
|
|
the image is native-resolution.
|
|
"""
|
|
height, width = rgba.shape[:2]
|
|
smallest = min(height, width)
|
|
for candidate in range(smallest, 1, -1):
|
|
divides = height % candidate == 0 and width % candidate == 0
|
|
if divides and block_impurity(rgba, candidate) == 0.0:
|
|
return candidate
|
|
return 1
|
|
|
|
|
|
def check_scale_invariance(rgba: NDArray[np.uint8], scale: int | None) -> list[Code]:
|
|
"""Verify the image is genuinely blocky at its declared scale.
|
|
|
|
Note:
|
|
This gate passes *trivially* at scale 1, so it is only meaningful for
|
|
assets delivered at an integer upscale of 2 or more. On native
|
|
small-resolution output the discriminating gates are alpha binarity
|
|
and colour count, not this one.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
scale: The declared factor, or None to auto-detect.
|
|
|
|
Returns:
|
|
``[SCALE_NOT_INVARIANT]`` if blocks are not uniform at ``scale``.
|
|
"""
|
|
factor = detect_scale(rgba) if scale is None else scale
|
|
if factor <= 1:
|
|
return []
|
|
if block_impurity(rgba, factor) > 0.0:
|
|
return [Code.SCALE_NOT_INVARIANT]
|
|
return []
|