feat(gamedev): comfyui_generate_character_set_oneshot — set completo de un personaje coherente (2D + direccional 8-way + 3D)
Promueve a un pipeline one-shot la secuencia que hoy exige 4 llamadas a mano: generar el set COMPLETO de un personaje de juego (imagen base 2D recortada, sprite direccional N-way SV3D/Zero123 y malla 3D Hunyuan3D .glb), todos del MISMO personaje. La coherencia cross-frontera se garantiza por construccion: el direccional y el 3D parten de la MISMA base 2D aplanada (base_flat), no de tres generaciones independientes. Es la culminacion de las 5 fronteras del grupo gamedev-2d (issue 0087). Compone builders del registry (enemy_creature/portrait_avatar/topdown_sprite por introspeccion) + comfyui_flatten_alpha_on_color (nueva, aplana el sprite recortado sobre fondo solido que SV3D/Hunyuan exigen) + comfyui_image_to_3d_oneshot + comfyui_build_directional_sprite_workflow + submit/wait/fetch + export Godot. Secuencial liberando VRAM entre pasos pesados (3D antes que SV3D) para caber en 8 GB; fallo aislado deja set PARCIAL sin abortar. Probado e2e en GPU (RTX 3070 8 GB) con 'armored paladin': base 2D RGBA 512 recortada + malla glTF 395600 triangulos + 8 vistas direccionales SV3D 576, todos del mismo personaje. 9 tests offline verdes (incluye coherencia mockeada). Ver reports/0189. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
---
|
||||
name: comfyui_flatten_alpha_on_color
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_flatten_alpha_on_color(image_path: str, *, out_path: str | None = None, color: tuple = (255, 255, 255), size: int | None = None, resample: str = 'lanczos') -> dict"
|
||||
description: "Aplana (compone) un PNG con canal alpha sobre un fondo de color SOLIDO y opaco, produciendo un PNG RGB sin transparencia. Sirve para preparar un sprite ya recortado (fondo transparente) como entrada de modelos 3D / multivista (SV3D, Stable Zero123, Hunyuan3D), que esperan el sujeto sobre fondo limpio y opaco (tipicamente blanco), no sobre alpha 0. Evita el bug del LoadImage de ComfyUI, que hace convert('RGB') y descarta el alpha mostrando los RGB crudos bajo la transparencia (basura -> mala reconstruccion 3D). Opcionalmente reescala a size x size para encajar el tamano nativo del modelo (SV3D 576, Zero123 256). Nucleo PIL puro, CPU-only: sin GPU, sin red. Devuelve {ok, out_path, size, error}. Impura solo por la lectura/escritura de disco."
|
||||
tags: [comfyui, gamedev-2d, ml, matting, alpha, flatten, composite, 3d, pil, image]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_py_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: image_path
|
||||
desc: "ruta del PNG de entrada (idealmente con alpha; un sprite ya recortado). Se expande '~'. Si no existe -> ok=False sin lanzar excepcion."
|
||||
- name: out_path
|
||||
desc: "ruta del PNG RGB de salida; si None se escribe junto al original con sufijo '_flat' antes de la extension. Se crea el directorio destino si falta. keyword-only."
|
||||
- name: color
|
||||
desc: "color de fondo (r,g,b) en 0..255. Si llega (r,g,b,a) se ignora el alpha entrante (el fondo siempre es opaco). Por defecto blanco (255,255,255). keyword-only."
|
||||
- name: size
|
||||
desc: "si no es None, reescala el resultado a size x size (cuadrado), para encajar el tamano nativo del modelo 3D (SV3D 576, Zero123 256). keyword-only."
|
||||
- name: resample
|
||||
desc: "filtro de reescalado: 'lanczos' (por defecto), 'nearest', 'bilinear', 'bicubic', 'area'. String desconocido -> LANCZOS. keyword-only."
|
||||
output: "dict con ok (bool), out_path (str, ruta del PNG RGB; vacio si error), size ([w,h] final), error (str, vacio si OK)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_flatten_alpha_on_color.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_flatten_alpha_on_color import comfyui_flatten_alpha_on_color
|
||||
|
||||
# Sprite recortado (fondo transparente) -> base limpia sobre BLANCO a 576x576,
|
||||
# lista como entrada de SV3D / Stable Zero123 / Hunyuan3D.
|
||||
res = comfyui_flatten_alpha_on_color(
|
||||
os.path.expanduser("~/ComfyUI/output/goblin_cutout.png"),
|
||||
out_path="/tmp/goblin_flat.png",
|
||||
color=(255, 255, 255),
|
||||
size=576,
|
||||
)
|
||||
# {'ok': True, 'out_path': '/tmp/goblin_flat.png', 'size': [576, 576], 'error': ''}
|
||||
|
||||
# Sin out_path: escribe junto al original con sufijo _flat (goblin_cutout_flat.png)
|
||||
comfyui_flatten_alpha_on_color("~/ComfyUI/output/goblin_cutout.png", size=256)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Justo ANTES de meter un sprite recortado (PNG con alpha, p.ej. salida de rembg o de
|
||||
`comfyui_matting_luma_to_alpha`) en un workflow image-to-3D / multivista (SV3D
|
||||
turntable, Stable Zero123, Hunyuan3D). Esos modelos asumen el sujeto sobre un fondo
|
||||
SOLIDO y opaco, no sobre alpha 0; si les pasas el PNG con transparencia tal cual, el
|
||||
`LoadImage` de ComfyUI tira el alpha y reconstruye sobre los RGB basura que habia
|
||||
debajo. Tambien para "poner fondo blanco" a cualquier asset recortado antes de un
|
||||
catalogo o un thumbnail. Usa `size` para dejar la base ya al tamano nativo del modelo.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura**: lee y escribe disco. Crea el directorio destino si no existe. No borra
|
||||
el original.
|
||||
- **El `LoadImage` de ComfyUI hace `convert("RGB")`** y descarta el alpha: por eso hay
|
||||
que aplanar ANTES, no confiar en que el nodo respete la transparencia.
|
||||
- Todo error es **dict `ok=False`** (no excepcion): path inexistente, fallo de I/O o
|
||||
de decodificado de Pillow -> `error` lo explica. No crashea.
|
||||
- **Fondo blanco por defecto** `(255,255,255)`: es lo que esperan SV3D/Zero123. Cambia
|
||||
`color` solo si el modelo concreto pide otro fondo (algunos preferen gris medio).
|
||||
- Si la imagen ya es RGB (sin alpha) se compone igual (alpha implicito 255): el
|
||||
resultado es la misma imagen sobre el color, idempotente respecto al fondo.
|
||||
- Reescalado con **LANCZOS por defecto** (suave); usa `resample="nearest"` si la base
|
||||
es pixelart y no quieres interpolar el grid.
|
||||
- `color` con 4 componentes `(r,g,b,a)`: el `a` se ignora, el fondo es siempre opaco.
|
||||
@@ -0,0 +1,124 @@
|
||||
"""comfyui_flatten_alpha_on_color — aplana un PNG con alpha sobre un fondo solido.
|
||||
|
||||
Compone una imagen con canal alpha (tipicamente un sprite ya recortado, fondo
|
||||
transparente) sobre un fondo de color SOLIDO y opaco, produciendo un PNG RGB sin
|
||||
transparencia. Sirve para preparar la entrada de modelos 3D / multivista (SV3D,
|
||||
Stable Zero123, Hunyuan3D), que esperan el sujeto sobre un fondo limpio y opaco
|
||||
(normalmente blanco), no sobre alpha 0.
|
||||
|
||||
Por que importa: el `LoadImage` de ComfyUI hace `convert("RGB")` y DESCARTA el
|
||||
canal alpha, mostrando los RGB crudos que habia debajo de la transparencia. Esos
|
||||
RGB suelen ser basura (negro residual, halos, restos del fondo original) -> la
|
||||
reconstruccion 3D sale mala. Aplanar antes garantiza un fondo controlado.
|
||||
|
||||
La funcion es impura solo por la lectura/escritura de disco. Sin red, sin GPU,
|
||||
sin servidor ComfyUI. Solo Pillow.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
_RESAMPLE = {
|
||||
"lanczos": "LANCZOS",
|
||||
"nearest": "NEAREST",
|
||||
"bilinear": "BILINEAR",
|
||||
"bicubic": "BICUBIC",
|
||||
"area": "BOX",
|
||||
}
|
||||
|
||||
|
||||
def comfyui_flatten_alpha_on_color(
|
||||
image_path: str,
|
||||
*,
|
||||
out_path: str | None = None,
|
||||
color: tuple = (255, 255, 255),
|
||||
size: int | None = None,
|
||||
resample: str = "lanczos",
|
||||
) -> dict:
|
||||
"""Aplana un PNG con alpha sobre un fondo de color solido -> PNG RGB.
|
||||
|
||||
Args:
|
||||
image_path: ruta del PNG de entrada (idealmente con alpha; un sprite ya
|
||||
recortado). Se expande `~`. Si no existe devuelve ok=False sin lanzar.
|
||||
out_path: ruta del PNG RGB de salida; si None se escribe junto al original
|
||||
con sufijo "_flat" antes de la extension. Se crea el directorio
|
||||
destino si falta. keyword-only.
|
||||
color: color de fondo (r, g, b) en 0..255. Si llega (r, g, b, a) se ignora
|
||||
el alpha entrante (el fondo siempre es opaco). Por defecto blanco.
|
||||
keyword-only.
|
||||
size: si no es None, redimensiona el resultado a size x size (cuadrado),
|
||||
util para encajar la base al tamano nativo del modelo 3D (SV3D 576,
|
||||
Zero123 256). keyword-only.
|
||||
resample: filtro de redimension ("lanczos" por defecto, "nearest",
|
||||
"bilinear", "bicubic", "area"). String desconocido -> LANCZOS.
|
||||
keyword-only.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- ok (bool): True si se compuso y guardo.
|
||||
- out_path (str): ruta del PNG RGB generado (vacio si error).
|
||||
- size (list[int]): [w, h] final del resultado (ausente si error temprano).
|
||||
- error (str): mensaje de error; cadena vacia si todo OK.
|
||||
"""
|
||||
out = {"ok": False, "out_path": "", "size": [0, 0], "error": ""}
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
out["error"] = f"falta dependencia (PIL): {exc}"
|
||||
return out
|
||||
|
||||
src_path = os.path.expanduser(image_path)
|
||||
if not os.path.isfile(src_path):
|
||||
out["error"] = f"imagen no existe: {src_path!r}"
|
||||
return out
|
||||
|
||||
# Color de fondo opaco: usa solo (r, g, b), descarta cualquier alpha entrante.
|
||||
rgb = tuple(int(c) for c in color[:3])
|
||||
if len(rgb) != 3:
|
||||
out["error"] = f"color debe tener al menos 3 componentes (r,g,b), recibido {color!r}"
|
||||
return out
|
||||
bg_rgba = rgb + (255,)
|
||||
|
||||
resample_const = getattr(Image, _RESAMPLE.get(str(resample).lower(), "LANCZOS"))
|
||||
|
||||
try:
|
||||
with Image.open(src_path) as src:
|
||||
img = src.convert("RGBA")
|
||||
canvas = Image.new("RGBA", img.size, bg_rgba)
|
||||
flat = Image.alpha_composite(canvas, img).convert("RGB")
|
||||
|
||||
if size is not None:
|
||||
flat = flat.resize((int(size), int(size)), resample_const)
|
||||
|
||||
if out_path is None:
|
||||
base, ext = os.path.splitext(src_path)
|
||||
dst_path = base + "_flat" + (ext if ext.lower() == ".png" else ".png")
|
||||
else:
|
||||
dst_path = os.path.expanduser(out_path)
|
||||
|
||||
dst_dir = os.path.dirname(os.path.abspath(dst_path))
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
flat.save(dst_path, "PNG")
|
||||
except (OSError, ValueError) as exc:
|
||||
out["error"] = str(exc)
|
||||
return out
|
||||
except Exception as exc: # pragma: no cover - Pillow puede lanzar tipos varios
|
||||
out["error"] = str(exc)
|
||||
return out
|
||||
|
||||
out.update(ok=True, out_path=dst_path, size=list(flat.size), error="")
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("uso: comfyui_flatten_alpha_on_color.py <sprite_con_alpha> [out] [size]",
|
||||
file=sys.stderr)
|
||||
sys.exit(2)
|
||||
src = sys.argv[1]
|
||||
dst = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
sz = int(sys.argv[3]) if len(sys.argv) > 3 else None
|
||||
print(json.dumps(comfyui_flatten_alpha_on_color(src, out_path=dst, size=sz), indent=2))
|
||||
Reference in New Issue
Block a user