mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
Closes the two CPU-only cells of the art bake-off and builds the bridge the diffusion cell will reuse. Pins the bake-off to the SAME twelve subjects item-icons measured, with its easy/hard split intact. Without this a route tested on distinctive silhouettes beats the 6/12 baseline for free and the decision table means nothing. It also makes the subject-routing rule falsifiable: a generator route is predicted to lift specifically the six human-prior failures, and if it does not, the rule is recorded as wrong. Route (a) procedural: all twelve subjects drawn deterministically from a 7-colour locked palette. Every shape gets a distinguishing feature up front, because item-icons measured that plain primitives read as placeholders however correct the geometry is. Machine gate: 12/12 -- which proves "not malformed", NOT quality. A procedural generator satisfies palette/alpha/canvas constraints trivially; the human contact sheet is the real verdict. Route (d) code-as-art: SVG emitted as text and rasterised by rsvg-convert, so no new Python dependency (cairosvg/svgwrite stay absent). The structural finding, measured on real rasteriser output rather than a synthetic stand-in: raw vector renders pass 0/5 -- every one fails ALPHA_NOT_BINARY, most also TOO_MANY_COLORS. After the finishing stage, 5/5. Continuous-tone routes are therefore GENERATORS FEEDING A FINISHER, not pixel-art generators. finish.py binarises alpha before quantising colour, mirroring the gate order: quantising first blends transparent pixels' arbitrary RGB into the palette. Verified: 92 tests, 100% branch coverage across all ten modules, zero suppressions, all hooks pass. The PLR2004 lint refactor was confirmed pixel-neutral by byte-comparing all 12 icons before and after (12/12 identical). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T9rQ12yZ7sJxiPQTCS5dUF
103 lines
3.4 KiB
Python
103 lines
3.4 KiB
Python
"""Finishing-stage tests.
|
|
|
|
The finisher is the bridge every continuous-tone route depends on, so its
|
|
contract is asserted directly: whatever goes in, what comes out satisfies the
|
|
alpha, colour and canvas gates.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageFilter
|
|
import pytest
|
|
|
|
from python_pkg.artgate.finish import (
|
|
ALPHA_CUTOFF,
|
|
binarize_alpha,
|
|
downscale,
|
|
finish,
|
|
quantize,
|
|
)
|
|
from python_pkg.artgate.pixels import (
|
|
check_alpha_binary,
|
|
check_color_count,
|
|
opaque_rgb,
|
|
)
|
|
|
|
|
|
@pytest.fixture(name="smooth")
|
|
def fixture_smooth() -> np.ndarray:
|
|
"""Return a blurred, many-coloured, soft-alpha source image.
|
|
|
|
Returns:
|
|
A ``(256, 256, 4)`` uint8 array standing in for generator output.
|
|
"""
|
|
rng = np.random.default_rng(5)
|
|
arr = rng.integers(0, 255, (256, 256, 4), dtype=np.uint8)
|
|
img = Image.fromarray(arr, "RGBA").filter(ImageFilter.GaussianBlur(6))
|
|
return np.asarray(img, dtype=np.uint8)
|
|
|
|
|
|
class TestContract:
|
|
"""Finished output satisfies the gates its route could not satisfy."""
|
|
|
|
def test_finished_output_passes_gates(self, smooth: np.ndarray) -> None:
|
|
"""The end-to-end pipeline yields gate-passing pixel art."""
|
|
out = finish(smooth, target=32, max_colors=16)
|
|
assert out.shape == (32, 32, 4)
|
|
assert check_alpha_binary(out) == []
|
|
assert check_color_count(out, 16) == []
|
|
|
|
def test_raw_input_would_have_failed(self, smooth: np.ndarray) -> None:
|
|
"""The source genuinely fails, so the test is not vacuous."""
|
|
assert check_alpha_binary(smooth) != []
|
|
|
|
|
|
class TestBinarizeAlpha:
|
|
"""Alpha thresholding also canonicalises transparent pixels."""
|
|
|
|
def test_threshold_splits_at_cutoff(self) -> None:
|
|
"""Values at the cutoff round up, below it round down."""
|
|
arr = np.zeros((1, 2, 4), dtype=np.uint8)
|
|
arr[0, 0] = (9, 9, 9, ALPHA_CUTOFF)
|
|
arr[0, 1] = (9, 9, 9, ALPHA_CUTOFF - 1)
|
|
out = binarize_alpha(arr)
|
|
assert out[0, 0, 3] == 255
|
|
assert out[0, 1, 3] == 0
|
|
|
|
def test_transparent_rgb_is_zeroed(self) -> None:
|
|
"""Leftover RGB under transparency would inflate colour counts."""
|
|
arr = np.zeros((1, 1, 4), dtype=np.uint8)
|
|
arr[0, 0] = (200, 100, 50, 0)
|
|
assert tuple(binarize_alpha(arr)[0, 0]) == (0, 0, 0, 0)
|
|
|
|
def test_source_is_not_mutated(self) -> None:
|
|
"""The finisher never edits its caller's array in place."""
|
|
arr = np.full((2, 2, 4), 200, dtype=np.uint8)
|
|
binarize_alpha(arr)
|
|
assert arr[0, 0, 3] == 200
|
|
|
|
|
|
class TestStages:
|
|
"""Individual stages behave as documented."""
|
|
|
|
def test_downscale_hits_target(self, smooth: np.ndarray) -> None:
|
|
"""Box downscaling produces the requested square."""
|
|
assert downscale(smooth, 16).shape == (16, 16, 4)
|
|
|
|
def test_quantize_caps_opaque_colours(self) -> None:
|
|
"""Quantisation respects the cap over opaque pixels."""
|
|
rng = np.random.default_rng(2)
|
|
arr = rng.integers(0, 255, (16, 16, 4), dtype=np.uint8)
|
|
arr[:, :, 3] = 255
|
|
out = quantize(arr, 8)
|
|
assert np.unique(opaque_rgb(out), axis=0).shape[0] <= 8
|
|
|
|
def test_quantize_keeps_transparency_clean(self) -> None:
|
|
"""Transparent pixels stay canonical through quantisation."""
|
|
arr = np.zeros((4, 4, 4), dtype=np.uint8)
|
|
arr[0, :, :3] = (10, 20, 30)
|
|
arr[0, :, 3] = 255
|
|
out = quantize(arr, 4)
|
|
assert not out[1:, :, :].any()
|