Files
amtarr-ComfyUI-TextureAlchemy/blend_utils.py
T
2025-12-02 16:37:00 -08:00

399 lines
13 KiB
Python

"""
Blending and Compositing Utilities for TextureAlchemy
Advanced image blending with multiple modes and layer support
"""
import torch
import torch.nn.functional as F
class BlendModeUtility:
"""
Apply Photoshop-style blend modes between two images
Supports all major blend modes for texture compositing
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"base": ("IMAGE",),
"blend": ("IMAGE",),
"mode": ([
"normal", "multiply", "screen", "overlay", "soft_light", "hard_light",
"color_dodge", "color_burn", "linear_dodge", "linear_burn",
"vivid_light", "linear_light", "pin_light", "hard_mix",
"difference", "exclusion", "subtract", "divide",
"hue", "saturation", "color", "luminosity",
"lighter", "darker", "lighten", "darken"
],),
"opacity": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "number",
"tooltip": "Layer opacity (0.0 = fully transparent, 1.0 = fully opaque)"
}),
},
"optional": {
"mask": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("result",)
FUNCTION = "apply_blend"
CATEGORY = "Texture Alchemist/Blending"
def apply_blend(self, base, blend, mode, opacity, mask=None):
"""Apply blend mode between two images"""
print(f"\n{'='*60}")
print(f"Blend Mode Utility - {mode.upper()}")
print(f"{'='*60}")
# Ensure same dimensions
if base.shape != blend.shape:
blend = F.interpolate(
blend.permute(0, 3, 1, 2),
size=(base.shape[1], base.shape[2]),
mode='bilinear',
align_corners=False
).permute(0, 2, 3, 1)
# Apply blend mode
result = self._blend(base, blend, mode)
# Apply opacity
if opacity < 1.0:
result = base * (1.0 - opacity) + result * opacity
# Apply mask if provided
if mask is not None:
if mask.shape != base.shape:
mask = F.interpolate(
mask.permute(0, 3, 1, 2),
size=(base.shape[1], base.shape[2]),
mode='bilinear',
align_corners=False
).permute(0, 2, 3, 1)
# Convert to grayscale if needed
if mask.shape[3] > 1:
mask = mask.mean(dim=3, keepdim=True)
result = base * (1.0 - mask) + result * mask
result = torch.clamp(result, 0.0, 1.0)
print(f"✓ Blend applied: {mode}")
print(f" Opacity: {opacity:.2f}")
print(f" Mask: {'Yes' if mask is not None else 'No'}")
print(f"{'='*60}\n")
return (result,)
def _blend(self, base, blend, mode):
"""Apply specific blend mode"""
# Basic blend modes
if mode == "normal":
return blend
elif mode == "multiply":
return base * blend
elif mode == "screen":
return 1.0 - (1.0 - base) * (1.0 - blend)
elif mode == "overlay":
return torch.where(
base < 0.5,
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend)
)
elif mode == "soft_light":
return torch.where(
blend < 0.5,
2.0 * base * blend + base * base * (1.0 - 2.0 * blend),
2.0 * base * (1.0 - blend) + torch.sqrt(base) * (2.0 * blend - 1.0)
)
elif mode == "hard_light":
return torch.where(
blend < 0.5,
2.0 * base * blend,
1.0 - 2.0 * (1.0 - base) * (1.0 - blend)
)
elif mode == "color_dodge":
return torch.clamp(base / (1.0 - blend + 1e-7), 0.0, 1.0)
elif mode == "color_burn":
return torch.clamp(1.0 - (1.0 - base) / (blend + 1e-7), 0.0, 1.0)
elif mode == "linear_dodge":
return torch.clamp(base + blend, 0.0, 1.0)
elif mode == "linear_burn":
return torch.clamp(base + blend - 1.0, 0.0, 1.0)
elif mode == "vivid_light":
return torch.where(
blend < 0.5,
torch.clamp(1.0 - (1.0 - base) / (2.0 * blend + 1e-7), 0.0, 1.0),
torch.clamp(base / (2.0 * (1.0 - blend) + 1e-7), 0.0, 1.0)
)
elif mode == "linear_light":
return torch.clamp(base + 2.0 * blend - 1.0, 0.0, 1.0)
elif mode == "pin_light":
return torch.where(
blend < 0.5,
torch.min(base, 2.0 * blend),
torch.max(base, 2.0 * (blend - 0.5))
)
elif mode == "hard_mix":
vl = self._blend(base, blend, "vivid_light")
return torch.where(vl < 0.5, torch.zeros_like(vl), torch.ones_like(vl))
elif mode == "difference":
return torch.abs(base - blend)
elif mode == "exclusion":
return base + blend - 2.0 * base * blend
elif mode == "subtract":
return torch.clamp(base - blend, 0.0, 1.0)
elif mode == "divide":
return torch.clamp(base / (blend + 1e-7), 0.0, 1.0)
elif mode == "lighter":
return torch.max(base, blend)
elif mode == "darker":
return torch.min(base, blend)
elif mode == "lighten":
return torch.max(base, blend)
elif mode == "darken":
return torch.min(base, blend)
# HSL blend modes
elif mode in ["hue", "saturation", "color", "luminosity"]:
return self._blend_hsl(base, blend, mode)
return blend
def _blend_hsl(self, base, blend, mode):
"""HSL-based blend modes"""
base_hsv = self._rgb_to_hsv(base)
blend_hsv = self._rgb_to_hsv(blend)
if mode == "hue":
result_hsv = torch.cat([
blend_hsv[:, :, :, 0:1],
base_hsv[:, :, :, 1:2],
base_hsv[:, :, :, 2:3]
], dim=3)
elif mode == "saturation":
result_hsv = torch.cat([
base_hsv[:, :, :, 0:1],
blend_hsv[:, :, :, 1:2],
base_hsv[:, :, :, 2:3]
], dim=3)
elif mode == "color":
result_hsv = torch.cat([
blend_hsv[:, :, :, 0:1],
blend_hsv[:, :, :, 1:2],
base_hsv[:, :, :, 2:3]
], dim=3)
else: # luminosity
result_hsv = torch.cat([
base_hsv[:, :, :, 0:1],
base_hsv[:, :, :, 1:2],
blend_hsv[:, :, :, 2:3]
], dim=3)
return self._hsv_to_rgb(result_hsv)
def _rgb_to_hsv(self, rgb):
"""Convert RGB to HSV"""
r, g, b = rgb[:, :, :, 0], rgb[:, :, :, 1], rgb[:, :, :, 2]
max_c = torch.max(torch.max(r, g), b)
min_c = torch.min(torch.min(r, g), b)
diff = max_c - min_c
# Hue
h = torch.zeros_like(max_c)
mask = diff != 0
h = torch.where(
(max_c == r) & mask,
((g - b) / (diff + 1e-7)) % 6.0,
h
)
h = torch.where(
(max_c == g) & mask,
((b - r) / (diff + 1e-7)) + 2.0,
h
)
h = torch.where(
(max_c == b) & mask,
((r - g) / (diff + 1e-7)) + 4.0,
h
)
h = h / 6.0
# Saturation
s = torch.where(max_c != 0, diff / (max_c + 1e-7), torch.zeros_like(max_c))
# Value
v = max_c
return torch.stack([h, s, v], dim=3)
def _hsv_to_rgb(self, hsv):
"""Convert HSV to RGB"""
h, s, v = hsv[:, :, :, 0] * 6.0, hsv[:, :, :, 1], hsv[:, :, :, 2]
c = v * s
x = c * (1.0 - torch.abs(h % 2.0 - 1.0))
m = v - c
h_floor = torch.floor(h) % 6
r = torch.zeros_like(h)
g = torch.zeros_like(h)
b = torch.zeros_like(h)
mask0 = h_floor == 0
mask1 = h_floor == 1
mask2 = h_floor == 2
mask3 = h_floor == 3
mask4 = h_floor == 4
mask5 = h_floor == 5
r = torch.where(mask0, c, r)
r = torch.where(mask1, x, r)
r = torch.where(mask2 | mask3, torch.zeros_like(r), r)
r = torch.where(mask4, x, r)
r = torch.where(mask5, c, r)
g = torch.where(mask0, x, g)
g = torch.where(mask1, c, g)
g = torch.where(mask2, c, g)
g = torch.where(mask3, x, g)
g = torch.where(mask4 | mask5, torch.zeros_like(g), g)
b = torch.where(mask0 | mask1, torch.zeros_like(b), b)
b = torch.where(mask2, x, b)
b = torch.where(mask3, c, b)
b = torch.where(mask4, c, b)
b = torch.where(mask5, x, b)
r = r + m
g = g + m
b = b + m
return torch.stack([r, g, b], dim=3)
class MultiTextureBlender:
"""
Blend up to 4 textures with individual blend modes, opacity, and masks
Professional layer-based compositing
"""
@classmethod
def INPUT_TYPES(cls):
blend_modes = [
"normal", "multiply", "screen", "overlay", "soft_light", "hard_light",
"color_dodge", "color_burn", "linear_dodge", "linear_burn",
"difference", "subtract", "lighter", "darker"
]
return {
"required": {
"base": ("IMAGE",),
},
"optional": {
"layer1": ("IMAGE",),
"layer1_mode": (blend_modes, {"default": "normal"}),
"layer1_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number"}),
"layer1_mask": ("IMAGE",),
"layer2": ("IMAGE",),
"layer2_mode": (blend_modes, {"default": "multiply"}),
"layer2_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number"}),
"layer2_mask": ("IMAGE",),
"layer3": ("IMAGE",),
"layer3_mode": (blend_modes, {"default": "overlay"}),
"layer3_opacity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01, "display": "number"}),
"layer3_mask": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("result",)
FUNCTION = "blend_multi"
CATEGORY = "Texture Alchemist/Blending"
def blend_multi(self, base, **kwargs):
"""Blend multiple layers"""
print(f"\n{'='*60}")
print(f"Multi-Texture Blender")
print(f"{'='*60}")
result = base.clone()
blender = BlendModeUtility()
layers_applied = 0
for i in range(1, 4):
layer_key = f"layer{i}"
mode_key = f"layer{i}_mode"
opacity_key = f"layer{i}_opacity"
mask_key = f"layer{i}_mask"
if layer_key in kwargs and kwargs[layer_key] is not None:
layer = kwargs[layer_key]
mode = kwargs.get(mode_key, "normal")
opacity = kwargs.get(opacity_key, 1.0)
mask = kwargs.get(mask_key, None)
print(f"\n Layer {i}:")
print(f" Mode: {mode}")
print(f" Opacity: {opacity:.2f}")
print(f" Masked: {'Yes' if mask is not None else 'No'}")
result = blender.apply_blend(result, layer, mode, opacity, mask)[0]
layers_applied += 1
print(f"\n✓ Blended {layers_applied} layers")
print(f"{'='*60}\n")
return (result,)
# Node registration
NODE_CLASS_MAPPINGS = {
"BlendModeUtility": BlendModeUtility,
"MultiTextureBlender": MultiTextureBlender,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BlendModeUtility": "Blend Mode Utility",
"MultiTextureBlender": "Multi-Texture Blender",
}