Files
fn_registry/python/functions/ml/comfyui_pixelize_image_test.py
T
egutierrez c79f33265e fix(comfyui): pixelart_real_oneshot — sprite llena el frame + fondo transparente
Arregla los dos defectos reportados del pipeline comfyui_pixelart_real_oneshot:
el sujeto salía diminuto respecto al frame y siempre traía fondo (sin opción de
transparencia).

Causa raíz: comfyui_pixelize_image hacía convert("RGB") y descartaba el alpha;
comfyui_build_pixelart_workflow no inyectaba rembg (a diferencia de sus hermanos
item_icon/enemy_creature); y no había ningún paso de auto-crop al contenido.

Orden correcto del pipeline ahora:
generar (rembg) -> autocrop al bbox + cuadrar -> downscale (alpha aparte por
PixelOE) -> cuantización alpha-aware -> PNG RGBA transparente.

Piezas:
- comfyui_pixelize_image (1.1.0): keep_alpha/alpha_threshold. Con RGBA cuantiza
  solo el RGB (fondo transparente relleno con la moda del sujeto, fuera de la
  paleta) y preserva/binariza el alpha aparte. RGB sin alpha intacto.
- crop_to_content (NUEVA, pura PIL): bbox del contenido (alpha o diff-fondo) ->
  recorta -> margen -> cuadra centrando. No-throw; imagen vacía -> copia intacta.
- comfyui_build_pixelart_workflow (1.1.0): transparent=True + rembg_model.
  Inyecta nodo Image Rembg tras VAEDecode (patrón de item_icon).
- comfyui_pixelart_real_oneshot (1.1.0): transparent + autocrop + crop_pad_ratio
  + rembg_model. Recombina el alpha aparte tras PixelOE (trabaja en RGB). Campos
  nuevos: has_alpha, autocrop_applied.

Verificado en GPU (knight 64px): RGBA con 4 esquinas alpha==0, contenido cubre
88% del frame (antes 48%), 16 colores, 64x64. 32 tests offline en verde.
Report: reports/0218-2026-06-28-pixelart-sprite-fix.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:59:26 +02:00

148 lines
5.5 KiB
Python

