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>
This commit is contained in:
2026-06-28 15:59:26 +02:00
parent 31c2f6ac7f
commit c79f33265e
11 changed files with 836 additions and 73 deletions
+76
View File
@@ -0,0 +1,76 @@
---
name: crop_to_content
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def crop_to_content(img, *, pad_ratio: float = 0.06, square: bool = True, alpha_threshold: int = 10, bg_tolerance: int = 16)"
description: "Recorta una imagen PIL al bounding box de su contenido y la cuadra, para que el sujeto llene el frame antes de un downscale a pixel-art. Detecta el contenido por alpha (region con alpha > alpha_threshold) si la imagen es RGBA/LA, o por diferencia contra el color de fondo de las esquinas (con bg_tolerance) si es RGB. Recorta al bbox, anade un margen pad_ratio y, si square, rellena a cuadrado centrando el sujeto sin deformar (fondo transparente si RGBA, color de fondo si RGB). Pura PIL (opera sobre el objeto PIL.Image, no toca disco ni red, no muta la entrada). Si no hay contenido (todo transparente o todo fondo) devuelve una copia intacta — no crashea."
tags: [pil, image, crop, bbox, pixelart, gamedev-2d, ml, alpha, sprite]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: img
desc: "PIL.Image de entrada (cualquier modo). No se muta. None lanza ValueError."
- name: pad_ratio
desc: "Margen anadido alrededor del sujeto como fraccion del lado mayor del bbox recortado (0.06 = 6%). 0 = sin margen. keyword-only."
- name: square
desc: "Si True rellena a un lienzo cuadrado de lado max(w,h)+2*pad con el sujeto centrado (fondo transparente si hay alpha, color de fondo si RGB); si False solo recorta al bbox + margen sin cuadrar. keyword-only."
- name: alpha_threshold
desc: "Umbral de alpha (0..255) para considerar un pixel 'contenido' cuando la imagen tiene canal alpha. keyword-only."
- name: bg_tolerance
desc: "Tolerancia (0..255) de diferencia contra el color de fondo de las esquinas para imagenes sin alpha (RGB). keyword-only."
output: "PIL.Image nueva recortada (y cuadrada si square) con el sujeto llenando el frame. Si la imagen no tiene contenido detectable, devuelve una copia intacta de la entrada (mismo tamano)."
tested: true
tests: [test_golden_corner_subject_fills_frame, test_edge_centered_subject_not_overcropped, test_edge_rgb_background_bbox, test_edge_no_square_only_crops, test_error_all_transparent_returns_copy, test_error_none_raises, test_does_not_mutate_input]
test_file_path: "python/functions/ml/crop_to_content_test.py"
file_path: "python/functions/ml/crop_to_content.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from PIL import Image
from ml.crop_to_content import crop_to_content
# Sprite RGBA tras rembg: el sujeto ocupa una esquina -> recortar al bbox y cuadrar.
with Image.open("/tmp/knight_rgba.png") as im:
out = crop_to_content(im, pad_ratio=0.06, square=True)
out.save("/tmp/knight_cropped.png") # RGBA cuadrada, sujeto centrado llenando el frame
# CLI directo:
# ./fn run crop_to_content (corre los tests)
# python3 crop_to_content.py /tmp/in.png /tmp/out.png 0.06
```
## Cuando usarla
Antes de bajar una imagen a pixel-art (32/64px): si el sujeto ocupa poca area del
lienzo, al downscalear queda diminuto y tosco. `crop_to_content` recorta el aire
alrededor y cuadra para que el sujeto aproveche todos los pixeles del grid. Es el
paso de encuadre del pipeline `comfyui_pixelart_real_oneshot` (autocrop). Funciona
con sprites recortados por rembg (detecta por alpha) o con imagenes de fondo plano
(detecta por diferencia contra el color de esquina).
## Gotchas
- **Pura sobre PIL.Image**: recibe y devuelve un objeto `PIL.Image`, NO rutas. El
caller hace el `Image.open` / `.save`. No muta la imagen de entrada.
- Deteccion del contenido: con **alpha** usa `alpha > alpha_threshold`; sin alpha
usa la **moda de las 4 esquinas** como color de fondo y `bg_tolerance` de
diferencia. Si el fondo no es uniforme (gradiente) la deteccion RGB puede fallar;
para esos casos pasa la imagen ya recortada por rembg (RGBA).
- Si no hay contenido (todo transparente o todo del color de fondo) devuelve una
**copia intacta** del original (mismo tamano), nunca lanza por una imagen vacia.
Solo lanza `ValueError` si `img` es `None`.
- `square=True` (default) cuadra a `max(w,h)+2*pad`: si el sujeto es muy alargado el
lienzo crece al lado mayor y el sujeto queda centrado con barras transparentes (o
de color de fondo) a los lados — sin deformar.
- `pad_ratio` es relativo al lado **mayor del bbox**, no del lienzo original.