update new BlendMode V2, commit ImageBlendV2, ImageBlendAdvanceV2, DropShadowV2, InnerShadowV2, OuterGlowV2, InnerGlowV2, StrokeV2, ColorOverlayV2, GradientOverlayV2 nodes

This commit is contained in:
chflame
2024-05-03 17:32:15 +08:00
parent f493767c0e
commit 420d57b552
17 changed files with 4625 additions and 10 deletions
+7 -2
View File
@@ -70,6 +70,7 @@ When this error has occurred, please check the network environment.
## Update
<font size="4">**If the dependency package error after updating, please reinstall the relevant dependency packages. </font><br />
* Add new types of [Blend Mode V2](#BlendModeV2) between images. now supports up to 30 blend modes. The new blend mode is available for all V2 versions that support mixed mode nodes, including ImageBlend V2, ImageBlendAdvance V2, DropShadow V2, InnerShadow V2, OuterGlow V2, InnerGlow V2, Stroke V2, ColorOverlay V2, GradientOverlay V2.
* Commit [YoloV8Detect](#YoloV8Detect) node.
* Commit [QWenImage2Prompt](#QWenImage2Prompt) node, this node is repackage of the [ComfyUI_VLM_nodes](https://github.com/gokayfem/ComfyUI_VLM_nodes)'s ```UForm-Gen2 Qwen Node```, thanks to the original author.
* Commit [BooleanOperator](#BooleanOperator), [NumberCalculator](#NumberCalculator), [TextBox](#TextBox), [Integer](#Integer), [Float](#Float), [Boolean](#Boolean)nodes. These nodes can perform mathematical and logical operations.
@@ -107,7 +108,7 @@ When this error has occurred, please check the network environment.
(The CropByMask node only supports cuts of the same size. if a batch mask_for_crop inputted, the data from the first sheet will be used.)
* Commit [RemBgUltra](#RemBgUltra) and [PixelSpread](#PixelSpread) nodes significantly improved mask quality. *RemBgUltra requires manual model download.
* Commit [TextImage](#TextImage) node, it generate text images and masks.
* Add new types of [blend mode](#blend) between images. now supports up to 19 blend modes. add **color_burn, color_dodge, linear_burn, linear_dodge, overlay, soft_light, hard_light, vivid_light, pin_light, linear_light** and **hard_mix**.
* Add new types of [blend mode](#Blend) between images. now supports up to 19 blend modes. add **color_burn, color_dodge, linear_burn, linear_dodge, overlay, soft_light, hard_light, vivid_light, pin_light, linear_light** and **hard_mix**.
The newly added blend mode is applicable to all nodes that support blend mode.
* Commit [ColorMap](#ColorMap) filter node to create a pseudo color heatmap effect.
* Commit [WaterColor](#WaterColor) and [SkinBeauty](#SkinBeauty) nodes。These are image filters that generate watercolor and skin smoothness effects.
@@ -1439,10 +1440,14 @@ Node options:
<sup>2</sup> The mask not a mandatory input item. the alpha channel of the image is used by default. If the image input does not include an alpha channel, the entire image's alpha channel will be automatically created. if have masks input simultaneously, the alpha channel will be overwrite by the mask.
<sup>3</sup> The <a id="table1">blend</a> mode include **normal, multply, screen, add, subtract, difference, darker, color_burn, color_dodge, linear_burn, linear_dodge, overlay, soft_light, hard_light, vivid_light, pin_light, linear_light, and hard_mix.** all of 19 blend modes in total.
<sup>3</sup> The <a id="table1">Blend</a> Mode include **normal, multply, screen, add, subtract, difference, darker, color_burn, color_dodge, linear_burn, linear_dodge, overlay, soft_light, hard_light, vivid_light, pin_light, linear_light, and hard_mix.** all of 19 blend modes in total.
![image](image/blend_mode_result.png)
<font size="1">*Preview of the blend mode </font><br />
<sup>3</sup> The <a id="table1">BlendModeV2</a> include **normal, dissolve, darken, multiply, color burn, linear burn, darker color, lighten, screen, color dodge, linear dodge(add), lighter color, dodge, overlay, soft light, hard light, vivid light, linear light, pin light, hard mix, difference, exclusion, subtract, divide, hue, saturation, color, luminosity, grain extract, grain merge** all of 30 blend modes in total.
![image](image/blend_mode_v2_example.png)
<font size="1">*Preview of the Blend Mode V2</font><br />
<sup>4</sup> The RGB color described by hexadecimal RGB format, like '#FA3D86'.
<sup>5</sup> The layer_image and layer_mask must be of the same size.
+7 -3
View File
@@ -70,6 +70,7 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
## 更新说明
<font size="4">**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。
* 更新 [混合模式](#混合模式)到V2版本,现在支持多达30种混合模式。新增的混合模式适用于所有支持混合模式节点的V2版本,包括ImageBlend V2, ImageBlendAdvance V2, DropShadow V2, InnerShadow V2, OuterGlow V2, InnerGlow V2, Stroke V2, ColorOverlay V2, GradientOverlay V2。
* 添加 [YoloV8Detect](#YoloV8Detect) 节点。
* 添加 [QWenImage2Prompt](#QWenImage2Prompt)节点, 用本地模型反推提示词。(需要下载模型到models文件夹)
* 添加 [BooleanOperator](#BooleanOperator), [NumberCalculator](#NumberCalculator), [TextBox](#TextBox), [Integer](#Integer), [Float](#Float), [Boolean](#Boolean)节点。这些节点可进行数学和逻辑运算。
@@ -1433,12 +1434,15 @@ mask反转
<sup>2</sup> mask不是必须的输入项,默认使用image的alpha通道,如果image输入不包含alpha通道将自动创建整个图像的alpha通道。如果输入mask,原本的alpha通道将被mask覆盖。
<sup>3</sup> <a id="table1">混合模式</a>包括normal、multply、screen、add、subtract、difference、darker、lighter、color_burn、color_dodge、linear_burn、linear_dodge、overlay、soft_light、hard_light、vivid_light、pin_light、linear_light、hard_mix, 共19种混合模式
<sup>3</sup> <a id="table1">混合模式</a> 包括normal、multply、screen、add、subtract、difference、darker、lighter、color_burn、color_dodge、linear_burn、linear_dodge、overlay、soft_light、hard_light、vivid_light、pin_light、linear_light、hard_mix, 共19种混合模式。
![image](image/blend_mode_result.png)
<font size="1">*混合模式预览</font><br />
<sup>3</sup> <a id="table1">混合模式V2</a> 包括normal, dissolve, darken, multiply, color burn, linear burn, darker color, lighten, screen, color dodge, linear dodge(add), lighter color, dodge, overlay, soft light, hard light, vivid light, linear light, pin light, hard mix, difference, exclusion, subtract, divide, hue, saturation, color, luminosity, grain extract, grain merge共30种模式。
![image](image/blend_mode_v2_example.png)
<font size="1">*混合模式V2版预览</font><br />
<sup>4</sup> 颜色使用16进制RGB字符串格式描述,例如 '#FA3D86'。
<sup>5</sup> image和mask这两项必须是相同的尺寸。
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

+239
View File
@@ -0,0 +1,239 @@
"""
@author: Chris Freilich
@title: Virtuoso Pack - Blend Modes
@nickname: Virtuoso Pack - Blend Nodes
@description: This extension provides a blend modes node with 30 blend modes.
"""
from PIL import Image
import numpy as np
import torch
import torch.nn.functional as F
from colorsys import rgb_to_hsv, hsv_to_rgb
from blend_modes import difference, normal, screen, soft_light, lighten_only, dodge, \
addition, darken_only, multiply, hard_light, \
grain_extract, grain_merge, divide, overlay
def dissolve(backdrop, source, opacity):
# Normalize the RGB and alpha values to 0-1
backdrop_norm = backdrop[:, :, :3] / 255
source_norm = source[:, :, :3] / 255
source_alpha_norm = source[:, :, 3] / 255
# Calculate the transparency of each pixel in the source image
transparency = opacity * source_alpha_norm
# Generate a random matrix with the same shape as the source image
random_matrix = np.random.random(source.shape[:2])
# Create a mask where the random values are less than the transparency
mask = random_matrix < transparency
# Use the mask to select pixels from the source or backdrop
blend = np.where(mask[..., None], source_norm, backdrop_norm)
# Apply the alpha channel of the source image to the blended image
new_rgb = (1 - source_alpha_norm[..., None]) * backdrop_norm + source_alpha_norm[..., None] * blend
# Ensure the RGB values are within the valid range
new_rgb = np.clip(new_rgb, 0, 1)
# Convert the RGB values back to 0-255
new_rgb = new_rgb * 255
# Calculate the new alpha value by taking the maximum of the backdrop and source alpha channels
new_alpha = np.maximum(backdrop[:, :, 3], source[:, :, 3])
# Create a new RGBA image with the calculated RGB and alpha values
result = np.dstack((new_rgb, new_alpha))
return result
def hsv(backdrop, source, opacity, channel):
# Convert RGBA to RGB, normalized
backdrop_rgb = backdrop[:, :, :3] / 255.0
source_rgb = source[:, :, :3] / 255.0
source_alpha = source[:, :, 3] / 255.0
# Convert RGB to HSV
backdrop_hsv = np.array([rgb_to_hsv(*rgb) for row in backdrop_rgb for rgb in row]).reshape(backdrop.shape[:2] + (3,))
source_hsv = np.array([rgb_to_hsv(*rgb) for row in source_rgb for rgb in row]).reshape(source.shape[:2] + (3,))
# Combine HSV values
new_hsv = backdrop_hsv.copy()
# Determine which channel to operate on
if channel == "saturation":
new_hsv[:, :, 1] = (1 - opacity * source_alpha) * backdrop_hsv[:, :, 1] + opacity * source_alpha * source_hsv[:, :, 1]
elif channel == "luminance":
new_hsv[:, :, 2] = (1 - opacity * source_alpha) * backdrop_hsv[:, :, 2] + opacity * source_alpha * source_hsv[:, :, 2]
elif channel == "hue":
new_hsv[:, :, 0] = (1 - opacity * source_alpha) * backdrop_hsv[:, :, 0] + opacity * source_alpha * source_hsv[:, :, 0]
elif channel == "color":
new_hsv[:, :, :2] = (1 - opacity * source_alpha[..., None]) * backdrop_hsv[:, :, :2] + opacity * source_alpha[..., None] * source_hsv[:, :, :2]
# Convert HSV back to RGB
new_rgb = np.array([hsv_to_rgb(*hsv) for row in new_hsv for hsv in row]).reshape(backdrop.shape[:2] + (3,))
# Apply the alpha channel of the source image to the new RGB image
new_rgb = (1 - source_alpha[..., None]) * backdrop_rgb + source_alpha[..., None] * new_rgb
# Ensure the RGB values are within the valid range
new_rgb = np.clip(new_rgb, 0, 1)
# Convert RGB back to RGBA and scale to 0-255 range
new_rgba = np.dstack((new_rgb * 255, backdrop[:, :, 3]))
return new_rgba.astype(np.uint8)
def saturation(backdrop, source, opacity):
return hsv(backdrop, source, opacity, "saturation")
def luminance(backdrop, source, opacity):
return hsv(backdrop, source, opacity, "luminance")
def hue(backdrop, source, opacity):
return hsv(backdrop, source, opacity, "hue")
def color(backdrop, source, opacity):
return hsv(backdrop, source, opacity, "color")
def darker_lighter_color(backdrop, source, opacity, type):
# Normalize the RGB and alpha values to 0-1
backdrop_norm = backdrop[:, :, :3] / 255
source_norm = source[:, :, :3] / 255
source_alpha_norm = source[:, :, 3] / 255
# Convert RGB to HSV
backdrop_hsv = np.array([rgb_to_hsv(*rgb) for row in backdrop_norm for rgb in row]).reshape(backdrop.shape[:2] + (3,))
source_hsv = np.array([rgb_to_hsv(*rgb) for row in source_norm for rgb in row]).reshape(source.shape[:2] + (3,))
# Create a mask where the value (brightness) of the source image is less than the value of the backdrop image
if type == "dark":
mask = source_hsv[:, :, 2] < backdrop_hsv[:, :, 2]
else:
mask = source_hsv[:, :, 2] > backdrop_hsv[:, :, 2]
# Use the mask to select pixels from the source or backdrop
blend = np.where(mask[..., None], source_norm, backdrop_norm)
# Apply the alpha channel of the source image to the blended image
new_rgb = (1 - source_alpha_norm[..., None] * opacity) * backdrop_norm + source_alpha_norm[..., None] * opacity * blend
# Ensure the RGB values are within the valid range
new_rgb = np.clip(new_rgb, 0, 1)
# Convert the RGB values back to 0-255
new_rgb = new_rgb * 255
# Calculate the new alpha value by taking the maximum of the backdrop and source alpha channels
new_alpha = np.maximum(backdrop[:, :, 3], source[:, :, 3])
# Create a new RGBA image with the calculated RGB and alpha values
result = np.dstack((new_rgb, new_alpha))
return result
def darker_color(backdrop, source, opacity):
return darker_lighter_color(backdrop, source, opacity, "dark")
def lighter_color(backdrop, source, opacity):
return darker_lighter_color(backdrop, source, opacity, "light")
def simple_mode(backdrop, source, opacity, mode):
# Normalize the RGB and alpha values to 0-1
backdrop_norm = backdrop[:, :, :3] / 255
source_norm = source[:, :, :3] / 255
source_alpha_norm = source[:, :, 3:4] / 255
# Calculate the blend without any transparency considerations
if mode == "linear_burn":
blend = backdrop_norm + source_norm - 1
elif mode == "linear_light":
blend = backdrop_norm + (2 * source_norm) - 1
elif mode == "color_dodge":
blend = backdrop_norm / (1 - source_norm)
blend = np.clip(blend, 0, 1)
elif mode == "color_burn":
blend = 1 - ((1 - backdrop_norm) / source_norm)
blend = np.clip(blend, 0, 1)
elif mode == "exclusion":
blend = backdrop_norm + source_norm - (2 * backdrop_norm * source_norm)
elif mode == "subtract":
blend = backdrop_norm - source_norm
elif mode == "vivid_light":
blend = np.where(source_norm <= 0.5, backdrop_norm / (1 - 2 * source_norm), 1 - (1 -backdrop_norm) / (2 * source_norm - 0.5) )
blend = np.clip(blend, 0, 1)
elif mode == "pin_light":
blend = np.where(source_norm <= 0.5, np.minimum(backdrop_norm, 2 * source_norm), np.maximum(backdrop_norm, 2 * (source_norm - 0.5)))
elif mode == "hard_mix":
blend = simple_mode(backdrop, source, opacity, "linear_light")
blend = np.round(blend[:, :, :3] / 255)
# Apply the blended layer back onto the backdrop layer while utilizing the alpha channel and opacity information
new_rgb = (1 - source_alpha_norm * opacity) * backdrop_norm + source_alpha_norm * opacity * blend
# Ensure the RGB values are within the valid range
new_rgb = np.clip(new_rgb, 0, 1)
# Convert the RGB values back to 0-255
new_rgb = new_rgb * 255
# Calculate the new alpha value by taking the maximum of the backdrop and source alpha channels
new_alpha = np.maximum(backdrop[:, :, 3], source[:, :, 3])
# Create a new RGBA image with the calculated RGB and alpha values
result = np.dstack((new_rgb, new_alpha))
return result
def linear_light(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "linear_light")
def vivid_light(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "vivid_light")
def pin_light(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "pin_light")
def hard_mix(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "hard_mix")
def linear_burn(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "linear_burn")
def color_dodge(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "color_dodge")
def color_burn(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "color_burn")
def exclusion(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "exclusion")
def subtract(backdrop, source, opacity):
return simple_mode(backdrop, source, opacity, "subtract")
BLEND_MODES = {
"normal": normal,
"dissolve": dissolve,
"darken": darken_only,
"multiply": multiply,
"color burn": color_burn,
"linear burn": linear_burn,
"darker color": darker_color,
"lighten": lighten_only,
"screen": screen,
"color dodge": color_dodge,
"linear dodge(add)": addition,
"lighter color": lighter_color,
"dodge": dodge,
"overlay": overlay,
"soft light": soft_light,
"hard light": hard_light,
"vivid light": vivid_light,
"linear light": linear_light,
"pin light": pin_light,
"hard mix": hard_mix,
"difference": difference,
"exclusion": exclusion,
"subtract": subtract,
"divide": divide,
"hue": hue,
"saturation": saturation,
"color": color,
"luminosity": luminance,
"grain extract": grain_extract,
"grain merge": grain_merge
}
-1
View File
@@ -64,7 +64,6 @@ class AutoBrightness:
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
# ret_images.append(pil2tensor(_l.convert('RGB')))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
+88
View File
@@ -0,0 +1,88 @@
from .imagefunc import *
NODE_NAME = 'ColorOverlayV2'
class ColorOverlayV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"color": ("STRING", {"default": "#FFBF30"}), # 渐变开始颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_overlay_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def color_overlay_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity, color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=color)
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image_v2(_layer, _color, blend_mode, opacity)
_canvas.paste(_comp, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: ColorOverlay V2": ColorOverlayV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: ColorOverlay V2": "LayerStyle: ColorOverlay V2"
}
+105
View File
@@ -0,0 +1,105 @@
from .imagefunc import *
NODE_NAME = 'DropShadowV2'
class DropShadowV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度
"distance_x": ("INT", {"default": 25, "min": -9999, "max": 9999, "step": 1}), # x_偏移
"distance_y": ("INT", {"default": 25, "min": -9999, "max": 9999, "step": 1}), # y_偏移
"grow": ("INT", {"default": 6, "min": -9999, "max": 9999, "step": 1}), # 扩张
"blur": ("INT", {"default": 18, "min": 0, "max": 100, "step": 1}), # 模糊
"shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'drop_shadow_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def drop_shadow_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity, distance_x, distance_y,
grow, blur, shadow_color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
distance_x = -distance_x
distance_y = -distance_y
shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color)
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image)
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
if distance_x != 0 or distance_y != 0:
__mask = shift_image(_mask, distance_x, distance_y) # 位移
shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊
# 合成阴影
alpha = tensor2pil(shadow_mask).convert('L')
_shadow = chop_image_v2(_canvas, shadow_color, blend_mode, opacity)
_canvas.paste(_shadow, mask=alpha)
# 合成layer
_canvas.paste(_layer, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: DropShadow V2": DropShadowV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: DropShadow V2": "LayerStyle: DropShadow V2"
}
+100
View File
@@ -0,0 +1,100 @@
from .imagefunc import *
NODE_NAME = 'GradientOverlayV2'
class GradientOverlayV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"start_color": ("STRING", {"default": "#FFBF30"}), # 渐变开始颜色
"start_alpha": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"end_color": ("STRING", {"default": "#FE0000"}), # 渐变结束颜色
"end_alpha": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
"angle": ("INT", {"default": 0, "min": -180, "max": 180, "step": 1}), # 渐变角度
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'gradient_overlay_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def gradient_overlay_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
start_color, start_alpha, end_color, end_alpha, angle,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
width, height = tensor2pil(l_images[0]).size
_gradient = gradient(start_color, end_color, width, height, float(angle))
start_color = RGB_to_Hex((start_alpha, start_alpha, start_alpha))
end_color = RGB_to_Hex((end_alpha, end_alpha, end_alpha))
comp_alpha = gradient(start_color, end_color, width, height, float(angle))
comp_alpha = ImageChops.invert(comp_alpha).convert('L')
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image_v2(_layer, _gradient, blend_mode, opacity)
if start_alpha < 255 or end_alpha < 255:
_comp.paste(_layer, comp_alpha)
_canvas.paste(_comp, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: GradientOverlay V2": GradientOverlayV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: GradientOverlay V2": "LayerStyle: GradientOverlay V2"
}
+132
View File
@@ -0,0 +1,132 @@
from .imagefunc import *
NODE_NAME = 'ImageBlendAdvanceV2'
class ImageBlendAdvanceV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
mirror_mode = ['None', 'horizontal', 'vertical']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"x_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}),
"y_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}),
"mirror": (mirror_mode,), # 镜像翻转
"scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
"aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
"rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}),
"transform_method": (method_mode,),
"anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}),
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
FUNCTION = 'image_blend_advance_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def image_blend_advance_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
x_percent, y_percent,
mirror, scale, aspect_ratio, rotate,
transform_method, anti_aliasing,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
ret_masks = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
else:
l_masks.append(Image.new('L', m.size, 'white'))
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(b_images), len(l_images), len(l_masks))
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image)
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
orig_layer_width = _layer.width
orig_layer_height = _layer.height
_mask = _mask.convert("RGB")
target_layer_width = int(orig_layer_width * scale)
target_layer_height = int(orig_layer_height * scale * aspect_ratio)
# mirror
if mirror == 'horizontal':
_layer = _layer.transpose(Image.FLIP_LEFT_RIGHT)
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
elif mirror == 'vertical':
_layer = _layer.transpose(Image.FLIP_TOP_BOTTOM)
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
# scale
_layer = _layer.resize((target_layer_width, target_layer_height))
_mask = _mask.resize((target_layer_width, target_layer_height))
# rotate
_layer, _mask, _ = image_rotate_extend_with_alpha(_layer, rotate, _mask, transform_method, anti_aliasing)
# 处理位置
x = int(_canvas.width * x_percent / 100 - _layer.width / 2)
y = int(_canvas.height * y_percent / 100 - _layer.height / 2)
# composit layer
_comp = copy.copy(_canvas)
_compmask = Image.new("RGB", _comp.size, color='black')
_comp.paste(_layer, (x, y))
_compmask.paste(_mask, (x, y))
_compmask = _compmask.convert('L')
_comp = chop_image_v2(_canvas, _comp, blend_mode, opacity)
# composition background
_canvas.paste(_comp, mask=_compmask)
ret_images.append(pil2tensor(_canvas))
ret_masks.append(image2mask(_compmask))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageBlendAdvance V2": ImageBlendAdvanceV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageBlendAdvance V2": "LayerUtility: ImageBlendAdvance V2"
}
+88
View File
@@ -0,0 +1,88 @@
import numpy as np
from .imagefunc import *
NODE_NAME = 'ImageBlendV2'
class ImageBlendV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'image_blend_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def image_blend_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
else:
l_masks.append(Image.new('L', m.size, 'white'))
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(b_images), len(l_images), len(l_masks))
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image_v2(_canvas, _layer, blend_mode, opacity)
_canvas.paste(_comp, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageBlend V2": ImageBlendV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageBlend V2": "LayerUtility: ImageBlend V2"
}
+95 -3
View File
@@ -31,6 +31,8 @@ from .filmgrainer import processing as processing_utils
from .filmgrainer import filmgrainer as filmgrainer
import wget
from .blendmodes import *
def log(message:str, message_type:str='info'):
name = 'LayerStyle'
@@ -359,6 +361,16 @@ def chop_image(background_image:Image, layer_image:Image, blend_mode:str, opacit
ret_image = Image.blend(ret_image, background_image, alpha)
return ret_image
def chop_image_v2(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image:
backdrop_prepped = np.asfarray(background_image.convert('RGBA'))
source_prepped = np.asfarray(layer_image.convert('RGBA'))
blended_np = BLEND_MODES[blend_mode](backdrop_prepped, source_prepped, opacity / 100)
# final_tensor = (torch.from_numpy(blended_np / 255)).unsqueeze(0)
# return tensor2pil(_tensor)
return Image.fromarray(np.uint8(blended_np))
def remove_background(image:Image, mask:Image, color:str) -> Image:
width = image.width
height = image.height
@@ -1326,6 +1338,50 @@ def image_to_colormap(image:Image, index:int) -> Image:
'''Color Functions'''
def color_balance(image:Image, shadows:list, midtones:list, highlights:list,
shadow_center:float=0.15, midtone_center:float=0.5, highlight_center:float=0.8,
shadow_max:float=0.1, midtone_max:float=0.3, highlight_max:float=0.2,
preserve_luminosity:bool=False) -> Image:
img = pil2tensor(image)
# Create a copy of the img tensor
img_copy = img.clone()
# Calculate the original luminance if preserve_luminosity is True
if preserve_luminosity:
original_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2]
# Define the adjustment curves
def adjust(x, center, value, max_adjustment):
# Scale the adjustment value
value = value * max_adjustment
# Define control points
points = torch.tensor([[0, 0], [center, center + value], [1, 1]])
# Create cubic spline
from scipy.interpolate import CubicSpline
cs = CubicSpline(points[:, 0], points[:, 1])
# Apply the cubic spline to the color channel
return torch.clamp(torch.from_numpy(cs(x)), 0, 1)
# Apply the adjustments to each color channel
# shadows, midtones, highlights are lists of length 3 (for R, G, B channels) with values between -1 and 1
for i, (s, m, h) in enumerate(zip(shadows, midtones, highlights)):
img_copy[..., i] = adjust(img_copy[..., i], shadow_center, s, shadow_max)
img_copy[..., i] = adjust(img_copy[..., i], midtone_center, m, midtone_max)
img_copy[..., i] = adjust(img_copy[..., i], highlight_center, h, highlight_max)
# If preserve_luminosity is True, adjust the RGB values to match the original luminance
if preserve_luminosity:
current_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2]
img_copy *= (original_luminance / current_luminance).unsqueeze(-1)
return tensor2pil(img_copy)
def RGB_to_Hex(RGB:tuple) -> str:
color = '#'
for i in RGB:
@@ -1462,6 +1518,20 @@ def is_contain_chinese(check_str:str) -> bool:
return True
return False
def tensor_info(tensor:object) -> str:
value = ''
if isinstance(tensor, torch.Tensor):
value += f"\n Input dim = {tensor.dim()}, shape[0] = {tensor.shape[0]} \n"
for i in range(tensor.shape[0]):
t = tensor[i]
image = tensor2pil(t)
value += f'\n index {i}: Image.size = {image.size}, Image.mode = {image.mode}, dim = {t.dim()}, '
for j in range(t.dim()):
value += f'shape[{j}] = {t.shape[j]}, '
else:
value = f"tensor_info: Not tensor, type is {type(tensor)}"
return value
'''CLASS'''
class AnyType(str):
@@ -1471,9 +1541,31 @@ class AnyType(str):
'''Constant'''
chop_mode = ['normal', 'multply', 'screen', 'add', 'subtract', 'difference', 'darker', 'lighter',
'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay',
'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix']
chop_mode = [
'normal',
'multply',
'screen',
'add',
'subtract',
'difference',
'darker',
'lighter',
'color_burn',
'color_dodge',
'linear_burn',
'linear_dodge',
'overlay',
'soft_light',
'hard_light',
'vivid_light',
'pin_light',
'linear_light',
'hard_mix'
]
# Blend Mode from Virtuoso Pack https://github.com/chrisfreilich/virtuoso-nodes
chop_mode_v2 = list(BLEND_MODES.keys())
'''Load INI File'''
+112
View File
@@ -0,0 +1,112 @@
import copy
from .imagefunc import *
NODE_NAME = 'InnerGlowV2'
class InnerGlowV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
modes = copy.copy(BLEND_MODES)
chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"]
for i in chop_mode_list:
modes.pop(i)
chop_mode_list.extend(list(modes.keys()))
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_list,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代
"glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张
"blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张
"light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色
"glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'inner_glow_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def inner_glow_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
brightness, glow_range, blur, light_color, glow_color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
blur_factor = blur / 20.0
grow = glow_range
inner_mask = _mask
for x in range(brightness):
blur = int(grow * blur_factor)
_color = step_color(glow_color, light_color, brightness, x)
glow_mask = expand_mask(image2mask(inner_mask), -grow, blur) #扩张,模糊
# 合成glow
color_image = Image.new("RGB", _layer.size, color=_color)
alpha = tensor2pil(mask_invert(glow_mask)).convert('L')
_glow = chop_image_v2(_layer, color_image, blend_mode, int(step_value(1, opacity, brightness, x)))
_layer.paste(_glow, mask=alpha)
grow = grow - int(glow_range/brightness)
# 合成layer
_layer.paste(_canvas, mask=ImageChops.invert(_mask))
ret_images.append(pil2tensor(_layer))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: InnerGlow V2": InnerGlowV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: InnerGlow V2": "LayerStyle: InnerGlow V2"
}
+101
View File
@@ -0,0 +1,101 @@
from .imagefunc import *
NODE_NAME = 'InnerShadowV2'
class InnerShadowV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度
"distance_x": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # x_偏移
"distance_y": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # y_偏移
"grow": ("INT", {"default": 2, "min": -9999, "max": 9999, "step": 1}), # 扩张
"blur": ("INT", {"default": 15, "min": 0, "max": 100, "step": 1}), # 模糊
"shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'inner_shadow_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def inner_shadow_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity, distance_x, distance_y,
grow, blur, shadow_color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
distance_x = -distance_x
distance_y = -distance_y
shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color)
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
if distance_x != 0 or distance_y != 0:
__mask = shift_image(_mask, distance_x, distance_y) # 位移
shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊
# 合成阴影
alpha = tensor2pil(shadow_mask).convert('L')
_shadow = chop_image_v2(_layer, shadow_color, blend_mode, opacity)
_layer.paste(_shadow, mask=ImageChops.invert(alpha))
# 合成layer
_canvas.paste(_layer, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: InnerShadow V2": InnerShadowV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: InnerShadow V2": "LayerStyle: InnerShadow V2"
}
+109
View File
@@ -0,0 +1,109 @@
from .imagefunc import *
NODE_NAME = 'OuterGlowV2'
class OuterGlowV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
modes = copy.copy(BLEND_MODES)
chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"]
for i in chop_mode_list:
modes.pop(i)
chop_mode_list.extend(list(modes.keys()))
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_list,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代
"glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张
"blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张
"light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色
"glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'outer_glow_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def outer_glow_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
brightness, glow_range, blur, light_color, glow_color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
blur_factor = blur / 20.0
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
grow = glow_range
for x in range(brightness):
blur = int(grow * blur_factor)
_color = step_color(glow_color, light_color, brightness, x)
glow_mask = expand_mask(image2mask(_mask), grow, blur) #扩张,模糊
# 合成glow
color_image = Image.new("RGB", _layer.size, color=_color)
alpha = tensor2pil(glow_mask).convert('L')
_glow = chop_image_v2(_canvas, color_image, blend_mode, int(step_value(1, opacity, brightness, x)))
_canvas.paste(_glow.convert('RGB'), mask=alpha)
grow = grow - int(glow_range/brightness)
# 合成layer
_canvas.paste(_layer, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: OuterGlow V2": OuterGlowV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: OuterGlow V2": "LayerStyle: OuterGlow V2"
}
+101
View File
@@ -0,0 +1,101 @@
from .imagefunc import *
NODE_NAME = 'StorkeV2'
class StrokeV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode_v2,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"stroke_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), # 收缩值
"stroke_width": ("INT", {"default": 8, "min": 0, "max": 999, "step": 1}), # 扩张值
"blur": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), # 模糊
"stroke_color": ("STRING", {"default": "#FF0000"}), # 描边颜色
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'stroke_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def stroke_v2(self, background_image, layer_image,
invert_mask, blend_mode, opacity,
stroke_grow, stroke_width, blur, stroke_color,
layer_mask=None
):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in background_image:
b_images.append(torch.unsqueeze(b, 0))
for l in layer_image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if layer_mask is not None:
if layer_mask.dim() == 2:
layer_mask = torch.unsqueeze(layer_mask, 0)
l_masks = []
for m in layer_mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
if len(l_masks) == 0:
log(f"Error: {NODE_NAME} skipped, because the available mask is not found.", message_type='error')
return (background_image,)
max_batch = max(len(b_images), len(l_images), len(l_masks))
grow_offset = int(stroke_width / 2)
inner_stroke = stroke_grow - grow_offset
outer_stroke = inner_stroke + stroke_width
for i in range(max_batch):
background_image = b_images[i] if i < len(b_images) else b_images[-1]
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
# preprocess
_canvas = tensor2pil(background_image).convert('RGB')
_layer = tensor2pil(layer_image).convert('RGB')
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {NODE_NAME} mask mismatch, dropped!", message_type='warning')
inner_mask = expand_mask(image2mask(_mask), inner_stroke, blur)
outer_mask = expand_mask(image2mask(_mask), outer_stroke, blur)
stroke_mask = subtract_mask(outer_mask, inner_mask)
color_image = Image.new('RGB', size=_layer.size, color=stroke_color)
blend_image = chop_image_v2(_layer, color_image, blend_mode, opacity)
_canvas.paste(_layer, mask=_mask)
_canvas.paste(blend_image, mask=tensor2pil(stroke_mask))
ret_images.append(pil2tensor(_canvas))
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: Stroke V2": StrokeV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: Stroke V2": "LayerStyle: Stroke V2"
}
+2 -1
View File
@@ -24,4 +24,5 @@ tqdm
transformers
kornia
image-reward
ultralytics
ultralytics
blend_modes
File diff suppressed because it is too large Load Diff