"""Tests de comfyui_pixelize_image (offline, sin red ni GPU; PIL/numpy)."""
import os
import sys
import numpy as np
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_pixelize_image import comfyui_pixelize_image # noqa: E402
def _noisy_png(path, w=256, h=256):
"""PNG ruidoso con cientos de colores (simula crudo borroso de IA)."""
rng = np.random.default_rng(7)
arr = rng.integers(0, 256, size=(h, w, 3), dtype=np.uint8)
Image.fromarray(arr, "RGB").save(path)
return path
def test_golden_downscale_quantize(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "pixel.png")
res = comfyui_pixelize_image(src, dst, downscale=8, colors=16)
assert res["ok"] is True, res["error"]
assert os.path.isfile(dst)
assert res["size"] == [256, 256] # upscale_back=True conserva tamano
assert res["n_colors_final"] <= 16 # cuantizado a <=16 colores
def test_no_upscale_back_keeps_small(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "small.png")
res = comfyui_pixelize_image(src, dst, downscale=8, colors=16, upscale_back=False)
assert res["ok"] is True
assert res["size"] == [32, 32] # 256//8
def test_edge_fixed_palette_game_boy(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "gb.png")
res = comfyui_pixelize_image(src, dst, palette="game-boy")
assert res["ok"] is True, res["error"]
assert res["n_colors_final"] <= 4 # paleta Game Boy = 4 colores
def test_edge_palette_list_hex(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "pal.png")
res = comfyui_pixelize_image(src, dst, palette=["#000000", "#ffffff", "#ff0000"])
assert res["ok"] is True
assert res["n_colors_final"] <= 3
def test_edge_downscale_1_only_quantizes(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "q.png")
res = comfyui_pixelize_image(src, dst, downscale=1, colors=8)
assert res["ok"] is True
assert res["size"] == [256, 256]
assert res["n_colors_final"] <= 8
def test_error_missing_src(tmp_path):
res = comfyui_pixelize_image(str(tmp_path / "nope.png"), str(tmp_path / "o.png"))
assert res["ok"] is False
assert "no existe" in res["error"]
def test_error_downscale_zero(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
res = comfyui_pixelize_image(src, str(tmp_path / "o.png"), downscale=0)
assert res["ok"] is False
assert "downscale" in res["error"]
def test_error_bad_palette(tmp_path):
src = _noisy_png(str(tmp_path / "raw.png"))
res = comfyui_pixelize_image(src, str(tmp_path / "o.png"), palette="not-a-palette")
assert res["ok"] is False
assert "paleta" in res["error"].lower()
# --- alpha-aware (sprites con fondo transparente) ---
def _rgba_subject_png(path, canvas=256, box=120):
"""RGBA: sujeto opaco de colores variados centrado, fondo transparente."""
rng = np.random.default_rng(3)
arr = np.zeros((canvas, canvas, 4), dtype=np.uint8)
o = (canvas - box) // 2
arr[o:o + box, o:o + box, :3] = rng.integers(0, 256, size=(box, box, 3), dtype=np.uint8)
arr[o:o + box, o:o + box, 3] = 255 # sujeto opaco
Image.fromarray(arr, "RGBA").save(path)
return path
def test_alpha_preserved_transparent_corners(tmp_path):
"""RGBA in -> RGBA out con esquinas transparentes y paleta limitada en lo opaco."""
src = _rgba_subject_png(str(tmp_path / "sprite.png"))
dst = str(tmp_path / "px.png")
res = comfyui_pixelize_image(src, dst, downscale=4, colors=16, upscale_back=False)
assert res["ok"] is True, res["error"]
assert res["has_alpha"] is True
out = Image.open(dst).convert("RGBA")
a = np.asarray(out)[..., 3]
w, h = out.size
# Las 4 esquinas deben ser transparentes (alpha == 0).
assert a[0, 0] == 0 and a[0, w - 1] == 0
assert a[h - 1, 0] == 0 and a[h - 1, w - 1] == 0
# Centro opaco.
assert a[h // 2, w // 2] == 255
# Colores limitados en la zona opaca.
assert res["n_colors_final"] <= 16
def test_alpha_off_flattens_to_rgb(tmp_path):
"""keep_alpha=False sobre RGBA -> sale RGB (sin canal alpha)."""
src = _rgba_subject_png(str(tmp_path / "sprite.png"))
dst = str(tmp_path / "flat.png")
res = comfyui_pixelize_image(src, dst, downscale=4, colors=16, keep_alpha=False)
assert res["ok"] is True
assert res["has_alpha"] is False
assert Image.open(dst).mode != "RGBA"
def test_rgb_input_unaffected_by_keep_alpha(tmp_path):
"""Imagen RGB (sin alpha) con keep_alpha=True sigue saliendo RGB, sin romper."""
src = _noisy_png(str(tmp_path / "raw.png"))
dst = str(tmp_path / "rgb.png")
res = comfyui_pixelize_image(src, dst, downscale=8, colors=16) # keep_alpha default True
assert res["ok"] is True
assert res["has_alpha"] is False
assert res["n_colors_final"] <= 16
def test_error_all_transparent_no_crash(tmp_path):
"""RGBA toda transparente (rembg sin sujeto): no crashea, 0 colores opacos."""
arr = np.zeros((64, 64, 4), dtype=np.uint8) # alpha 0 en todo
src = str(tmp_path / "empty.png")
Image.fromarray(arr, "RGBA").save(src)
dst = str(tmp_path / "out.png")
res = comfyui_pixelize_image(src, dst, downscale=1, colors=16)
assert res["ok"] is True, res["error"]
assert res["has_alpha"] is True
assert res["n_colors_final"] == 0
out = np.asarray(Image.open(dst).convert("RGBA"))
assert out[..., 3].max() == 0 # sigue toda transparente