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.
164 lines
5.6 KiB
Python
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()) |