feat(gamedev-2d): pipeline walk_cycle_oneshot — personaje andando en pixel-art animado
Promueve el caso 1 del report 0217 (animacion de sprites de personaje) a un pipeline one-shot: de un prompt de personaje a un sprite sheet + GIF/WEBP en loop, frame-by-frame dirigido por pose (ControlNet OpenPose + seed fija + Rembg) con cada frame pixelizado a NxN RGBA. Nuevas funciones reutilizables (issue 0087, crecimiento por composicion): - comfyui_walk_cycle_oneshot (pipeline): orquesta poses -> generacion -> pixelizado -> ensamblado. No-throw, salta frames que fallan. Modo openpose (esqueletos reales) con fallback prompt-pose. - render_openpose_walk_skeletons: dibuja N esqueletos OpenPose COCO-18 del walk cycle (el insumo que el report 0217 marco como faltante). - comfyui_pixelize_sprite_png: PNG existente -> NxN RGBA pixel-art real (compone crop_to_content + pixeloe_downscale + comfyui_pixelize_image). - assemble_animated_sprite: frames RGBA -> sprite sheet horizontal + WEBP/GIF loop. - comfyui_build_walk_cycle_workflow (pura): grafo API del workflow animado para la UI (ControlNet OpenPose -> KSampler xN seed fija -> ImageBatch -> Rembg -> SaveAnimatedWEBP). Verificado en GPU: GIF/WEBP de caballero andando, 4 frames 32x32 (y 64x64) RGBA con fondo transparente y 16 colores, identidad de silueta consistente, piernas que cambian. Metodo de poses usado: OpenPose real (sin fallback). Evidencia en report 0221. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
---
|
||||
name: comfyui_pixelize_sprite_png
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_pixelize_sprite_png(src_path: str, dst_path: str, *, size: int = 32, colors: int = 16, engine: str = 'pixeloe', palette=None, transparent: bool = True, autocrop: bool = True, crop_pad_ratio: float = 0.02, mode: str = 'contrast', patch_size: int = 16, thickness: int = 2, alpha_threshold: int = 128, comfy_python: str | None = None) -> dict"
|
||||
description: "Pixeliza un PNG existente (un render a alta resolucion, p.ej. 512x768 RGBA con fondo transparente) a un sprite pixel-art REAL de size x size RGBA. Extrae la logica de pixelizado de un PNG existente: la misma secuencia que comfyui_pixelart_real_oneshot aplica internamente (fases 1b/2a/2a-bis/2b), pero desacoplada de la generacion -> sirve para pixelizar cada frame de una animacion, una hoja de sprites o cualquier render existente sin volver a pasar por la difusion. Compone tres funciones del registry: crop_to_content (autocrop al contenido + cuadrar para llenar el frame) -> pixeloe_downscale (downscale contrast-aware que conserva la silueta, engine='pixeloe', con fallback automatico a nearest) -> comfyui_pixelize_image (cuantizacion dura a N colores libres o paleta fija pico-8/nes/game-boy, alpha-aware). PixelOE trabaja en RGB y pierde el alpha, asi que se downscalea el canal alpha aparte (nearest) y se reaplica al grid antes de cuantizar. Impura: lectura/escritura de disco + subprocess del bridge de pixeloe. No-throw: todo error viaja en el campo error del dict. Devuelve {ok, out_path, size, colors_final, has_alpha, engine_used, autocrop_applied, error}."
|
||||
tags: [gamedev-2d, comfyui, pixelart, sprite, ml, downscale, quantize, palette, alpha, transparent, animation]
|
||||
uses_functions: [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: []
|
||||
params:
|
||||
- name: src_path
|
||||
desc: "ruta del PNG de entrada (un render a alta resolucion, p.ej. 512x768 RGBA con fondo transparente). Debe existir."
|
||||
- name: dst_path
|
||||
desc: "ruta del PNG de salida size x size (se crea el directorio si falta)."
|
||||
- name: size
|
||||
desc: "lado del grid final en pixeles (32 iconos/objetos simples, 64 personajes/sprites). Debe ser >= 1. keyword-only."
|
||||
- name: colors
|
||||
desc: "numero de colores de la paleta libre cuando palette es None (cuantizacion MEDIANCUT). keyword-only."
|
||||
- name: engine
|
||||
desc: "'pixeloe' (downscale contrast-aware, para sujetos con silueta: personajes/criaturas/iconos) o 'nearest' (downscale nearest simple, mas barato, para tiles/texturas/fondos sin contorno). Si 'pixeloe' falla o la lib no esta disponible, cae automaticamente a 'nearest' y lo refleja en engine_used. keyword-only."
|
||||
- name: palette
|
||||
desc: "None (paleta libre a 'colors'), nombre builtin ('pico-8','nes','game-boy') o lista de hex. Una paleta fija ignora 'colors'. keyword-only."
|
||||
- name: transparent
|
||||
desc: "si True (default) trata la entrada como RGBA y produce un sprite RGBA con transparencia real (el fondo transparente no entra en la paleta). Para tiles/texturas opacas, False produce salida RGB. keyword-only."
|
||||
- name: autocrop
|
||||
desc: "si True (default) recorta el PNG al bounding box de su contenido y lo 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 RGB. keyword-only."
|
||||
- name: crop_pad_ratio
|
||||
desc: "margen relativo que deja el autocrop alrededor del sujeto (0.02 = 2% del lado). keyword-only."
|
||||
- name: mode
|
||||
desc: "modo de downscale de PixelOE ('contrast' SOTA, 'k-centroid', 'nearest', 'center', 'bicubic'); solo aplica con engine='pixeloe'. keyword-only."
|
||||
- name: patch_size
|
||||
desc: "tamano de patch de PixelOE (default 16). keyword-only."
|
||||
- name: thickness
|
||||
desc: "grosor del outline expansion de PixelOE (default 2). keyword-only."
|
||||
- name: alpha_threshold
|
||||
desc: "umbral (0..255) para binarizar el alpha en opaco (255) o transparente (0) en la cuantizacion final. Solo aplica si transparent. keyword-only."
|
||||
- name: comfy_python
|
||||
desc: "ruta al interprete de ComfyUI (con la lib pixeloe) para el bridge; None autodetecta COMFY_PYTHON y luego ~/ComfyUI/.venv/bin/python3. keyword-only."
|
||||
output: "dict con ok (bool, True si se produjo el PNG final), out_path (str, ruta del PNG final size x size; vacio si fallo), size (int, lado real del PNG final), colors_final (int, colores distintos en el resultado; en la zona opaca si es RGBA), has_alpha (bool, True si el PNG es RGBA con transparencia), engine_used (str, 'pixeloe' o 'nearest' reflejando el fallback real), autocrop_applied (bool, True si el autocrop recorto/cuadro la imagen), error (str, vacio si todo OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_pixelize_sprite_png.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_pixelize_sprite_png import comfyui_pixelize_sprite_png
|
||||
|
||||
# Un render existente de 512x768 RGBA con fondo transparente -> sprite pixel-art 32x32
|
||||
res = comfyui_pixelize_sprite_png(
|
||||
os.path.expanduser("~/ComfyUI/output/knight_hi_res.png"),
|
||||
"/tmp/knight_32.png",
|
||||
size=32, colors=16, transparent=True,
|
||||
)
|
||||
# {'ok': True, 'out_path': '/tmp/knight_32.png', 'size': 32, 'colors_final': 16,
|
||||
# 'has_alpha': True, 'engine_used': 'pixeloe', 'autocrop_applied': True, 'error': ''}
|
||||
|
||||
# Pixelizar cada frame de una animacion a 48px con paleta fija PICO-8
|
||||
for i, frame in enumerate(["walk_0.png", "walk_1.png", "walk_2.png", "walk_3.png"]):
|
||||
comfyui_pixelize_sprite_png(
|
||||
f"/tmp/anim/{frame}", f"/tmp/anim/px_{i}.png",
|
||||
size=48, palette="pico-8", transparent=True,
|
||||
)
|
||||
|
||||
# Un tile/textura sin silueta -> downscale nearest barato, sin transparencia
|
||||
comfyui_pixelize_sprite_png(
|
||||
"/tmp/grass_tile.png", "/tmp/grass_16.png",
|
||||
size=16, colors=8, engine="nearest", transparent=False,
|
||||
)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando ya tienes un PNG renderizado a alta resolucion y necesitas su version
|
||||
pixel-art REAL (grid duro + paleta limitada) **sin regenerar** con la difusion: cada
|
||||
frame de una animacion, una hoja de sprites entera, un render externo, o el resultado
|
||||
de cualquier otra funcion que produzca PNGs. Es la pieza desacoplada del pixelizado
|
||||
que `comfyui_pixelart_real_oneshot` usa por dentro tras generar — usala directamente
|
||||
cuando la generacion no es parte del trabajo. Usa `engine="pixeloe"` para sujetos con
|
||||
silueta (personajes, criaturas, iconos con contorno) y `engine="nearest"` para
|
||||
tiles/texturas/fondos planos sin contorno (mas barato). Para llevar el resultado a
|
||||
Godot con filtro Nearest, encadena con `comfyui_export_asset_to_godot`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Necesita la lib `pixeloe`** (en `~/ComfyUI/.venv`) para `engine="pixeloe"`: se
|
||||
invoca via bridge de subprocess (`pixeloe_downscale`). Si la lib no esta o falla,
|
||||
cae automaticamente a `engine="nearest"` y lo refleja en `engine_used` + deja la
|
||||
nota del fallo en `error` (el resultado sigue siendo valido). Pasa `comfy_python`
|
||||
para apuntar a otro interprete con pixeloe.
|
||||
- **Todo error es dict `ok=False`** (no excepcion): `src_path` inexistente, `size < 1`,
|
||||
`engine` distinto de pixeloe/nearest -> `error` lo explica. No crashea ni borra nada.
|
||||
- **`autocrop` es best-effort**: si el recorte falla (PIL/lectura), se sigue con el PNG
|
||||
original sin recortar, `autocrop_applied=False` y la nota va en `error` (no critico).
|
||||
`crop_to_content` cuadra el sujeto para que llene el frame — sin esto un sujeto que
|
||||
ocupa el 25% del lienzo queda diminuto a 32px.
|
||||
- **`transparent` espera entrada RGBA**: con `transparent=True` el alpha se preserva y
|
||||
el fondo transparente NO entra en la paleta. PixelOE trabaja en RGB y perderia el
|
||||
alpha, asi que se downscalea el canal alpha aparte (nearest) y se reaplica al grid
|
||||
antes de cuantizar (fase 2a-bis). Con `transparent=False` la salida es RGB opaca.
|
||||
- **`palette` fija (pico-8/nes/game-boy o lista de hex) ignora `colors`**. `colors_final`
|
||||
cuenta colores RGB distintos REALES de la zona opaca: puede ser **menor** que `colors`
|
||||
o que el tamano de la paleta si el sprite no usa todos (un sprite de un solo color
|
||||
solido devuelve `colors_final=1`, correcto).
|
||||
- **CPU-only en la cuantizacion**; el unico coste GPU/red es nulo (PixelOE es CPU via
|
||||
bridge). Los intermedios (crop, mid) se escriben en un directorio temporal y se
|
||||
limpian siempre, incluso si la cuantizacion falla.
|
||||
Reference in New Issue
Block a user