Files
MdONeilsl b6a2d1dd79 Add MD Color Mod nodes, operators, and web UI
Introduce a complete GIMP/GEGL-inspired color module for ComfyUI. Adds: node declarations (nodes/*) and matching operator implementations (py/*) for many color/image operations (balance, temperature, hue/chroma, hue/saturation, saturation, exposure, levels, invert, auto adjustments, maps, posterize, desaturate, tone mapping, component extract/mix, etc.). Includes support libs (py/libs: color, process, blur), layer compositing (layers_blending), VRAM‑aware chunking, mask handling and alpha preservation. Adds web UI extension and widgets (web/*), package metadata, dynamic __init__.py loader, README update and .gitignore tweaks.
2026-07-06 08:17:05 -04:00

210 lines
7.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import torch
# --------------------------------------------------------------------
# Blend mode helpers – kept as module-level functions for clean reuse
# --------------------------------------------------------------------
def blend_normal(bg_rgb, fg_rgb):
return fg_rgb
def blend_screen(bg_rgb, fg_rgb):
return 1.0 - (1.0 - bg_rgb) * (1.0 - fg_rgb)
def blend_multiply(bg_rgb, fg_rgb):
return bg_rgb * fg_rgb
def blend_overlay(bg_rgb, fg_rgb):
result = torch.where(bg_rgb <= 0.5, 2.0 * bg_rgb * fg_rgb,
1.0 - 2.0 * (1.0 - bg_rgb) * (1.0 - fg_rgb))
return result.clamp(0.0, 1.0)
def blend_difference(bg_rgb, fg_rgb):
return torch.abs(bg_rgb - fg_rgb)
def blend_dissolve(bg_rgb, fg_rgb):
noise = torch.rand_like(bg_rgb)
return torch.where(noise < 0.5, fg_rgb, bg_rgb)
def blend_clear(bg_rgb, fg_rgb):
return bg_rgb
def blend_add(bg_rgb, fg_rgb):
return torch.clamp(bg_rgb + fg_rgb, 0.0, 1.0)
def blend_subtract(bg_rgb, fg_rgb):
return torch.clamp(bg_rgb - fg_rgb, 0.0, 1.0)
def blend_replace(bg_rgb, fg_rgb):
return fg_rgb
def blend_darken_only(bg_rgb, fg_rgb):
return torch.min(bg_rgb, fg_rgb)
def blend_lighten_only(bg_rgb, fg_rgb):
return torch.max(bg_rgb, fg_rgb)
def blend_color_dodge(bg_rgb, fg_rgb):
result = bg_rgb / (1.0 - fg_rgb + 1e-8)
return result.clamp(0.0, 1.0)
def blend_color_burn(bg_rgb, fg_rgb):
result = 1.0 - (1.0 - bg_rgb) / (fg_rgb + 1e-8)
return result.clamp(0.0, 1.0)
def blend_hard_light(bg_rgb, fg_rgb):
result = torch.where(fg_rgb <= 0.5, 2.0 * bg_rgb * fg_rgb,
1.0 - 2.0 * (1.0 - bg_rgb) * (1.0 - fg_rgb))
return result.clamp(0.0, 1.0)
def blend_soft_light(bg_rgb, fg_rgb):
result = torch.where(fg_rgb <= 0.5,
(2.0 * bg_rgb * fg_rgb) + (bg_rgb * bg_rgb * (1.0 - 2.0 * fg_rgb)),
2.0 * bg_rgb * (1.0 - fg_rgb) + torch.sqrt(bg_rgb) * (2.0 * fg_rgb - 1.0))
return result.clamp(0.0, 1.0)
def blend_pin_light(bg_rgb, fg_rgb):
result = torch.where(fg_rgb <= 0.5,
torch.min(bg_rgb, 2.0 * fg_rgb),
torch.max(bg_rgb, 2.0 * fg_rgb - 1.0))
return result.clamp(0.0, 1.0)
def blend_linear_dodge_add(bg_rgb, fg_rgb):
return torch.clamp(bg_rgb + fg_rgb, 0.0, 1.0)
def blend_linear_burn_multiply(bg_rgb, fg_rgb):
return torch.clamp(bg_rgb + fg_rgb - 1.0, 0.0, 1.0)
def blend_vivid_light(bg_rgb, fg_rgb):
result = torch.where(fg_rgb <= 0.5,
1.0 - (1.0 - bg_rgb) / (2.0 * fg_rgb + 1e-8),
bg_rgb / (2.0 * (1.0 - fg_rgb) + 1e-8))
return result.clamp(0.0, 1.0)
def blend_halo(bg_rgb, fg_rgb):
edge = torch.abs(bg_rgb - fg_rgb)
glow = (bg_rgb + fg_rgb) * edge
return glow.clamp(0.0, 1.0)
def blend_negate(bg_rgb, fg_rgb):
return 1.0 - torch.abs(bg_rgb - fg_rgb)
def blend_merge_rgb(bg_rgb, fg_rgb):
return fg_rgb
def blend_merge_cyan(bg_rgb, fg_rgb):
cyan = 1.0 - fg_rgb[..., 0:1]
return cyan.repeat(1, 1, 1, 3)
def blend_merge_magenta(bg_rgb, fg_rgb):
magenta = 1.0 - fg_rgb[..., 1:2]
return magenta.repeat(1, 1, 1, 3)
def blend_merge_yellow(bg_rgb, fg_rgb):
yellow = 1.0 - fg_rgb[..., 2:3]
return yellow.repeat(1, 1, 1, 3)
def blend_merge_black(bg_rgb, fg_rgb):
intensity = fg_rgb.mean(dim=-1, keepdim=True)
return intensity.repeat(1, 1, 1, 3)
def blend_fill_fg_color(bg_rgb, fg_rgb):
return fg_rgb
def blend_fill_bg_color(bg_rgb, fg_rgb):
return bg_rgb
def blend_average(bg_rgb, fg_rgb):
return (bg_rgb + fg_rgb) * 0.5
def blend_composite(bg_rgb, fg_rgb):
return fg_rgb
def blend_behind(bg_rgb, fg_rgb):
return fg_rgb
def blend_template(bg_rgb, fg_rgb):
return fg_rgb
def composite_layer_stack(background, layer, blending_mode, transparency, mask=None):
"""
background, layer: tensors of shape [B, H, W, C] where C=3 or 4
mask: optional tensor [B, H, W] or [H, W]; values in [0,1]
transparency: global opacity multiplier
"""
device = background.device
layer = layer.to(device)
bg_h, bg_w = background.shape[1:3]
# ---------- 1. Ensure background and layer have 4 channels ----------
bg = background.clone()
if bg.shape[-1] == 3:
alpha_bg = torch.ones((bg.shape[0], bg_h, bg_w, 1), device=device)
bg = torch.cat([bg, alpha_bg], dim=-1)
else:
alpha_bg = bg[..., 3:4]
fg = layer.clone()
if fg.shape[-1] == 3:
alpha_fg = torch.ones((fg.shape[0], fg.shape[1], fg.shape[2], 1), device=device)
fg = torch.cat([fg, alpha_fg], dim=-1)
else:
alpha_fg = fg[..., 3:4]
bg_rgb = bg[..., :3]
fg_rgb = fg[..., :3]
# ---------- 2. Resize layer to match background dimensions ----------
if fg.shape[1:3] != (bg_h, bg_w):
fg_rgb = torch.nn.functional.interpolate(
fg_rgb.permute(0, 3, 1, 2), size=(bg_h, bg_w), mode='bilinear', align_corners=False
).permute(0, 2, 3, 1)
alpha_fg = torch.nn.functional.interpolate(
alpha_fg.permute(0, 3, 1, 2), size=(bg_h, bg_w), mode='bilinear', align_corners=False
).permute(0, 2, 3, 1)
# ---------- 3. Prepare mask ----------
if mask is None:
mask = torch.ones((bg.shape[0], bg_h, bg_w, 1), device=device)
else:
mask = mask.to(device)
if mask.dim() == 2: # (H, W)
mask = mask.unsqueeze(0).unsqueeze(-1)
elif mask.dim() == 3: # (B, H, W) or (B, H, W, 1)
if mask.shape[-1] != 1:
mask = mask.unsqueeze(-1)
if mask.shape[1:3] != (bg_h, bg_w):
mask = torch.nn.functional.interpolate(
mask.permute(0, 3, 1, 2), size=(bg_h, bg_w), mode='bilinear', align_corners=False
).permute(0, 2, 3, 1)
if mask.shape[0] < bg.shape[0]:
mask = mask.expand(bg.shape[0], -1, -1, -1)
# ---------- 4. Compute final opacity factor ----------
opacity = alpha_fg * mask * transparency # [B, H, W, 1]
# Special handling for "Clear" and "Behind"
if blending_mode.lower() == "clear":
opacity = torch.zeros_like(opacity)
elif blending_mode.lower() == "behind":
opacity = opacity * (1.0 - alpha_bg)
# ---------- 5. Apply RGB blend mode ----------
# Build a mapping from the existing module-level blend functions
blend_func_name = blending_mode.lower().replace(' ', '_').replace('(', '').replace(')', '').replace('-', '_')
# Use the functions defined at module level (they are in the same scope)
blend_func = globals().get(f"blend_{blend_func_name}", blend_normal)
blended_rgb = blend_func(bg_rgb, fg_rgb)
# ---------- 6. Composite RGB using opacity ----------
result_rgb = bg_rgb * (1.0 - opacity) + blended_rgb * opacity
# ---------- 7. Compute result alpha (standard over compositing) ----------
result_alpha = alpha_bg + (1.0 - alpha_bg) * opacity
# ---------- 8. Concatenate and clamp ----------
result = torch.cat([result_rgb, result_alpha], dim=-1).clamp(0.0, 1.0)
return (result,)