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:
@@ -3,17 +3,17 @@ name: comfyui_pixelart_real_oneshot
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_pixelart_real_oneshot(subject: str, *, size: int = 64, colors: int = 16, engine: str = \"pixeloe\", palette=None, server: str = \"127.0.0.1:8188\", dest_dir: str = \"~/ComfyUI/output\", seed: int = 0, negative: str | None = None, mode: str = \"contrast\", patch_size: int = 16, thickness: int = 2, fill_frame: bool = True, upscale_preview: int = 512, keep_base: bool = True, comfy_python: str | None = None, wait_timeout: float = 300.0, filename_prefix: str = \"pixelart_real\", **gen_kwargs) -> dict"
|
||||
description: "Pipeline one-shot prompt de texto -> sprite pixel-art REAL (grid duro + paleta limitada) en disco. Materializa el metodo ganador del report 0215: generar a alta-res con SDXL + LoRA SDXL_pixel-art, downscale contrast-aware con PixelOE (engine=pixeloe, sprites) o nearest (tiles), y cuantizacion dura con comfyui_pixelize_image (16 colores libres o paleta fija pico-8/nes/game-boy). Sweet-spot 64px personajes, 32px iconos. Fallback automatico pixeloe->nearest. Compone build_pixelart + submit + wait + fetch + pixeloe_downscale + pixelize_image. Impuro: HTTP + disco."
|
||||
tags: [comfyui, gamedev-2d, pixelart, pipelines, sprite, launcher]
|
||||
uses_functions: [comfyui_build_pixelart_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, pixeloe_downscale_py_ml, comfyui_pixelize_image_py_ml]
|
||||
signature: "def comfyui_pixelart_real_oneshot(subject: str, *, size: int = 64, colors: int = 16, engine: str = \"pixeloe\", palette=None, server: str = \"127.0.0.1:8188\", dest_dir: str = \"~/ComfyUI/output\", seed: int = 0, negative: str | None = None, mode: str = \"contrast\", patch_size: int = 16, thickness: int = 2, fill_frame: bool = True, transparent: bool = True, autocrop: bool = True, crop_pad_ratio: float = 0.06, rembg_model: str = \"u2net\", upscale_preview: int = 512, keep_base: bool = True, comfy_python: str | None = None, wait_timeout: float = 300.0, filename_prefix: str = \"pixelart_real\", **gen_kwargs) -> dict"
|
||||
description: "Pipeline one-shot prompt de texto -> sprite pixel-art REAL (grid duro + paleta limitada) en disco, con fondo transparente y sujeto que llena el frame. Materializa el metodo ganador del report 0215, ahora alpha-aware: generar a alta-res con SDXL + LoRA SDXL_pixel-art (rembg recorta el fondo si transparent), AUTOCROP al bbox del contenido + cuadrado (el sujeto llena el frame, no diminuto), downscale contrast-aware con PixelOE (engine=pixeloe, sprites; alpha recombinado aparte porque PixelOE trabaja en RGB) o nearest (tiles), y cuantizacion dura alpha-aware con comfyui_pixelize_image (16 colores libres o paleta fija pico-8/nes/game-boy). Salida PNG RGBA con transparencia real. Sweet-spot 64px personajes, 32px iconos. Fallback automatico pixeloe->nearest. Compone build_pixelart + submit + wait + fetch + crop_to_content + pixeloe_downscale + pixelize_image. Impuro: HTTP + disco."
|
||||
tags: [comfyui, gamedev-2d, pixelart, pipelines, sprite, launcher, alpha, transparent, autocrop]
|
||||
uses_functions: [comfyui_build_pixelart_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, crop_to_content_py_ml, pixeloe_downscale_py_ml, comfyui_pixelize_image_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_py_core
|
||||
imports: [comfyui_build_pixelart_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, pixeloe_downscale_py_ml, comfyui_pixelize_image_py_ml]
|
||||
imports: [comfyui_build_pixelart_workflow_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, crop_to_content_py_ml, pixeloe_downscale_py_ml, comfyui_pixelize_image_py_ml]
|
||||
params:
|
||||
- name: subject
|
||||
desc: "Prompt positivo (lo que se quiere ver: 'pixel art knight, full body, side view'). No puede estar vacio."
|
||||
@@ -41,6 +41,14 @@ params:
|
||||
desc: "Grosor del outline expansion de PixelOE (default 2). keyword-only."
|
||||
- name: fill_frame
|
||||
desc: "Si True anade un hint de encuadre al subject para que el sujeto llene el frame (mejor detalle por pixel tras el downscale). keyword-only."
|
||||
- name: transparent
|
||||
desc: "Si True (default) genera con fondo recortado (rembg en el workflow) y produce sprite RGBA con transparencia real. False para tiles/texturas sin alpha (PNG opaco). keyword-only."
|
||||
- name: autocrop
|
||||
desc: "Si True (default) recorta la imagen base al bbox del contenido + cuadrado antes del downscale, para que el sujeto llene el frame (evita el sprite diminuto). Usa el alpha si transparent, o el color de fondo si no. keyword-only."
|
||||
- name: crop_pad_ratio
|
||||
desc: "Margen relativo que deja el autocrop alrededor del sujeto (0.06 = 6% del lado). keyword-only."
|
||||
- name: rembg_model
|
||||
desc: "Modelo Rembg para recortar el fondo ('u2net' general, 'isnet-anime' anime). Solo aplica si transparent. keyword-only."
|
||||
- name: upscale_preview
|
||||
desc: "Si > 0 escribe ademas un PNG re-escalado nearest a ese lado (preview con pixeles duros, p.ej. 512). 0 lo desactiva. keyword-only."
|
||||
- name: keep_base
|
||||
@@ -53,7 +61,7 @@ params:
|
||||
desc: "Prefijo de los archivos de salida. keyword-only."
|
||||
- name: gen_kwargs
|
||||
desc: "Params extra para comfyui_build_pixelart_workflow (width, height, ckpt_name, lora_strength, use_lcm, steps, cfg, ...). keyword-only (**gen_kwargs)."
|
||||
output: "dict {ok, out_path, out_path_upscaled, base_path, size, colors_final, engine_used, prompt_id, error}. out_path = PNG final size x size; out_path_upscaled = preview re-escalado; engine_used refleja el fallback (pixeloe->nearest). Si falla, ok=False y error explica en que paso. No-throw."
|
||||
output: "dict {ok, out_path, out_path_upscaled, base_path, size, colors_final, engine_used, has_alpha, autocrop_applied, prompt_id, error}. out_path = PNG final size x size (RGBA si transparent); out_path_upscaled = preview re-escalado; has_alpha = True si lleva transparencia; autocrop_applied = True si el autocrop recorto la base; engine_used refleja el fallback (pixeloe->nearest). Si falla, ok=False y error explica en que paso. No-throw."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
@@ -63,8 +71,8 @@ file_path: "python/functions/pipelines/comfyui_pixelart_real_oneshot.py"
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Personaje 64px, 16 colores, motor pixeloe (sprites con silueta).
|
||||
./fn run comfyui_pixelart_real_oneshot "pixel art knight, full body, side view, game sprite"
|
||||
# Sprite de personaje 64px: RGBA transparente + autocrop (sujeto llena el frame).
|
||||
./fn run comfyui_pixelart_real_oneshot "pixel art knight, full body, centered"
|
||||
```
|
||||
|
||||
```python
|
||||
@@ -72,24 +80,26 @@ import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from pipelines.comfyui_pixelart_real_oneshot import comfyui_pixelart_real_oneshot
|
||||
|
||||
# (a) Personaje 64px, paleta libre 16 colores, PixelOE contrast.
|
||||
# (a) Sprite personaje 64px: fondo transparente + autocrop (defaults).
|
||||
res = comfyui_pixelart_real_oneshot(
|
||||
"pixel art knight, full body, side view, game sprite",
|
||||
"pixel art knight, full body, centered",
|
||||
size=64, colors=16, engine="pixeloe", seed=42,
|
||||
dest_dir="~/ComfyUI/output",
|
||||
transparent=True, autocrop=True, dest_dir="~/ComfyUI/output",
|
||||
)
|
||||
print(res["out_path"], res["colors_final"], res["engine_used"]) # ~16 colores, pixeloe
|
||||
print(res["out_path"], res["colors_final"], res["has_alpha"], res["engine_used"])
|
||||
# -> 64px RGBA, ~16 colores, has_alpha=True, esquinas transparentes, sujeto ~88% del frame
|
||||
|
||||
# (b) Icono 32px de un item.
|
||||
# (b) Icono 32px de un item (sprite con alpha).
|
||||
res = comfyui_pixelart_real_oneshot(
|
||||
"pixel art sword icon, single object",
|
||||
size=32, colors=16, engine="pixeloe", seed=7,
|
||||
)
|
||||
|
||||
# (c) Tile sin silueta -> nearest (mas barato) + paleta fija PICO-8.
|
||||
# (c) Tile sin silueta -> nearest + paleta fija PICO-8, SIN transparencia.
|
||||
res = comfyui_pixelart_real_oneshot(
|
||||
"pixel art grass texture tile, top down, seamless",
|
||||
size=64, engine="nearest", palette="pico-8", fill_frame=False,
|
||||
size=64, engine="nearest", palette="pico-8",
|
||||
transparent=False, autocrop=False, fill_frame=False,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -98,10 +108,13 @@ res = comfyui_pixelart_real_oneshot(
|
||||
Cuando quieres pixel-art **de verdad** (grid duro + paleta limitada, verificable
|
||||
por conteo de colores), no la salida cruda de la difusion (que parece pixelada
|
||||
pero tiene decenas de miles de colores y bordes con anti-aliasing). Una sola
|
||||
llamada hace generar -> downscale -> cuantizar. Usa `engine="pixeloe"` para
|
||||
personajes/criaturas/iconos con silueta (conserva el contorno) y
|
||||
`engine="nearest"` para tiles/texturas/fondos sin contorno (mas barato, CPU puro).
|
||||
64px es el sweet-spot de personajes; 32px solo para iconos/objetos simples.
|
||||
llamada hace generar -> recortar -> downscale -> cuantizar. Para **sprites de
|
||||
sujeto** (personajes, criaturas, objetos) deja los defaults `transparent=True` +
|
||||
`autocrop=True`: salen RGBA con fondo transparente y el sujeto llena el frame. Usa
|
||||
`engine="pixeloe"` para conservar la silueta. Para **tiles/texturas/fondos** sin
|
||||
contorno usa `engine="nearest"`, `transparent=False`, `autocrop=False` (mas barato,
|
||||
CPU puro, sin alpha). 64px es el sweet-spot de personajes; 32px solo para
|
||||
iconos/objetos simples.
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -120,13 +133,31 @@ personajes/criaturas/iconos con silueta (conserva el contorno) y
|
||||
son `<prefix>_<size>px_<engine>_<paleta|qN>.png` y `..._up.png` (preview).
|
||||
- Una **paleta fija** (`pico-8`/`nes`/`game-boy`/lista hex) ignora `colors` y
|
||||
puede dar menos colores que `colors` si el sujeto no cubre toda la paleta.
|
||||
- Encuadre: si el sujeto ocupa poca area del frame, a 64/32px queda diminuto.
|
||||
`fill_frame=True` (default) empuja al sujeto a llenar el frame; aun asi, para
|
||||
sprites conviene un subject que pida "full body, centered".
|
||||
- Encuadre: si el sujeto ocupa poca area del frame, a 64/32px queda diminuto. Dos
|
||||
mecanismos lo evitan: `fill_frame=True` (hint al prompt) y, sobre todo,
|
||||
`autocrop=True` (default) que recorta al bbox real del contenido + cuadrado tras
|
||||
generar. Con autocrop el sujeto llena ~85-90% del frame aunque el prompt no lo
|
||||
encuadre perfecto.
|
||||
- **transparencia (v1.1.0)**: `transparent=True` (default) mete el nodo `Image
|
||||
Rembg` en el workflow (requiere ese custom node en el server) y produce PNG
|
||||
**RGBA**. Las 4 esquinas salen `alpha==0`. Para tiles/fondos opacos: `transparent=False`.
|
||||
- **alpha a traves de PixelOE**: PixelOE trabaja en RGB y pierde el alpha; el
|
||||
pipeline downscalea el alpha del recorte por separado (nearest al mismo `size`) y
|
||||
lo recombina sobre el grid antes de cuantizar. Por eso el sprite final conserva la
|
||||
transparencia con `engine="pixeloe"`.
|
||||
- Si la generacion sale **toda transparente** (rembg no detecto sujeto), no crashea:
|
||||
el autocrop deja la imagen sin recortar y el resto del pipeline sigue (sprite
|
||||
vacio, `colors_final` bajo). Revisa el `subject` en ese caso.
|
||||
- No reintenta el sampler: para mejor toma, varia `seed`.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-28) — sprite-fix: `transparent`/`autocrop`/`crop_pad_ratio`/
|
||||
`rembg_model`. Arregla los 2 bugs reportados: (1) sprite diminuto -> autocrop al
|
||||
bbox del contenido + cuadrado antes del downscale (sujeto pasa de ~48% a ~88% del
|
||||
frame); (2) sin transparencia -> rembg en el workflow + cuantizacion alpha-aware +
|
||||
alpha recombinado tras PixelOE -> PNG RGBA con esquinas alpha==0. Anade
|
||||
`crop_to_content` a la composicion. Verificado en GPU (knight 64px).
|
||||
- v1.0.0 (2026-06-28) — pipeline inicial. Materializa el metodo ganador del
|
||||
report 0215 (PixelOE contrast downscale -> cuantizacion dura). Compone
|
||||
build_pixelart + submit + wait + fetch + pixeloe_downscale + pixelize_image
|
||||
|
||||
@@ -47,6 +47,7 @@ from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||
from ml.comfyui_pixelize_image import comfyui_pixelize_image
|
||||
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||
from ml.comfyui_wait_result import comfyui_wait_result
|
||||
from ml.crop_to_content import crop_to_content
|
||||
from ml.pixeloe_downscale import pixeloe_downscale
|
||||
|
||||
# Sufijo de encuadre: empuja al sujeto a llenar el frame para que tras el
|
||||
@@ -80,6 +81,10 @@ def comfyui_pixelart_real_oneshot(
|
||||
patch_size: int = 16,
|
||||
thickness: int = 2,
|
||||
fill_frame: bool = True,
|
||||
transparent: bool = True,
|
||||
autocrop: bool = True,
|
||||
crop_pad_ratio: float = 0.06,
|
||||
rembg_model: str = "u2net",
|
||||
upscale_preview: int = 512,
|
||||
keep_base: bool = True,
|
||||
comfy_python: str | None = None,
|
||||
@@ -118,6 +123,18 @@ def comfyui_pixelart_real_oneshot(
|
||||
fill_frame: si True, anade un hint de encuadre al subject para que el
|
||||
sujeto llene el frame (mejor detalle por pixel tras el downscale).
|
||||
keyword-only.
|
||||
transparent: si True (default) genera con fondo recortado (rembg en el
|
||||
workflow) y produce un sprite RGBA con transparencia real. Para
|
||||
tiles/texturas que NO quieren alpha, pasar transparent=False (el sprite
|
||||
sale RGB sobre fondo opaco). keyword-only.
|
||||
autocrop: si True (default) recorta la imagen base al bounding box de su
|
||||
contenido y la cuadra antes del downscale, para que el sujeto llene el
|
||||
frame (evita el sprite diminuto). Usa el alpha si transparent, o el color
|
||||
de fondo si no. keyword-only.
|
||||
crop_pad_ratio: margen relativo que deja el autocrop alrededor del sujeto
|
||||
(0.06 = 6% del lado). keyword-only.
|
||||
rembg_model: modelo Rembg para recortar el fondo ('u2net' general,
|
||||
'isnet-anime' para anime). Solo aplica si transparent. keyword-only.
|
||||
upscale_preview: si > 0, escribe ademas un PNG re-escalado nearest a
|
||||
ese lado (preview con pixeles duros, p.ej. 512). 0 lo desactiva.
|
||||
keyword-only.
|
||||
@@ -137,14 +154,18 @@ def comfyui_pixelart_real_oneshot(
|
||||
- out_path_upscaled (str): ruta del preview re-escalado, o "" si off.
|
||||
- base_path (str): ruta del PNG base de alta resolucion (o "" si se borro).
|
||||
- size (int): lado real del PNG final.
|
||||
- colors_final (int): numero de colores distintos en el resultado.
|
||||
- colors_final (int): numero de colores distintos en el resultado (en la
|
||||
zona opaca si es RGBA).
|
||||
- engine_used (str): "pixeloe" o "nearest" (refleja el fallback).
|
||||
- has_alpha (bool): True si el PNG final es RGBA con transparencia.
|
||||
- autocrop_applied (bool): True si el autocrop recorto la imagen base.
|
||||
- prompt_id (str): id del trabajo en ComfyUI.
|
||||
- error (str): mensaje de error; vacio si OK.
|
||||
"""
|
||||
out = {
|
||||
"ok": False, "out_path": "", "out_path_upscaled": "", "base_path": "",
|
||||
"size": int(size), "colors_final": 0, "engine_used": engine,
|
||||
"has_alpha": False, "autocrop_applied": False,
|
||||
"prompt_id": "", "error": "",
|
||||
}
|
||||
|
||||
@@ -170,12 +191,14 @@ def comfyui_pixelart_real_oneshot(
|
||||
try:
|
||||
if negative is None:
|
||||
workflow = comfyui_build_pixelart_workflow(
|
||||
positive, seed=seed, filename_prefix=f"{filename_prefix}_base",
|
||||
**gen_kwargs,
|
||||
positive, seed=seed, transparent=bool(transparent),
|
||||
rembg_model=rembg_model,
|
||||
filename_prefix=f"{filename_prefix}_base", **gen_kwargs,
|
||||
)
|
||||
else:
|
||||
workflow = comfyui_build_pixelart_workflow(
|
||||
positive, negative, seed=seed,
|
||||
positive, negative, seed=seed, transparent=bool(transparent),
|
||||
rembg_model=rembg_model,
|
||||
filename_prefix=f"{filename_prefix}_base", **gen_kwargs,
|
||||
)
|
||||
except (ValueError, TypeError) as exc:
|
||||
@@ -216,13 +239,37 @@ def comfyui_pixelart_real_oneshot(
|
||||
base_path = fetched["path"]
|
||||
out["base_path"] = base_path
|
||||
|
||||
# --- Fase 1b (opcional): autocrop al contenido + cuadrar (sujeto llena el frame). ---
|
||||
# La imagen sobre la que se hace el downscale: la recortada si autocrop, o la base.
|
||||
pre_ds_path = base_path
|
||||
crop_path = ""
|
||||
if autocrop:
|
||||
crop_path = os.path.join(dest, f"{filename_prefix}_{size}px_crop.png")
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(base_path) as base_im:
|
||||
src_im = base_im.convert("RGBA") if transparent else base_im.convert("RGB")
|
||||
before = src_im.size
|
||||
cropped = crop_to_content(
|
||||
src_im, pad_ratio=float(crop_pad_ratio), square=True,
|
||||
)
|
||||
cropped.save(crop_path)
|
||||
pre_ds_path = crop_path
|
||||
out["autocrop_applied"] = cropped.size != before
|
||||
except (ImportError, OSError, ValueError) as exc:
|
||||
# Autocrop es best-effort: si falla, se sigue con la base sin recortar.
|
||||
crop_path = ""
|
||||
pre_ds_path = base_path
|
||||
if not out["error"]:
|
||||
out["error"] = f"autocrop fallo (no critico): {exc}"
|
||||
|
||||
# --- Fase 2a: downscale a un grid `size` x `size` (mid). ---
|
||||
mid_path = os.path.join(dest, f"{filename_prefix}_{size}px_mid.png")
|
||||
engine_used = engine
|
||||
|
||||
if engine == "pixeloe":
|
||||
ds = pixeloe_downscale(
|
||||
base_path, mid_path, mode=mode, target_size=int(size),
|
||||
pre_ds_path, mid_path, mode=mode, target_size=int(size),
|
||||
patch_size=patch_size, thickness=thickness, no_upscale=True,
|
||||
comfy_python=comfy_python,
|
||||
)
|
||||
@@ -235,10 +282,14 @@ def comfyui_pixelart_real_oneshot(
|
||||
|
||||
if engine_used == "nearest":
|
||||
# Downscale nearest simple a size x size (PIL en el venv del registry).
|
||||
# nearest preserva el alpha por canal: si transparent, conserva la silueta.
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(base_path) as src:
|
||||
small = src.convert("RGB").resize((int(size), int(size)), Image.NEAREST)
|
||||
with Image.open(pre_ds_path) as src:
|
||||
target_mode = "RGBA" if transparent else "RGB"
|
||||
small = src.convert(target_mode).resize(
|
||||
(int(size), int(size)), Image.NEAREST
|
||||
)
|
||||
small.save(mid_path)
|
||||
except (ImportError, OSError) as exc:
|
||||
out["error"] = f"downscale nearest fallo: {exc}"
|
||||
@@ -248,6 +299,25 @@ def comfyui_pixelart_real_oneshot(
|
||||
out["error"] = "no se genero la imagen intermedia (mid)"
|
||||
return out
|
||||
|
||||
# --- Fase 2a-bis: recombinar alpha tras pixeloe (PixelOE trabaja en RGB). ---
|
||||
# El nucleo de PixelOE convierte a RGB: el grid `mid` sale sin transparencia. Se
|
||||
# downscalea el alpha de la imagen pre-downscale por separado (nearest al mismo
|
||||
# size) y se reaplica al grid para no perder el recorte ni la transparencia.
|
||||
if transparent and engine_used == "pixeloe":
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(pre_ds_path) as src_im:
|
||||
alpha = src_im.convert("RGBA").getchannel("A").resize(
|
||||
(int(size), int(size)), Image.NEAREST
|
||||
)
|
||||
with Image.open(mid_path) as mid_im:
|
||||
mid_rgba = mid_im.convert("RGBA")
|
||||
mid_rgba.putalpha(alpha)
|
||||
mid_rgba.save(mid_path)
|
||||
except (ImportError, OSError) as exc:
|
||||
if not out["error"]:
|
||||
out["error"] = f"recombinacion de alpha fallo (no critico): {exc}"
|
||||
|
||||
# --- Fase 2b: cuantizacion dura (paleta exacta) sobre el grid ya hecho. ---
|
||||
final_tag = palette if isinstance(palette, str) else f"q{colors}"
|
||||
final_path = os.path.join(
|
||||
@@ -255,7 +325,7 @@ def comfyui_pixelart_real_oneshot(
|
||||
)
|
||||
quant = comfyui_pixelize_image(
|
||||
mid_path, final_path, downscale=1, colors=int(colors),
|
||||
palette=palette, upscale_back=False,
|
||||
palette=palette, upscale_back=False, keep_alpha=bool(transparent),
|
||||
)
|
||||
if not quant.get("ok"):
|
||||
out["error"] = f"cuantizacion fallo: {quant.get('error')}"
|
||||
@@ -264,6 +334,7 @@ def comfyui_pixelart_real_oneshot(
|
||||
out["out_path"] = final_path
|
||||
out["size"] = quant["size"][0] if quant.get("size") else int(size)
|
||||
out["colors_final"] = quant.get("n_colors_final", 0)
|
||||
out["has_alpha"] = bool(quant.get("has_alpha", False))
|
||||
out["engine_used"] = engine_used
|
||||
|
||||
# --- Fase 3 (opcional): preview re-escalado nearest a pixeles duros. ---
|
||||
@@ -274,7 +345,8 @@ def comfyui_pixelart_real_oneshot(
|
||||
try:
|
||||
from PIL import Image
|
||||
with Image.open(final_path) as fin:
|
||||
up = fin.convert("RGB").resize(
|
||||
prev_mode = "RGBA" if transparent else "RGB"
|
||||
up = fin.convert(prev_mode).resize(
|
||||
(int(upscale_preview), int(upscale_preview)), Image.NEAREST
|
||||
)
|
||||
up.save(up_path)
|
||||
@@ -285,11 +357,13 @@ def comfyui_pixelart_real_oneshot(
|
||||
if not out["error"]:
|
||||
out["error"] = f"preview upscale fallo (no critico): {exc}"
|
||||
|
||||
# Limpieza opcional de la base y del intermedio.
|
||||
try:
|
||||
os.remove(mid_path)
|
||||
except OSError:
|
||||
pass
|
||||
# Limpieza de intermedios (mid + crop temporal).
|
||||
for tmp in (mid_path, crop_path):
|
||||
if tmp:
|
||||
try:
|
||||
os.remove(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
if not keep_base:
|
||||
try:
|
||||
os.remove(base_path)
|
||||
@@ -305,8 +379,9 @@ if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
res = comfyui_pixelart_real_oneshot(
|
||||
"pixel art knight, full body, side view, game sprite",
|
||||
"pixel art knight, full body, centered, game sprite",
|
||||
size=64, colors=16, engine="pixeloe", seed=42,
|
||||
transparent=True, autocrop=True,
|
||||
dest_dir="/tmp/comfy_pixelart_real",
|
||||
)
|
||||
print(json.dumps(res, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user