399 lines
13 KiB
Python
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",
|
|
}
|
|
|