Spaces:
Runtime error
Runtime error
File size: 6,803 Bytes
ebd87b7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """
tests/test_extraction.py
--------------------------
Tests for pixel-exact sprite extraction: cropping, padding, and the
NEAREST-only preview rule. This is the module responsible for the
project's most important guarantee: ORIGINAL PIXELS = OUTPUT PIXELS.
Run with: pytest tests/test_extraction.py -v
"""
import sys
from pathlib import Path
import numpy as np
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from background import detect_background
from image_utils import crop_exact, nearest_preview, pad_nearest, to_pil
from segmentation import DetectedSprite
from image_utils import BBox
from sprite_extractor import extract_sprite
def test_crop_exact_matches_source_pixels_exactly():
"""The crop must contain the EXACT same pixel values as the source region."""
sheet = np.zeros((30, 30, 4), dtype=np.uint8)
sheet[:, :, 3] = 255
rng = np.random.default_rng(1)
sheet[5:15, 5:15] = np.dstack([
rng.integers(0, 255, (10, 10)),
rng.integers(0, 255, (10, 10)),
rng.integers(0, 255, (10, 10)),
np.full((10, 10), 255),
]).astype(np.uint8)
crop = crop_exact(sheet, 5, 5, 10, 10)
assert crop.shape == (10, 10, 4)
assert np.array_equal(crop, sheet[5:15, 5:15])
def test_crop_clamps_out_of_bounds_without_crashing():
sheet = np.zeros((10, 10, 4), dtype=np.uint8)
crop = crop_exact(sheet, 5, 5, 100, 100) # way beyond sheet bounds
assert crop.shape[0] <= 5
assert crop.shape[1] <= 5
def test_pad_nearest_zero_padding_is_noop():
arr = np.ones((5, 5, 4), dtype=np.uint8) * 7
out = pad_nearest(arr, 0)
assert np.array_equal(out, arr)
def test_pad_nearest_replicates_edge_pixels():
arr = np.zeros((3, 3, 4), dtype=np.uint8)
arr[1, 1] = [255, 0, 0, 255] # center pixel distinct
arr[0, 0] = [10, 20, 30, 255] # corner pixel
out = pad_nearest(arr, 2)
assert out.shape == (7, 7, 4) # 3 + 2*2 on each axis
# The padded corner region should replicate the original corner pixel value.
assert np.array_equal(out[0, 0], arr[0, 0])
assert np.array_equal(out[0, 1], arr[0, 0])
def test_pad_nearest_does_not_alter_original_pixels():
arr = np.zeros((4, 4, 4), dtype=np.uint8)
rng = np.random.default_rng(2)
arr[:, :, :3] = rng.integers(0, 255, (4, 4, 3))
arr[:, :, 3] = 255
out = pad_nearest(arr, 3)
# Original pixels must appear unchanged, exactly offset by the padding.
assert np.array_equal(out[3:7, 3:7], arr)
def test_nearest_preview_uses_nearest_resampling_only():
"""
A tiny checkerboard, when upscaled via nearest_preview, must produce
HARD pixel boundaries (no blended/intermediate colors) -- this is the
signature of NEAREST resampling vs. any smooth interpolation method.
"""
tiny = np.zeros((2, 2, 4), dtype=np.uint8)
tiny[0, 0] = [255, 0, 0, 255]
tiny[0, 1] = [0, 255, 0, 255]
tiny[1, 0] = [0, 0, 255, 255]
tiny[1, 1] = [255, 255, 0, 255]
img = to_pil(tiny)
preview = nearest_preview(img, target_long_side=8)
arr = np.array(preview)
unique_colors = np.unique(arr.reshape(-1, 4), axis=0)
# NEAREST upscaling of a 4-color 2x2 image must produce EXACTLY those
# 4 colors, never a blended/interpolated fifth color.
assert len(unique_colors) == 4
expected = {(255, 0, 0, 255), (0, 255, 0, 255), (0, 0, 255, 255), (255, 255, 0, 255)}
actual = {tuple(c) for c in unique_colors}
assert actual == expected
def test_extract_sprite_produces_exact_pixel_crop_no_resize():
sheet = np.zeros((40, 40, 4), dtype=np.uint8)
sheet[:, :, 3] = 255
sheet[10:20, 10:25] = [90, 200, 90, 255] # 15x10 sprite region
bg_info = detect_background(sheet)
detection = DetectedSprite(
sprite_index=1, bbox=BBox(10, 10, 15, 10), confidence=100.0,
confidence_reasons=[], component_count=1, pixel_count=150,
)
extracted = extract_sprite(sheet, bg_info, detection, source_file="test.png")
assert extracted.rgba.shape == (10, 15, 4) # height, width, channels -- NOT resized
assert np.array_equal(extracted.rgba, sheet[10:20, 10:25])
def test_extract_sprite_with_padding_preserves_original_pixels_at_offset():
sheet = np.zeros((40, 40, 4), dtype=np.uint8)
sheet[:, :, 3] = 255
sheet[10:20, 10:20] = [90, 200, 90, 255]
bg_info = detect_background(sheet)
detection = DetectedSprite(
sprite_index=1, bbox=BBox(10, 10, 10, 10), confidence=100.0,
confidence_reasons=[], component_count=1, pixel_count=100,
)
extracted = extract_sprite(sheet, bg_info, detection, source_file="test.png", padding=2)
assert extracted.rgba.shape == (14, 14, 4) # 10 + 2*2
assert np.array_equal(extracted.rgba[2:12, 2:12], sheet[10:20, 10:20])
def test_extract_sprite_transparent_variant_only_clears_background():
"""
The background-transparent export variant must only clear pixels that
were actually part of the border-connected background -- a sprite's
own same-colored interior pixel must remain opaque.
"""
sheet = np.zeros((30, 30, 4), dtype=np.uint8)
sheet[:, :, 3] = 255 # black background
sheet[5:15, 5:15] = [200, 50, 50, 255]
sheet[9, 9] = [0, 0, 0, 255] # black pixel inside the sprite
bg_info = detect_background(sheet)
detection = DetectedSprite(
sprite_index=1, bbox=BBox(5, 5, 10, 10), confidence=100.0,
confidence_reasons=[], component_count=1, pixel_count=100,
)
extracted = extract_sprite(sheet, bg_info, detection, source_file="test.png")
# relative position of (9,9) within the crop starting at (5,5)
rel_y, rel_x = 9 - 5, 9 - 5
assert extracted.rgba_transparent_bg[rel_y, rel_x, 3] == 255 # untouched
assert extracted.rgba[rel_y, rel_x, 3] == 255
def test_png_roundtrip_is_lossless():
"""Saving to PNG and reloading must reproduce identical pixel values (PNG is lossless)."""
import io
arr = np.zeros((12, 12, 4), dtype=np.uint8)
rng = np.random.default_rng(3)
arr[:, :, :3] = rng.integers(0, 255, (12, 12, 3))
arr[:, :, 3] = 255
img = to_pil(arr)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
reloaded = np.array(Image.open(buf).convert("RGBA"))
assert np.array_equal(arr, reloaded)
if __name__ == "__main__":
import traceback
tests = [(name, fn) for name, fn in list(globals().items()) if name.startswith("test_") and callable(fn)]
passed, failed = 0, 0
for name, fn in tests:
try:
fn()
print(f"PASS {name}")
passed += 1
except Exception:
print(f"FAIL {name}")
traceback.print_exc()
failed += 1
print(f"\n{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
|