update new BlendMode V2, commit ImageBlendV2, ImageBlendAdvanceV2, DropShadowV2, InnerShadowV2, OuterGlowV2, InnerGlowV2, StrokeV2, ColorOverlayV2, GradientOverlayV2 nodes
This commit is contained in:
@@ -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.
|
||||

|
||||
<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.
|
||||

|
||||
<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
@@ -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种混合模式。
|
||||

|
||||
<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种模式。
|
||||

|
||||
<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 |
@@ -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
|
||||
}
|
||||
@@ -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),)
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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'''
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -24,4 +24,5 @@ tqdm
|
||||
transformers
|
||||
kornia
|
||||
image-reward
|
||||
ultralytics
|
||||
ultralytics
|
||||
blend_modes
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user