Files
orion4d-ComfyUI_DAO_master/dao_blur.py
T
2025-08-21 20:49:25 +02:00

187 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
# DAO_master — Blur (Gaussian) — IMAGE + MASK + drop shadow (couleur hex)
# Node: dao_Blur / class DAOBlur
import numpy as np
from PIL import Image, ImageOps, ImageFilter
try:
import torch
except Exception:
torch = None
# ---------- Helpers IMAGE/MASK ----------
def _tensor_to_pil(img):
if img is None:
return None
if (torch is not None) and isinstance(img, torch.Tensor):
arr = img[0].detach().cpu().numpy()
else:
arr = img[0]
arr = (np.clip(arr, 0.0, 1.0) * 255.0).astype(np.uint8)
if arr.ndim == 3 and arr.shape[-1] == 4:
return Image.fromarray(arr, "RGBA")
if arr.ndim == 3 and arr.shape[-1] >= 3:
return Image.fromarray(arr[..., :3], "RGB")
return Image.fromarray(arr.squeeze().astype(np.uint8), "L").convert("RGBA")
def _pil_to_tensor(img: Image.Image):
arr = np.asarray(img).astype(np.float32) / 255.0
if arr.ndim == 2:
arr = np.stack([arr, arr, arr], axis=-1)
return torch.from_numpy(arr).unsqueeze(0) if torch is not None else arr[None, ...]
def _mask_from_rgba(img: Image.Image):
if img.mode != "RGBA":
h, w = img.size[1], img.size[0]
m = np.ones((h, w), np.float32)
return torch.from_numpy(m).unsqueeze(0) if torch is not None else m[None, ...]
a = np.asarray(img.split()[-1], np.float32) / 255.0
return torch.from_numpy(a).unsqueeze(0) if torch is not None else a[None, ...]
def _mask_tensor_to_pil(mask):
if mask is None:
return None
if (torch is not None) and isinstance(mask, torch.Tensor):
arr = mask[0].detach().cpu().numpy()
else:
arr = mask[0]
arr = (np.clip(arr, 0.0, 1.0) * 255.0).astype(np.uint8)
return Image.fromarray(arr, "L")
def _pil_to_mask_tensor(img: Image.Image):
g = img.convert("L")
arr = np.asarray(g, dtype=np.float32) / 255.0
return torch.from_numpy(arr).unsqueeze(0) if torch is not None else arr[None, ...]
# ---------- Color utils ----------
def _parse_hex_color(s: str):
"""
Retourne (R,G,B,A) 0..255 depuis #RGB, #RGBA, #RRGGBB, #RRGGBBAA (insensible à la casse).
Si invalide -> noir opaque.
"""
if not isinstance(s, str):
return (0, 0, 0, 255)
x = s.strip()
if x.startswith("#"):
x = x[1:]
x = x.lower()
try:
if len(x) == 3: # RGB
r, g, b = [int(c * 2, 16) for c in x]
return (r, g, b, 255)
if len(x) == 4: # RGBA
r, g, b, a = [int(c * 2, 16) for c in x]
return (r, g, b, a)
if len(x) == 6: # RRGGBB
r = int(x[0:2], 16); g = int(x[2:4], 16); b = int(x[4:6], 16)
return (r, g, b, 255)
if len(x) == 8: # RRGGBBAA
r = int(x[0:2], 16); g = int(x[2:4], 16); b = int(x[4:6], 16); a = int(x[6:8], 16)
return (r, g, b, a)
except Exception:
pass
return (0, 0, 0, 255)
# ---------- NODE ----------
class DAOBlur:
CATEGORY = "DAO_master/Filter"
FUNCTION = "apply"
RETURN_TYPES = ("IMAGE", "MASK", "IMAGE")
RETURN_NAMES = ("image", "mask", "drop_shadow")
OUTPUT_NODE = False
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"radius": ("FLOAT", {"default": 5.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"shadow_opacity": ("FLOAT", {"default": 50.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"shadow_color": ("STRING", {"default": "#000000"}), # ← couleur hex
"move_x": ("INT", {"default": 0, "min": -8192, "max": 8192}),
"move_y": ("INT", {"default": 0, "min": -8192, "max": 8192}),
"invert_drop_shadow": ("BOOLEAN", {"default": True}),
},
"optional": {
"image": ("IMAGE", {}),
"mask": ("MASK", {}),
"mask_form": ("MASK", {}),
"apply_mask_to_alpha": ("BOOLEAN", {"default": True}),
"invert_mask": ("BOOLEAN", {"default": False}),
},
}
def apply(self, radius, shadow_opacity, shadow_color, move_x, move_y, invert_drop_shadow,
image=None, mask=None, mask_form=None,
apply_mask_to_alpha=True, invert_mask=False):
r = float(max(0.0, min(100.0, radius)))
opacity_scale = float(max(0.0, min(100.0, shadow_opacity))) / 100.0
cr, cg, cb, ca = _parse_hex_color(shadow_color)
color_alpha_scale = (ca / 255.0) * opacity_scale # alpha hex * opacité slider
# --- Entrées -> PIL ---
pil_img = _tensor_to_pil(image) if image is not None else None
pil_msk = _mask_tensor_to_pil(mask) if mask is not None else None
pil_form = _mask_tensor_to_pil(mask_form) if mask_form is not None else None
if pil_img is None:
pil_img = Image.new("RGBA", (1, 1), (0, 0, 0, 0))
if pil_msk is None:
pil_msk = _mask_tensor_to_pil(_mask_from_rgba(pil_img))
if invert_mask:
pil_msk = ImageOps.invert(pil_msk.convert("L"))
# --- Blur image & mask ---
pil_img = pil_img.convert("RGBA").filter(ImageFilter.GaussianBlur(r))
pil_msk = pil_msk.convert("L").filter(ImageFilter.GaussianBlur(r))
# --- Appliquer mask_form en intersection (multiplicative) ---
if pil_form is not None:
formL = pil_form.convert("L")
a = np.asarray(pil_msk, dtype=np.float32)
b = np.asarray(formL, dtype=np.float32) / 255.0
a = np.clip(a * b, 0, 255).astype(np.uint8)
pil_msk = Image.fromarray(a, "L")
# --- Image principale : alpha depuis mask final (optionnel) ---
if apply_mask_to_alpha:
rch, gch, bch, _ = pil_img.split()
pil_img = Image.merge("RGBA", (rch, gch, bch, pil_msk))
# --- Drop Shadow colorée ---
base_alpha = np.asarray(pil_msk, dtype=np.uint8)
alpha_arr = (255 - base_alpha) if invert_drop_shadow else base_alpha.copy()
if color_alpha_scale < 1.0:
alpha_arr = (alpha_arr.astype(np.float32) * color_alpha_scale).clip(0, 255).astype(np.uint8)
alpha_ds = Image.fromarray(alpha_arr, "L")
w, h = pil_img.size
r_img = Image.new("L", (w, h), int(cr))
g_img = Image.new("L", (w, h), int(cg))
b_img = Image.new("L", (w, h), int(cb))
drop_shadow = Image.merge("RGBA", (r_img, g_img, b_img, alpha_ds))
# offset
if move_x != 0 or move_y != 0:
canvas = Image.new("RGBA", (w, h), (0, 0, 0, 0))
canvas.paste(drop_shadow, (int(move_x), int(move_y)))
drop_shadow = canvas
# --- Sorties ---
return (_pil_to_tensor(pil_img),
_pil_to_mask_tensor(pil_msk),
_pil_to_tensor(drop_shadow))