mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
Closes the remaining bake-off cells. The phase's real result was not a route
score but a MISSING GATE.
All twelve SDXL icons measured 100% opaque coverage -- square tiles with no
sprite silhouette -- and passed every gate in the harness. Gate-pass was
measuring pipeline hygiene, not usability. MARGIN_TOO_SMALL inverts the table
from a meaningless 12/12/12 tie to:
diffusion 0/12 (every output full-bleed)
vector 10/12
procedural 11/12
The two non-diffusion failures were true positives in MY shapes, not gate
error: the procedural sword pommel ran off the bottom edge and the vector meat
bone clipped the top. Both fixed; both routes back to 12/12.
Diffusion recovers to 12/12 with flood-fill background removal, but visual
inspection shows the fill eats into the art (bone reduced to a fragment, sword
nearly erased). Reported as inadequate rather than shipped -- proper matting is
needed before route (b) is a usable pipeline.
Blender exits 0 even when its embedded script raises: BLENDER_EEVEE_NEXT does
not exist in 5.2, and the failure produced a clean exit and no file. render()
now asserts the output exists, or the route would report phantom successes.
Model gating verified at execution time, never recalled: SDXL base and
nerijs/pixel-art-xl are ungated, FLUX.1-dev is gated=auto, and
runwayml/stable-diffusion-v1-5 now 307-redirects.
The finishing stage now generalises across three continuous-tone sources
(SVG raster, Blender render, SDXL): each is 0/N raw and N/N finished.
Verified: 114 tests, 100% branch coverage on 13 modules, zero suppressions,
all hooks pass. Note: a local pytest run masked a missing branch that the
pre-commit hook caught -- the hook's run is authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
118 lines
3.7 KiB
Python
118 lines
3.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_margin(rgba: NDArray[np.uint8], min_margin: int) -> list[Code]:
|
|
"""Verify a transparent border surrounds the subject.
|
|
|
|
An item icon must be a *sprite*, not a square tile: without a transparent
|
|
margin it cannot sit on an inventory slot or a world background. This gate
|
|
exists because it was missed -- twelve diffusion icons scored 100% opaque
|
|
coverage and passed every other gate, because nothing checked whether the
|
|
asset had a silhouette at all.
|
|
|
|
Args:
|
|
rgba: An ``(h, w, 4)`` uint8 array.
|
|
min_margin: Required clear border width in pixels.
|
|
|
|
Returns:
|
|
``[MARGIN_TOO_SMALL]`` if any edge band contains opaque pixels.
|
|
"""
|
|
if min_margin <= 0:
|
|
return []
|
|
alpha = rgba[:, :, 3]
|
|
band = min_margin
|
|
edges = (
|
|
alpha[:band, :],
|
|
alpha[-band:, :],
|
|
alpha[:, :band],
|
|
alpha[:, -band:],
|
|
)
|
|
if any(int(edge.max(initial=0)) > 0 for edge in edges):
|
|
return [Code.MARGIN_TOO_SMALL]
|
|
return []
|
|
|
|
|
|
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 []
|