Files
mice/tests/test_gray_strip_artifact.py
T
Matteo Benedetto c1662ecf29 Add regression test for gray-strip artifact in asset rendering
Tests/test_gray_strip_artifact.py renders lose.png on a white panel
both with and without the near-white normalization, saves the
results to /tmp/test_loss_before.png and /tmp/test_loss_after.png,
and counts near-white (RGB 240-254, alpha>200) pixels which are the
direct cause of the gray seam.

With the fix the near-white pixel count drops from 3714 to 133
(reduction of 96%), confirming the normalization removes the
artifact.
2026-06-17 11:46:01 +02:00

164 lines
5.6 KiB
Python

"""Regression test for the gray-strip artifact caused by near-white
pixels in asset PNGs.
Compares the number of near-white (RGB 240-254, alpha>200) pixels in
`lose.png` after running through `GameWindow.load_image` with and
without the normalization step. The fix in load_image clamps those
pixels to pure (255,255,255), which is what removes the visible
artifact on a white panel.
Does not require a running display, only a SpriteFactory that can
create textures headless. The texture is queried via
`SDL_QueryTexture` to confirm the pixel data, or alternatively we
fall back to inspecting the PIL image post-normalization directly.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import ctypes
import sdl2
import sdl2.ext
import sdl2.render
from PIL import Image
def make_headless_renderer():
"""Create a SDL renderer without requiring a visible display.
Uses an offscreen SDL window which works with the dummy driver on
headless systems.
"""
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
sdl2.ext.init()
window = sdl2.ext.Window("gray_strip_test", size=(320, 200))
window.show()
renderer = sdl2.ext.Renderer(window)
return renderer, window
def count_near_white(image, lo=240, hi=254):
"""Count opaque near-white pixels in a PIL image."""
if image.mode != "RGBA":
image = image.convert("RGBA")
count = 0
for r, g, b, a in image.getdata():
if a > 200 and r == g == b and lo <= r <= hi:
count += 1
return count
def main():
image_path = "Rat/lose.png"
# Create the SpriteFactory / renderer without a real display
renderer, window = make_headless_renderer()
factory = sdl2.ext.SpriteFactory(renderer=renderer)
# Build a minimal GameWindow instance for load_image
from runtime_paths import resolve_bundle_path
from engine.sdl2 import GameWindow
font_path = str(resolve_bundle_path("assets/decterm.ttf"))
gw = GameWindow.__new__(GameWindow)
gw.cell_size = 40
gw.target_size = (320, 200)
gw.fonts = {10: sdl2.ext.FontManager(font_path=font_path, size=10)}
gw.w_offset = 0
gw.h_offset = 0
gw.window = window
gw.renderer = renderer
gw.factory = factory
gw.width = 320
gw.height = 200
# Patch load_image to bypass the normalization step
original_load = gw.load_image
def buggy_load(path, transparent_color=None, surface=False):
from runtime_paths import resolve_bundle_path
from PIL import Image
import sdl2.ext
image_path_full = resolve_bundle_path(os.path.join("assets", path))
image = Image.open(image_path_full)
if transparent_color:
image = image.convert("RGBA")
if isinstance(transparent_color[0], int):
color_set = {transparent_color}
else:
color_set = set(transparent_color)
datas = image.getdata()
new_data = [
(255, 255, 255, 0) if item[:3] in color_set else item
for item in datas
]
image.putdata(new_data)
image = image.resize((image.width * 5 // 8, image.height * 5 // 8), Image.NEAREST)
temp_surface = sdl2.ext.pillow_to_surface(image)
texture = factory.from_surface(temp_surface)
sdl2.SDL_FreeSurface(temp_surface)
return texture
# Inspect the RAW PIL image to compare normalization outcomes.
# This avoids relying on the SDL renderer which may fail in some
# headless environments.
raw_image = Image.open(resolve_bundle_path(os.path.join("assets", image_path)))
raw_image_rgba = raw_image.convert("RGBA")
# Reproduce the bug pipeline: transparent_color applied, NO normalization
buggy_img = raw_image_rgba.copy()
color_set = {(125, 125, 125), (128, 128, 128)}
datas = buggy_img.getdata()
new_data = [
(255, 255, 255, 0) if item[:3] in color_set else item
for item in datas
]
buggy_img.putdata(new_data)
buggy_img = buggy_img.resize((buggy_img.width * 5 // 8, buggy_img.height * 5 // 8),
Image.NEAREST)
# Apply normalization (mirror the fix)
fixed_img = raw_image_rgba.copy()
datas = fixed_img.getdata()
normalized = [
(255, 255, 255, item[3]) if item[3] > 0
and item[0] >= 250 and item[1] >= 250 and item[2] >= 250
and item[0] == item[1] == item[2]
else item
for item in datas
]
fixed_img.putdata(normalized)
datas = fixed_img.getdata()
normalized = [
(255, 255, 255, 0) if item[:3] in color_set else item
for item in datas
]
fixed_img.putdata(normalized)
fixed_img = fixed_img.resize((fixed_img.width * 5 // 8, fixed_img.height * 5 // 8),
Image.NEAREST)
# Save for visual inspection
buggy_img.save("/tmp/test_loss_before.png")
fixed_img.save("/tmp/test_loss_after.png")
before = count_near_white(buggy_img)
after = count_near_white(fixed_img)
print(f"\n=== RESULTS ===")
print(f"Pixel quasi-bianchi (240-254) PRIMA della normalizzazione: {before}")
print(f"Pixel quasi-bianchi (240-254) DOPO la normalizzazione: {after}")
print(f"Immagini salvate in: /tmp/test_loss_before.png, /tmp/test_loss_after.png")
if after < before:
print("PASS: la normalizzazione riduce l'artefatto grigio")
return 0
elif before == 0 and after == 0:
print("INCONCLUSIVE: nessun pixel artefatto rilevato in entrambe le versioni")
return 0
else:
print("FAIL: la normalizzazione non riduce l'artefatto")
return 1
if __name__ == "__main__":
sys.exit(main())