Delete reference_code/layerstyle directory

This commit is contained in:
Cyber Dick Lang
2025-06-24 16:24:10 +08:00
committed by GitHub
parent 81d2713967
commit 23705eda6d
131 changed files with 0 additions and 18966 deletions
-50
View File
@@ -1,50 +0,0 @@
import torch
import time
from .imagefunc import log, tensor2pil, image_add_grain, pil2tensor
class AddGrain:
def __init__(self):
self.NODE_NAME = 'AddGrain'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"grain_power": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"grain_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.1}),
"grain_sat": ("FLOAT", {"default": 1, "min": 0, "max": 1, "step": 0.01}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'add_grain'
CATEGORY = '😺dzNodes/LayerFilter'
def add_grain(self, image, grain_power, grain_scale, grain_sat):
ret_images = []
for i in range(len(image)):
_canvas = tensor2pil(torch.unsqueeze(image[i], 0)).convert('RGB')
_canvas = image_add_grain(_canvas, grain_scale, grain_power, grain_sat, toe=0, seed=int(time.time()) + i)
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: AddGrain": AddGrain
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: AddGrain": "LayerFilter: Add Grain"
}
-35
View File
@@ -1,35 +0,0 @@
from .imagefunc import AnyType
anything = AnyType('*')
class LS_AnyRerouter():
def __init__(self):
self.NODE_NAME = 'AnyRerouter'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"any": (anything, {}),
},
"optional": { #
}
}
RETURN_TYPES = (anything,)
RETURN_NAMES = ('any',)
FUNCTION = 'any_rerouter'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def any_rerouter(self, any,):
return (any,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: AnyRerouter": LS_AnyRerouter
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: AnyRerouter": "LayerUtility: Any Rerouter"
}
@@ -1,65 +0,0 @@
import torch
from .imagefunc import log, pil2tensor,image2mask, extract_numbers
from PIL import Image
class BatchSelector:
def __init__(self):
self.NODE_NAME = 'BatchSelector'
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"select": ("STRING", {"default": "0,"},),
},
"optional": {
"images": ("IMAGE",), #
"masks": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask",)
FUNCTION = 'batch_selector'
CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
def batch_selector(self, select, images=None, masks=None
):
ret_images = []
ret_masks = []
empty_image = pil2tensor(Image.new("RGBA", (64, 64), (0, 0, 0, 0)))
empty_mask = image2mask(Image.new("L", (64, 64), color="black"))
indexs = extract_numbers(select)
for i in indexs:
if images is not None:
if i < len(images):
ret_images.append(images[i].unsqueeze(0))
else:
ret_images.append(images[-1].unsqueeze(0))
if masks is not None:
if i < len(masks):
ret_masks.append(masks[i].unsqueeze(0))
else:
ret_masks.append(masks[-1].unsqueeze(0))
if len(ret_images) == 0:
ret_images.append(empty_image)
if len(ret_masks) == 0:
ret_masks.append(empty_mask)
log(f"{self.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: BatchSelector": BatchSelector
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: BatchSelector": "LayerUtility: Batch Selector"
}
-104
View File
@@ -1,104 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
from .imagefunc import image_channel_split, histogram_range
def norm_value(value):
if value < 0.01:
value = 0.01
if value > 0.99:
value = 0.99
return value
class BlendIfMask:
def __init__(self):
self.NODE_NAME = 'BlendIfMask'
@classmethod
def INPUT_TYPES(self):
blend_if_list = ["gray", "red", "green", "blue"]
return {
"required": {
"image": ("IMAGE", ),
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_if": (blend_if_list,),
"black_point": ("INT", {"default": 0, "min": 0, "max": 254, "step": 1, "display": "slider"}),
"black_range": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
"white_point": ("INT", {"default": 255, "min": 1, "max": 255, "step": 1, "display": "slider"}),
"white_range": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'blend_if_mask'
CATEGORY = '😺dzNodes/LayerMask'
def blend_if_mask(self, image, invert_mask, blend_if,
black_point, black_range,
white_point, white_range,
mask=None
):
ret_masks = []
input_images = []
input_masks = []
for i in image:
input_images.append(torch.unsqueeze(i, 0))
m = tensor2pil(i)
if m.mode == 'RGBA':
input_masks.append(m.split()[-1])
else:
input_masks.append(Image.new('L', size=m.size, color='white'))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
input_masks = []
for m in mask:
if invert_mask:
m = 1 - m
input_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(input_images), len(input_masks))
for i in range(max_batch):
_image = input_images[i] if i < len(input_images) else input_images[-1]
_image = tensor2pil(_image).convert('RGB')
if blend_if == "red":
gray = image_channel_split(_image, 'RGB')[0]
elif blend_if == "green":
gray = image_channel_split(_image, 'RGB')[1]
elif blend_if == "blue":
gray = image_channel_split(_image, 'RGB')[2]
else:
gray = _image.convert('L')
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
gray = histogram_range(gray, black_point, black_range, white_point, white_range)
black = Image.new('L', size=_image.size, color='black')
_mask = ImageChops.invert(_mask)
gray.paste(black, mask=_mask)
ret_masks.append(image2mask(gray))
log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
return (torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerMask: BlendIf Mask": BlendIfMask
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: BlendIf Mask": "LayerMask: BlendIf Mask"
}
-324
View File
@@ -1,324 +0,0 @@
"""
author: Chris Freilich
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
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 rgb_to_hsv_via_torch(rgb_numpy: np.ndarray, device=None) -> torch.Tensor:
"""
Convert an RGB image to HSV.
:param rgb: A tensor of shape (3, H, W) where the three channels correspond to R, G, B.
The values should be in the range [0, 1].
:return: A tensor of shape (3, H, W) where the three channels correspond to H, S, V.
The hue (H) will be in the range [0, 1], while S and V will be in the range [0, 1].
"""
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
rgb = torch.from_numpy(rgb_numpy).float().permute(2, 0, 1).to(device)
r, g, b = rgb[0], rgb[1], rgb[2]
max_val, _ = torch.max(rgb, dim=0)
min_val, _ = torch.min(rgb, dim=0)
delta = max_val - min_val
h = torch.zeros_like(max_val)
s = torch.zeros_like(max_val)
v = max_val
# calc hue... avoid div by zero (by masking the delta)
mask = delta != 0
r_eq_max = (r == max_val) & mask
g_eq_max = (g == max_val) & mask
b_eq_max = (b == max_val) & mask
h[r_eq_max] = (g[r_eq_max] - b[r_eq_max]) / delta[r_eq_max] % 6
h[g_eq_max] = (b[g_eq_max] - r[g_eq_max]) / delta[g_eq_max] + 2.0
h[b_eq_max] = (r[b_eq_max] - g[b_eq_max]) / delta[b_eq_max] + 4.0
h = (h / 6.0) % 1.0
# calc saturation
s[max_val != 0] = delta[max_val != 0] / max_val[max_val != 0]
hsv = torch.stack([h, s, v], dim=0)
hsv_numpy = hsv.permute(1, 2, 0).cpu().numpy()
return hsv_numpy
def hsv_to_rgb_via_torch(hsv_numpy: np.ndarray, device=None) -> torch.Tensor:
"""
Convert an HSV image to RGB.
:param hsv: A tensor of shape (3, H, W) where the three channels correspond to H, S, V.
The H channel values should be in the range [0, 1], while S and V will be in the range [0, 1].
:return: A tensor of shape (3, H, W) where the three channels correspond to R, G, B.
The RGB values will be in the range [0, 1].
"""
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
hsv = torch.from_numpy(hsv_numpy).float().permute(2, 0, 1).to(device)
h, s, v = hsv[0], hsv[1], hsv[2]
c = v * s # chroma
x = c * (1 - torch.abs((h * 6) % 2 - 1))
m = v - c # match value
z = torch.zeros_like(h)
rgb = torch.zeros_like(hsv)
# define conditions for different hue ranges
h_cond = [
(h < 1/6, torch.stack([c, x, z], dim=0)),
((1/6 <= h) & (h < 2/6), torch.stack([x, c, z], dim=0)),
((2/6 <= h) & (h < 3/6), torch.stack([z, c, x], dim=0)),
((3/6 <= h) & (h < 4/6), torch.stack([z, x, c], dim=0)),
((4/6 <= h) & (h < 5/6), torch.stack([x, z, c], dim=0)),
(h >= 5/6, torch.stack([c, z, x], dim=0)),
]
# conditionally set RGB values based on the hue range
for cond, result in h_cond:
rgb[:, cond] = result[:, cond]
# add match value to convert to final RGB values
rgb = rgb + m
rgb_numpy = rgb.permute(1, 2, 0).cpu().numpy()
return rgb_numpy
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 = rgb_to_hsv_via_torch(backdrop_rgb)
source_hsv = rgb_to_hsv_via_torch(source_rgb)
# 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 = hsv_to_rgb_via_torch(new_hsv)
# 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
}
-455
View File
@@ -1,455 +0,0 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class REBNCONV(nn.Module):
def __init__(self,in_ch=3,out_ch=3,dirate=1,stride=1):
super(REBNCONV,self).__init__()
self.conv_s1 = nn.Conv2d(in_ch,out_ch,3,padding=1*dirate,dilation=1*dirate,stride=stride)
self.bn_s1 = nn.BatchNorm2d(out_ch)
self.relu_s1 = nn.ReLU(inplace=True)
def forward(self,x):
hx = x
xout = self.relu_s1(self.bn_s1(self.conv_s1(hx)))
return xout
## upsample tensor 'src' to have the same spatial size with tensor 'tar'
def _upsample_like(src,tar):
src = F.interpolate(src,size=tar.shape[2:],mode='bilinear')
return src
### RSU-7 ###
class RSU7(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3, img_size=512):
super(RSU7,self).__init__()
self.in_ch = in_ch
self.mid_ch = mid_ch
self.out_ch = out_ch
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1) ## 1 -> 1/2
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool5 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv7 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv6d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
b, c, h, w = x.shape
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx = self.pool4(hx4)
hx5 = self.rebnconv5(hx)
hx = self.pool5(hx5)
hx6 = self.rebnconv6(hx)
hx7 = self.rebnconv7(hx6)
hx6d = self.rebnconv6d(torch.cat((hx7,hx6),1))
hx6dup = _upsample_like(hx6d,hx5)
hx5d = self.rebnconv5d(torch.cat((hx6dup,hx5),1))
hx5dup = _upsample_like(hx5d,hx4)
hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-6 ###
class RSU6(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU6,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool4 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv6 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv5d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx = self.pool4(hx4)
hx5 = self.rebnconv5(hx)
hx6 = self.rebnconv6(hx5)
hx5d = self.rebnconv5d(torch.cat((hx6,hx5),1))
hx5dup = _upsample_like(hx5d,hx4)
hx4d = self.rebnconv4d(torch.cat((hx5dup,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-5 ###
class RSU5(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU5,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool3 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv5 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv4d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx = self.pool3(hx3)
hx4 = self.rebnconv4(hx)
hx5 = self.rebnconv5(hx4)
hx4d = self.rebnconv4d(torch.cat((hx5,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.rebnconv3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-4 ###
class RSU4(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU4,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.pool1 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.pool2 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=1)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=1)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx = self.pool1(hx1)
hx2 = self.rebnconv2(hx)
hx = self.pool2(hx2)
hx3 = self.rebnconv3(hx)
hx4 = self.rebnconv4(hx3)
hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.rebnconv2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.rebnconv1d(torch.cat((hx2dup,hx1),1))
return hx1d + hxin
### RSU-4F ###
class RSU4F(nn.Module):
def __init__(self, in_ch=3, mid_ch=12, out_ch=3):
super(RSU4F,self).__init__()
self.rebnconvin = REBNCONV(in_ch,out_ch,dirate=1)
self.rebnconv1 = REBNCONV(out_ch,mid_ch,dirate=1)
self.rebnconv2 = REBNCONV(mid_ch,mid_ch,dirate=2)
self.rebnconv3 = REBNCONV(mid_ch,mid_ch,dirate=4)
self.rebnconv4 = REBNCONV(mid_ch,mid_ch,dirate=8)
self.rebnconv3d = REBNCONV(mid_ch*2,mid_ch,dirate=4)
self.rebnconv2d = REBNCONV(mid_ch*2,mid_ch,dirate=2)
self.rebnconv1d = REBNCONV(mid_ch*2,out_ch,dirate=1)
def forward(self,x):
hx = x
hxin = self.rebnconvin(hx)
hx1 = self.rebnconv1(hxin)
hx2 = self.rebnconv2(hx1)
hx3 = self.rebnconv3(hx2)
hx4 = self.rebnconv4(hx3)
hx3d = self.rebnconv3d(torch.cat((hx4,hx3),1))
hx2d = self.rebnconv2d(torch.cat((hx3d,hx2),1))
hx1d = self.rebnconv1d(torch.cat((hx2d,hx1),1))
return hx1d + hxin
class myrebnconv(nn.Module):
def __init__(self, in_ch=3,
out_ch=1,
kernel_size=3,
stride=1,
padding=1,
dilation=1,
groups=1):
super(myrebnconv,self).__init__()
self.conv = nn.Conv2d(in_ch,
out_ch,
kernel_size=kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups)
self.bn = nn.BatchNorm2d(out_ch)
self.rl = nn.ReLU(inplace=True)
def forward(self,x):
return self.rl(self.bn(self.conv(x)))
class BriaRMBG(nn.Module):
def __init__(self,in_ch=3,out_ch=1):
super(BriaRMBG,self).__init__()
self.conv_in = nn.Conv2d(in_ch,64,3,stride=2,padding=1)
self.pool_in = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage1 = RSU7(64,32,64)
self.pool12 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage2 = RSU6(64,32,128)
self.pool23 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage3 = RSU5(128,64,256)
self.pool34 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage4 = RSU4(256,128,512)
self.pool45 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage5 = RSU4F(512,256,512)
self.pool56 = nn.MaxPool2d(2,stride=2,ceil_mode=True)
self.stage6 = RSU4F(512,256,512)
# decoder
self.stage5d = RSU4F(1024,256,512)
self.stage4d = RSU4(1024,128,256)
self.stage3d = RSU5(512,64,128)
self.stage2d = RSU6(256,32,64)
self.stage1d = RSU7(128,16,64)
self.side1 = nn.Conv2d(64,out_ch,3,padding=1)
self.side2 = nn.Conv2d(64,out_ch,3,padding=1)
self.side3 = nn.Conv2d(128,out_ch,3,padding=1)
self.side4 = nn.Conv2d(256,out_ch,3,padding=1)
self.side5 = nn.Conv2d(512,out_ch,3,padding=1)
self.side6 = nn.Conv2d(512,out_ch,3,padding=1)
# self.outconv = nn.Conv2d(6*out_ch,out_ch,1)
def forward(self,x):
hx = x
hxin = self.conv_in(hx)
#hx = self.pool_in(hxin)
#stage 1
hx1 = self.stage1(hxin)
hx = self.pool12(hx1)
#stage 2
hx2 = self.stage2(hx)
hx = self.pool23(hx2)
#stage 3
hx3 = self.stage3(hx)
hx = self.pool34(hx3)
#stage 4
hx4 = self.stage4(hx)
hx = self.pool45(hx4)
#stage 5
hx5 = self.stage5(hx)
hx = self.pool56(hx5)
#stage 6
hx6 = self.stage6(hx)
hx6up = _upsample_like(hx6,hx5)
#-------------------- decoder --------------------
hx5d = self.stage5d(torch.cat((hx6up,hx5),1))
hx5dup = _upsample_like(hx5d,hx4)
hx4d = self.stage4d(torch.cat((hx5dup,hx4),1))
hx4dup = _upsample_like(hx4d,hx3)
hx3d = self.stage3d(torch.cat((hx4dup,hx3),1))
hx3dup = _upsample_like(hx3d,hx2)
hx2d = self.stage2d(torch.cat((hx3dup,hx2),1))
hx2dup = _upsample_like(hx2d,hx1)
hx1d = self.stage1d(torch.cat((hx2dup,hx1),1))
#side output
d1 = self.side1(hx1d)
d1 = _upsample_like(d1,x)
d2 = self.side2(hx2d)
d2 = _upsample_like(d2,x)
d3 = self.side3(hx3d)
d3 = _upsample_like(d3,x)
d4 = self.side4(hx4d)
d4 = _upsample_like(d4,x)
d5 = self.side5(hx5d)
d5 = _upsample_like(d5,x)
d6 = self.side6(hx6)
d6 = _upsample_like(d6,x)
return [F.sigmoid(d1), F.sigmoid(d2), F.sigmoid(d3), F.sigmoid(d4), F.sigmoid(d5), F.sigmoid(d6)],[hx1d,hx2d,hx3d,hx4d,hx5d,hx6]
@@ -1,70 +0,0 @@
import torch
import math
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import shift_image
class ChannelShake:
def __init__(self):
self.NODE_NAME = 'ChannelShake'
@classmethod
def INPUT_TYPES(self):
channel_mode = ['RGB', 'RBG', 'BGR', 'BRG', 'GBR', 'GRB']
return {
"required": {
"image": ("IMAGE", ), #
"distance": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 距离
"angle": ("FLOAT", {"default": 40, "min": -360, "max": 360, "step": 0.1}), # 角度
"mode": (channel_mode,), # 模式
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'channel_shake'
CATEGORY = '😺dzNodes/LayerFilter'
def channel_shake(self, image, distance, angle, mode, ):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
_canvas = tensor2pil(i).convert('RGB')
R, G, B = _canvas.split()
x = int(math.cos(angle) * distance)
y = int(math.sin(angle) * distance)
if mode.startswith('R'):
R = shift_image(R.convert('RGB'), -x, -y).convert('L')
if mode.startswith('G'):
G = shift_image(G.convert('RGB'), -x, -y).convert('L')
if mode.startswith('B'):
B = shift_image(B.convert('RGB'), -x, -y).convert('L')
if mode.endswith('R'):
R = shift_image(R.convert('RGB'), x, y).convert('L')
if mode.endswith('G'):
G = shift_image(G.convert('RGB'), x, y).convert('L')
if mode.endswith('B'):
B = shift_image(B.convert('RGB'), x, y).convert('L')
ret_image = Image.merge('RGB', [R, G, B])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: ChannelShake": ChannelShake
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: ChannelShake": "LayerFilter: ChannelShake"
}
-53
View File
@@ -1,53 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import mask_white_area
# 检查mask是否有效,如果mask面积少于指定比例则判为无效mask
class CheckMask:
def __init__(self):
self.NODE_NAME = 'CheckMask'
@classmethod
def INPUT_TYPES(self):
blank_mask_list = ['white', 'black']
return {
"required": {
"mask": ("MASK",), #
"white_point": ("INT", {"default": 1, "min": 1, "max": 254, "step": 1}), # 用于判断mask是否有效的白点值,高于此值被计入有效
"area_percent": ("INT", {"default": 1, "min": 1, "max": 99, "step": 1}), # 区域百分比,低于此则mask判定无效
},
"optional": { #
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ('bool',)
FUNCTION = 'check_mask'
CATEGORY = '😺dzNodes/LayerUtility'
def check_mask(self, mask, white_point, area_percent,):
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
mask = tensor2pil(mask[0])
if mask.width * mask.height > 262144:
target_width = 512
target_height = int(target_width * mask.height / mask.width)
mask = mask.resize((target_width, target_height), Image.LANCZOS)
ret = mask_white_area(mask, white_point) * 100 > area_percent
log(f"{self.NODE_NAME}:{ret}", message_type="finish")
return (ret,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CheckMask": CheckMask
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CheckMask": "LayerUtility: Check Mask"
}
@@ -1,60 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import mask_white_area, is_valid_mask
# 检查mask是否有效,如果mask面积少于指定比例则判为无效mask
class CheckMaskV2:
def __init__(self):
self.NODE_NAME = 'CheckMaskV2'
pass
@classmethod
def INPUT_TYPES(self):
method_list = ['simple', 'detect_percent']
blank_mask_list = ['white', 'black']
return {
"required": {
"mask": ("MASK",), #
"method": (method_list,), #
"white_point": ("INT", {"default": 1, "min": 1, "max": 254, "step": 1}), # 用于判断mask是否有效的白点值,高于此值被计入有效
"area_percent": ("FLOAT", {"default": 0.01, "min": 0, "max": 100, "step": 0.01}), # 区域百分比,低于此则mask判定无效
},
"optional": { #
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ('bool',)
FUNCTION = 'check_mask_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def check_mask_v2(self, mask, method, white_point, area_percent,):
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
tensor_mask = mask[0]
pil_mask = tensor2pil(tensor_mask)
if pil_mask.width * pil_mask.height > 262144:
target_width = 512
target_height = int(target_width * pil_mask.height / pil_mask.width)
pil_mask = pil_mask.resize((target_width, target_height), Image.LANCZOS)
ret_bool = False
if method == 'simple':
ret_bool = is_valid_mask(tensor_mask)
else:
ret_bool = mask_white_area(pil_mask, white_point) * 100 > area_percent
log(f"{self.NODE_NAME}: {ret_bool}", message_type='finish')
return (ret_bool,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CheckMaskV2": CheckMaskV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CheckMaskV2": "LayerUtility: Check Mask V2"
}
@@ -1,62 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import color_adapter, chop_image, RGB2RGBA
class ColorAdapter:
def __init__(self):
self.NODE_NAME = 'ColorAdapter'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"color_ref_image": ("IMAGE", ), #
"opacity": ("INT", {"default": 75, "min": 0, "max": 100, "step": 1}), # 透明度
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_adapter'
CATEGORY = '😺dzNodes/LayerColor'
def color_adapter(self, image, color_ref_image, opacity):
ret_images = []
l_images = []
r_images = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
for r in color_ref_image:
r_images.append(torch.unsqueeze(r, 0))
for i in range(len(l_images)):
_image = l_images[i]
_ref = r_images[i] if len(ret_images) > i else r_images[-1]
__image = tensor2pil(_image)
_canvas = __image.convert('RGB')
ret_image = color_adapter(_canvas, tensor2pil(_ref).convert('RGB'))
ret_image = chop_image(_canvas, ret_image, blend_mode='normal', opacity=opacity)
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: ColorAdapter": ColorAdapter
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: ColorAdapter": "LayerColor: ColorAdapter"
}
@@ -1,61 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_hue_offset, image_gray_offset, image_channel_merge, RGB2RGBA
class ColorCorrectHSV:
def __init__(self):
self.NODE_NAME = 'HSV'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"H": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"S": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"V": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_HSV'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_HSV(self, image, H, S, V):
ret_images = []
for i in image:
i = torch.unsqueeze(i,0)
__image = tensor2pil(i)
_h, _s, _v = tensor2pil(i).convert('HSV').split()
if H != 0 :
_h = image_hue_offset(_h, H)
if S != 0 :
_s = image_gray_offset(_s, S)
if V != 0 :
_v = image_gray_offset(_v, V)
ret_image = image_channel_merge((_h, _s, _v), 'HSV')
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: HSV": ColorCorrectHSV
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: HSV": "LayerColor: HSV"
}
@@ -1,61 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA
class ColorCorrectLAB:
def __init__(self):
self.NODE_NAME = 'LAB'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"L": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"A": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"B": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_LAB'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_LAB(self, image, L, A, B):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
__image = tensor2pil(i)
_l, _a, _b = tensor2pil(i).convert('LAB').split()
if L != 0 :
_l = image_gray_offset(_l, L)
if A != 0 :
_a = image_gray_offset(_a, A)
if B != 0 :
_b = image_gray_offset(_b, B)
ret_image = image_channel_merge((_l, _a, _b), 'LAB')
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: LAB": ColorCorrectLAB
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: LAB": "LayerColor: LAB"
}
@@ -1,62 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import get_resource_dir, apply_lut, RGB2RGBA
class ColorCorrectLUTapply:
def __init__(self):
self.NODE_NAME = 'LUT Apply'
@classmethod
def INPUT_TYPES(self):
(LUT_DICT, _) = get_resource_dir()
LUT_LIST = list(LUT_DICT.keys())
color_space_list = ['linear', 'log']
return {
"required": {
"image": ("IMAGE", ), #
"LUT": (LUT_LIST,), # LUT文件
"color_space": (color_space_list,),
"strength": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_LUTapply'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_LUTapply(self, image, LUT, color_space, strength):
(LUT_DICT, _) = get_resource_dir()
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i)
lut_file = LUT_DICT[LUT]
ret_image = apply_lut(_image, lut_file=lut_file, colorspace=color_space, strength=strength)
if _image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, _image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: LUT Apply": ColorCorrectLUTapply
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: LUT Apply": "LayerColor: LUT Apply"
}
@@ -1,61 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA
class ColorCorrectRGB:
def __init__(self):
self.NODE_NAME = 'RGB'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"R": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"G": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"B": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_RGB'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_RGB(self, image, R, G, B):
ret_images = []
for i in image:
i = torch.unsqueeze(i,0)
__image = tensor2pil(i)
_r, _g, _b = tensor2pil(i).convert('RGB').split()
if R != 0 :
_r = image_gray_offset(_r, R)
if G != 0 :
_g = image_gray_offset(_g, G)
if B != 0 :
_b = image_gray_offset(_b, B)
ret_image = image_channel_merge((_r, _g, _b), 'RGB')
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: RGB": ColorCorrectRGB
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: RGB": "LayerColor: RGB"
}
@@ -1,61 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA
class ColorCorrectYUV:
def __init__(self):
self.NODE_NAME = 'YUV'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"Y": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"U": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"V": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_YUV'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_YUV(self, image, Y, U, V):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
__image = tensor2pil(i)
_y, _u, _v = tensor2pil(i).convert('YCbCr').split()
if Y != 0 :
_y = image_gray_offset(_y, Y)
if U != 0 :
_u = image_gray_offset(_u, U)
if V != 0 :
_v = image_gray_offset(_v, V)
ret_image = image_channel_merge((_y, _u, _v), 'YCbCr')
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: YUV": ColorCorrectYUV
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: YUV": "LayerColor: YUV"
}
@@ -1,115 +0,0 @@
import torch
from PIL import Image, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_channel_split, image_channel_merge, normalize_gray, gamma_trans, chop_image_v2, RGB2RGBA
class AutoAdjust:
def __init__(self):
self.NODE_NAME = 'AutoAdjust'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"strength": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
"brightness": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"contrast": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"saturation": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"red": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"green": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"blue": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'auto_adjust'
CATEGORY = '😺dzNodes/LayerColor'
def auto_adjust(self, image, strength, brightness, contrast, saturation, red, green, blue):
if brightness < 0:
brightness_offset = brightness / 100 + 1
else:
brightness_offset = brightness / 50 + 1
if contrast < 0:
contrast_offset = contrast / 100 + 1
else:
contrast_offset = contrast / 50 + 1
if saturation < 0:
saturation_offset = saturation / 100 + 1
else:
saturation_offset = saturation / 50 + 1
red_gamma = self.balance_to_gamma(red)
green_gamma = self.balance_to_gamma(green)
blue_gamma = self.balance_to_gamma(blue)
l_images = []
l_masks = []
ret_images = []
for l in 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'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_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]
orig_image = tensor2pil(_image)
r, g, b, _ = image_channel_split(orig_image, mode = 'RGB')
r = normalize_gray(r)
g = normalize_gray(g)
b = normalize_gray(b)
if red:
r = gamma_trans(r, red_gamma).convert('L')
if green:
g = gamma_trans(g, green_gamma).convert('L')
if blue:
b = gamma_trans(b, blue_gamma).convert('L')
ret_image = image_channel_merge((r, g, b), 'RGB')
if brightness:
brightness_image = ImageEnhance.Brightness(ret_image)
ret_image = brightness_image.enhance(factor=brightness_offset)
if contrast:
contrast_image = ImageEnhance.Contrast(ret_image)
ret_image = contrast_image.enhance(factor=contrast_offset)
if saturation:
color_image = ImageEnhance.Color(ret_image)
ret_image = color_image.enhance(factor=saturation_offset)
ret_image = chop_image_v2(orig_image, ret_image, blend_mode="normal", opacity=strength)
if orig_image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
def balance_to_gamma(self, balance:int) -> float:
return 0.00005 * balance * balance - 0.01 * balance + 1
NODE_CLASS_MAPPINGS = {
"LayerColor: AutoAdjust": AutoAdjust
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: AutoAdjust": "LayerColor: AutoAdjust"
}
@@ -1,147 +0,0 @@
import torch
from PIL import Image, ImageEnhance, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_channel_split, image_channel_merge, normalize_gray, gamma_trans, chop_image_v2, RGB2RGBA
class AutoAdjustV2:
def __init__(self):
self.NODE_NAME = 'AutoAdjustV2'
@classmethod
def INPUT_TYPES(self):
mode_list = ["RGB", "lum + sat", "mono", "luminance", "saturation"]
return {
"required": {
"image": ("IMAGE", ), #
"strength": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
"brightness": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"contrast": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"saturation": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"red": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"green": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"blue": ("INT", {"default": 0, "min": -100, "max": 100, "step": 1}),
"mode": (mode_list, ),
},
"optional": {
"mask": ("MASK", ),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'auto_adjust_v2'
CATEGORY = '😺dzNodes/LayerColor'
def auto_adjust_v2(self, image, strength, brightness, contrast, saturation, red, green, blue, mode, mask=None):
def auto_level_gray(image, mask):
gray_image = Image.new("L", image.size, color='gray')
gray_image.paste(image.convert('L'), mask=mask)
return normalize_gray(gray_image)
if brightness < 0:
brightness_offset = brightness / 100 + 1
else:
brightness_offset = brightness / 50 + 1
if contrast < 0:
contrast_offset = contrast / 100 + 1
else:
contrast_offset = contrast / 50 + 1
if saturation < 0:
saturation_offset = saturation / 100 + 1
else:
saturation_offset = saturation / 50 + 1
l_images = []
l_masks = []
ret_images = []
for l in 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 mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_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]
orig_image = tensor2pil(_image)
if mode == 'RGB':
r, g, b, _ = image_channel_split(orig_image, mode = 'RGB')
r = auto_level_gray(r, _mask)
g = auto_level_gray(g, _mask)
b = auto_level_gray(b, _mask)
ret_image = image_channel_merge((r, g, b), 'RGB')
elif mode == 'lum + sat':
h, s, v, _ = image_channel_split(orig_image, mode = 'HSV')
s = auto_level_gray(s, _mask)
ret_image = image_channel_merge((h, s, v), 'HSV')
l, a, b, _ = image_channel_split(ret_image, mode = 'LAB')
l = auto_level_gray(l, _mask)
ret_image = image_channel_merge((l, a, b), 'LAB')
elif mode == 'luminance':
l, a, b, _ = image_channel_split(orig_image, mode = 'LAB')
l = auto_level_gray(l, _mask)
ret_image = image_channel_merge((l, a, b), 'LAB')
elif mode == 'saturation':
h, s, v, _ = image_channel_split(orig_image, mode = 'HSV')
s = auto_level_gray(s, _mask)
ret_image = image_channel_merge((h, s, v), 'HSV')
else: # mono
gray = orig_image.convert('L')
ret_image = auto_level_gray(gray, _mask).convert('RGB')
if (red or green or blue) and mode != "mono":
r, g, b, _ = image_channel_split(ret_image, mode='RGB')
if red:
r = gamma_trans(r, self.balance_to_gamma(red)).convert('L')
if green:
g = gamma_trans(g, self.balance_to_gamma(green)).convert('L')
if blue:
b = gamma_trans(b, self.balance_to_gamma(blue)).convert('L')
ret_image = image_channel_merge((r, g, b), 'RGB')
if brightness:
brightness_image = ImageEnhance.Brightness(ret_image)
ret_image = brightness_image.enhance(factor=brightness_offset)
if contrast:
contrast_image = ImageEnhance.Contrast(ret_image)
ret_image = contrast_image.enhance(factor=contrast_offset)
if saturation:
color_image = ImageEnhance.Color(ret_image)
ret_image = color_image.enhance(factor=saturation_offset)
ret_image = chop_image_v2(orig_image, ret_image, blend_mode="normal", opacity=strength)
ret_image.paste(orig_image, mask=ImageChops.invert(_mask))
if orig_image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
def balance_to_gamma(self, balance:int) -> float:
return 0.00005 * balance * balance - 0.01 * balance + 1
NODE_CLASS_MAPPINGS = {
"LayerColor: AutoAdjustV2": AutoAdjustV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: AutoAdjustV2": "LayerColor: AutoAdjust V2"
}
@@ -1,80 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import histogram_equalization, chop_image, image_channel_merge, image_gray_offset, RGB2RGBA
class AutoBrightness:
def __init__(self):
self.NODE_NAME = 'AutoBrightness'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"strength": ("INT", {"default": 75, "min": 0, "max": 100, "step": 1}),
"saturation": ("INT", {"default": 8, "min": -255, "max": 255, "step": 1}),
},
"optional": {
"mask": ("MASK", ),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'auto_brightness'
CATEGORY = '😺dzNodes/LayerColor'
def auto_brightness(self, image, strength, saturation, mask=None):
l_images = []
l_masks = []
ret_images = []
for l in 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 mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_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]
orig_image = tensor2pil(_image)
_l, _a, _b = orig_image.convert('LAB').split()
_histogram = histogram_equalization(_l, _mask, gamma_strength=strength/100)
_l = chop_image(_l, _histogram, 'normal', strength)
ret_image = image_channel_merge((_l, _a, _b), 'LAB')
if saturation != 0 :
_h, _s, _v = ret_image.convert('HSV').split()
_s = image_gray_offset(_s, saturation)
ret_image = image_channel_merge((_h, _s, _v), 'HSV')
if orig_image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: AutoBrightness": AutoBrightness
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: AutoBrightness": "LayerColor: AutoBrightness"
}
@@ -1,114 +0,0 @@
import torch
from PIL import Image, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import RGB2RGBA
class ColorCorrectBrightnessAndContrast:
def __init__(self):
self.NODE_NAME = 'Brightness & Contrast'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"contrast": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_brightness_and_contrast'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_brightness_and_contrast(self, image, brightness, contrast, saturation):
ret_images = []
for i in image:
i = torch.unsqueeze(i,0)
__image = tensor2pil(i)
ret_image = __image.convert('RGB')
if brightness != 1:
brightness_image = ImageEnhance.Brightness(ret_image)
ret_image = brightness_image.enhance(factor=brightness)
if contrast != 1:
contrast_image = ImageEnhance.Contrast(ret_image)
ret_image = contrast_image.enhance(factor=contrast)
if saturation != 1:
color_image = ImageEnhance.Color(ret_image)
ret_image = color_image.enhance(factor=saturation)
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
# 节点名称去掉“&”
class LS_ColorCorrect_Brightness_And_Contrast_V2:
def __init__(self):
self.NODE_NAME = 'Brightness Contrast V2'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"contrast": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_brightness_contrast_v2'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_brightness_contrast_v2(self, image, brightness, contrast, saturation):
ret_images = []
for i in image:
i = torch.unsqueeze(i,0)
__image = tensor2pil(i)
ret_image = __image.convert('RGB')
if brightness != 1:
brightness_image = ImageEnhance.Brightness(ret_image)
ret_image = brightness_image.enhance(factor=brightness)
if contrast != 1:
contrast_image = ImageEnhance.Contrast(ret_image)
ret_image = contrast_image.enhance(factor=contrast)
if saturation != 1:
color_image = ImageEnhance.Color(ret_image)
ret_image = color_image.enhance(factor=saturation)
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: Brightness & Contrast": ColorCorrectBrightnessAndContrast,
"LayerColor: BrightnessContrastV2": LS_ColorCorrect_Brightness_And_Contrast_V2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: BrightnessContrastV2": "LayerColor: Brightness Contrast V2"
}
@@ -1,78 +0,0 @@
import torch
from PIL import Image, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import color_balance, RGB2RGBA
class ColorBalance:
def __init__(self):
self.NODE_NAME = 'ColorBalance'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"cyan_red": ("FLOAT", {"default": 0, "min": -1.0, "max": 1.0, "step": 0.001}),
"magenta_green": ("FLOAT", {"default": 0, "min": -1.0, "max": 1.0, "step": 0.001}),
"yellow_blue": ("FLOAT", {"default": 0, "min": -1.0, "max": 1.0, "step": 0.001})
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_balance'
CATEGORY = '😺dzNodes/LayerColor'
def color_balance(self, image, cyan_red, magenta_green, yellow_blue):
l_images = []
l_masks = []
ret_images = []
for l in 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'))
for i in range(len(l_images)):
_image = l_images[i]
_mask = l_masks[i]
orig_image = tensor2pil(_image)
ret_image = color_balance(orig_image,
[cyan_red, magenta_green, yellow_blue],
[cyan_red, magenta_green, yellow_blue],
[cyan_red, magenta_green, yellow_blue],
shadow_center=0.15,
midtone_center=0.5,
midtone_max=1,
preserve_luminosity=True)
if orig_image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: ColorBalance": ColorBalance
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: ColorBalance": "LayerColor: ColorBalance"
}
@@ -1,61 +0,0 @@
# Adapt from https://github.com/EllangoK/ComfyUI-post-processing-nodes/blob/master/post_processing/color_correct.py
import torch
import numpy as np
from PIL import Image
from .imagefunc import log
class ColorTemperature:
def __init__(self):
self.NODE_NAME = 'ColorTemperature'
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"temperature": ("FLOAT", {"default": 0, "min": -100, "max": 100, "step": 1},),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "color_temperature"
CATEGORY = '😺dzNodes/LayerColor'
def color_temperature(self, image, temperature,
):
batch_size, height, width, _ = image.shape
result = torch.zeros_like(image)
temperature /= -100
for b in range(batch_size):
tensor_image = image[b].numpy()
modified_image = Image.fromarray((tensor_image * 255).astype(np.uint8))
modified_image = np.array(modified_image).astype(np.float32)
if temperature > 0:
modified_image[:, :, 0] *= 1 + temperature
modified_image[:, :, 1] *= 1 + temperature * 0.4
elif temperature < 0:
modified_image[:, :, 0] *= 1 + temperature * 0.2
modified_image[:, :, 2] *= 1 - temperature
modified_image = np.clip(modified_image, 0, 255)
modified_image = modified_image.astype(np.uint8)
modified_image = modified_image / 255
modified_image = torch.from_numpy(modified_image).unsqueeze(0)
result[b] = modified_image
log(f"{self.NODE_NAME} Processed {len(result)} image(s).", message_type='finish')
return (result,)
NODE_CLASS_MAPPINGS = {
"LayerColor: ColorTemperature": ColorTemperature
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: ColorTemperature": "LayerColor: ColorTemperature"
}
@@ -1,63 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import RGB2RGBA
class ColorCorrectExposure:
def __init__(self):
self.NODE_NAME = 'Exposure'
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"exposure": ("INT", {"default": 20, "min": -100, "max": 100, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_exposure'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_exposure(self, image, exposure):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
__image = tensor2pil(i)
t = i.detach().clone().cpu().numpy().astype(np.float32)
more = t[:, :, :, :3] > 0
t[:, :, :, :3][more] *= pow(2, exposure / 32)
if exposure < 0:
bp = -exposure / 250
scale = 1 / (1 - bp)
t = np.clip((t - bp) * scale, 0.0, 1.0)
ret_image = tensor2pil(torch.from_numpy(t))
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: Exposure": ColorCorrectExposure
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: Exposure": "LayerColor: Exposure"
}
@@ -1,52 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import gamma_trans, RGB2RGBA
class ColorCorrectGamma:
def __init__(self):
self.NODE_NAME = 'Gamma'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"gamma": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.01}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_correct_gamma'
CATEGORY = '😺dzNodes/LayerColor'
def color_correct_gamma(self, image, gamma):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
__image = tensor2pil(i)
ret_image = gamma_trans(tensor2pil(i), gamma)
if __image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, __image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: Gamma": ColorCorrectGamma
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: Gamma": "LayerColor: Gamma"
}
@@ -1,93 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import image_channel_merge, image_channel_split, RGB2RGBA, adjust_levels
class ColorCorrectLevels:
def __init__(self):
self.NODE_NAME = 'Levels'
@classmethod
def INPUT_TYPES(self):
channel_list = ["RGB", "red", "green", "blue"]
return {
"required": {
"image": ("IMAGE", ), #
"channel": (channel_list,),
"black_point": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1, "display": "slider"}),
"white_point": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1, "display": "slider"}),
"gray_point": ("FLOAT", {"default": 1, "min": 0.01, "max": 9.99, "step": 0.01}),
"output_black_point": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
"output_white_point": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'levels'
CATEGORY = '😺dzNodes/LayerColor'
def levels(self, image, channel,
black_point, white_point,
gray_point, output_black_point, output_white_point):
l_images = []
l_masks = []
ret_images = []
for l in 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'))
for i in range(len(l_images)):
_image = l_images[i]
_mask = l_masks[i]
orig_image = tensor2pil(_image)
if channel == "red":
r, g, b, _ = image_channel_split(orig_image, 'RGB')
r = adjust_levels(r, black_point, white_point, gray_point,
output_black_point, output_white_point)
ret_image = image_channel_merge((r.convert('L'), g, b), 'RGB')
elif channel == "green":
r, g, b, _ = image_channel_split(orig_image, 'RGB')
g = adjust_levels(g, black_point, white_point, gray_point,
output_black_point, output_white_point)
ret_image = image_channel_merge((r, g.convert('L'), b), 'RGB')
elif channel == "blue":
r, g, b, _ = image_channel_split(orig_image, 'RGB')
b = adjust_levels(b, black_point, white_point, gray_point,
output_black_point, output_white_point)
ret_image = image_channel_merge((r, g, b.convert('L')), 'RGB')
else:
ret_image = adjust_levels(orig_image, black_point, white_point, gray_point,
output_black_point, output_white_point)
if orig_image.mode == 'RGBA':
ret_image = RGB2RGBA(ret_image, orig_image.split()[-1])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: Levels": ColorCorrectLevels
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: Levels": "LayerColor: Levels"
}
@@ -1,247 +0,0 @@
import torch
from PIL import Image, ImageChops, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import get_gray_average, calculate_shadow_highlight_level, luminance_keyer, gaussian_blur, image_channel_merge, image_hue_offset
def norm_value(value):
if value < 0.01:
value = 0.01
if value > 0.99:
value = 0.99
return value
class ColorCorrectShadowAndHighlight:
def __init__(self):
self.NODE_NAME = 'Color of Shadow & Highlight'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"shadow_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"shadow_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"shadow_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"shadow_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
"shadow_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
"highlight_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"highlight_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"highlight_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"highlight_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
"highlight_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_shadow_and_highlight'
CATEGORY = '😺dzNodes/LayerColor'
def color_shadow_and_highlight(self, image,
shadow_brightness, shadow_saturation,
shadow_level_offset, shadow_range, shadow_hue,
highlight_brightness, highlight_saturation, highlight_hue,
highlight_level_offset, highlight_range,
mask=None
):
ret_images = []
input_images = []
input_masks = []
for i in image:
input_images.append(torch.unsqueeze(i, 0))
m = tensor2pil(i)
if m.mode == 'RGBA':
input_masks.append(m.split()[-1])
else:
input_masks.append(Image.new('L', size=m.size, color='white'))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
input_masks = []
for m in mask:
input_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(input_images), len(input_masks))
for i in range(max_batch):
_image = input_images[i] if i < len(input_images) else input_images[-1]
_image = tensor2pil(_image).convert('RGB')
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
avg_gray = get_gray_average(_image, _mask)
shadow_level, highlight_level = calculate_shadow_highlight_level(avg_gray)
_canvas = _image.copy()
if shadow_saturation !=1 or shadow_brightness !=1 or shadow_hue:
shadow_low_threshold = (shadow_level + shadow_level_offset) / 100 + shadow_range / 2
shadow_low_threshold = norm_value(shadow_low_threshold)
shadow_high_threshold = (shadow_level + shadow_level_offset) / 100 - shadow_range / 2
shadow_high_threshold = norm_value(shadow_high_threshold)
_shadow_mask = luminance_keyer(_image, shadow_low_threshold, shadow_high_threshold)
_shadow = _image.copy()
if shadow_brightness != 1:
brightness_image = ImageEnhance.Brightness(_shadow)
_shadow = brightness_image.enhance(factor=shadow_brightness)
if shadow_saturation != 1:
color_image = ImageEnhance.Color(_shadow)
_shadow = color_image.enhance(factor=shadow_saturation)
if shadow_hue:
_h, _s, _v = _shadow.convert('HSV').split()
_h = image_hue_offset(_h, shadow_hue)
_shadow = image_channel_merge((_h, _s, _v), 'HSV')
_canvas.paste(_shadow, mask=gaussian_blur(_shadow_mask,(_shadow_mask.width + _shadow_mask.height)//800))
_canvas.paste(_image, mask=ImageChops.invert(_mask))
if highlight_saturation != 1 or highlight_brightness != 1 or highlight_hue:
highlight_low_threshold = (highlight_level + highlight_level_offset) / 100 - highlight_range / 2
highlight_low_threshold = norm_value(highlight_low_threshold)
highlight_high_threshold = (highlight_level + highlight_level_offset) / 100 + highlight_range / 2
highlight_high_threshold = norm_value(highlight_high_threshold)
_highlight_mask = luminance_keyer(_image, highlight_low_threshold, highlight_high_threshold)
_highlight = _image.copy()
if highlight_brightness != 1:
brightness_image = ImageEnhance.Brightness(_highlight)
_highlight = brightness_image.enhance(factor=highlight_brightness)
if highlight_saturation != 1:
color_image = ImageEnhance.Color(_highlight)
_highlight = color_image.enhance(factor=highlight_saturation)
if highlight_hue:
_h, _s, _v = _highlight.convert('HSV').split()
_h = image_hue_offset(_h, highlight_hue)
_highlight = image_channel_merge((_h, _s, _v), 'HSV')
_canvas.paste(_highlight, mask=gaussian_blur(_highlight_mask, (_highlight_mask.width + _highlight_mask.height)//800))
_canvas.paste(_image, mask=ImageChops.invert(_mask))
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
# 名称去掉“&”
class LS_ColorCorrectShadow_And_Highlight_V2:
def __init__(self):
self.NODE_NAME = 'Color of Shadow & Highlight V2'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"shadow_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"shadow_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"shadow_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"shadow_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
"shadow_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
"highlight_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"highlight_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
"highlight_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"highlight_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
"highlight_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_shadow_and_highlight_v2'
CATEGORY = '😺dzNodes/LayerColor'
def color_shadow_and_highlight_v2(self, image,
shadow_brightness, shadow_saturation,
shadow_level_offset, shadow_range, shadow_hue,
highlight_brightness, highlight_saturation, highlight_hue,
highlight_level_offset, highlight_range,
mask=None
):
ret_images = []
input_images = []
input_masks = []
for i in image:
input_images.append(torch.unsqueeze(i, 0))
m = tensor2pil(i)
if m.mode == 'RGBA':
input_masks.append(m.split()[-1])
else:
input_masks.append(Image.new('L', size=m.size, color='white'))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
input_masks = []
for m in mask:
input_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(input_images), len(input_masks))
for i in range(max_batch):
_image = input_images[i] if i < len(input_images) else input_images[-1]
_image = tensor2pil(_image).convert('RGB')
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
avg_gray = get_gray_average(_image, _mask)
shadow_level, highlight_level = calculate_shadow_highlight_level(avg_gray)
_canvas = _image.copy()
if shadow_saturation !=1 or shadow_brightness !=1 or shadow_hue:
shadow_low_threshold = (shadow_level + shadow_level_offset) / 100 + shadow_range / 2
shadow_low_threshold = norm_value(shadow_low_threshold)
shadow_high_threshold = (shadow_level + shadow_level_offset) / 100 - shadow_range / 2
shadow_high_threshold = norm_value(shadow_high_threshold)
_shadow_mask = luminance_keyer(_image, shadow_low_threshold, shadow_high_threshold)
_shadow = _image.copy()
if shadow_brightness != 1:
brightness_image = ImageEnhance.Brightness(_shadow)
_shadow = brightness_image.enhance(factor=shadow_brightness)
if shadow_saturation != 1:
color_image = ImageEnhance.Color(_shadow)
_shadow = color_image.enhance(factor=shadow_saturation)
if shadow_hue:
_h, _s, _v = _shadow.convert('HSV').split()
_h = image_hue_offset(_h, shadow_hue)
_shadow = image_channel_merge((_h, _s, _v), 'HSV')
_canvas.paste(_shadow, mask=gaussian_blur(_shadow_mask,(_shadow_mask.width + _shadow_mask.height)//800))
_canvas.paste(_image, mask=ImageChops.invert(_mask))
if highlight_saturation != 1 or highlight_brightness != 1 or highlight_hue:
highlight_low_threshold = (highlight_level + highlight_level_offset) / 100 - highlight_range / 2
highlight_low_threshold = norm_value(highlight_low_threshold)
highlight_high_threshold = (highlight_level + highlight_level_offset) / 100 + highlight_range / 2
highlight_high_threshold = norm_value(highlight_high_threshold)
_highlight_mask = luminance_keyer(_image, highlight_low_threshold, highlight_high_threshold)
_highlight = _image.copy()
if highlight_brightness != 1:
brightness_image = ImageEnhance.Brightness(_highlight)
_highlight = brightness_image.enhance(factor=highlight_brightness)
if highlight_saturation != 1:
color_image = ImageEnhance.Color(_highlight)
_highlight = color_image.enhance(factor=highlight_saturation)
if highlight_hue:
_h, _s, _v = _highlight.convert('HSV').split()
_h = image_hue_offset(_h, highlight_hue)
_highlight = image_channel_merge((_h, _s, _v), 'HSV')
_canvas.paste(_highlight, mask=gaussian_blur(_highlight_mask, (_highlight_mask.width + _highlight_mask.height)//800))
_canvas.paste(_image, mask=ImageChops.invert(_mask))
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerColor: Color of Shadow & Highlight": ColorCorrectShadowAndHighlight,
"LayerColor: ColorofShadowHighlightV2": LS_ColorCorrectShadow_And_Highlight_V2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerColor: Color of Shadow & Highlight": "LayerColor: Color of Shadow & Highlight",
"LayerColor: ColorofShadowHighlightV2": "LayerColor: Colorof Shadow Highlight V2"
}
-39
View File
@@ -1,39 +0,0 @@
from PIL import Image
from .imagefunc import log, pil2tensor
class ColorImage:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"color": ("STRING", {"default": "#000000"},),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("image", )
FUNCTION = 'color_image'
CATEGORY = '😺dzNodes/LayerUtility'
def color_image(self, width, height, color, ):
ret_image = Image.new('RGB', (width, height), color=color)
return (pil2tensor(ret_image), )
NODE_CLASS_MAPPINGS = {
"LayerUtility: ColorImage": ColorImage
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ColorImage": "LayerUtility: ColorImage"
}
@@ -1,65 +0,0 @@
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, AnyType, load_custom_size
any = AnyType("*")
class ColorImageV2:
def __init__(self):
self.NODE_NAME = 'ColorImage V2'
@classmethod
def INPUT_TYPES(self):
size_list = ['custom']
size_list.extend(load_custom_size())
return {
"required": {
"size": (size_list,),
"custom_width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"custom_height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"color": ("STRING", {"default": "#000000"},),
},
"optional": {
"size_as": (any, {}),
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("image", )
FUNCTION = 'color_image_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def color_image_v2(self, size, custom_width, custom_height, color, size_as=None ):
if size_as is not None:
if size_as.shape[0] > 0:
_asimage = tensor2pil(size_as[0])
else:
_asimage = tensor2pil(size_as)
width, height = _asimage.size
else:
if size == 'custom':
width = custom_width
height = custom_height
else:
try:
_s = size.split('x')
width = int(_s[0].strip())
height = int(_s[1].strip())
except Exception as e:
log(f'Warning: {self.NODE_NAME} invalid size, check "custom_size.ini"', message_type='warning')
width = custom_width
height = custom_height
ret_image = Image.new('RGB', (width, height), color=color)
return (pil2tensor(ret_image), )
NODE_CLASS_MAPPINGS = {
"LayerUtility: ColorImage V2": ColorImageV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ColorImage V2": "LayerUtility: ColorImage V2"
}
-57
View File
@@ -1,57 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, chop_image
from .imagefunc import image_to_colormap
colormap_list = ['autumn', 'bone', 'jet', 'winter', 'rainbow', 'ocean',
'summer', 'sprint', 'cool', 'HSV', 'pink', 'hot',
'parula', 'magma', 'inferno', 'plasma', 'viridis', 'cividis',
'twilight', 'twilight_shifted', 'turbo', 'deepgreen']
class ColorMap:
def __init__(self):
self.NODE_NAME = 'ColorMap'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"color_map": (colormap_list,),
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'color_map'
CATEGORY = '😺dzNodes/LayerFilter'
def color_map(self, image, color_map, opacity
):
ret_images = []
for i in image:
i = torch.unsqueeze(i, 0)
_canvas = tensor2pil(i)
_image = image_to_colormap(_canvas, colormap_list.index(color_map))
ret_image = chop_image(_canvas, _image, 'normal', opacity)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: ColorMap": ColorMap
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: ColorMap": "LayerFilter: ColorMap"
}
File diff suppressed because it is too large Load Diff
@@ -1,91 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import chop_image_v2, chop_mode_v2
class ColorOverlayV2:
def __init__(self):
self.NODE_NAME = 'ColorOverlayV2'
@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: {self.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: {self.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"{self.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"
}
@@ -1,91 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import chop_mode,chop_image
class ColorOverlay:
def __init__(self):
self.NODE_NAME = 'ColorOverlay'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerStyle'
def color_overlay(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: {self.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: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image(_layer, _color, blend_mode, opacity)
_canvas.paste(_comp, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: ColorOverlay": ColorOverlay
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: ColorOverlay": "LayerStyle: ColorOverlay"
}
-40
View File
@@ -1,40 +0,0 @@
from .imagefunc import Hex_to_RGB
class ColorPicker:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
mode_list = ['HEX', 'DEC']
return {
"required": {
"color": ("COLOR", {"default": "#FFFFFF"},),
"mode": (mode_list,), # 输出模式
},
"optional": {
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("value",)
FUNCTION = 'picker'
CATEGORY = '😺dzNodes/LayerUtility'
def picker(self, color, mode):
ret = color
if mode == 'DEC':
ret = Hex_to_RGB(ret)
return (ret,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ColorPicker": ColorPicker
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ColorPicker": "LayerUtility: ColorPicker"
}
@@ -1,43 +0,0 @@
from .imagefunc import AnyType, Hex_to_HSV_255level, log
any = AnyType("*")
class ColorValuetoHSVValue:
def __init__(self):
self.NODE_NAME = 'HSV Value'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"color_value": (any, {}),
},
"optional": {
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("H", "S", "V")
FUNCTION = 'color_value_to_hsv_value'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def color_value_to_hsv_value(self, color_value,):
H, S, V = 0, 0, 0
if isinstance(color_value, str):
H, S, V = Hex_to_HSV_255level(color_value)
elif isinstance(color_value, tuple):
H, S, V = Hex_to_HSV_255level(RGB_to_Hex(color_value))
else:
log(f"{self.NODE_NAME}: color_value input type must be tuple or string.", message_type="error")
return (H, S, V,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: HSV Value": ColorValuetoHSVValue
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: HSV Value": "LayerUtility: HSV Value"
}
@@ -1,45 +0,0 @@
from .imagefunc import AnyType, Hex_to_RGB, log
any = AnyType("*")
class ColorValuetoRGBValue:
def __init__(self):
self.NODE_NAME = 'RGB Value'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"color_value": (any, {}),
},
"optional": {
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("R", "G", "B")
FUNCTION = 'color_value_to_rgb_value'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def color_value_to_rgb_value(self, color_value,):
R, G, B = 0, 0, 0
if isinstance(color_value, str):
color = Hex_to_RGB(color_value)
R, G, B = color[0], color[1], color[2]
elif isinstance(color_value, tuple):
R, G, B = color_value[0], color_value[1], color_value[2]
else:
log(f"{self.NODE_NAME}: color_value input type must be tuple or string.", message_type="error")
return (R, G, B,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: RGB Value": ColorValuetoRGBValue
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: RGB Value": "LayerUtility: RGB Value"
}
@@ -1,37 +0,0 @@
from .imagefunc import AnyType, rgb2gray
any = AnyType("*")
class ColorValuetoGrayValue:
def __init__(self):
self.NODE_NAME = 'Gray Value'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"color_value": (any, {}),
},
"optional": {
}
}
RETURN_TYPES = ("INT", "INT",)
RETURN_NAMES = ("gray(256_level)", "gray(100_level)",)
FUNCTION = 'color_value_to_gray_value'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def color_value_to_gray_value(self, color_value,):
gray = rgb2gray(color_value)
return (gray, int(gray / 2.55),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: GrayValue": ColorValuetoGrayValue
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: GrayValue": "LayerUtility: Gray Value"
}
@@ -1,125 +0,0 @@
import torch
import copy
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, chop_image, AnyType
from .imagefunc import create_gradient, create_box_gradient, gaussian_blur, gamma_trans, mask_area
any = AnyType("*")
class CreateGradientMask:
def __init__(self):
self.NODE_NAME = 'CreateGradientMask'
@classmethod
def INPUT_TYPES(self):
side = ['bottom', 'top', 'left', 'right', 'center']
return {
"required": {
"width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"gradient_side": (side,),
"gradient_scale": ("INT", {"default": 100, "min": 1, "max": 9999, "step": 1}),
"gradient_offset": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
},
"optional": {
"size_as": (any, {}),
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'create_gradient_mask'
CATEGORY = '😺dzNodes/LayerMask'
def create_gradient_mask(self, width, height, gradient_side, gradient_scale, gradient_offset, opacity, size_as=None):
if size_as is not None:
if size_as.shape[0] > 0:
_asimage = tensor2pil(size_as[0])
else:
_asimage = tensor2pil(size_as)
width, height = _asimage.size
_black = Image.new('L', size=(width, height), color='black')
_white = Image.new('L', size=(width, height), color='white')
_canvas = copy.deepcopy(_black)
debug_image1 = copy.deepcopy(_black).convert('RGB')
debug_image2 = copy.deepcopy(_black).convert('RGB')
start_color = '#FFFFFF'
end_color = '#000000'
if gradient_side == 'bottom':
_gradient = create_gradient(start_color, end_color, width, height, direction='bottom')
if gradient_scale != 100:
_gradient = _gradient.resize((width, int(height * gradient_scale / 100)))
_canvas.paste(_gradient.convert('L'), box=(0, gradient_offset))
if gradient_offset > height:
_canvas = _white
elif gradient_offset > 0:
_canvas.paste(_white, box=(0, gradient_offset - height))
elif gradient_side == 'top':
_gradient = create_gradient(start_color, end_color, width, height, direction='top')
if gradient_scale != 100:
_gradient = _gradient.resize((width, int(height * gradient_scale / 100)))
_canvas.paste(_gradient.convert('L'), box=(0, height - int(height * gradient_scale / 100) + gradient_offset))
if gradient_offset < -height:
_canvas = _white
elif gradient_offset < 0:
_canvas.paste(_white, box=(0, height + gradient_offset))
elif gradient_side == 'left':
_gradient = create_gradient(start_color, end_color, width, height, direction='left')
if gradient_scale != 100:
_gradient = _gradient.resize((int(width * gradient_scale / 100), height))
_canvas.paste(_gradient.convert('L'), box=(width - int(width * gradient_scale / 100) + gradient_offset, 0))
if gradient_offset < -width:
_canvas = _white
elif gradient_offset < 0:
_canvas.paste(_white, box=(width + gradient_offset, 0))
elif gradient_side == 'right':
_gradient = create_gradient(start_color, end_color, width, height, direction='right')
if gradient_scale != 100:
_gradient = _gradient.resize((int(width * gradient_scale / 100), height))
_canvas.paste(_gradient.convert('L'), box=(gradient_offset, 0))
if gradient_offset > width:
_canvas = _white
elif gradient_offset > 0:
_canvas.paste(_white, box=(gradient_offset - width, 0))
else:
_gradient = create_box_gradient(start_color_inhex='#000000', end_color_inhex='#FFFFFF',
width=width, height=height, scale=int(gradient_scale))
_gradient = _gradient.convert('L')
debug_image1 = _gradient
_blur_mask = Image.new('L', size=(width*2, height*2), color='black')
_blur_mask.paste(_gradient, box=(int(width/2), int(height/2)))
_blur_mask = gaussian_blur(_blur_mask, int((width + height) * gradient_scale / 100 / 16))
_gamma_mask = gamma_trans(_blur_mask, 0.15)
(crop_x, crop_y, crop_width, crop_height) = mask_area(_gamma_mask)
crop_box = (crop_x, crop_y, crop_x + crop_width, crop_y + crop_height)
_blur_mask = _blur_mask.crop(crop_box)
_blur_mask = _blur_mask.resize((width, height), Image.BILINEAR)
if gradient_offset != 0:
resize_width = int(width - gradient_offset)
resize_height = int(height - gradient_offset)
if resize_width < 1:
resize_width = 1
if resize_height < 1:
resize_height = 1
_blur_mask = _blur_mask.resize((resize_width, resize_height), Image.BILINEAR)
paste_box = (int((width - resize_width) / 2), int((height - resize_height) / 2))
else:
paste_box = (0,0)
_canvas.paste(_blur_mask, box=paste_box)
# opacity
if opacity < 100:
_canvas = chop_image(_black, _canvas, 'normal', opacity)
log(f"{self.NODE_NAME} Processed.", message_type='finish')
return (image2mask(_canvas),)
NODE_CLASS_MAPPINGS = {
"LayerMask: CreateGradientMask": CreateGradientMask
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: CreateGradientMask": "LayerMask: CreateGradientMask"
}
@@ -1,43 +0,0 @@
class CropBoxResolve:
def __init__(self):
self.NODE_NAME = 'CropBoxResolve'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"crop_box": ("BOX",),
},
"optional": {
}
}
RETURN_TYPES = ("INT", "INT", "INT", "INT")
RETURN_NAMES = ("x", "y", "width", "height")
FUNCTION = 'crop_box_resolve'
CATEGORY = '😺dzNodes/LayerUtility'
def crop_box_resolve(self, crop_box
):
(x1, y1, x2, y2) = crop_box
x1 = int(x1)
y1 = int(y1)
x2 = int(x2)
y2 = int(y2)
return (x1, y1, x2 - x1, y2 - y1,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CropBoxResolve": CropBoxResolve
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CropBoxResolve": "LayerUtility: CropBoxResolve"
}
-100
View File
@@ -1,100 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, gaussian_blur, min_bounding_rect, max_inscribed_rect, mask_area
from .imagefunc import num_round_up_to_multiple, draw_rect
class CropByMask:
def __init__(self):
self.NODE_NAME = 'CropByMask'
@classmethod
def INPUT_TYPES(self):
detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area']
return {
"required": {
"image": ("IMAGE", ), #
"mask_for_crop": ("MASK",),
"invert_mask": ("BOOLEAN", {"default": False}), # 反转mask#
"detect": (detect_mode,),
"top_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"bottom_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"left_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"right_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "IMAGE",)
RETURN_NAMES = ("croped_image", "croped_mask", "crop_box", "box_preview")
FUNCTION = 'crop_by_mask'
CATEGORY = '😺dzNodes/LayerUtility'
def crop_by_mask(self, image, mask_for_crop, invert_mask, detect,
top_reserve, bottom_reserve, left_reserve, right_reserve
):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
if mask_for_crop.dim() == 2:
mask_for_crop = torch.unsqueeze(mask_for_crop, 0)
# 如果有多张mask输入,使用第一张
if mask_for_crop.shape[0] > 1:
log(f"Warning: Multiple mask inputs, using the first.", message_type='warning')
mask_for_crop = torch.unsqueeze(mask_for_crop[0], 0)
if invert_mask:
mask_for_crop = 1 - mask_for_crop
l_masks.append(tensor2pil(torch.unsqueeze(mask_for_crop, 0)).convert('L'))
_mask = mask2image(mask_for_crop)
bluredmask = gaussian_blur(_mask, 20).convert('L')
x = 0
y = 0
width = 0
height = 0
if detect == "min_bounding_rect":
(x, y, width, height) = min_bounding_rect(bluredmask)
elif detect == "max_inscribed_rect":
(x, y, width, height) = max_inscribed_rect(bluredmask)
else:
(x, y, width, height) = mask_area(_mask)
width = num_round_up_to_multiple(width, 8)
height = num_round_up_to_multiple(height, 8)
log(f"{self.NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}")
canvas_width, canvas_height = tensor2pil(torch.unsqueeze(image[0], 0)).convert('RGB').size
x1 = x - left_reserve if x - left_reserve > 0 else 0
y1 = y - top_reserve if y - top_reserve > 0 else 0
x2 = x + width + right_reserve if x + width + right_reserve < canvas_width else canvas_width
y2 = y + height + bottom_reserve if y + height + bottom_reserve < canvas_height else canvas_height
preview_image = tensor2pil(mask_for_crop).convert('RGB')
preview_image = draw_rect(preview_image, x, y, width, height, line_color="#F00000", line_width=(width+height)//100)
preview_image = draw_rect(preview_image, x1, y1, x2 - x1, y2 - y1,
line_color="#00F000", line_width=(width+height)//200)
crop_box = (x1, y1, x2, y2)
for i in range(len(l_images)):
_canvas = tensor2pil(l_images[i]).convert('RGB')
_mask = l_masks[0]
ret_images.append(pil2tensor(_canvas.crop(crop_box)))
ret_masks.append(image2mask(_mask.crop(crop_box)))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), list(crop_box), pil2tensor(preview_image),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CropByMask": CropByMask
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CropByMask": "LayerUtility: CropByMask"
}
@@ -1,116 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, gaussian_blur, min_bounding_rect, max_inscribed_rect, mask_area
from .imagefunc import num_round_up_to_multiple, draw_rect
class CropByMaskV2:
def __init__(self):
self.NODE_NAME = 'CropByMask V2'
@classmethod
def INPUT_TYPES(self):
detect_mode = ['mask_area', 'min_bounding_rect', 'max_inscribed_rect']
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
return {
"required": {
"image": ("IMAGE", ), #
"mask": ("MASK",),
"invert_mask": ("BOOLEAN", {"default": False}), # 反转mask#
"detect": (detect_mode,),
"top_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"bottom_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"left_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"right_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"round_to_multiple": (multiple_list,),
},
"optional": {
"crop_box": ("BOX",),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "IMAGE",)
RETURN_NAMES = ("croped_image", "croped_mask", "crop_box", "box_preview")
FUNCTION = 'crop_by_mask_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def crop_by_mask_v2(self, image, mask, invert_mask, detect,
top_reserve, bottom_reserve,
left_reserve, right_reserve, round_to_multiple,
crop_box=None
):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
# 如果有多张mask输入,使用第一张
if mask.shape[0] > 1:
log(f"Warning: Multiple mask inputs, using the first.", message_type='warning')
mask = torch.unsqueeze(mask[0], 0)
if invert_mask:
mask = 1 - mask
l_masks.append(tensor2pil(torch.unsqueeze(mask, 0)).convert('L'))
_mask = mask2image(mask)
preview_image = tensor2pil(mask).convert('RGB')
if crop_box is None:
bluredmask = gaussian_blur(_mask, 20).convert('L')
x = 0
y = 0
width = 0
height = 0
if detect == "min_bounding_rect":
(x, y, w, h) = min_bounding_rect(bluredmask)
elif detect == "max_inscribed_rect":
(x, y, w, h) = max_inscribed_rect(bluredmask)
else:
(x, y, w, h) = mask_area(_mask)
canvas_width, canvas_height = tensor2pil(torch.unsqueeze(image[0], 0)).convert('RGB').size
x1 = x - left_reserve if x - left_reserve > 0 else 0
y1 = y - top_reserve if y - top_reserve > 0 else 0
x2 = x + w + right_reserve if x + w + right_reserve < canvas_width else canvas_width
y2 = y + h + bottom_reserve if y + h + bottom_reserve < canvas_height else canvas_height
if round_to_multiple != 'None':
multiple = int(round_to_multiple)
width = num_round_up_to_multiple(x2 - x1, multiple)
height = num_round_up_to_multiple(y2 - y1, multiple)
x1 = x1 - (width - (x2 - x1)) // 2
y1 = y1 - (height - (y2 - y1)) // 2
x2 = x1 + width
y2 = y1 + height
log(f"{self.NODE_NAME}: Box detected. x={x1},y={y1},width={width},height={height}")
crop_box = (x1, y1, x2, y2)
preview_image = draw_rect(preview_image, x, y, w, h, line_color="#F00000",
line_width=(w + h) // 100)
preview_image = draw_rect(preview_image, crop_box[0], crop_box[1],
crop_box[2] - crop_box[0], crop_box[3] - crop_box[1],
line_color="#00F000",
line_width=(crop_box[2] - crop_box[0] + crop_box[3] - crop_box[1]) // 200)
for i in range(len(l_images)):
_canvas = tensor2pil(l_images[i]).convert('RGB')
_mask = l_masks[0]
ret_images.append(pil2tensor(_canvas.crop(crop_box)))
ret_masks.append(image2mask(_mask.crop(crop_box)))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), list(crop_box), pil2tensor(preview_image),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CropByMask V2": CropByMaskV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CropByMask V2": "LayerUtility: CropByMask V2"
}
@@ -1,116 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, gaussian_blur, min_bounding_rect, max_inscribed_rect, mask_area
from .imagefunc import num_round_up_to_multiple, draw_rect
class CropByMaskV3:
def __init__(self):
self.NODE_NAME = 'CropByMask V3'
@classmethod
def INPUT_TYPES(self):
detect_mode = ['mask_area', 'min_bounding_rect', 'max_inscribed_rect']
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
return {
"required": {
"image": ("IMAGE", ), #
"mask": ("MASK",),
"invert_mask": ("BOOLEAN", {"default": False}), # 反转mask#
"detect": (detect_mode,),
"top_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"bottom_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"left_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"right_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}),
"round_to_multiple": (multiple_list,),
},
"optional": {
"crop_box": ("BOX",),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "IMAGE",)
RETURN_NAMES = ("croped_image", "croped_mask", "crop_box", "box_preview")
FUNCTION = 'crop_by_mask_v3'
CATEGORY = '😺dzNodes/LayerUtility'
def crop_by_mask_v3(self, image, mask, invert_mask, detect,
top_reserve, bottom_reserve,
left_reserve, right_reserve, round_to_multiple,
crop_box=None
):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
# 如果有多张mask输入,使用第一张
if mask.shape[0] > 1:
log(f"Warning: Multiple mask inputs, using the first.", message_type='warning')
mask = torch.unsqueeze(mask[0], 0)
if invert_mask:
mask = 1 - mask
l_masks.append(tensor2pil(torch.unsqueeze(mask, 0)).convert('L'))
_mask = mask2image(mask)
preview_image = tensor2pil(mask).convert('RGBA')
if crop_box is None:
bluredmask = gaussian_blur(_mask, 20).convert('L')
x = 0
y = 0
width = 0
height = 0
if detect == "min_bounding_rect":
(x, y, w, h) = min_bounding_rect(bluredmask)
elif detect == "max_inscribed_rect":
(x, y, w, h) = max_inscribed_rect(bluredmask)
else:
(x, y, w, h) = mask_area(_mask)
canvas_width, canvas_height = tensor2pil(torch.unsqueeze(image[0], 0)).convert('RGBA').size
x1 = x - left_reserve if x - left_reserve > 0 else 0
y1 = y - top_reserve if y - top_reserve > 0 else 0
x2 = x + w + right_reserve if x + w + right_reserve < canvas_width else canvas_width
y2 = y + h + bottom_reserve if y + h + bottom_reserve < canvas_height else canvas_height
if round_to_multiple != 'None':
multiple = int(round_to_multiple)
width = num_round_up_to_multiple(x2 - x1, multiple)
height = num_round_up_to_multiple(y2 - y1, multiple)
x1 = x1 - (width - (x2 - x1)) // 2
y1 = y1 - (height - (y2 - y1)) // 2
x2 = x1 + width
y2 = y1 + height
log(f"{self.NODE_NAME}: Box detected. x={x1},y={y1},width={width},height={height}")
crop_box = (x1, y1, x2, y2)
preview_image = draw_rect(preview_image, x, y, w, h, line_color="#F00000",
line_width=(w + h) // 100)
preview_image = draw_rect(preview_image, crop_box[0], crop_box[1],
crop_box[2] - crop_box[0], crop_box[3] - crop_box[1],
line_color="#00F000",
line_width=(crop_box[2] - crop_box[0] + crop_box[3] - crop_box[1]) // 200)
for i in range(len(l_images)):
_canvas = tensor2pil(l_images[i]).convert('RGBA')
_mask = l_masks[0]
ret_images.append(pil2tensor(_canvas.crop(crop_box)))
ret_masks.append(image2mask(_mask.crop(crop_box)))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), list(crop_box), pil2tensor(preview_image),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: CropByMask V3": CropByMaskV3
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: CropByMask V3": "LayerUtility: CropByMask V3"
}
-496
View File
@@ -1,496 +0,0 @@
from .imagefunc import AnyType, log, extract_all_numbers_from_str
any = AnyType("*")
class SeedNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"seed":("INT", {"default": 0, "min": 0, "max": 1e18, "step": 1}),
},}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("seed",)
FUNCTION = 'seed_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def seed_node(self, seed):
return (seed,)
class BooleanOperator:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
operator_list = ["==", "!=", ">", "<", ">=", "<=", "and", "or", "xor", "not(a)", "min", "max"]
return {"required": {
"a": (any, ),
"b": (any, ),
"operator": (operator_list,),
},}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("output",)
FUNCTION = 'bool_operator_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def bool_operator_node(self, a, b, operator):
ret_value = False
if operator == "==":
ret_value = a == b
if operator == "!=":
ret_value = a != b
if operator == ">":
ret_value = a > b
if operator == "<":
ret_value = a < b
if operator == ">=":
ret_value = a >= b
if operator == "<=":
ret_value = a <= b
if operator == "and":
ret_value = a and b
if operator == "or":
ret_value = a or b
if operator == "xor":
ret_value = not(a == b)
if operator == "not(a)":
ret_value = not a
if operator == "min":
ret_value = min(a, b)
if operator == "max":
ret_value = max(a, b)
return (ret_value,)
class BooleanOperatorV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
operator_list = ["==", "!=", ">", "<", ">=", "<=", "and", "or", "xor", "not(a)", "min", "max"]
return {
"required":
{
"a_value": ("STRING", {"default": "", "multiline": False}),
"b_value": ("STRING", {"default": "", "multiline": False}),
"operator": (operator_list,),
},
"optional": {
"a": (any,),
"b": (any,),
}
}
RETURN_TYPES = ("BOOLEAN", "STRING",)
RETURN_NAMES = ("output", "string",)
FUNCTION = 'bool_operator_node_v2'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def bool_operator_node_v2(self, a_value, b_value, operator, a = None, b = None):
if a is None:
if a_value != "":
_numbers = extract_all_numbers_from_str(a_value, checkint=True)
if len(_numbers) > 0:
a = _numbers[0]
else:
a = 0
else:
a = 0
if b is None:
if b_value != "":
_numbers = extract_all_numbers_from_str(b_value, checkint=True)
if len(_numbers) > 0:
b = _numbers[0]
else:
b = 0
else:
b = 0
ret_value = False
if operator == "==":
ret_value = a == b
if operator == "!=":
ret_value = a != b
if operator == ">":
ret_value = a > b
if operator == "<":
ret_value = a < b
if operator == ">=":
ret_value = a >= b
if operator == "<=":
ret_value = a <= b
if operator == "and":
ret_value = a and b
if operator == "or":
ret_value = a or b
if operator == "xor":
ret_value = not(a == b)
if operator == "not(a)":
ret_value = not a
if operator == "min":
ret_value = min(a, b)
if operator == "max":
ret_value = max(a, b)
return (ret_value, str(ret_value))
class NumberCalculator:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
operator_list = ["+", "-", "*", "/", "**", "//", "%", "nth_root", "min", "max"]
return {"required": {
"a": (any, {}),
"b": (any, {}),
"operator": (operator_list,),
},}
RETURN_TYPES = ("INT", "FLOAT",)
RETURN_NAMES = ("int", "float",)
FUNCTION = 'number_calculator_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def number_calculator_node(self, a, b, operator):
ret_value = 0
if operator == "+":
ret_value = a + b
if operator == "-":
ret_value = a - b
if operator == "*":
ret_value = a * b
if operator == "**":
ret_value = a ** b
if operator == "%":
ret_value = a % b
if operator == "nth_root":
ret_value = a ** (1/b)
if operator == "min":
ret_value = min(a, b)
if operator == "max":
ret_value = max(a, b)
if operator == "/":
if b != 0:
ret_value = a / b
else:
ret_value = 0
if operator == "//":
if b != 0:
ret_value = a // b
else:
ret_value = 0
return (int(ret_value), float(ret_value),)
class NumberCalculatorV2:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
operator_list = ["+", "-", "*", "/", "**", "//", "%" , "nth_root", "min", "max"]
return {
"required":
{
"a_value": ("STRING", {"default": "", "multiline": False}),
"b_value": ("STRING", {"default": "", "multiline": False}),
"operator": (operator_list,),
},
"optional": {
"a": (any,),
"b": (any,),
}
}
RETURN_TYPES = ("INT", "FLOAT", "STRING",)
RETURN_NAMES = ("int", "float", "string",)
FUNCTION = 'number_calculator_node_v2'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def number_calculator_node_v2(self, a_value, b_value, operator, a = None, b = None):
if a is None:
if a_value != "":
_numbers = extract_all_numbers_from_str(a_value, checkint=True)
if len(_numbers) > 0:
a = _numbers[0]
else:
a = 0
else:
a = 0
if b is None:
if b_value != "":
_numbers = extract_all_numbers_from_str(b_value, checkint=True)
if len(_numbers) > 0:
b = _numbers[0]
else:
b = 0
else:
b = 0
ret_value = 0
if operator == "+":
ret_value = a + b
if operator == "-":
ret_value = a - b
if operator == "*":
ret_value = a * b
if operator == "**":
ret_value = a ** b
if operator == "%":
ret_value = a % b
if operator == "nth_root":
ret_value = a ** (1/b)
if operator == "min":
ret_value = min(a, b)
if operator == "max":
ret_value = max(a, b)
if operator == "/":
if b != 0:
ret_value = a / b
else:
ret_value = 0
if operator == "//":
if b != 0:
ret_value = a // b
else:
ret_value = 0
return (int(ret_value), float(ret_value), str(ret_value))
class StringCondition:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
string_condition_list = ["include", "exclude", "equal"]
return {"required": {
"text": ("STRING", {"multiline": False}),
"condition": (string_condition_list,),
"sub_string": ("STRING", {"multiline": False}),
},}
RETURN_TYPES = ("BOOLEAN", "STRING",)
RETURN_NAMES = ("output", "string",)
FUNCTION = 'string_condition'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def string_condition(self, text, condition, sub_string):
ret = False
if condition == "include":
ret = sub_string in text
if condition == "exclude":
ret = sub_string not in text
if condition == "equal":
ret = text == sub_string
return (ret, str(ret))
class TextBoxNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"text": ("STRING", {"multiline": True}),
},}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = 'text_box_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def text_box_node(self, text):
return (text,)
class StringNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"string": ("STRING", {"multiline": False}),
},}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = 'string_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def string_node(self, string):
return (string,)
class IntegerNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"int_value":("INT", {"default": 0, "min": -1e18, "max": 1e18, "step": 1}),
},}
RETURN_TYPES = ("INT", "STRING",)
RETURN_NAMES = ("int", "string",)
FUNCTION = 'integer_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def integer_node(self, int_value):
return (int(int_value), str(int_value))
class FloatNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"float_value": ("FLOAT", {"default": 0, "min": -1e18, "max": 1e18, "step": 0.00001}),
},}
RETURN_TYPES = ("FLOAT", "STRING",)
RETURN_NAMES = ("float", "string",)
FUNCTION = 'float_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def float_node(self, float_value):
return (float_value, str(float_value))
class BooleanNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {"required": {
"bool_value": ("BOOLEAN", {"default": False}),
},}
RETURN_TYPES = ("BOOLEAN", "STRING",)
RETURN_NAMES = ("boolean", "string",)
FUNCTION = 'boolean_node'
CATEGORY = '😺dzNodes/LayerUtility/Data'
def boolean_node(self, bool_value):
return (bool_value, str(bool_value))
class IfExecute:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"if_condition": (any,),
"when_TRUE": (any,),
"when_FALSE": (any,),
},
}
RETURN_TYPES = (any,)
RETURN_NAMES = ("?",)
FUNCTION = "if_execute"
CATEGORY = '😺dzNodes/LayerUtility/Data'
def if_execute(self, if_condition, when_TRUE, when_FALSE):
return (when_TRUE if if_condition else when_FALSE,)
class SwitchCaseNode:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"switch_condition": ("STRING", {"default": "", "multiline": False}),
"case_1": ("STRING", {"default": "", "multiline": False}),
"case_2": ("STRING", {"default": "", "multiline": False}),
"case_3": ("STRING", {"default": "", "multiline": False}),
"input_default": (any,),
},
"optional": {
"input_1": (any,),
"input_2": (any,),
"input_3": (any,),
}
}
RETURN_TYPES = (any,)
RETURN_NAMES = ("?",)
FUNCTION = "switch_case"
CATEGORY = '😺dzNodes/LayerUtility/Data'
def switch_case(self, switch_condition, case_1, case_2, case_3, input_default, input_1=None, input_2=None, input_3=None):
output=input_default
if switch_condition == case_1 and input_1 is not None:
output=input_1
elif switch_condition == case_2 and input_2 is not None:
output=input_2
elif switch_condition == case_3 and input_3 is not None:
output=input_3
return (output,)
class QueueStopNode():
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
mode_list = ["stop", "continue"]
return {
"required": {
"any": (any, ),
"mode": (mode_list,),
"stop": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = (any,)
RETURN_NAMES = ("any",)
FUNCTION = 'stop_node'
CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
def stop_node(self, any, mode,stop):
if mode == "stop":
if stop:
log(f"Queue stopped, it was terminated by node.", "error")
from comfy.model_management import InterruptProcessingException
raise InterruptProcessingException()
return (any,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: QueueStop": QueueStopNode,
"LayerUtility: SwitchCase": SwitchCaseNode,
"LayerUtility: If ": IfExecute,
"LayerUtility: StringCondition": StringCondition,
"LayerUtility: BooleanOperator": BooleanOperator,
"LayerUtility: NumberCalculator": NumberCalculator,
"LayerUtility: BooleanOperatorV2": BooleanOperatorV2,
"LayerUtility: NumberCalculatorV2": NumberCalculatorV2,
"LayerUtility: TextBox": TextBoxNode,
"LayerUtility: String": StringNode,
"LayerUtility: Integer": IntegerNode,
"LayerUtility: Float": FloatNode,
"LayerUtility: Boolean": BooleanNode,
"LayerUtility: Seed": SeedNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: QueueStop": "LayerUtility: Queue Stop",
"LayerUtility: SwitchCase": "LayerUtility: Switch Case",
"LayerUtility: If ": "LayerUtility: If",
"LayerUtility: StringCondition": "LayerUtility: String Condition",
"LayerUtility: BooleanOperator": "LayerUtility: Boolean Operator",
"LayerUtility: NumberCalculator": "LayerUtility: Number Calculator",
"LayerUtility: BooleanOperatorV2": "LayerUtility: Boolean Operator V2",
"LayerUtility: NumberCalculatorV2": "LayerUtility: Number Calculator V2",
"LayerUtility: TextBox": "LayerUtility: TextBox",
"LayerUtility: String": "LayerUtility: String",
"LayerUtility: Integer": "LayerUtility: Integer",
"LayerUtility: Float": "LayerUtility: Float",
"LayerUtility: Boolean": "LayerUtility: Boolean",
"LayerUtility: Seed": "LayerUtility: Seed"
}
-108
View File
@@ -1,108 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image
from .imagefunc import chop_image, chop_mode, shift_image, expand_mask
class DropShadow:
def __init__(self):
self.NODE_NAME = 'DropShadow'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerStyle'
def drop_shadow(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: {self.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: {self.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(_canvas, shadow_color, blend_mode, opacity)
_canvas.paste(_shadow, mask=alpha)
# 合成layer
_canvas.paste(_layer, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: DropShadow": DropShadow
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: DropShadow": "LayerStyle: DropShadow"
}
-111
View File
@@ -1,111 +0,0 @@
import torch
import time
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image
from .imagefunc import chop_image_v2, chop_mode_v2, shift_image, expand_mask
class DropShadowV2:
def __init__(self):
self.NODE_NAME = 'DropShadowV2'
@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: {self.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: {self.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"{self.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"
}
-114
View File
@@ -1,114 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image
from .imagefunc import chop_image_v2, chop_mode_v2, shift_image, expand_mask
class DropShadowV3:
def __init__(self):
self.NODE_NAME = 'DropShadowV3'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"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": 1000, "step": 1}), # 模糊
"shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色
},
"optional": {
"background_image": ("IMAGE", ), #
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'drop_shadow_v2'
CATEGORY = '😺dzNodes/LayerStyle'
def drop_shadow_v2(self, layer_image, invert_mask, blend_mode, opacity,
distance_x, distance_y, grow, blur, shadow_color,
background_image=None, layer_mask=None
):
# If background image is empty, create transparent background image for each layer image
if background_image == None:
background_image = []
for l in layer_image:
m = tensor2pil(l)
background_image.append(pil2tensor(Image.new('RGBA', (m.width, m.height), (0, 0, 0, 0))))
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: {self.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("RGBA", 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('RGBA')
_layer = tensor2pil(layer_image)
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {self.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"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: DropShadow V3": DropShadowV3
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: DropShadow V3": "LayerStyle: DropShadow V3"
}
@@ -1,90 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
class ExtendCanvas:
def __init__(self):
self.NODE_NAME = 'ExtendCanvas'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"top": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
"bottom": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
"left": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
"right": ("INT", {"default": 0, "min": 0, "max": 99999, "step": 1}),
"color": ("COLOR", {"default": "#000000"},),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask")
FUNCTION = 'extend_canvas'
CATEGORY = '😺dzNodes/LayerUtility'
def extend_canvas(self, image, invert_mask,
top, bottom, left, right, color,
mask=None,
):
l_images = []
l_masks = []
ret_images = []
ret_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
else:
if len(l_masks) == 0:
l_masks.append(Image.new('L', size=tensor2pil(l_images[0]).size, color='white'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_image = l_images[i] if i < len(l_images) else l_images[-1]
_image = tensor2pil(_image).convert('RGB')
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
width = _image.width + left + right
height = _image.height + top + bottom
_canvas = Image.new('RGB', (width, height), color)
_mask_canvas = Image.new('L', (width, height), "black")
_canvas.paste(_image, box=(left,top))
_mask_canvas.paste(_mask.convert('L'), box=(left, top))
ret_images.append(pil2tensor(_canvas))
ret_masks.append(image2mask(_mask_canvas))
log(f"{self.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: ExtendCanvas": ExtendCanvas
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ExtendCanvas": "LayerUtility: ExtendCanvas"
}
@@ -1,97 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
class ExtendCanvasV2:
def __init__(self):
self.NODE_NAME = 'ExtendCanvasV2'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"top": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"bottom": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"left": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"right": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"color": ("STRING", {"default": "#000000"}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask")
FUNCTION = 'extend_canvas_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def extend_canvas_v2(self, image, invert_mask,
top, bottom, left, right, color,
mask=None,
):
l_images = []
l_masks = []
ret_images = []
ret_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
else:
if len(l_masks) == 0:
l_masks.append(Image.new('L', size=tensor2pil(l_images[0]).size, color='white'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_image = l_images[i] if i < len(l_images) else l_images[-1]
_image = tensor2pil(_image).convert('RGB')
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
width = _image.width + left + right
height = _image.height + top + bottom
if width < 1:
width = 1
if height < 1:
height = 1
_canvas = Image.new('RGB', (width, height), color)
_mask_canvas = Image.new('L', (width, height), "black")
_canvas.paste(_image, box=(left,top))
_mask_canvas.paste(_mask.convert('L'), box=(left, top))
ret_images.append(pil2tensor(_canvas))
ret_masks.append(image2mask(_mask_canvas))
log(f"{self.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: ExtendCanvasV2": ExtendCanvasV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ExtendCanvasV2": "LayerUtility: ExtendCanvas V2"
}
-91
View File
@@ -1,91 +0,0 @@
import torch
import time
from PIL import Image, ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import gamma_trans, depthblur_image, radialblur_image, vignette_image, filmgrain_image
class Film:
def __init__(self):
self.NODE_NAME = 'Film'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"center_x": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"center_y": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"saturation": ("FLOAT", {"default": 1, "min": 0.01, "max": 3, "step": 0.01}),
"vignette_intensity": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"grain_power": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01}),
"grain_scale": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 10, "step": 0.1}),
"grain_sat": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"grain_shadows": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}),
"grain_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01}),
"blur_strength": ("INT", {"default": 90, "min": 0, "max": 256, "step": 1}),
"blur_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1}),
"focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01}),
},
"optional": {
"depth_map": ("IMAGE",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'film'
CATEGORY = '😺dzNodes/LayerFilter'
def film(self, image, center_x, center_y, saturation, vignette_intensity,
grain_power, grain_scale, grain_sat, grain_shadows, grain_highs,
blur_strength, blur_focus_spread, focal_depth,
depth_map=None
):
ret_images = []
seed = int(time.time())
for i in image:
i = torch.unsqueeze(i, 0)
_canvas = tensor2pil(i).convert('RGB')
if saturation != 1:
color_image = ImageEnhance.Color(_canvas)
_canvas = color_image.enhance(factor= saturation)
if blur_strength:
if depth_map is not None:
depth_map = tensor2pil(depth_map).convert('L').convert('RGB')
if depth_map.size != _canvas.size:
depth_map.resize((_canvas.size), Image.BILINEAR)
_canvas = depthblur_image(_canvas, depth_map, blur_strength, focal_depth, blur_focus_spread)
else:
_canvas = radialblur_image(_canvas, blur_strength, center_x, center_y, blur_focus_spread * 2)
if vignette_intensity:
# adjust image gamma and saturation
_canvas = gamma_trans(_canvas, 1 - vignette_intensity / 3)
color_image = ImageEnhance.Color(_canvas)
_canvas = color_image.enhance(factor= 1+ vignette_intensity / 3)
# add vignette
_canvas = vignette_image(_canvas, vignette_intensity, center_x, center_y)
if grain_power:
_canvas = filmgrain_image(_canvas, grain_scale, grain_power, grain_shadows, grain_highs, grain_sat, seed=seed)
seed += 1
ret_image = _canvas
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: Film": Film
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: Film": "LayerFilter: Film"
}
-97
View File
@@ -1,97 +0,0 @@
import torch
import time
from PIL import Image,ImageEnhance
from .imagefunc import log, tensor2pil, pil2tensor
from .imagefunc import gamma_trans, depthblur_image, radialblur_image, vignette_image, filmgrain_image, image_add_grain
class FilmV2:
def __init__(self):
self.NODE_NAME = 'FilmV2'
@classmethod
def INPUT_TYPES(self):
grain_method_list = ["fastgrain", "filmgrainer", ]
return {
"required": {
"image": ("IMAGE", ), #
"center_x": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"center_y": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"saturation": ("FLOAT", {"default": 1, "min": 0.01, "max": 3, "step": 0.01}),
"vignette_intensity": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"grain_method": (grain_method_list,),
"grain_power": ("FLOAT", {"default": 0.15, "min": 0, "max": 1, "step": 0.01}),
"grain_scale": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.1}),
"grain_sat": ("FLOAT", {"default": 0.5, "min": 0, "max": 1, "step": 0.01}),
"filmgrainer_shadows": ("FLOAT", {"default": 0.6, "min": 0, "max": 1, "step": 0.01}),
"filmgrainer_highs": ("FLOAT", {"default": 0.2, "min": 0, "max": 1, "step": 0.01}),
"blur_strength": ("INT", {"default": 90, "min": 0, "max": 256, "step": 1}),
"blur_focus_spread": ("FLOAT", {"default": 2.2, "min": 0.1, "max": 8, "step": 0.1}),
"focal_depth": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 1, "step": 0.01}),
},
"optional": {
"depth_map": ("IMAGE",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'film_v2'
CATEGORY = '😺dzNodes/LayerFilter'
def film_v2(self, image, center_x, center_y, saturation, vignette_intensity,
grain_method, grain_power, grain_scale, grain_sat, filmgrainer_shadows, filmgrainer_highs,
blur_strength, blur_focus_spread, focal_depth,
depth_map=None
):
ret_images = []
seed = int(time.time())
for i in image:
i = torch.unsqueeze(i, 0)
_canvas = tensor2pil(i).convert('RGB')
if saturation != 1:
color_image = ImageEnhance.Color(_canvas)
_canvas = color_image.enhance(factor= saturation)
if blur_strength:
if depth_map is not None:
depth_map = tensor2pil(depth_map).convert('RGB')
if depth_map.size != _canvas.size:
depth_map.resize((_canvas.size), Image.BILINEAR)
_canvas = depthblur_image(_canvas, depth_map, blur_strength, focal_depth, blur_focus_spread)
else:
_canvas = radialblur_image(_canvas, blur_strength, center_x, center_y, blur_focus_spread * 2)
if vignette_intensity:
# adjust image gamma and saturation
_canvas = gamma_trans(_canvas, 1 - vignette_intensity / 3)
color_image = ImageEnhance.Color(_canvas)
_canvas = color_image.enhance(factor= 1+ vignette_intensity / 3)
# add vignette
_canvas = vignette_image(_canvas, vignette_intensity, center_x, center_y)
if grain_power:
if grain_method == "fastgrain":
_canvas = image_add_grain(_canvas, grain_scale,grain_power, grain_sat, toe=0, seed=seed)
elif grain_method == "filmgrainer":
_canvas = filmgrain_image(_canvas, grain_scale, grain_power, filmgrainer_shadows, filmgrainer_highs, grain_sat, seed=seed)
seed += 1
ret_image = _canvas
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: FilmV2": FilmV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: FilmV2": "LayerFilter: Film V2"
}
@@ -1 +0,0 @@
__version__ = "1.0.2"
@@ -1,117 +0,0 @@
# Filmgrainer - by Lars Ole Pontoppidan - MIT License
from PIL import Image, ImageFilter
import os
import tempfile
import numpy as np
import filmgrainer.graingamma as graingamma
import filmgrainer.graingen as graingen
def _grainTypes(typ):
# After rescaling to make different grain sizes, the standard deviation
# of the pixel values change. The following values of grain size and power
# have been imperically chosen to end up with approx the same standard
# deviation in the result:
if typ == 1:
return (0.8, 63) # more interesting fine grain
elif typ == 2:
return (1, 45) # basic fine grain
elif typ == 3:
return (1.5, 50) # coarse grain
elif typ == 4:
return (1.6666, 50) # coarser grain
else:
raise ValueError("Unknown grain type: " + str(typ))
# Grain mask cache
MASK_CACHE_PATH = os.path.join(tempfile.gettempdir(), "mask-cache")
def _getGrainMask(img_width:int, img_height:int, saturation:float, grayscale:bool, grain_size:float, grain_gauss:float, seed):
if grayscale:
str_sat = "BW"
sat = -1.0 # Graingen makes a grayscale image if sat is negative
else:
str_sat = str(saturation)
sat = saturation
# filename = MASK_CACHE_PATH + "grain-%d-%d-%s-%s-%s-%d.png" % (
# img_width, img_height, str_sat, str(grain_size), str(grain_gauss), seed)
# if os.path.isfile(filename):
# # print("Reusing: %s" % filename)
# mask = Image.open(filename)
# else:
# mask = graingen.grainGen(img_width, img_height, grain_size, grain_gauss, sat, seed)
# # print("Saving: %s" % filename)
# if not os.path.isdir(MASK_CACHE_PATH):
# os.mkdir(MASK_CACHE_PATH)
# mask.save(filename, format="png", compress_level=1)
mask = graingen.grainGen(img_width, img_height, grain_size, grain_gauss, sat, seed)
return mask
def process(image:Image, scale:float, src_gamma:float, grain_power:float, shadows:float,
highs:float, grain_type:int, grain_sat:float, gray_scale:bool, sharpen:int, seed:int):
# image = np.clip(image, 0, 1) # Ensure the values are within [0, 1]
# image = (image * 255).astype(np.uint8)
# img = Image.fromarray(image).convert("RGB")
img = image
org_width = img.size[0]
org_height = img.size[1]
if scale != 1.0:
# print("Scaling source image ...")
img = img.resize((int(org_width / scale), int(org_height / scale)),
resample = Image.LANCZOS)
img_width = img.size[0]
img_height = img.size[1]
# print("Size: %d x %d" % (img_width, img_height))
# print("Calculating map ...")
map = graingamma.Map.calculate(src_gamma, grain_power, shadows, highs)
# map.saveToFile("map.png")
# print("Calculating grain stock ...")
(grain_size, grain_gauss) = _grainTypes(grain_type)
mask = _getGrainMask(img_width, img_height, grain_sat, gray_scale, grain_size, grain_gauss, seed)
mask_pixels = mask.load()
img_pixels = img.load()
# Instead of calling map.lookup(a, b) for each pixel, use the map directly:
lookup = map.map
if gray_scale:
# print("Film graining image ... (grayscale)")
for y in range(0, img_height):
for x in range(0, img_width):
m = mask_pixels[x, y]
(r, g, b) = img_pixels[x, y]
gray = int(0.21*r + 0.72*g + 0.07*b)
#gray_lookup = map.lookup(gray, m)
gray_lookup = lookup[gray, m]
img_pixels[x, y] = (gray_lookup, gray_lookup, gray_lookup)
else:
# print("Film graining image ...")
for y in range(0, img_height):
for x in range(0, img_width):
(mr, mg, mb) = mask_pixels[x, y]
(r, g, b) = img_pixels[x, y]
r = lookup[r, mr]
g = lookup[g, mg]
b = lookup[b, mb]
img_pixels[x, y] = (r, g, b)
if scale != 1.0:
# print("Scaling image back to original size ...")
img = img.resize((org_width, org_height), resample = Image.LANCZOS)
if sharpen > 0:
# print("Sharpening image: %d pass ..." % sharpen)
for x in range(sharpen):
img = img.filter(ImageFilter.SHARPEN)
return np.array(img).astype('float32') / 255.0
@@ -1,113 +0,0 @@
import numpy as np
_ShadowEnd = 160
_HighlightStart = 200
def _gammaCurve(gamma, x):
""" Returns from 0.0 to 1.0"""
return pow((x / 255.0), (1.0 / gamma))
def _calcDevelopment(shadow_level, high_level, x):
"""
This function returns a development like this:
(return)
^
|
0.5 | o - o <-- mids level, always 0.5
| - -
| - -
| - o <-- high_level eg. 0.25
| -
| o <-- shadow_level eg. 0.15
|
0 -+-----------------|-------|------------|-----> x (input)
0 160 200 255
"""
if x < _ShadowEnd:
power = 0.5 - (_ShadowEnd - x) * (0.5 - shadow_level) / _ShadowEnd
elif x < _HighlightStart:
power = 0.5
else:
power = 0.5 - (x - _HighlightStart) * (0.5 - high_level) / (255 - _HighlightStart)
return power
class Map:
def __init__(self, map):
self.map = map
@staticmethod
def calculate(src_gamma, noise_power, shadow_level, high_level) -> 'Map':
map = np.zeros([256, 256], dtype=np.uint8)
# We need to level off top end and low end to leave room for the noise to breathe
crop_top = noise_power * high_level / 12
crop_low = noise_power * shadow_level / 20
pic_scale = 1 - (crop_top + crop_low)
pic_offs = 255 * crop_low
for src_value in range(0, 256):
# Gamma compensate picture source value itself
pic_value = _gammaCurve(src_gamma, src_value) * 255.0
# In the shadows we want noise gamma to be 0.5, in the highs, 2.0:
gamma = pic_value * (1.5 / 256) + 0.5
gamma_offset = _gammaCurve(gamma, 128)
# Power is determined by the development
power = _calcDevelopment(shadow_level, high_level, pic_value)
for noise_value in range(0, 256):
gamma_compensated = _gammaCurve(gamma, noise_value) - gamma_offset
value = pic_value * pic_scale + pic_offs + 255.0 * power * noise_power * gamma_compensated
if value < 0:
value = 0
elif value < 255.0:
value = int(value)
else:
value = 255
map[src_value, noise_value] = value
return Map(map)
def lookup(self, pic_value, noise_value):
return self.map[pic_value, noise_value]
def saveToFile(self, filename):
from PIL import Image
img = Image.fromarray(self.map)
img.save(filename)
if __name__ == "__main__":
import matplotlib.pyplot as plt
import numpy as np
def plotfunc(x_min, x_max, step, func):
x_all = np.arange(x_min, x_max, step)
y = []
for x in x_all:
y.append(func(x))
plt.figure()
plt.plot(x_all, y)
plt.grid()
def development1(x):
return _calcDevelopment(0.2, 0.3, x)
def gamma05(x):
return _gammaCurve(0.5, x)
def gamma1(x):
return _gammaCurve(1, x)
def gamma2(x):
return _gammaCurve(2, x)
plotfunc(0.0, 255.0, 1.0, development1)
plotfunc(0.0, 255.0, 1.0, gamma05)
plotfunc(0.0, 255.0, 1.0, gamma1)
plotfunc(0.0, 255.0, 1.0, gamma2)
plt.show()
@@ -1,61 +0,0 @@
from PIL import Image
import random
import numpy as np
def _makeGrayNoise(width, height, power):
buffer = np.zeros([height, width], dtype=int)
for y in range(0, height):
for x in range(0, width):
buffer[y, x] = random.gauss(128, power)
buffer = buffer.clip(0, 255)
return Image.fromarray(buffer.astype(dtype=np.uint8))
def _makeRgbNoise(width, height, power, saturation):
buffer = np.zeros([height, width, 3], dtype=int)
intens_power = power * (1.0 - saturation)
for y in range(0, height):
for x in range(0, width):
intens = random.gauss(128, intens_power)
buffer[y, x, 0] = random.gauss(0, power) * saturation + intens
buffer[y, x, 1] = random.gauss(0, power) * saturation + intens
buffer[y, x, 2] = random.gauss(0, power) * saturation + intens
buffer = buffer.clip(0, 255)
return Image.fromarray(buffer.astype(dtype=np.uint8))
def grainGen(width, height, grain_size, power, saturation, seed = 1):
# A grain_size of 1 means the noise buffer will be made 1:1
# A grain_size of 2 means the noise buffer will be resampled 1:2
noise_width = int(width / grain_size)
noise_height = int(height / grain_size)
random.seed(seed)
if saturation < 0.0:
print("Making B/W grain, width: %d, height: %d, grain-size: %s, power: %s, seed: %d" % (
noise_width, noise_height, str(grain_size), str(power), seed))
img = _makeGrayNoise(noise_width, noise_height, power)
else:
print("Making RGB grain, width: %d, height: %d, saturation: %s, grain-size: %s, power: %s, seed: %d" % (
noise_width, noise_height, str(saturation), str(grain_size), str(power), seed))
img = _makeRgbNoise(noise_width, noise_height, power, saturation)
# Resample
if grain_size != 1.0:
img = img.resize((width, height), resample = Image.LANCZOS)
return img
if __name__ == "__main__":
import sys
if len(sys.argv) == 8:
width = int(sys.argv[2])
height = int(sys.argv[3])
grain_size = float(sys.argv[4])
power = float(sys.argv[5])
sat = float(sys.argv[6])
seed = int(sys.argv[7])
out = grainGen(width, height, grain_size, power, sat, seed)
out.save(sys.argv[1])
@@ -1,32 +0,0 @@
import cv2
import numpy as np
def generate_blurred_images(image, blur_strength, steps, focus_spread=1):
blurred_images = []
for step in range(1, steps + 1):
# Adjust the curve based on the curve_weight
blur_factor = (step / steps) ** focus_spread * blur_strength
blur_size = max(1, int(blur_factor))
blur_size = blur_size if blur_size % 2 == 1 else blur_size + 1 # Ensure blur_size is odd
# Apply Gaussian Blur
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
blurred_images.append(blurred_image)
return blurred_images
def apply_blurred_images(image, blurred_images, mask):
steps = len(blurred_images) # Calculate the number of steps based on the blurred images provided
final_image = np.zeros_like(image)
step_size = 1.0 / steps
for i, blurred_image in enumerate(blurred_images):
# Calculate the mask for the current step
current_mask = np.clip((mask - i * step_size) * steps, 0, 1)
next_mask = np.clip((mask - (i + 1) * step_size) * steps, 0, 1)
blend_mask = current_mask - next_mask
# Apply the blend mask
final_image += blend_mask[:, :, np.newaxis] * blurred_image
# Ensure no division by zero; add the original image for areas without blurring
final_image += (1 - np.clip(mask * steps, 0, 1))[:, :, np.newaxis] * image
return final_image
@@ -1,86 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur
class GaussianBlur:
def __init__(self):
self.NODE_NAME = 'GaussianBlur'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"blur": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 模糊
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'gaussian_blur'
CATEGORY = '😺dzNodes/LayerFilter'
def gaussian_blur(self, image, blur):
ret_images = []
for i in image:
_canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB')
ret_images.append(pil2tensor(gaussian_blur(_canvas, blur)))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
class LS_GaussianBlurV2:
def __init__(self):
self.NODE_NAME = 'GaussianBlurV2'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"blur": ("FLOAT", {"default": 20, "min": 0, "max": 1000, "step": 0.05}), # 模糊
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'gaussian_blur_v2'
CATEGORY = '😺dzNodes/LayerFilter'
def gaussian_blur_v2(self, image, blur):
ret_images = []
if blur:
for i in image:
_canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB')
ret_images.append(pil2tensor(gaussian_blur(_canvas, blur)))
else:
return (image,)
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: GaussianBlur": GaussianBlur,
"LayerFilter: GaussianBlurV2": LS_GaussianBlurV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: GaussianBlur": "LayerFilter: GaussianBlur",
"LayerFilter: GaussianBlurV2": "LayerFilter: Gaussian Blur V2"
}
@@ -1,39 +0,0 @@
import torch
from .imagefunc import tensor2pil
class GetImageSize:
def __init__(self):
self.NODE_NAME = 'GetImageSize'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ),
},
"optional": {
}
}
RETURN_TYPES = ("INT", "INT", "BOX")
RETURN_NAMES = ("width", "height", "original_size")
FUNCTION = 'get_image_size'
CATEGORY = '😺dzNodes/LayerUtility'
def get_image_size(self, image,):
if image.shape[0] > 0:
image = torch.unsqueeze(image[0], 0)
_image = tensor2pil(image)
return (_image.width, _image.height, [_image.width, _image.height],)
NODE_CLASS_MAPPINGS = {
"LayerUtility: GetImageSize": GetImageSize
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: GetImageSize": "LayerUtility: GetImageSize"
}
@@ -1,236 +0,0 @@
import torch
from PIL import Image, ImageDraw, ImageFont
from collections import Counter
import colorsys
from .imagefunc import AnyType, log, tensor2pil, pil2tensor, load_custom_size, gaussian_blur
from .imagefunc import RGB_to_Hex
any = AnyType("*")
class LS_GetMainColorsV2:
def __init__(self):
self.NODE_NAME = 'Get Main Colors V2'
@classmethod
def INPUT_TYPES(self):
size_list = ['custom']
size_list.extend(load_custom_size())
k_means_algorithm_list = ["lloyd", "elkan"]
return {
"required": {
"image": ("IMAGE",),
"k_means_algorithm": (k_means_algorithm_list,),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "STRING","STRING", "STRING", "STRING", "STRING",)
RETURN_NAMES = ("preview_image", "color_1", "color_2", "color_3", "color_4", "color_5",)
FUNCTION = 'get_main_colors_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def get_main_colors_v2(self, image, k_means_algorithm):
ret_images = []
grid_width = 512
grid_height = 64 # Reduced height to fit 10 colors
for i in range(len(image)):
pil_img = tensor2pil(torch.unsqueeze(image[i], 0)).convert("RGB")
blured_image = gaussian_blur(pil_img, (pil_img.width + pil_img.height) // 400)
accuracy = 60
num_colors = 5 # Increased to 5 colors
num_iterations = int(512 * (accuracy / 100))
original_colors, color_percentages = self.interrogate_colors(
pil2tensor(blured_image), num_colors=num_colors, algorithm=k_means_algorithm, mix_iter=num_iterations,
random_state=0)
main_colors = self.ndarrays_to_colorhex(original_colors)
# Sort colors by percentage
sorted_colors = sorted(zip(main_colors, color_percentages), key=lambda x: x[1], reverse=True)
print(f"sorted_colors={sorted_colors},type={type(sorted_colors)}")
# Create color info string with HSB values
color_info = "\n".join([
f"RGB {color[1:]} HSB {self.rgb_to_hsb(color)[0]:03.0f} {self.rgb_to_hsb(color)[1]:03.0f} {self.rgb_to_hsb(color)[2]:03.0f} 占比 {percentage:.2f}%"
for color, percentage in sorted_colors
])
# draw colors image
ret_image = Image.new('RGB', size=(grid_width, grid_height * len(main_colors)), color="white")
draw = ImageDraw.Draw(ret_image)
# Use default font with size 20
font = ImageFont.load_default().font_variant(size=20)
for j, (color, percentage) in enumerate(sorted_colors):
x1 = 0
y1 = grid_height * j
draw.rectangle((x1, y1, x1 + grid_width, y1 + grid_height), fill=color, outline=color)
# Calculate contrast color
contrast_color = self.get_contrast_color(color)
# Add text with contrast color and HSB values
h, s, b = self.rgb_to_hsb(color)
text = f"RGB {color[1:]} HSB {h:03.0f} {s:03.0f} {b:03.0f} {percentage:.2f}%"
# 使用 font.getbbox() 来获取文本的边界框
bbox = font.getbbox(text)
text_height = bbox[3] - bbox[1]
# 计算文本的垂直位置,使其在色块中垂直居中
text_x = 10 # 固定左边距为10像素
text_y = y1 + (grid_height - text_height) // 2
# 绘制文本
draw.text((text_x, text_y), text, fill=contrast_color, font=font)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), sorted_colors[0][0], sorted_colors[1][0], sorted_colors[2][0], sorted_colors[3][0], sorted_colors[4][0],)
def ndarrays_to_colorhex(self, colors: list) -> list:
return [RGB_to_Hex((int(color[0]), int(color[1]), int(color[2]))) for color in colors]
def interrogate_colors(self, image: torch.Tensor, num_colors: int, algorithm: str, mix_iter: int,
random_state: int) -> tuple:
from sklearn.cluster import KMeans
pixels = image.view(-1, image.shape[-1]).numpy()
kmeans = KMeans(
n_clusters=num_colors,
algorithm=algorithm,
max_iter=mix_iter,
random_state=random_state,
).fit(pixels)
colors = kmeans.cluster_centers_ * 255
# Count pixels in each cluster
labels = kmeans.labels_
label_counts = Counter(labels)
total_pixels = len(labels)
# Calculate percentages
color_percentages = [label_counts[i] / total_pixels * 100 for i in range(num_colors)]
return colors, color_percentages
def get_contrast_color(self, hex_color):
# Convert hex to RGB
rgb = tuple(int(hex_color[i:i + 2], 16) for i in (1, 3, 5))
# Calculate luminance
luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255
# Choose black or white based on luminance
if luminance > 0.5:
return "#000000" # Black for light backgrounds
else:
return "#FFFFFF" # White for dark backgrounds
def rgb_to_hsb(self, hex_color):
# Convert hex to RGB
rgb = tuple(int(hex_color[i:i + 2], 16) for i in (1, 3, 5))
# Convert RGB to HSB
h, s, v = colorsys.rgb_to_hsv(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255)
# Convert to degrees and percentages
h = h * 360
s = s * 100
b = v * 100
return h, s, b
class LS_GetMainColors:
def __init__(self):
self.NODE_NAME = 'Get Main Colors'
@classmethod
def INPUT_TYPES(self):
size_list = ['custom']
size_list.extend(load_custom_size())
k_means_algorithm_list = ["lloyd", "elkan"]
return {
"required": {
"image": ("IMAGE", ), #
"k_means_algorithm": (k_means_algorithm_list,),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING", "STRING", "STRING",)
RETURN_NAMES = ("preview_image", "color_1", "color_2", "color_3", "color_4", "color_5",)
FUNCTION = 'get_main_colors'
CATEGORY = '😺dzNodes/LayerUtility'
def get_main_colors(self, image, k_means_algorithm):
ret_images = []
grid_width = 512
grid_height = 128
line_width = 5
for i in range(len(image)):
pil_img = tensor2pil(torch.unsqueeze(image[i], 0)).convert("RGB")
blured_image = gaussian_blur(pil_img, (pil_img.width + pil_img.height) // 400)
accuracy = 60 # Adjusts accuracy by changing number of iterations of the K-means algorithm
num_colors = 5
num_iterations = int(512 * (accuracy / 100))
original_colors = self.interrogate_colors(
pil2tensor(blured_image), num_colors=num_colors, algorithm=k_means_algorithm, mix_iter=num_iterations, random_state=0)
main_colors = self.ndarrays_to_colorhex(original_colors)
log(f"main_colors={main_colors}")
# draw colors image
ret_image = Image.new('RGB', size=(grid_width, grid_height * len(main_colors)), color="white")
draw = ImageDraw.Draw(ret_image)
for j in range(len(main_colors)):
x1 = 0
y1 = grid_height * j
draw.rectangle((x1, y1, x1 + grid_width, y1 + grid_height), fill=main_colors[j], outline=main_colors[j])
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), main_colors[0], main_colors[1], main_colors[2], main_colors[3], main_colors[4],)
def ndarrays_to_colorhex(self, colors:list) -> list:
return [RGB_to_Hex((int(color[0]), int(color[1]), int(color[2]))) for color in colors]
def interrogate_colors(self, image:torch.Tensor, num_colors:int, algorithm:str, mix_iter:int, random_state:int) -> list:
from sklearn.cluster import KMeans
pixels = image.view(-1, image.shape[-1]).numpy()
colors = (
KMeans(
n_clusters=num_colors,
algorithm=algorithm,
max_iter=mix_iter,
random_state=random_state,
)
.fit(pixels)
.cluster_centers_
* 255
)
return colors
NODE_CLASS_MAPPINGS = {
"LayerUtility: GetMainColors": LS_GetMainColors,
"LayerUtility: GetMainColorsV2": LS_GetMainColorsV2,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: GetMainColors": "LayerUtility: Get Main Colors",
"LayerUtility: GetMainColorsV2": "LayerUtility: Get Main Colors V2",
}
@@ -1,41 +0,0 @@
import torch
from .imagefunc import gradient, pil2tensor
class GradientImage:
def __init__(self):
self.NODE_NAME = 'GradientImage'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"angle": ("INT", {"default": 0, "min": -360, "max": 360, "step": 1}),
"start_color": ("STRING", {"default": "#FFFFFF"},),
"end_color": ("STRING", {"default": "#000000"},),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("image", )
FUNCTION = 'gradient_image'
CATEGORY = '😺dzNodes/LayerUtility'
def gradient_image(self, width, height, angle, start_color, end_color, ):
ret_image = gradient(start_color, end_color, width, height, angle)
return (pil2tensor(ret_image), )
NODE_CLASS_MAPPINGS = {
"LayerUtility: GradientImage": GradientImage
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: GradientImage": "LayerUtility: GradientImage"
}
@@ -1,71 +0,0 @@
import torch
from .imagefunc import log, AnyType, gradient, pil2tensor, tensor2pil, load_custom_size
any = AnyType("*")
class GradientImageV2:
def __init__(self):
self.NODE_NAME = 'GradientImage V2'
@classmethod
def INPUT_TYPES(self):
size_list = ['custom']
size_list.extend(load_custom_size())
return {
"required": {
"size": (size_list,),
"custom_width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"custom_height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
"angle": ("INT", {"default": 0, "min": -360, "max": 360, "step": 1}),
"start_color": ("STRING", {"default": "#FFFFFF"},),
"end_color": ("STRING", {"default": "#000000"},),
},
"optional": {
"size_as": (any, {}),
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("image", )
FUNCTION = 'gradient_image_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def gradient_image_v2(self, size, custom_width, custom_height, angle, start_color, end_color, size_as=None):
if size_as is not None:
if size_as.shape[0] > 0:
_asimage = tensor2pil(size_as[0])
else:
_asimage = tensor2pil(size_as)
width, height = _asimage.size
else:
if size == 'custom':
width = custom_width
height = custom_height
else:
try:
_s = size.split('x')
width = int(_s[0].strip())
height = int(_s[1].strip())
except Exception as e:
log(f'Warning: {self.NODE_NAME} invalid size, check "custom_size.ini"', message_type='warning')
width = custom_width
height = custom_height
ret_image = gradient(start_color, end_color, width, height, angle)
return (pil2tensor(ret_image), )
NODE_CLASS_MAPPINGS = {
"LayerUtility: GradientImage V2": GradientImageV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: GradientImage V2": "LayerUtility: GradientImage V2"
}
-86
View File
@@ -1,86 +0,0 @@
import torch
from PIL import Image
import numpy as np
from .imagefunc import log, tensor2pil, pil2tensor, gradient, Hex_to_RGB
class GradientMap:
def __init__(self):
self.NODE_NAME = 'GradientMap'
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"start_color": ("STRING", {"default": "#015A52"}),
"mid_color": ("STRING", {"default": "#02AF9F"}),
"end_color": ("STRING", {"default": "#7FFFEC"}),
"mid_point": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.01}),
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
},
"optional": {
"layer_mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("image", "gradient")
FUNCTION = 'apply_gradient_map'
CATEGORY = '😺dzNodes/LayerStyle'
def apply_gradient_map(self, image, start_color, mid_color, end_color, mid_point, opacity, layer_mask=None):
def create_gradient_array(start_color, mid_color, end_color, mid_point):
start_rgb = Hex_to_RGB(start_color)
mid_rgb = Hex_to_RGB(mid_color)
end_rgb = Hex_to_RGB(end_color)
mid_index = int(255 * mid_point)
gradient1 = np.array([np.linspace(start_rgb[i], mid_rgb[i], mid_index + 1) for i in range(3)]).T
gradient2 = np.array([np.linspace(mid_rgb[i], end_rgb[i], 256 - mid_index) for i in range(3)]).T
return np.vstack((gradient1[:-1], gradient2))
gradient_array = create_gradient_array(start_color, mid_color, end_color, mid_point)
gradient_image = Image.fromarray(np.uint8(gradient_array.reshape(1, -1, 3).repeat(50, axis=0)))
gradient_tensor = pil2tensor(gradient_image)
ret_images = []
for img in image:
pil_image = tensor2pil(img)
# Convert to grayscale to get luminance
gray_image = np.array(pil_image.convert('L'))
# Apply gradient map
gradient_mapped = gradient_array[gray_image]
# Preserve luminance of original image
original_array = np.array(pil_image)
luminance = np.sum(original_array * [0.299, 0.587, 0.114], axis=2, keepdims=True) / 255.0
gradient_mapped = gradient_mapped * luminance + original_array * (1 - luminance)
gradient_mapped_image = Image.fromarray(np.uint8(gradient_mapped))
# Apply opacity
if opacity < 100:
gradient_mapped_image = Image.blend(pil_image, gradient_mapped_image, opacity / 100)
# Apply mask if provided
if layer_mask is not None:
mask = tensor2pil(layer_mask).convert('L')
pil_image.paste(gradient_mapped_image, (0, 0), mask)
else:
pil_image = gradient_mapped_image
ret_images.append(pil2tensor(pil_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), gradient_tensor)
NODE_CLASS_MAPPINGS = {
"LayerStyle: Gradient Map": GradientMap
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: Gradient Map": "LayerStyle: Gradient Map"
}
@@ -1,102 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, gradient, RGB_to_Hex, chop_image, chop_mode
class GradientOverlay:
def __init__(self):
self.NODE_NAME = 'GradientOverlay'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerStyle'
def gradient_overlay(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: {self.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: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image(_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"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: GradientOverlay": GradientOverlay
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: GradientOverlay": "LayerStyle: GradientOverlay"
}
@@ -1,102 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, gradient, RGB_to_Hex, chop_image_v2, chop_mode_v2
class GradientOverlayV2:
def __init__(self):
self.NODE_NAME = 'GradientOverlayV2'
@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: {self.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: {self.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"{self.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"
}
-186
View File
@@ -1,186 +0,0 @@
import torch
import numpy as np
from PIL import Image, ImageDraw
import math
import random
from .imagefunc import log, tensor2pil, pil2tensor, mask2image
def create_dot_mask(size:int, shape:str='circle') -> np.ndarray:
"""创建不同形状的点阵掩码
Args:
size (int): 掩码大小
shape (str): 形状类型 ('circle', 'diamond', 'square')
Returns:
numpy.ndarray: 掩码数组
"""
mask = np.zeros((size, size))
center = size / 2
for x in range(size):
for y in range(size):
if shape == 'circle':
distance = math.sqrt((x - center + 0.5) ** 2 + (y - center + 0.5) ** 2)
radius = center if size > 4 else center * 1.1
mask[y, x] = 1 if distance <= radius else 0
elif shape == 'diamond':
distance = abs(x - center + 0.5) + abs(y - center + 0.5)
radius = center if size > 4 else center * 1.1
mask[y, x] = 1 if distance <= radius else 0
elif shape == 'square':
mask[y, x] = 1
return mask
def halftone(image: Image, dot_size:int = 10, shape: str = 'circle', angle: float = 45) -> Image:
if image.mode != 'L':
image = image.convert('L')
width, height = image.size
output = Image.new('L', (width, height), 0)
draw = ImageDraw.Draw(output)
angle_rad = math.radians(angle)
cos_angle = math.cos(angle_rad)
sin_angle = math.sin(angle_rad)
img_array = np.array(image)
random_offset = dot_size * 0.05 # 添加 5% 的随机偏移,避免出现规则条纹
diagonal = math.sqrt(width ** 2 + height ** 2)
margin = int(diagonal)
x_start = -margin // 2
x_end = width + margin // 2
y_start = -margin // 2
y_end = height + margin // 2
step = dot_size
rotated_step_x = math.sqrt(2) * step * cos_angle
rotated_step_y = math.sqrt(2) * step * sin_angle
y = y_start
while y < y_end:
x = x_start
while x < x_end:
offset_x = random.uniform(-random_offset, random_offset)
offset_y = random.uniform(-random_offset, random_offset)
grid_x = (x + offset_x) * cos_angle + (y + offset_y) * sin_angle
grid_y = -(x + offset_x) * sin_angle + (y + offset_y) * cos_angle
if 0 <= grid_x < width and 0 <= grid_y < height:
sample_x = int(grid_x)
sample_y = int(grid_y)
region_x = min(sample_x, width - dot_size)
region_y = min(sample_y, height - dot_size)
region = img_array[region_y:region_y + dot_size, region_x:region_x + dot_size]
if region.size > 0:
gaussian_kernel = np.exp(-np.linspace(-2, 2, dot_size) ** 2 / 2)
gaussian_kernel = gaussian_kernel[:, np.newaxis] * gaussian_kernel[np.newaxis, :]
gaussian_kernel = gaussian_kernel / gaussian_kernel.sum()
if region.shape[0] == gaussian_kernel.shape[0] and region.shape[1] == gaussian_kernel.shape[1]:
mean_value = np.sum(region * gaussian_kernel)
else:
mean_value = np.mean(region)
dot_radius = math.sqrt(1 - mean_value / 255) * dot_size / 2
if dot_radius > 0:
mask_size = int(dot_radius * 2)
if mask_size > 0:
dot_mask = create_dot_mask(mask_size, shape)
for dy in range(mask_size):
for dx in range(mask_size):
if dot_mask[dy, dx] > 0:
px = int(grid_x - mask_size // 2 + dx)
py = int(grid_y - mask_size // 2 + dy)
if 0 <= px < width and 0 <= py < height:
output.putpixel((px, py), 255)
x += step
y += step
return output
class LS_HalfTone:
def __init__(self):
self.NODE_NAME = 'HalfTone'
@classmethod
def INPUT_TYPES(self):
shape_list = ['circle', 'diamond', 'square']
return {
"required": {
"image": ("IMAGE", ), #
"dot_size": ("INT", {"default": 10, "min": 4, "max": 100, "step": 1}), # 点大小
"angle": ("FLOAT", {"default": 45, "min": -90, "max": 90, "step": 0.1}), # 角度
"shape": (shape_list,),
"dot_color":("STRING",{"default": "#000000"}),
"background_color": ("STRING", {"default": "#FFFFFF"}),
"anti_aliasing": ("INT", {"default": 1, "min": 0, "max": 4, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'halftone'
CATEGORY = '😺dzNodes/LayerFilter'
def halftone(self, image, dot_size, angle, shape, dot_color, background_color, anti_aliasing, mask=None,
):
l_masks = []
ret_images = []
upscale = anti_aliasing + 1
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
else:
l_masks.append(Image.new('L', tensor2pil(image[0]).size, color='white'))
for idx,img in enumerate(image):
orig_image = tensor2pil(img.unsqueeze(0)).convert('RGB')
orig_mask = l_masks[idx] if len(l_masks) > idx else l_masks[-1]
if orig_mask.size != orig_image.size:
orig_mask = orig_mask.resize(orig_image.size, Image.LANCZOS)
upscaled_image = orig_image.resize((orig_image.width * upscale, orig_image.height * upscale), Image.LANCZOS)
halftone_image = halftone(upscaled_image, dot_size * upscale, shape=shape, angle=angle)
halftone_image = halftone_image.resize(orig_image.size, Image.LANCZOS)
color_image = Image.new('RGB', halftone_image.size, color=dot_color)
background_image = Image.new('RGB', halftone_image.size, color=background_color)
background_image.paste(color_image, mask=halftone_image)
ret_image = Image.new('RGB', halftone_image.size, color=background_color)
ret_image.paste(background_image, mask=orig_mask)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: HalfTone": LS_HalfTone
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: HalfTone": "LayerFilter: HalfTone"
}
-164
View File
@@ -1,164 +0,0 @@
import torch
import numpy as np
from .imagefunc import log, tensor2pil, pil2tensor, apply_to_batch
from PIL import ImageCms, Image, ImageEnhance
from PIL.PngImagePlugin import PngInfo
NODE_NAME = 'HDR Effects'
sRGB_profile = ImageCms.createProfile("sRGB")
Lab_profile = ImageCms.createProfile("LAB")
def adjust_shadows(luminance_array, shadow_intensity, hdr_intensity):
# Darken shadows more as shadow_intensity increases, scaled by hdr_intensity
return np.clip(luminance_array - luminance_array * shadow_intensity * hdr_intensity * 0.5, 0, 255)
def adjust_highlights(luminance_array, highlight_intensity, hdr_intensity):
# Brighten highlights more as highlight_intensity increases, scaled by hdr_intensity
return np.clip(luminance_array + (255 - luminance_array) * highlight_intensity * hdr_intensity * 0.5, 0, 255)
def apply_adjustment(base, factor, intensity_scale):
"""Apply positive adjustment scaled by intensity."""
# Ensure the adjustment increases values within [0, 1] range, scaling by intensity
adjustment = base + (base * factor * intensity_scale)
# Ensure adjustment stays within bounds
return np.clip(adjustment, 0, 1)
def multiply_blend(base, blend):
"""Multiply blend mode."""
return np.clip(base * blend, 0, 255)
def overlay_blend(base, blend):
"""Overlay blend mode."""
# Normalize base and blend to [0, 1] for blending calculation
base = base / 255.0
blend = blend / 255.0
return np.where(base < 0.5, 2 * base * blend, 1 - 2 * (1 - base) * (1 - blend)) * 255
def adjust_shadows_non_linear(luminance, shadow_intensity, max_shadow_adjustment=1.5):
lum_array = np.array(luminance, dtype=np.float32) / 255.0 # Normalize
# Apply a non-linear darkening effect based on shadow_intensity
shadows = lum_array ** (1 / (1 + shadow_intensity * max_shadow_adjustment))
return np.clip(shadows * 255, 0, 255).astype(np.uint8) # Re-scale to [0, 255]
def adjust_highlights_non_linear(luminance, highlight_intensity, max_highlight_adjustment=1.5):
lum_array = np.array(luminance, dtype=np.float32) / 255.0 # Normalize
# Brighten highlights more aggressively based on highlight_intensity
highlights = 1 - (1 - lum_array) ** (1 + highlight_intensity * max_highlight_adjustment)
return np.clip(highlights * 255, 0, 255).astype(np.uint8) # Re-scale to [0, 255]
def merge_adjustments_with_blend_modes(luminance, shadows, highlights, hdr_intensity, shadow_intensity,
highlight_intensity):
# Ensure the data is in the correct format for processing
base = np.array(luminance, dtype=np.float32)
# Scale the adjustments based on hdr_intensity
scaled_shadow_intensity = shadow_intensity ** 2 * hdr_intensity
scaled_highlight_intensity = highlight_intensity ** 2 * hdr_intensity
# Create luminance-based masks for shadows and highlights
shadow_mask = np.clip((1 - (base / 255)) ** 2, 0, 1)
highlight_mask = np.clip((base / 255) ** 2, 0, 1)
# Apply the adjustments using the masks
adjusted_shadows = np.clip(base * (1 - shadow_mask * scaled_shadow_intensity), 0, 255)
adjusted_highlights = np.clip(base + (255 - base) * highlight_mask * scaled_highlight_intensity, 0, 255)
# Combine the adjusted shadows and highlights
adjusted_luminance = np.clip(adjusted_shadows + adjusted_highlights - base, 0, 255)
# Blend the adjusted luminance with the original luminance based on hdr_intensity
final_luminance = np.clip(base * (1 - hdr_intensity) + adjusted_luminance * hdr_intensity, 0, 255).astype(np.uint8)
return Image.fromarray(final_luminance)
def apply_gamma_correction(lum_array, intensity, base_gamma):
"""
Apply gamma correction to the luminance array.
:param lum_array: Luminance channel as a NumPy array.
:param intensity: HDR intensity factor.
:param base_gamma: Base gamma value for correction.
"""
if intensity == 0: # If intensity is 0, return the array as is.
return lum_array
gamma = 1 + (base_gamma - 1) * intensity # Scale gamma based on intensity.
adjusted = 255 * (lum_array / 255) ** gamma
return np.clip(adjusted, 0, 255).astype(np.uint8)
class LS_HDREffects:
@classmethod
def INPUT_TYPES(cls):
return {'required': {'image': ('IMAGE', {'default': None}),
'hdr_intensity': ('FLOAT', {'default': 0.5, 'min': 0.0, 'max': 5.0, 'step': 0.01}),
'shadow_intensity': ('FLOAT', {'default': 0.25, 'min': 0.0, 'max': 1.0, 'step': 0.01}),
'highlight_intensity': ('FLOAT', {'default': 0.75, 'min': 0.0, 'max': 1.0, 'step': 0.01}),
'gamma_intensity': ('FLOAT', {'default': 0.25, 'min': 0.0, 'max': 1.0, 'step': 0.01}),
'contrast': ('FLOAT', {'default': 0.1, 'min': 0.0, 'max': 1.0, 'step': 0.01}),
'enhance_color': ('FLOAT', {'default': 0.25, 'min': 0.0, 'max': 1.0, 'step': 0.01})
}}
RETURN_TYPES = ('IMAGE',)
RETURN_NAMES = ('image',)
FUNCTION = 'hdr_effects'
CATEGORY = '😺dzNodes/LayerFilter'
@apply_to_batch
def hdr_effects(self, image, hdr_intensity=0.5, shadow_intensity=0.25, highlight_intensity=0.75,
gamma_intensity=0.25, contrast=0.1, enhance_color=0.25):
# Load the image
img = tensor2pil(image)
# Step 1: Convert RGB to LAB for better color preservation
img_lab = ImageCms.profileToProfile(img, sRGB_profile, Lab_profile, outputMode='LAB')
# Extract L, A, and B channels
luminance, a, b = img_lab.split()
# Convert luminance to a NumPy array for processing
lum_array = np.array(luminance, dtype=np.float32)
# Preparing adjustment layers (shadows, midtones, highlights)
# This example assumes you have methods to extract or calculate these adjustments
shadows_adjusted = adjust_shadows_non_linear(luminance, shadow_intensity)
highlights_adjusted = adjust_highlights_non_linear(luminance, highlight_intensity)
merged_adjustments = merge_adjustments_with_blend_modes(lum_array, shadows_adjusted, highlights_adjusted,
hdr_intensity, shadow_intensity, highlight_intensity)
# Apply gamma correction with a base_gamma value (define based on desired effect)
gamma_corrected = apply_gamma_correction(np.array(merged_adjustments), hdr_intensity, gamma_intensity)
# Merge L channel back with original A and B channels
adjusted_lab = Image.merge('LAB', (merged_adjustments, a, b))
# Step 3: Convert LAB back to RGB
img_adjusted = ImageCms.profileToProfile(adjusted_lab, Lab_profile, sRGB_profile, outputMode='RGB')
# Enhance contrast
enhancer = ImageEnhance.Contrast(img_adjusted)
contrast_adjusted = enhancer.enhance(1 + contrast)
# Enhance color saturation
enhancer = ImageEnhance.Color(contrast_adjusted)
color_adjusted = enhancer.enhance(1 + enhance_color * 0.2)
return pil2tensor(color_adjusted)
NODE_CLASS_MAPPINGS = {
"LayerFilter: HDREffects": LS_HDREffects
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: HDREffects": "LayerFilter: HDR Effects"
}
@@ -1,87 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, chop_image_v2, gaussian_blur
class HLFrequencyDetailRestore:
def __init__(self):
self.NODE_NAME = 'HLFrequencyDetailRestore'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"detail_image": ("IMAGE",),
"keep_high_freq": ("INT", {"default": 64, "min": 0, "max": 1023}),
"erase_low_freq": ("INT", {"default": 32, "min": 0, "max": 1023}),
"mask_blur": ("INT", {"default": 16, "min": 0, "max": 1023}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'hl_frequency_detail_restore'
CATEGORY = '😺dzNodes/LayerUtility'
def hl_frequency_detail_restore(self, image, detail_image, keep_high_freq, erase_low_freq, mask_blur, mask=None):
b_images = []
l_images = []
l_masks = []
ret_images = []
for b in image:
b_images.append(torch.unsqueeze(b, 0))
for l in detail_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 mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
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]
background_image = tensor2pil(background_image).convert('RGB')
detail_image = l_images[i] if i < len(l_images) else l_images[-1]
detail_image = tensor2pil(detail_image).convert('RGB')
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
high_ferq = chop_image_v2(ImageChops.invert(detail_image),
gaussian_blur(detail_image, keep_high_freq),
blend_mode='normal', opacity=50)
high_ferq = ImageChops.invert(high_ferq)
if erase_low_freq:
low_freq = gaussian_blur(background_image, erase_low_freq)
else:
low_freq = background_image.copy()
ret_image = chop_image_v2(low_freq, high_ferq, blend_mode="linear light", opacity=100)
_mask = ImageChops.invert(_mask)
if mask_blur > 0:
_mask = gaussian_blur(_mask, mask_blur)
ret_image.paste(background_image, _mask)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: HLFrequencyDetailRestore": HLFrequencyDetailRestore
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: HLFrequencyDetailRestore": "LayerUtility: H/L Frequency Detail Restore"
}
-244
View File
@@ -1,244 +0,0 @@
# code from https://github.com/lrzjason/Comfyui-In-Context-Lora-Utils
import torch
import numpy as np
from PIL import Image
import cv2
from .imagefunc import log, fit_resize_image, tensor2pil, pil2tensor
def resize_img(img, resolution, interpolation=cv2.INTER_CUBIC):
# print(img)
# print(resolution)
return cv2.resize(img, resolution, interpolation=interpolation)
def create_image_from_color(width, height, color=(255, 255, 255)):
# OpenCV uses BGR, so convert hex color to BGR if necessary
if isinstance(color, str) and color.startswith('#'):
color = tuple(int(color[i:i + 2], 16) for i in (5, 3, 1))[::-1]
# Create a blank image with the specified color
blank_image = np.full((height, width, 3), color, dtype=np.uint8)
return blank_image
def fit_image(image, mask=None, output_length=1536, patch_mode="auto"):
image = image.detach().cpu().numpy()
if mask is not None:
mask = mask.detach().cpu().numpy()
base_length = int(output_length / 3 * 2)
half_length = int(output_length / 2)
image_height, image_width, _ = image.shape
target_width = int(half_length)
target_height = int(base_length)
if patch_mode == "auto":
if image_width > image_height:
patch_mode = "patch_bottom"
target_width = int(base_length)
target_height = int(half_length)
else:
patch_mode = "patch_right"
elif patch_mode == "patch_bottom":
target_width = int(base_length)
target_height = int(half_length)
# 等比例缩放并填充逻辑
scale_ratio = min(target_width / image_width, target_height / image_height)
# 计算缩放后的尺寸
new_width = int(image_width * scale_ratio)
new_height = int(image_height * scale_ratio)
# 缩放图片
image = resize_img(image, (new_width, new_height))
if mask is not None:
mask = resize_img(mask, (new_width, new_height), cv2.INTER_NEAREST_EXACT)
# 计算填充的差值
diff_x = target_width - new_width
diff_y = target_height - new_height
# 计算填充上下左右的像素
pad_x = diff_x // 2
pad_y = diff_y // 2
# 添加白色填充到图片,黑色填充到掩码
resized_image = cv2.copyMakeBorder(
image,
pad_y, diff_y - pad_y,
pad_x, diff_x - pad_x,
cv2.BORDER_CONSTANT, value=(255, 255, 255)
)
if mask is not None:
resized_mask = cv2.copyMakeBorder(
mask,
pad_y, diff_y - pad_y,
pad_x, diff_x - pad_x,
cv2.BORDER_CONSTANT, value=(0, 0, 0)
)
else:
resized_mask = torch.zeros((target_width, target_height))
return resized_image, resized_mask, target_width, target_height, patch_mode
def crop_and_scale_as(image:Image, size:tuple):
target_width, target_height = size
_image = Image.new('RGB', size=size, color='black')
ret_image = fit_resize_image(image, target_width, target_height, "crop", Image.LANCZOS)
return ret_image
class ICMask_Data:
def __init__(self, x_offset, y_offset, target_width, target_height, total_width, total_height, orig_width, orig_height):
self.x_offset = x_offset
self.y_offset = y_offset
self.target_width = target_width
self.target_height = target_height
self.total_width = total_width
self.total_height = total_height
self.orig_width = orig_width
self.orig_height = orig_height
class LS_ICMask:
def __init__(self):
self.NODE_NAME = 'IC_Mask'
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"first_image": ("IMAGE",),
"patch_mode": (["auto", "patch_right", "patch_bottom"], {
"default": "auto",
}),
"output_length": ("INT", {
"default": 1536,
}),
"patch_color": (["#FF0000", "#00FF00", "#0000FF", "#FFFFFF"], {
"default": "#FFFFFF",
}),
},
"optional": {
"first_mask": ("MASK",),
"second_image": ("IMAGE",),
"second_mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "ICMASK_DATA",)
RETURN_NAMES = ("image", "mask", "icmask_data",)
FUNCTION = "ic_mask"
CATEGORY = '😺dzNodes/LayerUtility'
def ic_mask(self, first_image, patch_mode, output_length, patch_color, first_mask=None, second_image=None,
second_mask=None):
orig_width = 0
orig_height = 0
if output_length % 64 != 0:
output_length = output_length - (output_length % 64)
image1 = first_image[0]
if first_mask is None:
image1_mask = torch.zeros((image1.shape[0], image1.shape[1]))
else:
image1_mask = first_mask[0]
image1, image1_mask, target_width, target_height, patch_mode = fit_image(image1, image1_mask, output_length,
patch_mode)
if second_image is not None:
image2 = second_image[0]
if second_mask is None:
image2_mask = torch.zeros((image2.shape[0], image2.shape[1]))
else:
image2_mask = second_mask[0]
orig_width = image2.shape[1]
orig_height = image2.shape[0]
image2, image2_mask, _, _, _ = fit_image(image2, image2_mask, output_length, patch_mode)
else:
image2 = create_image_from_color(target_width, target_height, color=patch_color)
image2 = torch.from_numpy(image2)
if second_mask is None:
image2_mask = torch.zeros((image2.shape[0], image2.shape[1]))
else:
image2_mask = second_mask[0]
orig_width = image2.shape[1]
orig_height = image2.shape[0]
image2, image2_mask, _, _, _ = fit_image(image2, image2_mask, output_length)
min_y = 0
min_x = 0
if second_mask is None or np.all(image2_mask == 0):
image2_mask = torch.ones((image1.shape[0], image1.shape[1]))
if patch_mode == "patch_right":
concatenated_image = np.hstack((image1, image2))
concatenated_mask = np.hstack((image1_mask, image2_mask))
min_x = 50
else:
concatenated_image = np.vstack((image1, image2))
concatenated_mask = np.vstack((image1_mask, image2_mask))
min_y = 50
min_y = int(min_y / 100.0 * concatenated_image.shape[0])
min_x = int(min_x / 100.0 * concatenated_image.shape[1])
return_masks = torch.from_numpy(concatenated_mask)[None,]
concatenated_image = np.clip(255. * concatenated_image, 0, 255).astype(np.float32) / 255.0
concatenated_image = torch.from_numpy(concatenated_image)[None,]
return_images = concatenated_image
icmask_data = ICMask_Data(min_x, min_y, target_width, target_height, concatenated_image.shape[1],
concatenated_image.shape[0], orig_width, orig_height)
return (return_images, return_masks, icmask_data)
class LS_ICMask_CropBack:
def __init__(self):
self.NODE_NAME = 'IC_Mask_Crop_Back'
@classmethod
def INPUT_TYPES(s):
return {"required": { "image": ("IMAGE",),
"icmask_data": ("ICMASK_DATA",),
}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "crop_back"
CATEGORY = '😺dzNodes/LayerUtility'
def crop_back(self, image, icmask_data):
width = icmask_data.target_width
height = icmask_data.target_height
x = icmask_data.x_offset
y = icmask_data.y_offset
orig_width = icmask_data.orig_width
orig_height = icmask_data.orig_height
x = min(x, image.shape[2] - 1)
y = min(y, image.shape[1] - 1)
to_x = width + x
to_y = height + y
img = image[:,y:to_y, x:to_x, :]
pil_image = tensor2pil(img)
ret_image = crop_and_scale_as(pil_image, (orig_width, orig_height))
return (pil2tensor(ret_image,),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ICMask": LS_ICMask,
"LayerUtility: ICMaskCropBack": LS_ICMask_CropBack,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ICMask": "LayerUtility: IC Mask",
"LayerUtility: ICMaskCropBack": "LayerUtility: IC Mask Crop Back",
}
-86
View File
@@ -1,86 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image, chop_mode
class ImageBlend:
def __init__(self):
self.NODE_NAME = 'ImageBlend'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
},
"optional": {
"layer_mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'image_blend'
CATEGORY = '😺dzNodes/LayerUtility'
def image_blend(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: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning')
# 合成layer
_comp = chop_image(_canvas, _layer, blend_mode, opacity)
_canvas.paste(_comp, mask=_mask)
ret_images.append(pil2tensor(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageBlend": ImageBlend
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageBlend": "LayerUtility: ImageBlend"
}
@@ -1,135 +0,0 @@
import torch
import copy
from PIL import Image
from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image, chop_mode, image_rotate_extend_with_alpha
class ImageBlendAdvance:
def __init__(self):
self.NODE_NAME = 'ImageBlendAdvance'
@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,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerUtility'
def image_blend_advance(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: {self.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(_canvas, _comp, blend_mode, opacity)
# composition background
_canvas.paste(_comp, mask=_compmask)
ret_images.append(pil2tensor(_canvas))
ret_masks.append(image2mask(_compmask))
log(f"{self.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": ImageBlendAdvance
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageBlendAdvance": "LayerUtility: ImageBlendAdvance"
}
@@ -1,135 +0,0 @@
import torch
import copy
from PIL import Image
from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image_v2, chop_mode_v2, image_rotate_extend_with_alpha
class ImageBlendAdvanceV2:
def __init__(self):
self.NODE_NAME = 'ImageBlendAdvanceV2'
@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: {self.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"{self.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"
}
@@ -1,141 +0,0 @@
import torch
import copy
from PIL import Image
from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image_v2, chop_mode_v2, image_rotate_extend_with_alpha
class ImageBlendAdvanceV3:
def __init__(self):
self.NODE_NAME = 'ImageBlendAdvanceV3'
@classmethod
def INPUT_TYPES(self):
mirror_mode = ['None', 'horizontal', 'vertical']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"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": {
"background_image": ("IMAGE", ), #
"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, layer_image, invert_mask, blend_mode, opacity,
x_percent, y_percent, mirror, scale, aspect_ratio, rotate,
transform_method, anti_aliasing, background_image=None, layer_mask=None
):
# If background image is empty, create transparent background image for each layer image
if background_image == None:
background_image = []
for l in layer_image:
m = tensor2pil(l)
background_image.append(pil2tensor(Image.new('RGBA', (m.width, m.height), (0, 0, 0, 0))))
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('RGBA')
_layer = tensor2pil(layer_image)
if _mask.size != _layer.size:
_mask = Image.new('L', _layer.size, 'white')
log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning')
orig_layer_width = _layer.width
orig_layer_height = _layer.height
_mask = _mask.convert("RGBA")
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("RGBA", _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"{self.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 V3": ImageBlendAdvanceV3
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageBlendAdvance V3": "LayerUtility: ImageBlendAdvance V3"
}
@@ -1,91 +0,0 @@
import torch
import numpy as np
from PIL import Image
from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image_v2, chop_mode_v2
class ImageBlendV2:
def __init__(self):
self.NODE_NAME = 'ImageBlendV2'
@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: {self.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"{self.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"
}
@@ -1,72 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image_channel_merge
class ImageChannelMerge:
def __init__(self):
self.NODE_NAME = 'ImageChannelMerge'
@classmethod
def INPUT_TYPES(self):
channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV']
return {
"required": {
"channel_1": ("IMAGE", ), #
"channel_2": ("IMAGE",), #
"channel_3": ("IMAGE",), #
"mode": (channel_mode,), # 通道设置
},
"optional": {
"channel_4": ("IMAGE",), #
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'image_channel_merge'
CATEGORY = '😺dzNodes/LayerUtility'
def image_channel_merge(self, channel_1, channel_2, channel_3, mode, channel_4=None):
c1_images = []
c2_images = []
c3_images = []
c4_images = []
ret_images = []
width, height = tensor2pil(torch.unsqueeze(channel_1[0], 0)).size
for c in channel_1:
c1_images.append(torch.unsqueeze(c, 0))
for c in channel_2:
c2_images.append(torch.unsqueeze(c, 0))
for c in channel_3:
c3_images.append(torch.unsqueeze(c, 0))
if channel_4 is not None:
for c in channel_4:
c4_images.append(torch.unsqueeze(c, 0))
else:
c4_images.append(pil2tensor(Image.new('L', size=(width, height), color='white')))
max_batch = max(len(c1_images), len(c2_images), len(c3_images), len(c4_images))
for i in range(max_batch):
c_1 = c1_images[i] if i < len(c1_images) else c1_images[-1]
c_2 = c2_images[i] if i < len(c2_images) else c2_images[-1]
c_3 = c3_images[i] if i < len(c3_images) else c3_images[-1]
c_4 = c4_images[i] if i < len(c4_images) else c4_images[-1]
ret_image = image_channel_merge((tensor2pil(c_1), tensor2pil(c_2), tensor2pil(c_3), tensor2pil(c_4)), mode)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageChannelMerge": ImageChannelMerge
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageChannelMerge": "LayerUtility: ImageChannelMerge"
}
@@ -1,53 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, image_channel_split
class ImageChannelSplit:
def __init__(self):
self.NODE_NAME = 'ImageChannelSplit'
@classmethod
def INPUT_TYPES(self):
channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV']
return {
"required": {
"image": ("IMAGE", ), #
"mode": (channel_mode,), # 通道设置
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE",)
RETURN_NAMES = ("channel_1", "channel_2", "channel_3", "channel_4",)
FUNCTION = 'image_channel_split'
CATEGORY = '😺dzNodes/LayerUtility'
def image_channel_split(self, image, mode):
c1_images = []
c2_images = []
c3_images = []
c4_images = []
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i).convert('RGBA')
channel1, channel2, channel3, channel4 = image_channel_split(_image, mode)
c1_images.append(pil2tensor(channel1))
c2_images.append(pil2tensor(channel2))
c3_images.append(pil2tensor(channel3))
c4_images.append(pil2tensor(channel4))
log(f"{self.NODE_NAME} Processed {len(c1_images)} image(s).", message_type='finish')
return (torch.cat(c1_images, dim=0), torch.cat(c2_images, dim=0), torch.cat(c3_images, dim=0), torch.cat(c4_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageChannelSplit": ImageChannelSplit
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageChannelSplit": "LayerUtility: ImageChannelSplit"
}
@@ -1,59 +0,0 @@
import torch
from .imagefunc import log, tensor2pil, pil2tensor, image_channel_split, image_channel_merge
class ImageCombineAlpha:
def __init__(self):
self.NODE_NAME = 'ImageCombineAlpha'
@classmethod
def INPUT_TYPES(self):
channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV']
return {
"required": {
"RGB_image": ("IMAGE", ), #
"mask": ("MASK",), #
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("RGBA_image",)
FUNCTION = 'image_combine_alpha'
CATEGORY = '😺dzNodes/LayerUtility'
def image_combine_alpha(self, RGB_image, mask):
ret_images = []
input_images = []
input_masks = []
for i in RGB_image:
input_images.append(torch.unsqueeze(i, 0))
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
input_masks.append(torch.unsqueeze(m, 0))
max_batch = max(len(input_images), len(input_masks))
for i in range(max_batch):
_image = input_images[i] if i < len(input_images) else input_images[-1]
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
r, g, b, _ = image_channel_split(tensor2pil(_image).convert('RGB'), 'RGB')
ret_image = image_channel_merge((r, g, b, tensor2pil(_mask).convert('L')), 'RGBA')
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageCombineAlpha": ImageCombineAlpha
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageCombineAlpha": "LayerUtility: ImageCombineAlpha"
}
-152
View File
@@ -1,152 +0,0 @@
import torch
import random
from .imagefunc import log
class ImageHub:
def __init__(self):
self.NODE_NAME = 'ImageHub'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"output": ("INT", {"default": 1, "min": 1, "max": 9, "step": 1}),
"random_output": ("BOOLEAN", {"default": False}),
},
"optional": {
"input1_image": ("IMAGE",),
"input1_mask": ("MASK",),
"input2_image": ("IMAGE",),
"input2_mask": ("MASK",),
"input3_image": ("IMAGE",),
"input3_mask": ("MASK",),
"input4_image": ("IMAGE",),
"input4_mask": ("MASK",),
"input5_image": ("IMAGE",),
"input5_mask": ("MASK",),
"input6_image": ("IMAGE",),
"input6_mask": ("MASK",),
"input7_image": ("IMAGE",),
"input7_mask": ("MASK",),
"input8_image": ("IMAGE",),
"input8_mask": ("MASK",),
"input9_image": ("IMAGE",),
"input9_mask": ("MASK",),
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask")
FUNCTION = 'image_hub'
CATEGORY = '😺dzNodes/LayerUtility'
def image_hub(self, output, random_output,
input1_image=None, input1_mask=None,
input2_image=None, input2_mask=None,
input3_image=None, input3_mask=None,
input4_image=None, input4_mask=None,
input5_image=None, input5_mask=None,
input6_image=None, input6_mask=None,
input7_image=None, input7_mask=None,
input8_image=None, input8_mask=None,
input9_image=None, input9_mask=None,
):
output_list = []
if input1_image is not None or input1_mask is not None:
output_list.append(1)
if input2_image is not None or input2_mask is not None:
output_list.append(2)
if input3_image is not None or input3_mask is not None:
output_list.append(3)
if input4_image is not None or input4_mask is not None:
output_list.append(4)
if input5_image is not None or input5_mask is not None:
output_list.append(5)
if input6_image is not None or input6_mask is not None:
output_list.append(6)
if input7_image is not None or input7_mask is not None:
output_list.append(7)
if input8_image is not None or input8_mask is not None:
output_list.append(8)
if input9_image is not None or input9_mask is not None:
output_list.append(9)
log(f"output_list={output_list}")
if len(output_list) == 0:
log(f"{self.NODE_NAME} is skip, because No Input.", message_type='error')
return (None, None)
if random_output:
index = random.randint(1, len(output_list))
output = output_list[index - 1]
ret_image = None
ret_mask = None
if output == 1:
if input1_image is not None:
ret_image = input1_image
if input1_mask is not None:
ret_mask = input1_mask
elif output == 2:
if input2_image is not None:
ret_image = input2_image
if input2_mask is not None:
ret_mask = input2_mask
elif output == 3:
if input3_image is not None:
ret_image = input3_image
if input3_mask is not None:
ret_mask = input3_mask
elif output == 4:
if input4_image is not None:
ret_image = input4_image
if input4_mask is not None:
ret_mask = input4_mask
elif output == 5:
if input5_image is not None:
ret_image = input5_image
if input5_mask is not None:
ret_mask = input5_mask
elif output == 6:
if input6_image is not None:
ret_image = input6_image
if input6_mask is not None:
ret_mask = input6_mask
elif output == 7:
if input7_image is not None:
ret_image = input7_image
if input7_mask is not None:
ret_mask = input7_mask
elif output == 8:
if input8_image is not None:
ret_image = input8_image
if input8_mask is not None:
ret_mask = input8_mask
else:
if input9_image is not None:
ret_image = input9_image
if input9_mask is not None:
ret_mask = input9_mask
if ret_image is None and ret_mask is None:
log(f"{self.NODE_NAME} have {output_list} inputs, output is {output}, but there is no corresponding input.", message_type="error")
elif ret_image is None:
log(f"{self.NODE_NAME} have {output_list} inputs, output is {output}, but image is None.", message_type='finish')
elif ret_mask is None:
log(f"{self.NODE_NAME} have {output_list} inputs, output is {output}, but mask is None.", message_type='finish')
else:
log(f"{self.NODE_NAME} have {output_list} inputs, output is {output}.", message_type='finish')
return (ret_image, ret_mask)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageHub": ImageHub
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageHub": "LayerUtility: ImageHub"
}
@@ -1,189 +0,0 @@
import torch
from PIL import Image
from .imagefunc import AnyType, log, tensor2pil, pil2tensor, image2mask, fit_resize_image
any = AnyType("*")
class ImageMaskScaleAs:
def __init__(self):
self.NODE_NAME = 'ImageMaskScaleAs'
@classmethod
def INPUT_TYPES(self):
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"scale_as": (any, {}),
"fit": (fit_mode,),
"method": (method_mode,),
},
"optional": {
"image": ("IMAGE",), #
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT")
RETURN_NAMES = ("image", "mask", "original_size", "widht", "height",)
FUNCTION = 'image_mask_scale_as'
CATEGORY = '😺dzNodes/LayerUtility'
def image_mask_scale_as(self, scale_as, fit, method,
image=None, mask = None,
):
if scale_as.shape[0] > 0:
_asimage = tensor2pil(scale_as[0])
else:
_asimage = tensor2pil(scale_as)
target_width, target_height = _asimage.size
_mask = Image.new('L', size=_asimage.size, color='black')
_image = Image.new('RGB', size=_asimage.size, color='black')
orig_width = 4
orig_height = 4
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
ret_images = []
ret_masks = []
if image is not None:
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i).convert('RGB')
orig_width, orig_height = _image.size
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler)
ret_images.append(pil2tensor(_image))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
m = torch.unsqueeze(m, 0)
_mask = tensor2pil(m).convert('L')
orig_width, orig_height = _mask.size
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) >0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height],target_width, target_height,)
elif len(ret_images) > 0 and len(ret_masks) == 0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height],target_width, target_height,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
else:
log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.", message_type='error')
return (None, None, [orig_width, orig_height], 0, 0,)
class LS_ImageMaskScaleAsV2:
def __init__(self):
self.NODE_NAME = 'ImageMaskScaleAsV2'
@classmethod
def INPUT_TYPES(self):
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"scale_as": (any, {}),
"fit": (fit_mode,),
"method": (method_mode,),
"background_color": ("STRING", {"default": "#FFFFFF"},),
},
"optional": {
"image": ("IMAGE",), #
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT")
RETURN_NAMES = ("image", "mask", "original_size", "widht", "height",)
FUNCTION = 'image_mask_scale_as_v2'
CATEGORY = '😺dzNodes/LayerUtility'
def image_mask_scale_as_v2(self, scale_as, fit, method, background_color,
image=None, mask=None,
):
if scale_as.shape[0] > 0:
_asimage = tensor2pil(scale_as[0])
else:
_asimage = tensor2pil(scale_as)
target_width, target_height = _asimage.size
_mask = Image.new('L', size=_asimage.size, color='black')
_image = Image.new('RGB', size=_asimage.size, color=background_color)
orig_width = 4
orig_height = 4
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
ret_images = []
ret_masks = []
if image is not None:
for i in image:
i = torch.unsqueeze(i, 0)
_image = tensor2pil(i).convert('RGB')
orig_width, orig_height = _image.size
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler, background_color=background_color)
ret_images.append(pil2tensor(_image))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
m = torch.unsqueeze(m, 0)
_mask = tensor2pil(m).convert('L')
orig_width, orig_height = _mask.size
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler, background_color=background_color).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) > 0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width,
target_height,)
elif len(ret_images) > 0 and len(ret_masks) == 0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
else:
log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.",
message_type='error')
return (None, None, [orig_width, orig_height], 0, 0,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageMaskScaleAs": ImageMaskScaleAs,
"LayerUtility: ImageMaskScaleAsV2": LS_ImageMaskScaleAsV2,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageMaskScaleAs": "LayerUtility: Image Mask Scale As",
"LayerUtility: ImageMaskScaleAsV2": "LayerUtility: Image Mask Scale As V2",
}
@@ -1,87 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
class ImageOpacity:
def __init__(self):
self.NODE_NAME = 'ImageOpacity'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image", "mask",)
FUNCTION = 'image_opacity'
CATEGORY = '😺dzNodes/LayerUtility'
def image_opacity(self, image, opacity, invert_mask,
mask=None,
):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
for l in 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', size=m.size, color='white'))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(l_images), len(l_masks))
for i in range(max_batch):
_image = l_images[i] if i < len(l_images) else l_images[-1]
_image = tensor2pil(_image)
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
if invert_mask:
_color = Image.new("L", _image.size, color=('white'))
_mask = ImageChops.invert(_mask)
else:
_color = Image.new("L", _image.size, color=('black'))
alpha = 1 - opacity / 100.0
ret_mask = Image.blend(_mask, _color, alpha)
R, G, B, = _image.convert('RGB').split()
if invert_mask:
ret_mask = ImageChops.invert(ret_mask)
ret_image = Image.merge('RGBA', (R, G, B, ret_mask))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(ret_mask))
log(f"{self.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: ImageOpacity": ImageOpacity
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageOpacity": "LayerUtility: ImageOpacity"
}
-224
View File
@@ -1,224 +0,0 @@
import torch
from PIL import Image, ImageFont, ImageDraw
from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur, adjust_levels, get_resource_dir
class ImageReelPipeline:
def __init__(self):
self.image = None
self.texts = {}
self.reel_height = 0
self.reel_border = 0
Reel = ImageReelPipeline()
class ImageReel:
def __init__(self):
self.NODE_NAME = 'ImageReel'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image1": ("IMAGE",),
"image1_text": ("STRING", {"multiline": False, "default": "image1"}),
"image2_text": ("STRING", {"multiline": False, "default": "image2"}),
"image3_text": ("STRING", {"multiline": False, "default": "image3"}),
"image4_text": ("STRING", {"multiline": False, "default": "image4"}),
"reel_height": ("INT", {"default": 512, "min": 64, "max": 2048}),
"border": ("INT", {"default": 32, "min": 8, "max": 512}),
},
"optional": {
"image2": ("IMAGE",),
"image3": ("IMAGE",),
"image4": ("IMAGE",),
}
}
RETURN_TYPES = ("Reel",)
RETURN_NAMES = ("reel",)
FUNCTION = 'image_reel'
CATEGORY = '😺dzNodes/LayerUtility'
def image_reel(self, image1, image1_text, image2_text, image3_text, image4_text,
reel_height, border,
image2=None, image3=None, image4=None,):
image_list = []
texts = []
for img in image1:
i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
image_list.append(i)
texts.append([image1_text,i.width])
if image2 is not None:
for img in image2:
i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
image_list.append(i)
texts.append([image2_text,i.width])
if image3 is not None:
for img in image3:
i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
image_list.append(i)
texts.append([image3_text,i.width])
if image4 is not None:
for img in image4:
i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
image_list.append(i)
texts.append([image4_text,i.width])
reel = ImageReel()
reel.image = self.draw_reel_image(image_list, border, reel_height)
reel.texts = texts
reel.reel_height = reel_height
reel.reel_border = border
return (reel,)
def resize_image_to_height(self, image, target_height) -> Image:
w = int(target_height / image.height * image.width)
return image.resize((w, target_height), Image.LANCZOS)
def draw_reel_image(self, image_list, border, reel_height) -> Image:
reel_width = 0
for img in image_list:
reel_width += img.width + border
reel_img = Image.new('RGBA', (reel_width, reel_height + border), color=(0, 0, 0, 0))
#paste images
w = border // 2
for img in image_list:
reel_img.paste(img, (w, border // 2))
w += img.width + border
return reel_img
class ImageReelComposit:
def __init__(self):
self.NODE_NAME = 'ImageReelComposit'
(_, self.FONT_DICT) = get_resource_dir()
self.FONT_LIST = list(self.FONT_DICT.keys())
@classmethod
def INPUT_TYPES(self):
(LUT_DICT, FONT_DICT) = get_resource_dir()
FONT_LIST = list(FONT_DICT.keys())
LUT_LIST = list(LUT_DICT.keys())
color_theme_list = ['light', 'dark']
return {
"required": {
"reel_1": ("Reel",),
"font_file": (FONT_LIST,),
"font_size": ("INT", {"default": 40, "min": 4, "max": 1024}),
"border": ("INT", {"default": 32, "min": 8, "max": 512}),
"color_theme": (color_theme_list,),
},
"optional": {
"reel_2": ("Reel",),
"reel_3": ("Reel",),
"reel_4": ("Reel",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image1",)
FUNCTION = 'image_reel_composit'
CATEGORY = '😺dzNodes/LayerUtility'
def image_reel_composit(self, reel_1, font_file, font_size, border, color_theme, reel_2=None, reel_3=None, reel_4=None,):
ret_images = []
if color_theme == 'light':
bg_color = "#E5E5E5"
text_color = "#121212"
else:
bg_color = "#121212"
text_color = "#E5E5E5"
font_space = int(font_size * 1.5)
width = reel_1.image.width
height = reel_1.image.height + font_space + border
if reel_2 is not None:
width = max(width, reel_2.image.width)
height += reel_2.image.height + font_space + border
if reel_3 is not None:
width = max(width, reel_3.image.width)
height += reel_3.image.height + font_space + border
if reel_4 is not None:
width = max(width, reel_4.image.width)
height += reel_4.image.height + font_space + border
ret_image = Image.new('RGB', (width, height), color=bg_color)
paste_y = 0
reel1_text_image = self.draw_reel_text(reel_1, font_file, font_size, text_color)
shadow_size = reel_1.image.height // 80
ret_image = self.paste_drop_shadow(ret_image, reel_1.image, reel1_text_image, ((width - reel_1.image.width) // 2, paste_y),
shadow_size, text_color)
paste_y += reel_1.image.height + font_space + border
if reel_2 is not None:
reel2_text_image = self.draw_reel_text(reel_2, font_file, font_size, text_color)
shadow_size = reel_2.image.height // 80
ret_image = self.paste_drop_shadow(ret_image, reel_2.image, reel2_text_image, ((width - reel_2.image.width) // 2, paste_y),
shadow_size, text_color)
paste_y += reel_2.image.height + font_space + border
if reel_3 is not None:
reel3_text_image = self.draw_reel_text(reel_3, font_file, font_size, text_color)
shadow_size = reel_3.image.height // 80
ret_image = self.paste_drop_shadow(ret_image, reel_3.image, reel3_text_image,((width - reel_3.image.width) // 2, paste_y),
shadow_size, text_color)
paste_y += reel_3.image.height + font_space + border
if reel_4 is not None:
reel4_text_image = self.draw_reel_text(reel_4, font_file, font_size, text_color)
shadow_size = reel_4.image.height // 80
ret_image = self.paste_drop_shadow(ret_image, reel_4.image, reel4_text_image,((width - reel_4.image.width) // 2, paste_y),
shadow_size, text_color)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
def paste_drop_shadow(self, background_image, image, text_image, box, shadow_size, text_color) -> Image:
# drop shadow
_mask = image.split()[3]
_blured_mask = gaussian_blur(_mask, shadow_size//1.3)
_blured_mask = adjust_levels(_blured_mask, 0, 255, 0.5, 0, output_white=54).convert('L')
background_image.paste(Image.new('RGBA', image.size, color="black"), (box[0]+shadow_size, box[1]+shadow_size), mask=_blured_mask)
background_image.paste(image, box, mask=_mask)
background_image.paste(Image.new('RGB', text_image.size, color=text_color), (box[0], box[1] + image.height), mask=text_image.split()[3])
return background_image
def draw_reel_text(self, reel, font_file, font_size, text_color) -> Image:
font_path = self.FONT_DICT.get(font_file)
font = ImageFont.truetype(font_path, font_size)
texts = reel.texts
text_image = Image.new('RGBA', (reel.image.width, reel.reel_border + int(font_size * 1.5)), color=(0, 0, 0, 0))
draw = ImageDraw.Draw(text_image)
x = reel.reel_border
for t in texts:
text = t[0]
width = t[1]
text_width = font.getbbox(text)[2]
draw.text(
xy=(x + width // 2 - text_width//2, reel.reel_border//4),
text=text,
fill=text_color,
font=font,
)
x += width + reel.reel_border
return text_image
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageReel": ImageReel,
"LayerUtility: ImageReelComposit": ImageReelComposit
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageReel": "LayerUtility: Image Reel",
"LayerUtility: ImageReelComposit": "LayerUtility: Image Reel Composit"
}
@@ -1,64 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor
class ImageRemoveAlpha:
def __init__(self):
self.NODE_NAME = 'ImageRemoveAlpha'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"RGBA_image": ("IMAGE", ), #
"fill_background": ("BOOLEAN", {"default": False}),
"background_color": ("STRING", {"default": "#000000"}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("RGB_image", )
FUNCTION = 'image_remove_alpha'
CATEGORY = '😺dzNodes/LayerUtility'
def image_remove_alpha(self, RGBA_image, fill_background, background_color, mask=None):
ret_images = []
for index, img in enumerate(RGBA_image):
_image = tensor2pil(img)
if fill_background:
if mask is not None:
m = mask[index].unsqueeze(0) if index < len(mask) else mask[-1].unsqueeze(0)
alpha = tensor2pil(m).convert('L')
elif _image.mode == "RGBA":
alpha = _image.split()[-1]
else:
log(f"Error: {self.NODE_NAME} skipped, because the input image is not RGBA and mask is None.",
message_type='error')
return (RGBA_image,)
ret_image = Image.new('RGB', size=_image.size, color=background_color)
ret_image.paste(_image, mask=alpha)
ret_images.append(pil2tensor(ret_image))
else:
ret_images.append(pil2tensor(tensor2pil(img).convert('RGB')))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), )
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageRemoveAlpha": ImageRemoveAlpha
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageRemoveAlpha": "LayerUtility: ImageRemoveAlpha"
}
@@ -1,158 +0,0 @@
import torch
from PIL import Image
import math
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, num_round_up_to_multiple, fit_resize_image
class ImageScaleByAspectRatio:
def __init__(self):
self.NODE_NAME = 'ImageScaleByAspectRatio'
@classmethod
def INPUT_TYPES(self):
ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16']
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
return {
"required": {
"aspect_ratio": (ratio_list,),
"proportional_width": ("INT", {"default": 2, "min": 1, "max": 999, "step": 1}),
"proportional_height": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}),
"fit": (fit_mode,),
"method": (method_mode,),
"round_to_multiple": (multiple_list,),
"scale_to_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放
"longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}),
},
"optional": {
"image": ("IMAGE",), #
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT",)
RETURN_NAMES = ("image", "mask", "original_size", "width", "height",)
FUNCTION = 'image_scale_by_aspect_ratio'
CATEGORY = '😺dzNodes/LayerUtility'
def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height,
fit, method, round_to_multiple, scale_to_longest_side, longest_side,
image=None, mask = None,
):
orig_images = []
orig_masks = []
orig_width = 0
orig_height = 0
target_width = 0
target_height = 0
ratio = 1.0
ret_images = []
ret_masks = []
if image is not None:
for i in image:
i = torch.unsqueeze(i, 0)
orig_images.append(i)
orig_width, orig_height = tensor2pil(orig_images[0]).size
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
m = torch.unsqueeze(m, 0)
orig_masks.append(m)
_width, _height = tensor2pil(orig_masks[0]).size
if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
log(f"Error: {self.NODE_NAME} skipped, because the mask is does'nt match image.", message_type='error')
return (None, None, None, 0, 0,)
elif orig_width + orig_height == 0:
orig_width = _width
orig_height = _height
if orig_width + orig_height == 0:
log(f"Error: {self.NODE_NAME} skipped, because the image or mask at least one must be input.", message_type='error')
return (None, None, None, 0, 0,)
if aspect_ratio == 'original':
ratio = orig_width / orig_height
elif aspect_ratio == 'custom':
ratio = proportional_width / proportional_height
else:
s = aspect_ratio.split(":")
ratio = int(s[0]) / int(s[1])
# calculate target width and height
if orig_width > orig_height:
if scale_to_longest_side:
target_width = longest_side
else:
target_width = orig_width
target_height = int(target_width / ratio)
else:
if scale_to_longest_side:
target_height = longest_side
else:
target_height = orig_height
target_width = int(target_height * ratio)
if ratio < 1:
if scale_to_longest_side:
_r = longest_side / target_height
target_height = longest_side
else:
_r = orig_height / target_height
target_height = orig_height
target_width = int(target_width * _r)
if round_to_multiple != 'None':
multiple = int(round_to_multiple)
target_width = num_round_up_to_multiple(target_width, multiple)
target_height = num_round_up_to_multiple(target_height, multiple)
_mask = Image.new('L', size=(target_width, target_height), color='black')
_image = Image.new('RGB', size=(target_width, target_height), color='black')
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
if len(orig_images) > 0:
for i in orig_images:
_image = tensor2pil(i).convert('RGB')
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler)
ret_images.append(pil2tensor(_image))
if len(orig_masks) > 0:
for m in orig_masks:
_mask = tensor2pil(m).convert('L')
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) >0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,)
elif len(ret_images) > 0 and len(ret_masks) == 0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), None,[orig_width, orig_height], target_width, target_height,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
return (None, torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,)
else:
log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.", message_type='error')
return (None, None, None, 0, 0,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageScaleByAspectRatio": ImageScaleByAspectRatio
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageScaleByAspectRatio": "LayerUtility: ImageScaleByAspectRatio"
}
@@ -1,186 +0,0 @@
import torch
from PIL import Image
import math
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, num_round_up_to_multiple, fit_resize_image, is_valid_mask
class ImageScaleByAspectRatioV2:
def __init__(self):
self.NODE_NAME = 'ImageScaleByAspectRatio V2'
@classmethod
def INPUT_TYPES(self):
ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16']
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
scale_to_list = ['None', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)']
return {
"required": {
"aspect_ratio": (ratio_list,),
"proportional_width": ("INT", {"default": 1, "min": 1, "max": 1e8, "step": 1}),
"proportional_height": ("INT", {"default": 1, "min": 1, "max": 1e8, "step": 1}),
"fit": (fit_mode,),
"method": (method_mode,),
"round_to_multiple": (multiple_list,),
"scale_to_side": (scale_to_list,), # 是否按长边缩放
"scale_to_length": ("INT", {"default": 1024, "min": 4, "max": 1e8, "step": 1}),
"background_color": ("STRING", {"default": "#000000"}), # 背景颜色
},
"optional": {
"image": ("IMAGE",), #
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT",)
RETURN_NAMES = ("image", "mask", "original_size", "width", "height",)
FUNCTION = 'image_scale_by_aspect_ratio'
CATEGORY = '😺dzNodes/LayerUtility'
def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height,
fit, method, round_to_multiple, scale_to_side, scale_to_length,
background_color,
image=None, mask = None,
):
orig_images = []
orig_masks = []
orig_width = 0
orig_height = 0
target_width = 0
target_height = 0
ratio = 1.0
ret_images = []
ret_masks = []
if image is not None:
for i in image:
i = torch.unsqueeze(i, 0)
orig_images.append(i)
orig_width, orig_height = tensor2pil(orig_images[0]).size
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
m = torch.unsqueeze(m, 0)
if not is_valid_mask(m) and m.shape==torch.Size([1,64,64]):
log(f"Warning: {self.NODE_NAME} input mask is empty, ignore it.", message_type='warning')
else:
orig_masks.append(m)
if len(orig_masks) > 0:
_width, _height = tensor2pil(orig_masks[0]).size
if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
log(f"Error: {self.NODE_NAME} execute failed, because the mask is does'nt match image.", message_type='error')
return (None, None, None, 0, 0,)
elif orig_width + orig_height == 0:
orig_width = _width
orig_height = _height
if orig_width + orig_height == 0:
log(f"Error: {self.NODE_NAME} execute failed, because the image or mask at least one must be input.", message_type='error')
return (None, None, None, 0, 0,)
if aspect_ratio == 'original':
ratio = orig_width / orig_height
elif aspect_ratio == 'custom':
ratio = proportional_width / proportional_height
else:
s = aspect_ratio.split(":")
ratio = int(s[0]) / int(s[1])
# calculate target width and height
if ratio > 1:
if scale_to_side == 'longest':
target_width = scale_to_length
target_height = int(target_width / ratio)
elif scale_to_side == 'shortest':
target_height = scale_to_length
target_width = int(target_height * ratio)
elif scale_to_side == 'width':
target_width = scale_to_length
target_height = int(target_width / ratio)
elif scale_to_side == 'height':
target_height = scale_to_length
target_width = int(target_height * ratio)
elif scale_to_side == 'total_pixel(kilo pixel)':
target_width = math.sqrt(ratio * scale_to_length * 1000)
target_height = target_width / ratio
target_width = int(target_width)
target_height = int(target_height)
else:
target_width = orig_width
target_height = int(target_width / ratio)
else:
if scale_to_side == 'longest':
target_height = scale_to_length
target_width = int(target_height * ratio)
elif scale_to_side == 'shortest':
target_width = scale_to_length
target_height = int(target_width / ratio)
elif scale_to_side == 'width':
target_width = scale_to_length
target_height = int(target_width / ratio)
elif scale_to_side == 'height':
target_height = scale_to_length
target_width = int(target_height * ratio)
elif scale_to_side == 'total_pixel(kilo pixel)':
target_width = math.sqrt(ratio * scale_to_length * 1000)
target_height = target_width / ratio
target_width = int(target_width)
target_height = int(target_height)
else:
target_height = orig_height
target_width = int(target_height * ratio)
if round_to_multiple != 'None':
multiple = int(round_to_multiple)
target_width = num_round_up_to_multiple(target_width, multiple)
target_height = num_round_up_to_multiple(target_height, multiple)
_mask = Image.new('L', size=(target_width, target_height), color='black')
_image = Image.new('RGB', size=(target_width, target_height), color='black')
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
if len(orig_images) > 0:
for i in orig_images:
_image = tensor2pil(i).convert('RGB')
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler, background_color)
ret_images.append(pil2tensor(_image))
if len(orig_masks) > 0:
for m in orig_masks:
_mask = tensor2pil(m).convert('L')
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) >0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,)
elif len(ret_images) > 0 and len(ret_masks) == 0:
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
else:
log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.", message_type='error')
return (None, None, None, 0, 0,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageScaleByAspectRatio V2": ImageScaleByAspectRatioV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageScaleByAspectRatio V2": "LayerUtility: ImageScaleByAspectRatio V2"
}
@@ -1,112 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
class ImageScaleRestore:
def __init__(self):
self.NODE_NAME = 'ImageScaleRestore'
@classmethod
def INPUT_TYPES(self):
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"image": ("IMAGE", ), #
"scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
"method": (method_mode,),
"scale_by_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放
"longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
"original_size": ("BOX",),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT")
RETURN_NAMES = ("image", "mask", "original_size", "width", "height",)
FUNCTION = 'image_scale_restore'
CATEGORY = '😺dzNodes/LayerUtility'
def image_scale_restore(self, image, scale, method,
scale_by_longest_side, longest_side,
mask = None, original_size = None
):
l_images = []
l_masks = []
ret_images = []
ret_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(l_images), len(l_masks))
orig_width, orig_height = tensor2pil(l_images[0]).size
if original_size is not None:
target_width = original_size[0]
target_height = original_size[1]
else:
target_width = int(orig_width * scale)
target_height = int(orig_height * scale)
if scale_by_longest_side:
if orig_width > orig_height:
target_width = longest_side
target_height = int(target_width * orig_height / orig_width)
else:
target_height = longest_side
target_width = int(target_height * orig_width / orig_height)
if target_width < 4:
target_width = 4
if target_height < 4:
target_height = 4
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
for i in range(max_batch):
_image = l_images[i] if i < len(l_images) else l_images[-1]
_canvas = tensor2pil(_image).convert('RGB')
ret_image = _canvas.resize((target_width, target_height), resize_sampler)
ret_mask = Image.new('L', size=ret_image.size, color='white')
if mask is not None:
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
ret_mask = _mask.resize((target_width, target_height), resize_sampler)
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(ret_mask))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageScaleRestore": ImageScaleRestore
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageScaleRestore": "LayerUtility: ImageScaleRestore"
}
@@ -1,132 +0,0 @@
import math
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask
class ImageScaleRestoreV2:
def __init__(self):
self.NODE_NAME = 'ImageScaleRestore V2'
@classmethod
def INPUT_TYPES(self):
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
scale_by_list = ['by_scale', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)']
return {
"required": {
"image": ("IMAGE", ), #
"scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
"method": (method_mode,),
"scale_by": (scale_by_list,), # 是否按长边缩放
"scale_by_length": ("INT", {"default": 1024, "min": 4, "max": 99999999, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
"original_size": ("BOX",),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT")
RETURN_NAMES = ("image", "mask", "original_size", "width", "height",)
FUNCTION = 'image_scale_restore'
CATEGORY = '😺dzNodes/LayerUtility'
def image_scale_restore(self, image, scale, method,
scale_by, scale_by_length,
mask = None, original_size = None
):
l_images = []
l_masks = []
ret_images = []
ret_masks = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
max_batch = max(len(l_images), len(l_masks))
orig_width, orig_height = tensor2pil(l_images[0]).size
if original_size is not None:
target_width = original_size[0]
target_height = original_size[1]
else:
target_width = int(orig_width * scale)
target_height = int(orig_height * scale)
if scale_by == 'longest':
if orig_width > orig_height:
target_width = scale_by_length
target_height = int(target_width * orig_height / orig_width)
else:
target_height = scale_by_length
target_width = int(target_height * orig_width / orig_height)
if scale_by == 'shortest':
if orig_width < orig_height:
target_width = scale_by_length
target_height = int(target_width * orig_height / orig_width)
else:
target_height = scale_by_length
target_width = int(target_height * orig_width / orig_height)
if scale_by == 'width':
target_width = scale_by_length
target_height = int(target_width * orig_height / orig_width)
if scale_by == 'height':
target_height = scale_by_length
target_width = int(target_height * orig_width / orig_height)
if scale_by == 'total_pixel(kilo pixel)':
r = orig_width / orig_height
target_width = math.sqrt(r * scale_by_length * 1000)
target_height = target_width / r
target_width = int(target_width)
target_height = int(target_height)
if target_width < 4:
target_width = 4
if target_height < 4:
target_height = 4
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
for i in range(max_batch):
_image = l_images[i] if i < len(l_images) else l_images[-1]
_canvas = tensor2pil(_image).convert('RGB')
ret_image = _canvas.resize((target_width, target_height), resize_sampler)
ret_mask = Image.new('L', size=ret_image.size, color='white')
if mask is not None:
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
ret_mask = _mask.resize((target_width, target_height), resize_sampler)
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(ret_mask))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageScaleRestore V2": ImageScaleRestoreV2
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageScaleRestore V2": "LayerUtility: ImageScaleRestore V2"
}
-88
View File
@@ -1,88 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, draw_border, gaussian_blur, shift_image
class ImageShift:
def __init__(self):
self.NODE_NAME = 'ImageShift'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE", ), #
"shift_x": ("INT", {"default": 256, "min": -9999, "max": 9999, "step": 1}),
"shift_y": ("INT", {"default": 256, "min": -9999, "max": 9999, "step": 1}),
"cyclic": ("BOOLEAN", {"default": True}), # 是否循环重复
"background_color": ("STRING", {"default": "#000000"}),
"border_mask_width": ("INT", {"default": 20, "min": 0, "max": 999, "step": 1}),
"border_mask_blur": ("INT", {"default": 12, "min": 0, "max": 999, "step": 1}),
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "MASK",)
RETURN_NAMES = ("image", "mask", "border_mask")
FUNCTION = 'image_shift'
CATEGORY = '😺dzNodes/LayerUtility'
def image_shift(self, image, shift_x, shift_y,
cyclic, background_color,
border_mask_width, border_mask_blur,
mask=None
):
ret_images = []
ret_masks = []
ret_border_masks = []
l_images = []
l_masks = []
for l in 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', size=m.size, color='white'))
if mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
shift_x, shift_y = -shift_x, -shift_y
for i in range(len(l_images)):
_image = l_images[i]
_canvas = tensor2pil(_image).convert('RGB')
_mask = l_masks[i] if len(l_masks) < i else l_masks[-1]
_border = Image.new('L', size=_canvas.size, color='black')
_border = draw_border(_border, border_width=border_mask_width, color='#FFFFFF')
_border = _border.resize(_canvas.size)
_canvas = shift_image(_canvas, shift_x, shift_y, background_color=background_color, cyclic=cyclic)
_mask = shift_image(_mask, shift_x, shift_y, background_color='#000000', cyclic=cyclic)
_border = shift_image(_border, shift_x, shift_y, background_color='#000000', cyclic=cyclic)
_border = gaussian_blur(_border, border_mask_blur)
ret_images.append(pil2tensor(_canvas))
ret_masks.append(image2mask(_mask))
ret_border_masks.append(image2mask(_border))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), torch.cat(ret_border_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageShift": ImageShift
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageShift": "LayerUtility: ImageShift"
}
@@ -1,142 +0,0 @@
import os.path
import shutil
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import datetime
import torch
import numpy as np
import folder_paths
from .imagefunc import log, generate_random_name, remove_empty_lines
class LSImageTaggerSave:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.prefix_append = ""
self.compress_level = 4
self.NODE_NAME = 'ImageTaggerSave'
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("IMAGE", ),
"tag_text": ("STRING", {"default": "", "forceInput":True}),
"custom_path": ("STRING", {"default": ""}),
"filename_prefix": ("STRING", {"default": "comfyui"}),
"timestamp": (["None", "second", "millisecond"],),
"format": (["png", "jpg"],),
"quality": ("INT", {"default": 80, "min": 10, "max": 100, "step": 1}),
"preview": ("BOOLEAN", {"default": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
FUNCTION = "image_tagger_save"
OUTPUT_NODE = True
CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
def image_tagger_save(self, image, tag_text, custom_path, filename_prefix, timestamp, format, quality,
preview,
prompt=None, extra_pnginfo=None):
now = datetime.datetime.now()
custom_path = custom_path.replace("%date", now.strftime("%Y-%m-%d"))
custom_path = custom_path.replace("%time", now.strftime("%H-%M-%S"))
filename_prefix = filename_prefix.replace("%date", now.strftime("%Y-%m-%d"))
filename_prefix = filename_prefix.replace("%time", now.strftime("%H-%M-%S"))
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, image[0].shape[1], image[0].shape[0])
results = list()
temp_sub_dir = generate_random_name('_savepreview_', '_temp', 16)
temp_dir = os.path.join(folder_paths.get_temp_directory(), temp_sub_dir)
metadata = None
i = 255. * image[0].cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
if timestamp == "millisecond":
file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}'
elif timestamp == "second":
file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}'
else:
file = f'{filename}_{counter:08}'
preview_filename = ""
if custom_path != "":
if not os.path.exists(custom_path):
try:
os.makedirs(custom_path)
except Exception as e:
log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary folder.",
message_type='warning')
raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}")
else:
custom_path = folder_paths.get_output_directory()
full_output_folder = os.path.normpath(custom_path)
# save preview image to temp_dir
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir)
try:
os.makedirs(temp_dir)
except Exception as e:
print(e)
log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary folder.",
message_type='warning')
try:
preview_filename = os.path.join(generate_random_name('saveimage_preview_', '_temp', 16) + '.png')
img.save(os.path.join(temp_dir, preview_filename))
except Exception as e:
print(e)
log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary file.", message_type='warning')
# check if file exists, change filename
while os.path.isfile(os.path.join(full_output_folder, f"{file}.{format}")):
counter += 1
if timestamp == "millisecond":
file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}_{counter:08}'
elif timestamp == "second":
file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}_{counter:08}'
else:
file = f"{filename}_{counter:08}"
image_file_name = os.path.join(full_output_folder, f"{file}.{format}")
tag_file_name = os.path.join(full_output_folder, f"{file}.txt")
if format == "png":
img.save(image_file_name, pnginfo=metadata, compress_level= (100 - quality) // 10)
else:
if img.mode == "RGBA":
img = img.convert("RGB")
img.save(image_file_name, quality=quality)
with open(tag_file_name, "w", encoding="utf-8") as f:
f.write(remove_empty_lines(tag_text))
log(f"{self.NODE_NAME} -> Saving image to {image_file_name}")
if preview:
if custom_path == "":
results.append({
"filename": f"{file}.{format}",
"subfolder": subfolder,
"type": self.type
})
else:
results.append({
"filename": preview_filename,
"subfolder": temp_sub_dir,
"type": "temp"
})
counter += 1
return { "ui": { "images": results } }
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageTaggerSave": LSImageTaggerSave
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save"
}
-111
View File
@@ -1,111 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, image2mask, image_channel_split, normalize_gray, adjust_levels
class ImageToMask:
def __init__(self):
self.NODE_NAME = 'ImageToMask'
@classmethod
def INPUT_TYPES(s):
channel_list = ["L(LAB)", "A(Lab)", "B(Lab)",
"R(RGB)", "G(RGB)", "B(RGB)", "alpha",
"Y(YUV)", "U(YUV)", "V(YUV)",
"H(HSV)", "S(HSV", "V(HSV)"]
return {
"required": {
"image": ("IMAGE", ),
"channel": (channel_list,),
"black_point": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1, "display": "slider"}),
"white_point": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1, "display": "slider"}),
"gray_point": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 9.99, "step": 0.01}),
"invert_output_mask": ("BOOLEAN", {"default": False}), # 反转mask
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "image_to_mask"
CATEGORY = '😺dzNodes/LayerMask'
def image_to_mask(self, image, channel,
black_point, white_point, gray_point,
invert_output_mask, mask=None
):
ret_masks = []
l_images = []
l_masks = []
for l in 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 mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
for i in range(len(l_images)):
orig_image = l_images[i] if i < len(l_images) else l_images[-1]
orig_image = tensor2pil(orig_image)
orig_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
mask = Image.new('L', orig_image.size, 'black')
if channel == "L(LAB)":
mask, _, _, _ = image_channel_split(orig_image, 'LAB')
elif channel == "A(Lab)":
_, mask, _, _ = image_channel_split(orig_image, 'LAB')
elif channel == "B(Lab)":
_, _, mask, _ = image_channel_split(orig_image, 'LAB')
elif channel == "R(RGB)":
mask, _, _, _ = image_channel_split(orig_image, 'RGB')
elif channel == "G(RGB)":
_, mask, _, _ = image_channel_split(orig_image, 'RGB')
elif channel == "B(RGB)":
_, _, mask, _ = image_channel_split(orig_image, 'RGB')
elif channel == "alpha":
_, _, _, mask = image_channel_split(orig_image, 'RGBA')
elif channel == "Y(YUV)":
mask, _, _, _ = image_channel_split(orig_image, 'YCbCr')
elif channel == "U(YUV)":
_, mask, _, _ = image_channel_split(orig_image, 'YCbCr')
elif channel == "V(YUV)":
_, _, mask, _ = image_channel_split(orig_image, 'YCbCr')
elif channel == "H(HSV)":
mask, _, _, _ = image_channel_split(orig_image, 'HSV')
elif channel == "S(HSV)":
_, mask, _, _ = image_channel_split(orig_image, 'HSV')
elif channel == "V(HSV)":
_, _, mask, _ = image_channel_split(orig_image, 'HSV')
mask = normalize_gray(mask)
mask = adjust_levels(mask, black_point, white_point, gray_point,
0, 255)
if invert_output_mask:
mask = ImageChops.invert(mask)
ret_mask = Image.new('L', mask.size, 'black')
ret_mask.paste(mask, mask=orig_mask)
ret_mask = image2mask(ret_mask)
ret_masks.append(ret_mask)
return (torch.cat(ret_masks, dim=0), )
NODE_CLASS_MAPPINGS = {
"LayerMask: ImageToMask": ImageToMask
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: ImageToMask": "LayerMask: Image To Mask"
}
File diff suppressed because it is too large Load Diff
-107
View File
@@ -1,107 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color, expand_mask, mask_invert, chop_mode, chop_image, step_value
class InnerGlow:
def __init__(self):
self.NODE_NAME = 'InnerGlow'
@classmethod
def INPUT_TYPES(self):
chop_mode = ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract','difference','darker',
'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay',
'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix']
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerStyle'
def inner_glow(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: {self.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: {self.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(_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"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: InnerGlow": InnerGlow
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: InnerGlow": "LayerStyle: InnerGlow"
}
-114
View File
@@ -1,114 +0,0 @@
import copy
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color, expand_mask, mask_invert, chop_mode_v2, chop_image_v2, BLEND_MODES, step_value
class InnerGlowV2:
def __init__(self):
self.NODE_NAME = 'InnerGlowV2'
@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: {self.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: {self.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"{self.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"
}
-102
View File
@@ -1,102 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, shift_image, expand_mask, chop_image, chop_mode
class InnerShadow:
def __init__(self):
self.NODE_NAME = 'InnerShadow'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"background_image": ("IMAGE", ), #
"layer_image": ("IMAGE",), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"blend_mode": (chop_mode,), # 混合模式
"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'
CATEGORY = '😺dzNodes/LayerStyle'
def inner_shadow(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: {self.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: {self.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(_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"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerStyle: InnerShadow": InnerShadow
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerStyle: InnerShadow": "LayerStyle: InnerShadow"
}
@@ -1,103 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, shift_image, expand_mask, chop_image_v2, chop_mode_v2
class InnerShadowV2:
def __init__(self):
self.NODE_NAME = 'InnerShadowV2'
@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: {self.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: {self.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"{self.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"
}
@@ -1,94 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image_rotate_extend_with_alpha, RGB2RGBA
class LayerImageTransform:
def __init__(self):
self.NODE_NAME = 'LayerImageTransform'
@classmethod
def INPUT_TYPES(self):
mirror_mode = ['None', 'horizontal', 'vertical']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"image": ("IMAGE",), #
"x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"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": 2, "min": 0, "max": 16, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'layer_image_transform'
CATEGORY = '😺dzNodes/LayerUtility'
def layer_image_transform(self, image, x, y, mirror, scale, aspect_ratio, rotate,
transform_method, anti_aliasing,
):
l_images = []
l_masks = []
ret_images = []
for l in image:
l_images.append(torch.unsqueeze(l, 0))
m = tensor2pil(l)
if m.mode == 'RGBA':
l_masks.append(m.split()[-1])
for i in range(len(l_images)):
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
_image = tensor2pil(layer_image).convert('RGB')
if i < len(l_masks):
_mask = l_masks[i]
else:
_mask = Image.new('L', size=_image.size, color='white')
_image_canvas = Image.new('RGB', size=_image.size, color='black')
_mask_canvas = Image.new('L', size=_mask.size, color='black')
orig_layer_width = _image.width
orig_layer_height = _image.height
target_layer_width = int(orig_layer_width * scale)
target_layer_height = int(orig_layer_height * scale * aspect_ratio)
# mirror
if mirror == 'horizontal':
_image = _image.transpose(Image.FLIP_LEFT_RIGHT)
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
elif mirror == 'vertical':
_image = _image.transpose(Image.FLIP_TOP_BOTTOM)
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
# scale
_image = _image.resize((target_layer_width, target_layer_height))
_mask = _mask.resize((target_layer_width, target_layer_height))
# rotate
_image, _mask, _ = image_rotate_extend_with_alpha(_image, rotate, _mask, transform_method, anti_aliasing)
# composit layer
paste_x = (orig_layer_width - _image.width) // 2 + x
paste_y = (orig_layer_height - _image.height) // 2 + y
_image_canvas.paste(_image, (paste_x, paste_y))
_mask_canvas.paste(_mask, (paste_x, paste_y))
if tensor2pil(layer_image).mode == 'RGBA':
_image_canvas = RGB2RGBA(_image_canvas, _mask_canvas)
ret_images.append(pil2tensor(_image_canvas))
log(f"{self.NODE_NAME} Processed {len(l_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: LayerImageTransform": LayerImageTransform
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: LayerImageTransform": "LayerUtility: LayerImageTransform"
}
@@ -1,81 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, image_rotate_extend_with_alpha, RGB2RGBA
class LayerMaskTransform:
def __init__(self):
self.NODE_NAME = 'LayerMaskTransform'
@classmethod
def INPUT_TYPES(self):
mirror_mode = ['None', 'horizontal', 'vertical']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"mask": ("MASK",), #
"x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
"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": 2, "min": 0, "max": 16, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'layer_mask_transform'
CATEGORY = '😺dzNodes/LayerUtility'
def layer_mask_transform(self, mask, x, y, mirror, scale, aspect_ratio, rotate,
transform_method, anti_aliasing,
):
l_masks = []
ret_masks = []
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
l_masks.append(torch.unsqueeze(m, 0))
for i in range(len(l_masks)):
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
_mask = tensor2pil(_mask).convert('L')
_mask_canvas = Image.new('L', size=_mask.size, color='black')
orig_width = _mask.width
orig_height = _mask.height
target_layer_width = int(orig_width * scale)
target_layer_height = int(orig_height * scale * aspect_ratio)
# mirror
if mirror == 'horizontal':
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
elif mirror == 'vertical':
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
# scale
_mask = _mask.resize((target_layer_width, target_layer_height))
# rotate
_, _mask, _ = image_rotate_extend_with_alpha(_mask.convert('RGB'), rotate, _mask, transform_method, anti_aliasing)
paste_x = (orig_width - _mask.width) // 2 + x
paste_y = (orig_height - _mask.height) // 2 + y
# composit layer
_mask_canvas.paste(_mask, (paste_x, paste_y))
ret_masks.append(image2mask(_mask_canvas))
log(f"{self.NODE_NAME} Processed {len(l_masks)} mask(s).", message_type='finish')
return (torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerUtility: LayerMaskTransform": LayerMaskTransform
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: LayerMaskTransform": "LayerUtility: LayerMaskTransform"
}
-85
View File
@@ -1,85 +0,0 @@
import os.path
import random
import time
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, load_light_leak_images, image_hue_offset, image_gray_offset, image_channel_merge, fit_resize_image, chop_image
blend_mode = 'screen'
class LightLeak:
def __init__(self):
self.NODE_NAME = 'LightLeak'
@classmethod
def INPUT_TYPES(self):
light_list = ['random', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10',
'11', '12', '13', '14', '15', '16', '17', '18', '19', '20',
'21', '22', '23', '24', '25', '26', '27', '28', '29', '30',
'31', '32']
corner_list = ['left_top', 'right_top', 'left_bottom', 'right_bottom']
return {
"required": {
"image": ("IMAGE", ),
"light": (light_list,),
"corner": (corner_list,),
"hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"saturation": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1})
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = 'light_leak'
CATEGORY = '😺dzNodes/LayerFilter'
def light_leak(self, image, light, corner, hue, saturation, opacity):
ret_images = []
light_leak_images = load_light_leak_images()
if light == 'random':
random.seed(time.time())
light_index = random.randint(0,31)
else:
light_index = int(light) - 1
for i in image:
i = torch.unsqueeze(i, 0)
_canvas = tensor2pil(i).convert('RGB')
_light = light_leak_images[light_index]
if _canvas.width < _canvas.height:
_light = _light.transpose(Image.ROTATE_90).transpose(Image.FLIP_TOP_BOTTOM)
if corner == 'right_top':
_light = _light.transpose(Image.FLIP_LEFT_RIGHT)
elif corner == 'left_bottom':
_light = _light.transpose(Image.FLIP_TOP_BOTTOM)
elif corner == 'right_bottom':
_light = _light.transpose(Image.ROTATE_180)
if hue != 0 or saturation != 0:
_h, _s, _v = _light.convert('HSV').split()
if hue != 0:
_h = image_hue_offset(_h, hue)
if saturation != 0:
_s = image_gray_offset(_s, saturation)
_light = image_channel_merge((_h, _s, _v), 'HSV')
resize_sampler = Image.BILINEAR
_light = fit_resize_image(_light, _canvas.width, _canvas.height, fit='crop', resize_sampler=resize_sampler)
ret_image = chop_image(_canvas, _light, blend_mode=blend_mode, opacity = opacity)
ret_images.append(pil2tensor(ret_image))
log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
return (torch.cat(ret_images, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerFilter: LightLeak": LightLeak
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerFilter: LightLeak": "LayerFilter: LightLeak"
}
@@ -1,78 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur, mask2image
from .imagefunc import min_bounding_rect, max_inscribed_rect, mask_area, draw_rect
class MaskBoxDetect:
def __init__(self):
self.NODE_NAME = 'MaskBoxDetect'
@classmethod
def INPUT_TYPES(self):
detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area']
return {
"required": {
"mask": ("MASK", ),
"detect": (detect_mode,), # 探测类型:最小外接矩形/最大内接矩形
"x_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # x轴修正
"y_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # y轴修正
"scale_adjust": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100, "step": 0.01}), # 比例修正
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "FLOAT", "FLOAT", "INT", "INT", "INT", "INT",)
RETURN_NAMES = ("box_preview", "x_percent", "y_percent", "width", "height", "x", "y",)
FUNCTION = 'mask_box_detect'
CATEGORY = '😺dzNodes/LayerMask'
def mask_box_detect(self,mask, detect, x_adjust, y_adjust, scale_adjust):
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
if mask.shape[0] > 0:
mask = torch.unsqueeze(mask[0], 0)
_mask = mask2image(mask).convert('RGB')
_mask = gaussian_blur(_mask, 20).convert('L')
x = 0
y = 0
width = 0
height = 0
if detect == "min_bounding_rect":
(x, y, width, height) = min_bounding_rect(_mask)
elif detect == "max_inscribed_rect":
(x, y, width, height) = max_inscribed_rect(_mask)
else:
(x, y, width, height) = mask_area(_mask)
log(f"{self.NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}")
_width = width
_height = height
if scale_adjust != 1.0:
_width = int(width * scale_adjust)
_height = int(height * scale_adjust)
x = x - int((_width - width) / 2)
y = y - int((_height - height) / 2)
x += x_adjust
y += y_adjust
x_percent = (x + _width / 2) / _mask.width * 100
y_percent = (y + _height / 2) / _mask.height * 100
preview_image = tensor2pil(mask).convert('RGB')
preview_image = draw_rect(preview_image, x - x_adjust, y - y_adjust, width, height, line_color="#F00000", line_width=int(preview_image.height / 60))
preview_image = draw_rect(preview_image, x, y, width, height, line_color="#00F000", line_width=int(preview_image.height / 40))
log(f"{self.NODE_NAME} Processed.", message_type='finish')
return ( pil2tensor(preview_image), round(x_percent, 2), round(y_percent, 2), _width, _height, x, y,)
NODE_CLASS_MAPPINGS = {
"LayerMask: MaskBoxDetect": MaskBoxDetect
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskBoxDetect": "LayerMask: MaskBoxDetect"
}
@@ -1,84 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, create_mask_from_color_tensor, mask_fix
class MaskByColor:
def __init__(self):
self.NODE_NAME = 'MaskByColor'
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", ),
"color": ("COLOR", {"default": "#FFFFFF"},),
"color_in_HEX": ("STRING", {"default": ""}),
"threshold": ("INT", { "default": 50, "min": 0, "max": 100, "step": 1, }),
"fix_gap": ("INT", {"default": 2, "min": 0, "max": 32, "step": 1}),
"fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}),
"invert_mask": ("BOOLEAN", {"default": False}), # 反转mask
},
"optional": {
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "mask_by_color"
CATEGORY = '😺dzNodes/LayerMask'
def mask_by_color(self, image, color, color_in_HEX, threshold,
fix_gap, fix_threshold, invert_mask, mask=None):
if color_in_HEX != "" and color_in_HEX.startswith('#') and len(color_in_HEX) == 7:
color = color_in_HEX
ret_masks = []
l_images = []
l_masks = []
for l in 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 mask is not None:
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
for i in range(len(l_images)):
img = l_images[i] if i < len(l_images) else l_images[-1]
img = tensor2pil(img)
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
mask = Image.new('L', _mask.size, 'black')
mask.paste(create_mask_from_color_tensor(img, color, threshold), mask=_mask)
mask = image2mask(mask)
if invert_mask:
mask = 1 - mask
if fix_gap:
mask = mask_fix(mask, 1, fix_gap, fix_threshold, fix_threshold)
ret_masks.append(mask)
return (torch.cat(ret_masks, dim=0), )
NODE_CLASS_MAPPINGS = {
"LayerMask: MaskByColor": MaskByColor
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskByColor": "LayerMask: Mask by Color"
}
@@ -1,81 +0,0 @@
import torch
from PIL import Image, ImageChops
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask_invert, step_color, expand_mask, step_value, chop_image
class MaskEdgeShrink:
def __init__(self):
self.NODE_NAME = 'MaskEdgeShrink'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"mask": ("MASK", ), #
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"shrink_level": ("INT", {"default": 4, "min": 0, "max": 16, "step": 1}),
"soft": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}),
"edge_shrink": ("INT", {"default": 1, "min": 0, "max": 999, "step": 1}),
"edge_reserve": ("INT", {"default": 25, "min": 0, "max": 100, "step": 1}), # 透明度
},
"optional": {
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'mask_edge_shrink'
CATEGORY = '😺dzNodes/LayerMask'
def mask_edge_shrink(self, mask, invert_mask, shrink_level, soft, edge_shrink, edge_reserve):
l_masks = []
ret_masks = []
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
glow_range = shrink_level * soft
blur = 12
for i in range(len(l_masks)):
_mask = l_masks[i]
_canvas = Image.new('RGB', size=_mask.size, color='black')
_layer = Image.new('RGB', size=_mask.size, color='white')
loop_grow = glow_range
inner_mask = _mask
for x in range(shrink_level):
_color = step_color('#FFFFFF', '#000000', shrink_level, x)
glow_mask = expand_mask(image2mask(inner_mask), -loop_grow, blur / (x+0.1)) #扩张,模糊
# 合成
color_image = Image.new("RGB", _layer.size, color=_color)
alpha = tensor2pil(mask_invert(glow_mask)).convert('L')
_glow = chop_image(_layer, color_image, 'subtract', int(step_value(1, 100, shrink_level, x)))
_layer.paste(_glow, mask=alpha)
loop_grow = loop_grow - int(glow_range / shrink_level)
# 合成layer
_edge = tensor2pil(expand_mask(image2mask(_mask), -edge_shrink, 0)).convert('RGB')
_layer = chop_image(_layer, _edge, 'normal', edge_reserve)
_layer.paste(_canvas, mask=ImageChops.invert(_mask))
ret_masks.append(image2mask(_layer))
log(f"{self.NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish')
return (torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerMask: MaskEdgeShrink": MaskEdgeShrink
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskEdgeShrink": "LayerMask: MaskEdgeShrink"
}
@@ -1,80 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, mask_fix
from .imagefunc import guided_filter_alpha, histogram_remap, mask_edge_detail ,RGB2RGBA
class MaskEdgeUltraDetail:
def __init__(self):
self.NODE_NAME = 'MaskEdgeUltraDetail'
@classmethod
def INPUT_TYPES(cls):
method_list = ['PyMatting', 'OpenCV-GuidedFilter']
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"method": (method_list,),
"mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}),
"fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}),
"fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}),
"detail_range": ("INT", {"default": 12, "min": 1, "max": 256, "step": 1}),
"black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}),
"white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "MASK", )
RETURN_NAMES = ("image", "mask", )
FUNCTION = "mask_edge_ultra_detail"
CATEGORY = '😺dzNodes/LayerMask'
def mask_edge_ultra_detail(self, image, mask, method, mask_grow, fix_gap, fix_threshold,
detail_range, black_point, white_point,):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for l in image:
l_images.append(torch.unsqueeze(l, 0))
for m in mask:
l_masks.append(torch.unsqueeze(m, 0))
if len(l_images) != len(l_masks) or tensor2pil(l_images[0]).size != tensor2pil(l_masks[0]).size:
log(f"Error: {self.NODE_NAME} skipped, because mask does'nt match image.", message_type='error')
return (image, mask,)
for i in range(len(l_images)):
_image = l_images[i]
orig_image = tensor2pil(_image).convert('RGB')
_image = pil2tensor(orig_image)
_mask = l_masks[i]
if mask_grow != 0:
_mask = expand_mask(_mask, mask_grow, mask_grow//2)
if fix_gap:
_mask = mask_fix(_mask, 1, fix_gap, fix_threshold, fix_threshold)
if method == 'OpenCV-GuidedFilter':
_mask = guided_filter_alpha(_image, _mask, detail_range)
_mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
else:
_mask = tensor2pil(mask_edge_detail(_image, _mask, detail_range, black_point, white_point))
ret_image = RGB2RGBA(orig_image, _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
log(f"{self.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 = {
"LayerMask: MaskEdgeUltraDetail": MaskEdgeUltraDetail,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskEdgeUltraDetail": "LayerMask: MaskEdgeUltraDetail",
}
@@ -1,96 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, mask_fix
from .imagefunc import guided_filter_alpha, histogram_remap, mask_edge_detail ,RGB2RGBA, generate_VITMatte, generate_VITMatte_trimap
class MaskEdgeUltraDetailV2:
def __init__(self):
self.NODE_NAME = 'MaskEdgeUltraDetail V2'
@classmethod
def INPUT_TYPES(cls):
method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ]
device_list = ['cuda','cpu']
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"method": (method_list,),
"mask_grow": ("INT", {"default": 0, "min": 0, "max": 256, "step": 1}),
"fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}),
"fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}),
"edge_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}),
"edte_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}),
"black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}),
"white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}),
"device": (device_list,),
"max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}),
},
"optional": {
}
}
RETURN_TYPES = ("IMAGE", "MASK", )
RETURN_NAMES = ("image", "mask", )
FUNCTION = "mask_edge_ultra_detail_v2"
CATEGORY = '😺dzNodes/LayerMask'
def mask_edge_ultra_detail_v2(self, image, mask, method, mask_grow, fix_gap, fix_threshold,
edge_erode, edte_dilate, black_point, white_point, device, max_megapixels,):
ret_images = []
ret_masks = []
l_images = []
l_masks = []
if method == 'VITMatte(local)':
local_files_only = True
else:
local_files_only = False
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for l in image:
l_images.append(torch.unsqueeze(l, 0))
for m in mask:
l_masks.append(torch.unsqueeze(m, 0))
if len(l_images) != len(l_masks) or tensor2pil(l_images[0]).size != tensor2pil(l_masks[0]).size:
log(f"Error: {self.NODE_NAME} skipped, because mask does'nt match image.", message_type='error')
return (image, mask,)
detail_range = edge_erode + edte_dilate
for i in range(len(l_images)):
_image = l_images[i]
orig_image = tensor2pil(_image).convert('RGB')
_image = pil2tensor(orig_image)
_mask = l_masks[i]
if mask_grow != 0:
_mask = expand_mask(_mask, mask_grow, mask_grow//2)
if fix_gap:
_mask = mask_fix(_mask, 1, fix_gap, fix_threshold, fix_threshold)
log(f"{self.NODE_NAME} Processing...")
if method == 'GuidedFilter':
_mask = guided_filter_alpha(_image, _mask, detail_range//6)
_mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
elif method == 'PyMatting':
_mask = tensor2pil(mask_edge_detail(_image, _mask, detail_range//8, black_point, white_point))
else:
_trimap = generate_VITMatte_trimap(_mask, edge_erode, edte_dilate)
_mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only, device=device, max_megapixels=max_megapixels)
_mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point))
ret_image = RGB2RGBA(orig_image, _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
log(f"{self.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 = {
"LayerMask: MaskEdgeUltraDetail V2": MaskEdgeUltraDetailV2,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskEdgeUltraDetail V2": "LayerMask: MaskEdgeUltraDetail V2",
}
-152
View File
@@ -1,152 +0,0 @@
import torch
import copy
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, chop_image, gradient, mask_area
class MaskGradient:
def __init__(self):
self.NODE_NAME = 'MaskGradient'
@classmethod
def INPUT_TYPES(self):
side = ['top', 'bottom', 'left', 'right']
return {
"required": {
"mask": ("MASK",),
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"gradient_side": (side,),
"gradient_scale": ("INT", {"default": 100, "min": 1, "max": 9999, "step": 1}),
"gradient_offset": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
},
"optional": {
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'mask_gradient'
CATEGORY = '😺dzNodes/LayerMask'
def mask_gradient(self, mask, invert_mask, gradient_side, gradient_scale, gradient_offset, opacity, ):
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
l_masks = []
ret_masks = []
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
for i in range(len(l_masks)):
_mask = l_masks[i]
_canvas = copy.copy(_mask)
width = _mask.width
height = _mask.height
_gradient = gradient('#000000', '#FFFFFF',
1024, 1024, 0)
# (box_x, box_y, box_width, box_height) = min_bounding_rect(_mask)
(box_x, box_y, box_width, box_height) = mask_area(_mask)
log(f"{self.NODE_NAME}: Box detected. x={box_x},y={box_y},width={box_width},height={box_height}")
if box_width < 1 or box_height < 1:
log(f"Error: {self.NODE_NAME} skipped, because the mask is does'nt have valid area", message_type='error')
return (mask,)
if gradient_side == 'top':
boxsize = (width, box_height)
_gradient = _gradient.transpose(Image.FLIP_TOP_BOTTOM)
_gradient = _gradient.resize(boxsize)
_black = Image.new('RGB', size = boxsize, color = 'black')
if gradient_scale != 100:
_box = Image.new('RGB', size = boxsize, color = 'black')
_gradient = _gradient.resize((width, int(box_height * gradient_scale / 100)))
_box.paste(_gradient, box = (0, 0))
_gradient = _box
if gradient_offset != 0:
_box = Image.new('RGB', size = boxsize, color = 'black')
_boxwhite = Image.new('RGB', size = boxsize, color = 'white')
_box.paste(_gradient, box=(0, gradient_offset))
_box.paste(_boxwhite, box = (0, gradient_offset - _box.height))
_gradient = _box
if gradient_offset > box_height:
_gradient = Image.new('RGB', size = boxsize, color = 'white')
_canvas.paste(_black, box = (0, box_y), mask = _gradient.convert('L'))
elif gradient_side == 'bottom':
boxsize = (width, box_height)
_gradient = _gradient.resize((width, box_height))
_black = Image.new('RGB', size = boxsize, color = 'black')
if gradient_scale != 100:
_box = Image.new('RGB', size = boxsize, color = 'black')
_gradient = _gradient.resize((width, int(box_height * gradient_scale / 100)))
_box.paste(_gradient, box = (0, box_height - _gradient.height))
_gradient = _box
if gradient_offset != 0:
_box = Image.new('RGB', size = boxsize, color = 'black')
_boxwhite = Image.new('RGB', size = boxsize, color = 'white')
_box.paste(_gradient, box=(0, gradient_offset))
_box.paste(_boxwhite, box = (0, gradient_offset + _box.height))
_gradient = _box
if gradient_offset < -box_height:
_gradient = Image.new('RGB', size=boxsize, color='white')
_canvas.paste(_black, box = (0, box_y + 1), mask = _gradient.convert('L'))
elif gradient_side == 'left':
boxsize = (box_width, height)
_gradient = _gradient.transpose(Image.ROTATE_270)
_gradient = _gradient.resize(boxsize)
_black = Image.new('RGB', size = boxsize, color = 'black')
if gradient_scale != 100:
_box = Image.new('RGB', size = boxsize, color = 'black')
_gradient = _gradient.resize((int(box_width * gradient_scale / 100), height))
_box.paste(_gradient, box = (0, 0))
_gradient = _box
if gradient_offset != 0:
_box = Image.new('RGB', size = boxsize, color = 'black')
_boxwhite = Image.new('RGB', size = boxsize, color = 'white')
_box.paste(_gradient, box=(gradient_offset, 0))
_box.paste(_boxwhite, box = (gradient_offset - _box.width, 0))
_gradient = _box
if gradient_offset > box_width:
_gradient = Image.new('RGB', size=boxsize, color='white')
_canvas.paste(_black, box = (box_x, 0), mask = _gradient.convert('L'))
elif gradient_side == 'right':
boxsize = (box_width, height)
_gradient = _gradient.transpose(Image.ROTATE_90)
_gradient = _gradient.resize(boxsize)
_black = Image.new('RGB', size = boxsize, color = 'black')
if gradient_scale != 100:
_box = Image.new('RGB', size = boxsize, color = 'black')
_gradient = _gradient.resize((int(box_width * gradient_scale / 100), height))
_box.paste(_gradient, box = (box_width - _gradient.width, 0))
_gradient = _box
if gradient_offset != 0:
_box = Image.new('RGB', size = boxsize, color = 'black')
_boxwhite = Image.new('RGB', size = boxsize, color = 'white')
_box.paste(_gradient, box=(gradient_offset, 0))
_box.paste(_boxwhite, box = (gradient_offset + _box.width, 0))
_gradient = _box
if gradient_offset < -box_width:
_gradient = Image.new('RGB', size=boxsize, color='white')
_canvas.paste(_black, box = (box_x + 1, 0), mask = _gradient.convert('L'))
# opacity
if opacity < 100:
_canvas = chop_image(_mask, _canvas, 'normal', opacity)
ret_masks.append(image2mask(_canvas))
log(f"{self.NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish')
return (torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerMask: MaskGradient": MaskGradient
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskGradient": "LayerMask: MaskGradient"
}
-63
View File
@@ -1,63 +0,0 @@
import torch
from PIL import Image
from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, chop_image_v2
class MaskGrain:
def __init__(self):
self.NODE_NAME = 'MaskGrain'
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"mask": ("MASK", ), #
"grain": ("INT", {"default": 6, "min": 0, "max": 127, "step": 1}),
"invert_mask": ("BOOLEAN", {"default": False}), # 反转mask
},
"optional": {
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'mask_grain'
CATEGORY = '😺dzNodes/LayerMask'
def mask_grain(self, mask, grain, invert_mask):
l_masks = []
ret_masks = []
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
if invert_mask:
m = 1 - m
l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
for mask in l_masks:
if grain:
white_mask = Image.new('L', mask.size, color="white")
inner_mask = tensor2pil(expand_mask(image2mask(mask), 0 - grain, int(grain))).convert('L')
outter_mask = tensor2pil(expand_mask(image2mask(mask), grain, int(grain * 2))).convert('L')
ret_mask = Image.new('L', mask.size, color="black")
ret_mask = chop_image_v2(ret_mask, outter_mask, blend_mode="dissolve", opacity=50).convert('L')
ret_mask.paste(white_mask, mask=inner_mask)
ret_masks.append(image2mask(ret_mask))
else:
ret_masks.append(image2mask(mask))
log(f"{self.NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish')
return (torch.cat(ret_masks, dim=0),)
NODE_CLASS_MAPPINGS = {
"LayerMask: MaskGrain": MaskGrain
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerMask: MaskGrain": "LayerMask: Mask Grain"
}

Some files were not shown because too many files have changed in this diff Show More