--- name: comfyui_pixelize_image kind: function lang: py domain: ml version: "1.1.0" purity: impure signature: "def comfyui_pixelize_image(src_path: str, dst_path: str, *, downscale: int = 8, colors: int = 16, palette=None, dither: bool = False, upscale_back: bool = True, keep_alpha: bool = True, alpha_threshold: int = 128) -> dict" description: "Post-proceso pixel-perfect (Fase 2 pixelart): imagen -> downscale nearest-neighbor por factor (colapsa cada bloque borroso a un pixel duro) -> cuantizacion a N colores (MEDIANCUT) o a una paleta fija embebida (game-boy / pico-8 / nes / lista de hex) -> opcional re-upscale nearest conservando los pixeles duros. Alpha-aware: si la entrada es RGBA y keep_alpha, cuantiza SOLO el RGB (el fondo transparente no entra en la paleta) y preserva/binariza el alpha por separado -> PNG RGBA con transparencia real. Convierte el 'pixelart borroso de IA' en pixelart de verdad. Nucleo PIL puro, CPU-only: sin GPU, sin red. Devuelve {ok, out_path, size, n_colors_final, has_alpha, error}. Impura solo por la lectura/escritura de disco." tags: [comfyui, gamedev-2d, pixelart, ml, pil, quantize, palette, image, alpha, transparent] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_py_core" imports: [] params: - name: src_path desc: "ruta de la imagen de entrada (PNG/JPG/...)." - name: dst_path desc: "ruta del PNG de salida (se crea el directorio si falta)." - name: downscale desc: "factor entero de reduccion nearest (>=1); cada bloque downscale x downscale px colapsa a 1 pixel. 1 = solo cuantiza sin colapsar el grid. keyword-only." - name: colors desc: "numero de colores objetivo (2..256) cuando palette es None; cuantizacion MEDIANCUT determinista. keyword-only." - name: palette desc: "None (auto a 'colors'), nombre de paleta fija builtin ('game-boy','pico-8','nes') o lista de hex ('#rrggbb'/'rrggbb'). Una paleta fija ignora 'colors'. keyword-only." - name: dither desc: "aplica Floyd-Steinberg al cuantizar (off por defecto = pixelart limpio). keyword-only." - name: upscale_back desc: "re-escala nearest al tamano original (preview con pixeles duros). False deja la imagen pequena. keyword-only." - name: keep_alpha desc: "si True (default) y la entrada tiene canal alpha, preserva la transparencia: cuantiza solo el RGB y downscalea/binariza el alpha aparte -> PNG RGBA. Sin efecto si la imagen no tiene alpha (sale RGB igual que antes). keyword-only." - name: alpha_threshold desc: "umbral (0..255) para binarizar el alpha en opaco (255) o transparente (0). Solo aplica cuando se preserva el alpha. keyword-only." output: "dict con ok (bool), out_path (str), size ([w,h] de la imagen final), n_colors_final (int, colores RGB distintos; en la zona opaca si es RGBA), has_alpha (bool, True si la salida es RGBA), error (str, vacio si OK)." tested: true tests: [test_golden_downscale_quantize, test_no_upscale_back_keeps_small, test_edge_fixed_palette_game_boy, test_edge_palette_list_hex, test_edge_downscale_1_only_quantizes, test_error_missing_src, test_error_downscale_zero, test_error_bad_palette, test_alpha_preserved_transparent_corners, test_alpha_off_flattens_to_rgb, test_rgb_input_unaffected_by_keep_alpha, test_error_all_transparent_no_crash] test_file_path: "python/functions/ml/comfyui_pixelize_image_test.py" file_path: "python/functions/ml/comfyui_pixelize_image.py" --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) from ml.comfyui_pixelize_image import comfyui_pixelize_image # Crudo SDXL+SDXL_pixel-art 1024x1024 -> pixelart 16 colores, grid de 128 res = comfyui_pixelize_image( os.path.expanduser("~/ComfyUI/output/pixelart_00001_.png"), "/tmp/hero_pixel.png", downscale=8, colors=16, ) # {'ok': True, 'out_path': '/tmp/hero_pixel.png', 'size': [1024, 1024], 'n_colors_final': 16, 'error': ''} # Forzar la paleta retro Game Boy (4 colores) y dejar la imagen pequena (sin upscale) comfyui_pixelize_image("/tmp/hero_pixel.png", "/tmp/hero_gb.png", palette="game-boy", upscale_back=False) # Sprite RGBA (tras rembg): preserva la transparencia, cuantiza solo el sujeto res = comfyui_pixelize_image("/tmp/knight_rgba.png", "/tmp/knight_px.png", downscale=1, colors=16, keep_alpha=True) # {'ok': True, 'has_alpha': True, 'n_colors_final': 16, ...} -> fondo transparente intacto ``` ## Cuando usarla Fase 2 del pipeline pixelart: tras generar el crudo (SDXL + LoRA `SDXL_pixel-art`), para colapsar el grid borroso a pixeles duros y limitar la paleta. Si la imagen viene de `rembg` con fondo recortado (RGBA), `keep_alpha=True` mantiene la transparencia y deja el fondo fuera de la paleta. Tambien sirve para "pixelizar" cualquier imagen (sprite, render, foto) a estetica retro sin tocar la GPU. Para llevar el resultado a Godot con filtro Nearest: `comfyui_export_asset_to_godot(out, "pixelart", proj)`. ## Gotchas - **nearest, no lanczos**: el downscale usa NEAREST a proposito; interpolar suave re-difumina el grid. No lo cambies por "calidad". - `palette` fija (game-boy/pico-8/nes o lista de hex) **ignora** `colors`. La paleta se rellena internamente repitiendo su ultimo color para que `quantize` no introduzca un negro extra por entradas vacias (bug arreglado en v1.0.0). - `downscale` con `upscale_back=False` deja la imagen de `w//downscale x h//downscale`: util para spritesheets compactos; con `True` vuelve al tamano original con bordes duros (preview). - Todo error es **dict `ok=False`** (no excepcion): `src_path` inexistente, `downscale<1`, paleta desconocida -> `error` explica. No crashea ni borra nada. - `n_colors_final` cuenta colores RGB distintos reales del PNG escrito; con salida RGBA cuenta **solo la zona opaca** (el transparente no es un color del pixel-art); con paleta fija puede ser **menor** que el tamano de la paleta si la imagen no usa todos. - **alpha-aware (v1.1.0)**: con entrada RGBA y `keep_alpha=True` (default), el fondo transparente se rellena internamente con la moda del sujeto antes de cuantizar, asi NO gasta una entrada de la paleta; el alpha se downscalea nearest aparte y se binariza por `alpha_threshold` (0/255 = bordes duros pixel-art). Entrada sin alpha -> comportamiento RGB identico al de antes (retrocompatible). - Si la entrada RGBA esta **toda transparente** (rembg sin sujeto), no crashea: devuelve `ok=True`, `has_alpha=True`, `n_colors_final=0` y el PNG sigue transparente. - CPU-only: no toca la GPU ni el servidor ComfyUI; corre en cualquier interprete con Pillow (numpy acelera el relleno alpha; sin numpy degrada limpio). ## Capability growth log - v1.1.0 (2026-06-28) — alpha-aware: `keep_alpha`/`alpha_threshold`. Si la entrada es RGBA, cuantiza solo el RGB (fondo transparente fuera de la paleta) y preserva el alpha binarizado -> PNG RGBA con transparencia real. Cierra el bug del pipeline pixelart que perdia el fondo transparente por el `convert("RGB")` (issue sprite-fix).