diff --git a/py/add_grain.py b/py/add_grain.py old mode 100644 new mode 100755 index b7d4c103..d534325f --- a/py/add_grain.py +++ b/py/add_grain.py @@ -1,50 +1,50 @@ -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" +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" } \ No newline at end of file diff --git a/py/any_rerouter.py b/py/any_rerouter.py old mode 100644 new mode 100755 index 1e80079a..3b5e6c3d --- a/py/any_rerouter.py +++ b/py/any_rerouter.py @@ -1,35 +1,35 @@ -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" +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" } \ No newline at end of file diff --git a/py/batch_selector.py b/py/batch_selector.py old mode 100644 new mode 100755 index 3d9ed66b..2759e889 --- a/py/batch_selector.py +++ b/py/batch_selector.py @@ -1,65 +1,65 @@ -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" +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" } \ No newline at end of file diff --git a/py/blend_if_mask.py b/py/blend_if_mask.py old mode 100644 new mode 100755 index c5eca0b0..6864f56a --- a/py/blend_if_mask.py +++ b/py/blend_if_mask.py @@ -1,104 +1,104 @@ -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" +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" } \ No newline at end of file diff --git a/py/blendmodes.py b/py/blendmodes.py old mode 100644 new mode 100755 index fac31af2..649a3caf --- a/py/blendmodes.py +++ b/py/blendmodes.py @@ -1,324 +1,324 @@ -""" -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 -} +""" +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 +} diff --git a/py/briarmbg.py b/py/briarmbg.py old mode 100644 new mode 100755 index 647bdc0c..c9015a71 --- a/py/briarmbg.py +++ b/py/briarmbg.py @@ -1,455 +1,455 @@ -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] - +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] + diff --git a/py/channel_shake.py b/py/channel_shake.py old mode 100644 new mode 100755 diff --git a/py/check_mask.py b/py/check_mask.py old mode 100644 new mode 100755 index bfefaa66..b44d4493 --- a/py/check_mask.py +++ b/py/check_mask.py @@ -1,53 +1,53 @@ -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" +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" } \ No newline at end of file diff --git a/py/check_mask_v2.py b/py/check_mask_v2.py old mode 100644 new mode 100755 index e9984ddf..a54f7720 --- a/py/check_mask_v2.py +++ b/py/check_mask_v2.py @@ -1,60 +1,60 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_adapter.py b/py/color_adapter.py old mode 100644 new mode 100755 diff --git a/py/color_correct_HSV.py b/py/color_correct_HSV.py old mode 100644 new mode 100755 diff --git a/py/color_correct_LAB.py b/py/color_correct_LAB.py old mode 100644 new mode 100755 diff --git a/py/color_correct_LUTapply.py b/py/color_correct_LUTapply.py old mode 100644 new mode 100755 diff --git a/py/color_correct_RGB.py b/py/color_correct_RGB.py old mode 100644 new mode 100755 diff --git a/py/color_correct_YUV.py b/py/color_correct_YUV.py old mode 100644 new mode 100755 diff --git a/py/color_correct_auto_adjust.py b/py/color_correct_auto_adjust.py old mode 100644 new mode 100755 index 9a0de9cf..e8484f0e --- a/py/color_correct_auto_adjust.py +++ b/py/color_correct_auto_adjust.py @@ -1,115 +1,115 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_auto_adjust_v2.py b/py/color_correct_auto_adjust_v2.py old mode 100644 new mode 100755 index 796f4377..1c12fad1 --- a/py/color_correct_auto_adjust_v2.py +++ b/py/color_correct_auto_adjust_v2.py @@ -1,147 +1,147 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_auto_brightness.py b/py/color_correct_auto_brightness.py old mode 100644 new mode 100755 index 45803387..48900f6b --- a/py/color_correct_auto_brightness.py +++ b/py/color_correct_auto_brightness.py @@ -1,80 +1,80 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_brightness_and_contrast.py b/py/color_correct_brightness_and_contrast.py old mode 100644 new mode 100755 index 1a48e10c..3593c329 --- a/py/color_correct_brightness_and_contrast.py +++ b/py/color_correct_brightness_and_contrast.py @@ -1,114 +1,114 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_color_balance.py b/py/color_correct_color_balance.py old mode 100644 new mode 100755 index 8ae2971f..37634cb9 --- a/py/color_correct_color_balance.py +++ b/py/color_correct_color_balance.py @@ -1,78 +1,78 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_color_temperature.py b/py/color_correct_color_temperature.py old mode 100644 new mode 100755 index 62b6e70b..2b435d1a --- a/py/color_correct_color_temperature.py +++ b/py/color_correct_color_temperature.py @@ -1,61 +1,61 @@ -# 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" +# 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" } \ No newline at end of file diff --git a/py/color_correct_exposure.py b/py/color_correct_exposure.py old mode 100644 new mode 100755 index b51a5f8b..fc63f323 --- a/py/color_correct_exposure.py +++ b/py/color_correct_exposure.py @@ -1,63 +1,63 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_gamma.py b/py/color_correct_gamma.py old mode 100644 new mode 100755 diff --git a/py/color_correct_levels.py b/py/color_correct_levels.py old mode 100644 new mode 100755 index ff91776f..742834e2 --- a/py/color_correct_levels.py +++ b/py/color_correct_levels.py @@ -1,93 +1,93 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_correct_shadow_and_highlight.py b/py/color_correct_shadow_and_highlight.py old mode 100644 new mode 100755 index 0eff8e7d..fec1769b --- a/py/color_correct_shadow_and_highlight.py +++ b/py/color_correct_shadow_and_highlight.py @@ -1,247 +1,247 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_image.py b/py/color_image.py old mode 100644 new mode 100755 diff --git a/py/color_image_v2.py b/py/color_image_v2.py old mode 100644 new mode 100755 index 6d214531..a03c31c5 --- a/py/color_image_v2.py +++ b/py/color_image_v2.py @@ -1,65 +1,65 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_map.py b/py/color_map.py old mode 100644 new mode 100755 index fa674006..8239639e --- a/py/color_map.py +++ b/py/color_map.py @@ -1,57 +1,57 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_name.py b/py/color_name.py old mode 100644 new mode 100755 index c3440d93..679993da --- a/py/color_name.py +++ b/py/color_name.py @@ -1,2481 +1,2481 @@ -# 预定义一组标准颜色名称和对应的 RGB 值 -FLUX_SDXL_NAME_TO_HEX = { - 'coral': '#FA7060', - 'gray': '#ADB0B0', - 'sepia': '#FBD396', - 'buff': '#F7D095', - 'peach': '#FEC6A5', - 'maroon': '#670106', - 'white': '#FCFCFB', - 'may green': '#35D042', - 'cocoa': '#A4411C', - 'carmine': '#F80718', - 'cyan': '#15E2E5', - 'celadon': '#98E9BC', - 'yinmn blue': '#0698E2', - 'indigo': '#001E5C', - 'nickel': '#A8A7A5', - 'dodgerblue': '#0081CE', - 'hot pink': '#FE539F', - 'navy blue': '#001B45', - 'blue': '#08B1E9', - 'sandy brown': '#FAC27E', - 'savoy blue': '#015EA2', - 'tan': '#FDC47B', - 'spring green': '#58FB29', - 'amber': '#F17701', - 'olive green': '#5B6404', - 'plum purple': '#66006F', - 'mulberry': '#BB0967', - 'eggplant': '#860F6E', - 'wisteria purple': '#D28FFA', - 'lemon chiffon': '#FEF672', - 'melon': '#FEB684', - 'yellow orange': '#FEB405', - 'aubergine': '#790055', - 'orange': '#FB7600', - 'amaranth': '#AD0025', - 'bisque': '#FED7AE', - 'ebony': '#05090C', - 'deep pink': '#F9588F', - 'burgundy': '#850018', - 'rust': '#E02801', - 'persimmon': '#F66F03', - 'prussian blue': '#0057C0', - 'brass': '#DD8E13', - 'purple': '#9319C7', - 'blue gray': '#5D9BC0', - 'puce': '#F86985', - 'caribbean green': '#06C56B', - 'burnt sienna': '#FA720B', - 'hunter green': '#197B29', - 'russet': '#BC4A05', - 'khaki': '#C49E6A', - 'lilac': '#E4BFF9', - 'jade': '#04B16F', - 'midnight blue': '#001D4C', - 'slate gray': '#464E53', - 'goldenrod': '#FECB05', - 'charcoal': '#0F1216', - 'silver': '#D1D2D2', - 'teal': '#14C5B0', - 'emerald green': '#00914A', - 'violet': '#A52DD7', - 'powder blue': '#92E5F9', - 'aquamarine': '#63F4ED', - 'claret': '#94001E', - 'honeydew': '#FEE66A', - 'cerulean': '#1BCFE2', - 'lavender': '#DABFF7', - 'cadmium yellow': '#FEDA03', - 'oxblood': '#88000C', - 'gold': '#E5B01D', - 'amethyst purple': '#9214B4', - 'rosegold': '#FEA577', - 'magenta': '#F10B86', - 'venetian red': '#CB0003', - 'ultramarine blue': '#005FC5', - 'mint green': '#9BFCC7', - 'persian green': '#26B84B', - 'tangerine orange': '#FE8403', - 'bronze': '#865C26', - 'onyx': '#1D1F28', - 'black': '#010101', - 'sky blue': '#69E2FC', - 'sea green': '#5AE0B7', - 'mauve': '#D76C77', - 'pacific blue': '#109FD7', - 'blue violet': '#5131E4', - 'royal blue': '#005CD6', - 'saffron': '#FEBC05', - 'yellow': '#FCDE00', - 'umber': '#F06212', - 'pink': '#FEABC7', - 'apricot': '#FEB171', - 'chocolate': '#6B2108', - 'linen': '#F4EFE4', - 'wine': '#930020', - 'plum': '#5D0051', - 'lime green': '#A2FE10', - 'blanched almond': '#FDE4C6', - 'sage': '#9FDDB0', - 'mahogany': '#9D1902', - 'cornflower blue': '#54C4FE', - 'turmeric': '#FEBB00', - 'tyrian purple': '#5E0071', - 'turquoise': '#2BF1DC', - 'taupe': '#DEBB95', - 'carnelian': '#FC3106', - 'blush': '#FEC0C3', - 'alice blue': '#8AE5FA', - 'pullman brown': '#51270B', - 'lapis lazuli': '#0038B0', - 'aureolin': '#FEC132', - 'saddlebrown': '#C76728', - 'orange red': '#FE4207', - 'maize': '#FECD17', - 'cobalt blue': '#0049CA', - 'chestnut': '#C85817', - 'cornsilk': '#FEE66B', - 'royal purple': '#7000B4', - 'copper': '#E7561C', - 'terra cotta': '#FC6029', - 'scarlet': '#D30000', - 'red': '#E10000', - 'cream': '#FCF0D3', - 'vermilion': '#E90001', - 'rebecca purple': '#B63DCD', - 'robin egg blue': '#8AEEEC', - 'vanilla': '#FEEFD0', - 'sienna': '#FC8D29', - 'cerise': '#FE4277', - 'alabaster': '#FAEEDC', - 'baby blue': '#A0EDFE', - 'beige': '#FAE1BB', - 'jazzberry jam': '#F3238A', - 'carnation pink': '#FEAEC3', - 'seafoam': '#A4F6E5', - 'ochre': '#F69402', - 'salmon': '#F97D4D', - 'viridian': '#02AB41', - 'sand': '#FED99E', - 'rufous': '#FE5111', - 'ivory': '#F8F0D3', - 'heliotrope': '#B36FCD', - 'antique white': '#F8F7EE', - 'slate blue': '#0D5984', - 'citrine': '#FEC83C', - 'ash gray': '#9FA5A8', - 'brown': '#99470C', - 'periwinkle': '#96BCFA', - 'green': '#37DF42', - 'caramel': '#FE9F36', - 'yellow green': '#D2FE14', - 'chartreuse': '#B9FE00', - 'crimson': '#C20000', - 'mustard': '#F9B800', - 'lemon yellow': '#FEF01B' -} - - -CSS3_NAMES_TO_HEX = { - "aliceblue": "#f0f8ff", - "antiquewhite": "#faebd7", - "aqua": "#00ffff", - "aquamarine": "#7fffd4", - "azure": "#f0ffff", - "beige": "#f5f5dc", - "bisque": "#ffe4c4", - "black": "#000000", - "blanchedalmond": "#ffebcd", - "blue": "#0000ff", - "blueviolet": "#8a2be2", - "brown": "#a52a2a", - "burlywood": "#deb887", - "cadetblue": "#5f9ea0", - "chartreuse": "#7fff00", - "chocolate": "#d2691e", - "coral": "#ff7f50", - "cornflowerblue": "#6495ed", - "cornsilk": "#fff8dc", - "crimson": "#dc143c", - "cyan": "#00ffff", - "darkblue": "#00008b", - "darkcyan": "#008b8b", - "darkgoldenrod": "#b8860b", - "darkgray": "#a9a9a9", - "darkgrey": "#a9a9a9", - "darkgreen": "#006400", - "darkkhaki": "#bdb76b", - "darkmagenta": "#8b008b", - "darkolivegreen": "#556b2f", - "darkorange": "#ff8c00", - "darkorchid": "#9932cc", - "darkred": "#8b0000", - "darksalmon": "#e9967a", - "darkseagreen": "#8fbc8f", - "darkslateblue": "#483d8b", - "darkslategray": "#2f4f4f", - "darkslategrey": "#2f4f4f", - "darkturquoise": "#00ced1", - "darkviolet": "#9400d3", - "deeppink": "#ff1493", - "deepskyblue": "#00bfff", - "dimgray": "#696969", - "dimgrey": "#696969", - "dodgerblue": "#1e90ff", - "firebrick": "#b22222", - "floralwhite": "#fffaf0", - "forestgreen": "#228b22", - "fuchsia": "#ff00ff", - "gainsboro": "#dcdcdc", - "ghostwhite": "#f8f8ff", - "gold": "#ffd700", - "goldenrod": "#daa520", - "gray": "#808080", - "grey": "#808080", - "green": "#008000", - "greenyellow": "#adff2f", - "honeydew": "#f0fff0", - "hotpink": "#ff69b4", - "indianred": "#cd5c5c", - "indigo": "#4b0082", - "ivory": "#fffff0", - "khaki": "#f0e68c", - "lavender": "#e6e6fa", - "lavenderblush": "#fff0f5", - "lawngreen": "#7cfc00", - "lemonchiffon": "#fffacd", - "lightblue": "#add8e6", - "lightcoral": "#f08080", - "lightcyan": "#e0ffff", - "lightgoldenrodyellow": "#fafad2", - "lightgray": "#d3d3d3", - "lightgrey": "#d3d3d3", - "lightgreen": "#90ee90", - "lightpink": "#ffb6c1", - "lightsalmon": "#ffa07a", - "lightseagreen": "#20b2aa", - "lightskyblue": "#87cefa", - "lightslategray": "#778899", - "lightslategrey": "#778899", - "lightsteelblue": "#b0c4de", - "lightyellow": "#ffffe0", - "lime": "#00ff00", - "limegreen": "#32cd32", - "linen": "#faf0e6", - "magenta": "#ff00ff", - "maroon": "#800000", - "mediumaquamarine": "#66cdaa", - "mediumblue": "#0000cd", - "mediumorchid": "#ba55d3", - "mediumpurple": "#9370db", - "mediumseagreen": "#3cb371", - "mediumslateblue": "#7b68ee", - "mediumspringgreen": "#00fa9a", - "mediumturquoise": "#48d1cc", - "mediumvioletred": "#c71585", - "midnightblue": "#191970", - "mintcream": "#f5fffa", - "mistyrose": "#ffe4e1", - "moccasin": "#ffe4b5", - "navajowhite": "#ffdead", - "navy": "#000080", - "oldlace": "#fdf5e6", - "olive": "#808000", - "olivedrab": "#6b8e23", - "orange": "#ffa500", - "orangered": "#ff4500", - "orchid": "#da70d6", - "palegoldenrod": "#eee8aa", - "palegreen": "#98fb98", - "paleturquoise": "#afeeee", - "palevioletred": "#db7093", - "papayawhip": "#ffefd5", - "peachpuff": "#ffdab9", - "peru": "#cd853f", - "pink": "#ffc0cb", - "plum": "#dda0dd", - "powderblue": "#b0e0e6", - "purple": "#800080", - "red": "#ff0000", - "rosybrown": "#bc8f8f", - "royalblue": "#4169e1", - "saddlebrown": "#8b4513", - "salmon": "#fa8072", - "sandybrown": "#f4a460", - "seagreen": "#2e8b57", - "seashell": "#fff5ee", - "sienna": "#a0522d", - "silver": "#c0c0c0", - "skyblue": "#87ceeb", - "slateblue": "#6a5acd", - "slategray": "#708090", - "slategrey": "#708090", - "snow": "#fffafa", - "springgreen": "#00ff7f", - "steelblue": "#4682b4", - "tan": "#d2b48c", - "teal": "#008080", - "thistle": "#d8bfd8", - "tomato": "#ff6347", - "turquoise": "#40e0d0", - "violet": "#ee82ee", - "wheat": "#f5deb3", - "white": "#ffffff", - "whitesmoke": "#f5f5f5", - "yellow": "#ffff00", - "yellowgreen": "#9acd32" -} - -XKCD_NAME_TO_HEX = { - "cloudy blue": "#acc2d9", - "dark pastel green": "#56ae57", - "dust": "#b2996e", - "electric lime": "#a8ff04", - "fresh green": "#69d84f", - "light eggplant": "#894585", - "nasty green": "#70b23f", - "really light blue": "#d4ffff", - "tea": "#65ab7c", - "warm purple": "#952e8f", - "yellowish tan": "#fcfc81", - "cement": "#a5a391", - "dark grass green": "#388004", - "dusty teal": "#4c9085", - "grey teal": "#5e9b8a", - "macaroni and cheese": "#efb435", - "pinkish tan": "#d99b82", - "spruce": "#0a5f38", - "strong blue": "#0c06f7", - "toxic green": "#61de2a", - "windows blue": "#3778bf", - "blue blue": "#2242c7", - "blue with a hint of purple": "#533cc6", - "booger": "#9bb53c", - "bright sea green": "#05ffa6", - "dark green blue": "#1f6357", - "deep turquoise": "#017374", - "green teal": "#0cb577", - "strong pink": "#ff0789", - "bland": "#afa88b", - "deep aqua": "#08787f", - "lavender pink": "#dd85d7", - "light moss green": "#a6c875", - "light seafoam green": "#a7ffb5", - "olive yellow": "#c2b709", - "pig pink": "#e78ea5", - "deep lilac": "#966ebd", - "desert": "#ccad60", - "dusty lavender": "#ac86a8", - "purpley grey": "#947e94", - "purply": "#983fb2", - "candy pink": "#ff63e9", - "light pastel green": "#b2fba5", - "boring green": "#63b365", - "kiwi green": "#8ee53f", - "light grey green": "#b7e1a1", - "orange pink": "#ff6f52", - "tea green": "#bdf8a3", - "very light brown": "#d3b683", - "egg shell": "#fffcc4", - "eggplant purple": "#430541", - "powder pink": "#ffb2d0", - "reddish grey": "#997570", - "baby shit brown": "#ad900d", - "liliac": "#c48efd", - "stormy blue": "#507b9c", - "ugly brown": "#7d7103", - "custard": "#fffd78", - "darkish pink": "#da467d", - "deep brown": "#410200", - "greenish beige": "#c9d179", - "manilla": "#fffa86", - "off blue": "#5684ae", - "battleship grey": "#6b7c85", - "browny green": "#6f6c0a", - "bruise": "#7e4071", - "kelley green": "#009337", - "sickly yellow": "#d0e429", - "sunny yellow": "#fff917", - "azul": "#1d5dec", - "darkgreen": "#054907", - "green/yellow": "#b5ce08", - "lichen": "#8fb67b", - "light light green": "#c8ffb0", - "pale gold": "#fdde6c", - "sun yellow": "#ffdf22", - "tan green": "#a9be70", - "burple": "#6832e3", - "butterscotch": "#fdb147", - "toupe": "#c7ac7d", - "dark cream": "#fff39a", - "indian red": "#850e04", - "light lavendar": "#efc0fe", - "poison green": "#40fd14", - "baby puke green": "#b6c406", - "bright yellow green": "#9dff00", - "charcoal grey": "#3c4142", - "squash": "#f2ab15", - "cinnamon": "#ac4f06", - "light pea green": "#c4fe82", - "radioactive green": "#2cfa1f", - "raw sienna": "#9a6200", - "baby purple": "#ca9bf7", - "cocoa": "#875f42", - "light royal blue": "#3a2efe", - "orangeish": "#fd8d49", - "rust brown": "#8b3103", - "sand brown": "#cba560", - "swamp": "#698339", - "tealish green": "#0cdc73", - "burnt siena": "#b75203", - "camo": "#7f8f4e", - "dusk blue": "#26538d", - "fern": "#63a950", - "old rose": "#c87f89", - "pale light green": "#b1fc99", - "peachy pink": "#ff9a8a", - "rosy pink": "#f6688e", - "light bluish green": "#76fda8", - "light bright green": "#53fe5c", - "light neon green": "#4efd54", - "light seafoam": "#a0febf", - "tiffany blue": "#7bf2da", - "washed out green": "#bcf5a6", - "browny orange": "#ca6b02", - "nice blue": "#107ab0", - "sapphire": "#2138ab", - "greyish teal": "#719f91", - "orangey yellow": "#fdb915", - "parchment": "#fefcaf", - "straw": "#fcf679", - "very dark brown": "#1d0200", - "terracota": "#cb6843", - "ugly blue": "#31668a", - "clear blue": "#247afd", - "creme": "#ffffb6", - "foam green": "#90fda9", - "grey/green": "#86a17d", - "light gold": "#fddc5c", - "seafoam blue": "#78d1b6", - "topaz": "#13bbaf", - "violet pink": "#fb5ffc", - "wintergreen": "#20f986", - "yellow tan": "#ffe36e", - "dark fuchsia": "#9d0759", - "indigo blue": "#3a18b1", - "light yellowish green": "#c2ff89", - "pale magenta": "#d767ad", - "rich purple": "#720058", - "sunflower yellow": "#ffda03", - "green/blue": "#01c08d", - "leather": "#ac7434", - "racing green": "#014600", - "vivid purple": "#9900fa", - "dark royal blue": "#02066f", - "hazel": "#8e7618", - "muted pink": "#d1768f", - "booger green": "#96b403", - "canary": "#fdff63", - "cool grey": "#95a3a6", - "dark taupe": "#7f684e", - "darkish purple": "#751973", - "true green": "#089404", - "coral pink": "#ff6163", - "dark sage": "#598556", - "dark slate blue": "#214761", - "flat blue": "#3c73a8", - "mushroom": "#ba9e88", - "rich blue": "#021bf9", - "dirty purple": "#734a65", - "greenblue": "#23c48b", - "icky green": "#8fae22", - "light khaki": "#e6f2a2", - "warm blue": "#4b57db", - "dark hot pink": "#d90166", - "deep sea blue": "#015482", - "carmine": "#9d0216", - "dark yellow green": "#728f02", - "pale peach": "#ffe5ad", - "plum purple": "#4e0550", - "golden rod": "#f9bc08", - "neon red": "#ff073a", - "old pink": "#c77986", - "very pale blue": "#d6fffe", - "blood orange": "#fe4b03", - "grapefruit": "#fd5956", - "sand yellow": "#fce166", - "clay brown": "#b2713d", - "dark blue grey": "#1f3b4d", - "flat green": "#699d4c", - "light green blue": "#56fca2", - "warm pink": "#fb5581", - "dodger blue": "#3e82fc", - "gross green": "#a0bf16", - "ice": "#d6fffa", - "metallic blue": "#4f738e", - "pale salmon": "#ffb19a", - "sap green": "#5c8b15", - "algae": "#54ac68", - "bluey grey": "#89a0b0", - "greeny grey": "#7ea07a", - "highlighter green": "#1bfc06", - "light light blue": "#cafffb", - "light mint": "#b6ffbb", - "raw umber": "#a75e09", - "vivid blue": "#152eff", - "deep lavender": "#8d5eb7", - "dull teal": "#5f9e8f", - "light greenish blue": "#63f7b4", - "mud green": "#606602", - "pinky": "#fc86aa", - "red wine": "#8c0034", - "shit green": "#758000", - "tan brown": "#ab7e4c", - "darkblue": "#030764", - "rosa": "#fe86a4", - "lipstick": "#d5174e", - "pale mauve": "#fed0fc", - "claret": "#680018", - "dandelion": "#fedf08", - "orangered": "#fe420f", - "poop green": "#6f7c00", - "ruby": "#ca0147", - "dark": "#1b2431", - "greenish turquoise": "#00fbb0", - "pastel red": "#db5856", - "piss yellow": "#ddd618", - "bright cyan": "#41fdfe", - "dark coral": "#cf524e", - "algae green": "#21c36f", - "darkish red": "#a90308", - "reddy brown": "#6e1005", - "blush pink": "#fe828c", - "camouflage green": "#4b6113", - "lawn green": "#4da409", - "putty": "#beae8a", - "vibrant blue": "#0339f8", - "dark sand": "#a88f59", - "purple/blue": "#5d21d0", - "saffron": "#feb209", - "twilight": "#4e518b", - "warm brown": "#964e02", - "bluegrey": "#85a3b2", - "bubble gum pink": "#ff69af", - "duck egg blue": "#c3fbf4", - "greenish cyan": "#2afeb7", - "petrol": "#005f6a", - "royal": "#0c1793", - "butter": "#ffff81", - "dusty orange": "#f0833a", - "off yellow": "#f1f33f", - "pale olive green": "#b1d27b", - "orangish": "#fc824a", - "leaf": "#71aa34", - "light blue grey": "#b7c9e2", - "dried blood": "#4b0101", - "lightish purple": "#a552e6", - "rusty red": "#af2f0d", - "lavender blue": "#8b88f8", - "light grass green": "#9af764", - "light mint green": "#a6fbb2", - "sunflower": "#ffc512", - "velvet": "#750851", - "brick orange": "#c14a09", - "lightish red": "#fe2f4a", - "pure blue": "#0203e2", - "twilight blue": "#0a437a", - "violet red": "#a50055", - "yellowy brown": "#ae8b0c", - "carnation": "#fd798f", - "muddy yellow": "#bfac05", - "dark seafoam green": "#3eaf76", - "deep rose": "#c74767", - "dusty red": "#b9484e", - "grey/blue": "#647d8e", - "lemon lime": "#bffe28", - "purple/pink": "#d725de", - "brown yellow": "#b29705", - "purple brown": "#673a3f", - "wisteria": "#a87dc2", - "banana yellow": "#fafe4b", - "lipstick red": "#c0022f", - "water blue": "#0e87cc", - "brown grey": "#8d8468", - "vibrant purple": "#ad03de", - "baby green": "#8cff9e", - "barf green": "#94ac02", - "eggshell blue": "#c4fff7", - "sandy yellow": "#fdee73", - "cool green": "#33b864", - "pale": "#fff9d0", - "blue/grey": "#758da3", - "hot magenta": "#f504c9", - "greyblue": "#77a1b5", - "purpley": "#8756e4", - "baby shit green": "#889717", - "brownish pink": "#c27e79", - "dark aquamarine": "#017371", - "diarrhea": "#9f8303", - "light mustard": "#f7d560", - "pale sky blue": "#bdf6fe", - "turtle green": "#75b84f", - "bright olive": "#9cbb04", - "dark grey blue": "#29465b", - "greeny brown": "#696006", - "lemon green": "#adf802", - "light periwinkle": "#c1c6fc", - "seaweed green": "#35ad6b", - "sunshine yellow": "#fffd37", - "ugly purple": "#a442a0", - "medium pink": "#f36196", - "puke brown": "#947706", - "very light pink": "#fff4f2", - "viridian": "#1e9167", - "bile": "#b5c306", - "faded yellow": "#feff7f", - "very pale green": "#cffdbc", - "vibrant green": "#0add08", - "bright lime": "#87fd05", - "spearmint": "#1ef876", - "light aquamarine": "#7bfdc7", - "light sage": "#bcecac", - "yellowgreen": "#bbf90f", - "baby poo": "#ab9004", - "dark seafoam": "#1fb57a", - "deep teal": "#00555a", - "heather": "#a484ac", - "rust orange": "#c45508", - "dirty blue": "#3f829d", - "fern green": "#548d44", - "bright lilac": "#c95efb", - "weird green": "#3ae57f", - "peacock blue": "#016795", - "avocado green": "#87a922", - "faded orange": "#f0944d", - "grape purple": "#5d1451", - "hot green": "#25ff29", - "lime yellow": "#d0fe1d", - "mango": "#ffa62b", - "shamrock": "#01b44c", - "bubblegum": "#ff6cb5", - "purplish brown": "#6b4247", - "vomit yellow": "#c7c10c", - "pale cyan": "#b7fffa", - "key lime": "#aeff6e", - "tomato red": "#ec2d01", - "lightgreen": "#76ff7b", - "merlot": "#730039", - "night blue": "#040348", - "purpleish pink": "#df4ec8", - "apple": "#6ecb3c", - "baby poop green": "#8f9805", - "green apple": "#5edc1f", - "heliotrope": "#d94ff5", - "yellow/green": "#c8fd3d", - "almost black": "#070d0d", - "cool blue": "#4984b8", - "leafy green": "#51b73b", - "mustard brown": "#ac7e04", - "dusk": "#4e5481", - "dull brown": "#876e4b", - "frog green": "#58bc08", - "vivid green": "#2fef10", - "bright light green": "#2dfe54", - "fluro green": "#0aff02", - "kiwi": "#9cef43", - "seaweed": "#18d17b", - "navy green": "#35530a", - "ultramarine blue": "#1805db", - "iris": "#6258c4", - "pastel orange": "#ff964f", - "yellowish orange": "#ffab0f", - "perrywinkle": "#8f8ce7", - "tealish": "#24bca8", - "dark plum": "#3f012c", - "pear": "#cbf85f", - "pinkish orange": "#ff724c", - "midnight purple": "#280137", - "light urple": "#b36ff6", - "dark mint": "#48c072", - "greenish tan": "#bccb7a", - "light burgundy": "#a8415b", - "turquoise blue": "#06b1c4", - "ugly pink": "#cd7584", - "sandy": "#f1da7a", - "electric pink": "#ff0490", - "muted purple": "#805b87", - "mid green": "#50a747", - "greyish": "#a8a495", - "neon yellow": "#cfff04", - "banana": "#ffff7e", - "carnation pink": "#ff7fa7", - "tomato": "#ef4026", - "sea": "#3c9992", - "muddy brown": "#886806", - "turquoise green": "#04f489", - "buff": "#fef69e", - "fawn": "#cfaf7b", - "muted blue": "#3b719f", - "pale rose": "#fdc1c5", - "dark mint green": "#20c073", - "amethyst": "#9b5fc0", - "blue/green": "#0f9b8e", - "chestnut": "#742802", - "sick green": "#9db92c", - "pea": "#a4bf20", - "rusty orange": "#cd5909", - "stone": "#ada587", - "rose red": "#be013c", - "pale aqua": "#b8ffeb", - "deep orange": "#dc4d01", - "earth": "#a2653e", - "mossy green": "#638b27", - "grassy green": "#419c03", - "pale lime green": "#b1ff65", - "light grey blue": "#9dbcd4", - "pale grey": "#fdfdfe", - "asparagus": "#77ab56", - "blueberry": "#464196", - "purple red": "#990147", - "pale lime": "#befd73", - "greenish teal": "#32bf84", - "caramel": "#af6f09", - "deep magenta": "#a0025c", - "light peach": "#ffd8b1", - "milk chocolate": "#7f4e1e", - "ocher": "#bf9b0c", - "off green": "#6ba353", - "purply pink": "#f075e6", - "lightblue": "#7bc8f6", - "dusky blue": "#475f94", - "golden": "#f5bf03", - "light beige": "#fffeb6", - "butter yellow": "#fffd74", - "dusky purple": "#895b7b", - "french blue": "#436bad", - "ugly yellow": "#d0c101", - "greeny yellow": "#c6f808", - "orangish red": "#f43605", - "shamrock green": "#02c14d", - "orangish brown": "#b25f03", - "tree green": "#2a7e19", - "deep violet": "#490648", - "gunmetal": "#536267", - "blue/purple": "#5a06ef", - "cherry": "#cf0234", - "sandy brown": "#c4a661", - "warm grey": "#978a84", - "dark indigo": "#1f0954", - "midnight": "#03012d", - "bluey green": "#2bb179", - "grey pink": "#c3909b", - "soft purple": "#a66fb5", - "blood": "#770001", - "brown red": "#922b05", - "medium grey": "#7d7f7c", - "berry": "#990f4b", - "poo": "#8f7303", - "purpley pink": "#c83cb9", - "light salmon": "#fea993", - "snot": "#acbb0d", - "easter purple": "#c071fe", - "light yellow green": "#ccfd7f", - "dark navy blue": "#00022e", - "drab": "#828344", - "light rose": "#ffc5cb", - "rouge": "#ab1239", - "purplish red": "#b0054b", - "slime green": "#99cc04", - "baby poop": "#937c00", - "irish green": "#019529", - "pink/purple": "#ef1de7", - "dark navy": "#000435", - "greeny blue": "#42b395", - "light plum": "#9d5783", - "pinkish grey": "#c8aca9", - "dirty orange": "#c87606", - "rust red": "#aa2704", - "pale lilac": "#e4cbff", - "orangey red": "#fa4224", - "primary blue": "#0804f9", - "kermit green": "#5cb200", - "brownish purple": "#76424e", - "murky green": "#6c7a0e", - "wheat": "#fbdd7e", - "very dark purple": "#2a0134", - "bottle green": "#044a05", - "watermelon": "#fd4659", - "deep sky blue": "#0d75f8", - "fire engine red": "#fe0002", - "yellow ochre": "#cb9d06", - "pumpkin orange": "#fb7d07", - "pale olive": "#b9cc81", - "light lilac": "#edc8ff", - "lightish green": "#61e160", - "carolina blue": "#8ab8fe", - "mulberry": "#920a4e", - "shocking pink": "#fe02a2", - "auburn": "#9a3001", - "bright lime green": "#65fe08", - "celadon": "#befdb7", - "pinkish brown": "#b17261", - "poo brown": "#885f01", - "bright sky blue": "#02ccfe", - "celery": "#c1fd95", - "dirt brown": "#836539", - "strawberry": "#fb2943", - "dark lime": "#84b701", - "copper": "#b66325", - "medium brown": "#7f5112", - "muted green": "#5fa052", - "robin's egg": "#6dedfd", - "bright aqua": "#0bf9ea", - "bright lavender": "#c760ff", - "ivory": "#ffffcb", - "very light purple": "#f6cefc", - "light navy": "#155084", - "pink red": "#f5054f", - "olive brown": "#645403", - "poop brown": "#7a5901", - "mustard green": "#a8b504", - "ocean green": "#3d9973", - "very dark blue": "#000133", - "dusty green": "#76a973", - "light navy blue": "#2e5a88", - "minty green": "#0bf77d", - "adobe": "#bd6c48", - "barney": "#ac1db8", - "jade green": "#2baf6a", - "bright light blue": "#26f7fd", - "light lime": "#aefd6c", - "dark khaki": "#9b8f55", - "orange yellow": "#ffad01", - "ocre": "#c69c04", - "maize": "#f4d054", - "faded pink": "#de9dac", - "british racing green": "#05480d", - "sandstone": "#c9ae74", - "mud brown": "#60460f", - "light sea green": "#98f6b0", - "robin egg blue": "#8af1fe", - "aqua marine": "#2ee8bb", - "dark sea green": "#11875d", - "soft pink": "#fdb0c0", - "orangey brown": "#b16002", - "cherry red": "#f7022a", - "burnt yellow": "#d5ab09", - "brownish grey": "#86775f", - "camel": "#c69f59", - "purplish grey": "#7a687f", - "marine": "#042e60", - "greyish pink": "#c88d94", - "pale turquoise": "#a5fbd5", - "pastel yellow": "#fffe71", - "bluey purple": "#6241c7", - "canary yellow": "#fffe40", - "faded red": "#d3494e", - "sepia": "#985e2b", - "coffee": "#a6814c", - "bright magenta": "#ff08e8", - "mocha": "#9d7651", - "ecru": "#feffca", - "purpleish": "#98568d", - "cranberry": "#9e003a", - "darkish green": "#287c37", - "brown orange": "#b96902", - "dusky rose": "#ba6873", - "melon": "#ff7855", - "sickly green": "#94b21c", - "silver": "#c5c9c7", - "purply blue": "#661aee", - "purpleish blue": "#6140ef", - "hospital green": "#9be5aa", - "shit brown": "#7b5804", - "mid blue": "#276ab3", - "amber": "#feb308", - "easter green": "#8cfd7e", - "soft blue": "#6488ea", - "cerulean blue": "#056eee", - "golden brown": "#b27a01", - "bright turquoise": "#0ffef9", - "red pink": "#fa2a55", - "red purple": "#820747", - "greyish brown": "#7a6a4f", - "vermillion": "#f4320c", - "russet": "#a13905", - "steel grey": "#6f828a", - "lighter purple": "#a55af4", - "bright violet": "#ad0afd", - "prussian blue": "#004577", - "slate green": "#658d6d", - "dirty pink": "#ca7b80", - "dark blue green": "#005249", - "pine": "#2b5d34", - "yellowy green": "#bff128", - "dark gold": "#b59410", - "bluish": "#2976bb", - "darkish blue": "#014182", - "dull red": "#bb3f3f", - "pinky red": "#fc2647", - "bronze": "#a87900", - "pale teal": "#82cbb2", - "military green": "#667c3e", - "barbie pink": "#fe46a5", - "bubblegum pink": "#fe83cc", - "pea soup green": "#94a617", - "dark mustard": "#a88905", - "shit": "#7f5f00", - "medium purple": "#9e43a2", - "very dark green": "#062e03", - "dirt": "#8a6e45", - "dusky pink": "#cc7a8b", - "red violet": "#9e0168", - "lemon yellow": "#fdff38", - "pistachio": "#c0fa8b", - "dull yellow": "#eedc5b", - "dark lime green": "#7ebd01", - "denim blue": "#3b5b92", - "teal blue": "#01889f", - "lightish blue": "#3d7afd", - "purpley blue": "#5f34e7", - "light indigo": "#6d5acf", - "swamp green": "#748500", - "brown green": "#706c11", - "dark maroon": "#3c0008", - "hot purple": "#cb00f5", - "dark forest green": "#002d04", - "faded blue": "#658cbb", - "drab green": "#749551", - "light lime green": "#b9ff66", - "snot green": "#9dc100", - "yellowish": "#faee66", - "light blue green": "#7efbb3", - "bordeaux": "#7b002c", - "light mauve": "#c292a1", - "ocean": "#017b92", - "marigold": "#fcc006", - "muddy green": "#657432", - "dull orange": "#d8863b", - "steel": "#738595", - "electric purple": "#aa23ff", - "fluorescent green": "#08ff08", - "yellowish brown": "#9b7a01", - "blush": "#f29e8e", - "soft green": "#6fc276", - "bright orange": "#ff5b00", - "lemon": "#fdff52", - "purple grey": "#866f85", - "acid green": "#8ffe09", - "pale lavender": "#eecffe", - "violet blue": "#510ac9", - "light forest green": "#4f9153", - "burnt red": "#9f2305", - "khaki green": "#728639", - "cerise": "#de0c62", - "faded purple": "#916e99", - "apricot": "#ffb16d", - "dark olive green": "#3c4d03", - "grey brown": "#7f7053", - "green grey": "#77926f", - "true blue": "#010fcc", - "pale violet": "#ceaefa", - "periwinkle blue": "#8f99fb", - "light sky blue": "#c6fcff", - "blurple": "#5539cc", - "green brown": "#544e03", - "bluegreen": "#017a79", - "bright teal": "#01f9c6", - "brownish yellow": "#c9b003", - "pea soup": "#929901", - "forest": "#0b5509", - "barney purple": "#a00498", - "ultramarine": "#2000b1", - "purplish": "#94568c", - "puke yellow": "#c2be0e", - "bluish grey": "#748b97", - "dark periwinkle": "#665fd1", - "dark lilac": "#9c6da5", - "reddish": "#c44240", - "light maroon": "#a24857", - "dusty purple": "#825f87", - "terra cotta": "#c9643b", - "avocado": "#90b134", - "marine blue": "#01386a", - "teal green": "#25a36f", - "slate grey": "#59656d", - "lighter green": "#75fd63", - "electric green": "#21fc0d", - "dusty blue": "#5a86ad", - "golden yellow": "#fec615", - "bright yellow": "#fffd01", - "light lavender": "#dfc5fe", - "umber": "#b26400", - "poop": "#7f5e00", - "dark peach": "#de7e5d", - "jungle green": "#048243", - "eggshell": "#ffffd4", - "denim": "#3b638c", - "yellow brown": "#b79400", - "dull purple": "#84597e", - "chocolate brown": "#411900", - "wine red": "#7b0323", - "neon blue": "#04d9ff", - "dirty green": "#667e2c", - "light tan": "#fbeeac", - "ice blue": "#d7fffe", - "cadet blue": "#4e7496", - "dark mauve": "#874c62", - "very light blue": "#d5ffff", - "grey purple": "#826d8c", - "pastel pink": "#ffbacd", - "very light green": "#d1ffbd", - "dark sky blue": "#448ee4", - "evergreen": "#05472a", - "dull pink": "#d5869d", - "aubergine": "#3d0734", - "mahogany": "#4a0100", - "reddish orange": "#f8481c", - "deep green": "#02590f", - "vomit green": "#89a203", - "purple pink": "#e03fd8", - "dusty pink": "#d58a94", - "faded green": "#7bb274", - "camo green": "#526525", - "pinky purple": "#c94cbe", - "pink purple": "#db4bda", - "brownish red": "#9e3623", - "dark rose": "#b5485d", - "mud": "#735c12", - "brownish": "#9c6d57", - "emerald green": "#028f1e", - "pale brown": "#b1916e", - "dull blue": "#49759c", - "burnt umber": "#a0450e", - "medium green": "#39ad48", - "clay": "#b66a50", - "light aqua": "#8cffdb", - "light olive green": "#a4be5c", - "brownish orange": "#cb7723", - "dark aqua": "#05696b", - "purplish pink": "#ce5dae", - "dark salmon": "#c85a53", - "greenish grey": "#96ae8d", - "jade": "#1fa774", - "ugly green": "#7a9703", - "dark beige": "#ac9362", - "emerald": "#01a049", - "pale red": "#d9544d", - "light magenta": "#fa5ff7", - "sky": "#82cafc", - "light cyan": "#acfffc", - "yellow orange": "#fcb001", - "reddish purple": "#910951", - "reddish pink": "#fe2c54", - "orchid": "#c875c4", - "dirty yellow": "#cdc50a", - "orange red": "#fd411e", - "deep red": "#9a0200", - "orange brown": "#be6400", - "cobalt blue": "#030aa7", - "neon pink": "#fe019a", - "rose pink": "#f7879a", - "greyish purple": "#887191", - "raspberry": "#b00149", - "aqua green": "#12e193", - "salmon pink": "#fe7b7c", - "tangerine": "#ff9408", - "brownish green": "#6a6e09", - "red brown": "#8b2e16", - "greenish brown": "#696112", - "pumpkin": "#e17701", - "pine green": "#0a481e", - "charcoal": "#343837", - "baby pink": "#ffb7ce", - "cornflower": "#6a79f7", - "blue violet": "#5d06e9", - "chocolate": "#3d1c02", - "greyish green": "#82a67d", - "scarlet": "#be0119", - "green yellow": "#c9ff27", - "dark olive": "#373e02", - "sienna": "#a9561e", - "pastel purple": "#caa0ff", - "terracotta": "#ca6641", - "aqua blue": "#02d8e9", - "sage green": "#88b378", - "blood red": "#980002", - "deep pink": "#cb0162", - "grass": "#5cac2d", - "moss": "#769958", - "pastel blue": "#a2bffe", - "bluish green": "#10a674", - "green blue": "#06b48b", - "dark tan": "#af884a", - "greenish blue": "#0b8b87", - "pale orange": "#ffa756", - "vomit": "#a2a415", - "forrest green": "#154406", - "dark lavender": "#856798", - "dark violet": "#34013f", - "purple blue": "#632de9", - "dark cyan": "#0a888a", - "olive drab": "#6f7632", - "pinkish": "#d46a7e", - "cobalt": "#1e488f", - "neon purple": "#bc13fe", - "light turquoise": "#7ef4cc", - "apple green": "#76cd26", - "dull green": "#74a662", - "wine": "#80013f", - "powder blue": "#b1d1fc", - "off white": "#ffffe4", - "electric blue": "#0652ff", - "dark turquoise": "#045c5a", - "blue purple": "#5729ce", - "azure": "#069af3", - "bright red": "#ff000d", - "pinkish red": "#f10c45", - "cornflower blue": "#5170d7", - "light olive": "#acbf69", - "grape": "#6c3461", - "greyish blue": "#5e819d", - "purplish blue": "#601ef9", - "yellowish green": "#b0dd16", - "greenish yellow": "#cdfd02", - "medium blue": "#2c6fbb", - "dusty rose": "#c0737a", - "light violet": "#d6b4fc", - "midnight blue": "#020035", - "bluish purple": "#703be7", - "red orange": "#fd3c06", - "dark magenta": "#960056", - "greenish": "#40a368", - "ocean blue": "#03719c", - "coral": "#fc5a50", - "cream": "#ffffc2", - "reddish brown": "#7f2b0a", - "burnt sienna": "#b04e0f", - "brick": "#a03623", - "sage": "#87ae73", - "grey green": "#789b73", - "white": "#ffffff", - "robin's egg blue": "#98eff9", - "moss green": "#658b38", - "steel blue": "#5a7d9a", - "eggplant": "#380835", - "light yellow": "#fffe7a", - "leaf green": "#5ca904", - "light grey": "#d8dcd6", - "puke": "#a5a502", - "pinkish purple": "#d648d7", - "sea blue": "#047495", - "pale purple": "#b790d4", - "slate blue": "#5b7c99", - "blue grey": "#607c8e", - "hunter green": "#0b4008", - "fuchsia": "#ed0dd9", - "crimson": "#8c000f", - "pale yellow": "#ffff84", - "ochre": "#bf9005", - "mustard yellow": "#d2bd0a", - "light red": "#ff474c", - "cerulean": "#0485d1", - "pale pink": "#ffcfdc", - "deep blue": "#040273", - "rust": "#a83c09", - "light teal": "#90e4c1", - "slate": "#516572", - "goldenrod": "#fac205", - "dark yellow": "#d5b60a", - "dark grey": "#363737", - "army green": "#4b5d16", - "grey blue": "#6b8ba4", - "seafoam": "#80f9ad", - "puce": "#a57e52", - "spring green": "#a9f971", - "dark orange": "#c65102", - "sand": "#e2ca76", - "pastel green": "#b0ff9d", - "mint": "#9ffeb0", - "light orange": "#fdaa48", - "bright pink": "#fe01b1", - "chartreuse": "#c1f80a", - "deep purple": "#36013f", - "dark brown": "#341c02", - "taupe": "#b9a281", - "pea green": "#8eab12", - "puke green": "#9aae07", - "kelly green": "#02ab2e", - "seafoam green": "#7af9ab", - "blue green": "#137e6d", - "khaki": "#aaa662", - "burgundy": "#610023", - "dark teal": "#014d4e", - "brick red": "#8f1402", - "royal purple": "#4b006e", - "plum": "#580f41", - "mint green": "#8fff9f", - "gold": "#dbb40c", - "baby blue": "#a2cffe", - "yellow green": "#c0fb2d", - "bright purple": "#be03fd", - "dark red": "#840000", - "pale blue": "#d0fefe", - "grass green": "#3f9b0b", - "navy": "#01153e", - "aquamarine": "#04d8b2", - "burnt orange": "#c04e01", - "neon green": "#0cff0c", - "bright blue": "#0165fc", - "rose": "#cf6275", - "light pink": "#ffd1df", - "mustard": "#ceb301", - "indigo": "#380282", - "lime": "#aaff32", - "sea green": "#53fca1", - "periwinkle": "#8e82fe", - "dark pink": "#cb416b", - "olive green": "#677a04", - "peach": "#ffb07c", - "pale green": "#c7fdb5", - "light brown": "#ad8150", - "hot pink": "#ff028d", - "black": "#000000", - "lilac": "#cea2fd", - "navy blue": "#001146", - "royal blue": "#0504aa", - "beige": "#e6daa6", - "salmon": "#ff796c", - "olive": "#6e750e", - "maroon": "#650021", - "bright green": "#01ff07", - "dark purple": "#35063e", - "mauve": "#ae7181", - "forest green": "#06470c", - "aqua": "#13eac9", - "cyan": "#00ffff", - "tan": "#d1b26f", - "dark blue": "#00035b", - "lavender": "#c79fef", - "turquoise": "#06c2ac", - "dark green": "#033500", - "violet": "#9a0eea", - "light purple": "#bf77f6", - "lime green": "#89fe05", - "grey": "#929591", - "sky blue": "#75bbfd", - "yellow": "#ffff14", - "magenta": "#c20078", - "light green": "#96f97b", - "orange": "#f97306", - "teal": "#029386", - "light blue": "#95d0fc", - "red": "#e50000", - "brown": "#653700", - "pink": "#ff81c0", - "blue": "#0343df", - "green": "#15b01a", - "purple": "#7e1e9c" -} - -HTML4_NAMES_TO_HEX = { - "aqua": "#00ffff", - "black": "#000000", - "blue": "#0000ff", - "fuchsia": "#ff00ff", - "green": "#008000", - "gray": "#808080", - "lime": "#00ff00", - "maroon": "#800000", - "navy": "#000080", - "olive": "#808000", - "purple": "#800080", - "red": "#ff0000", - "silver": "#c0c0c0", - "teal": "#008080", - "white": "#ffffff", - "yellow": "#ffff00" -} - -CSS4_NAME_TO_HEX = { - 'aliceblue': '#F0F8FF', - 'antiquewhite': '#FAEBD7', - 'aqua': '#00FFFF', - 'aquamarine': '#7FFFD4', - 'azure': '#F0FFFF', - 'beige': '#F5F5DC', - 'bisque': '#FFE4C4', - 'black': '#000000', - 'blanchedalmond': '#FFEBCD', - 'blue': '#0000FF', - 'blueviolet': '#8A2BE2', - 'brown': '#A52A2A', - 'burlywood': '#DEB887', - 'cadetblue': '#5F9EA0', - 'chartreuse': '#7FFF00', - 'chocolate': '#D2691E', - 'coral': '#FF7F50', - 'cornflowerblue': '#6495ED', - 'cornsilk': '#FFF8DC', - 'crimson': '#DC143C', - 'cyan': '#00FFFF', - 'darkblue': '#00008B', - 'darkcyan': '#008B8B', - 'darkgoldenrod': '#B8860B', - 'darkgray': '#A9A9A9', - 'darkgreen': '#006400', - 'darkgrey': '#A9A9A9', - 'darkkhaki': '#BDB76B', - 'darkmagenta': '#8B008B', - 'darkolivegreen': '#556B2F', - 'darkorange': '#FF8C00', - 'darkorchid': '#9932CC', - 'darkred': '#8B0000', - 'darksalmon': '#E9967A', - 'darkseagreen': '#8FBC8F', - 'darkslateblue': '#483D8B', - 'darkslategray': '#2F4F4F', - 'darkslategrey': '#2F4F4F', - 'darkturquoise': '#00CED1', - 'darkviolet': '#9400D3', - 'deeppink': '#FF1493', - 'deepskyblue': '#00BFFF', - 'dimgray': '#696969', - 'dimgrey': '#696969', - 'dodgerblue': '#1E90FF', - 'firebrick': '#B22222', - 'floralwhite': '#FFFAF0', - 'forestgreen': '#228B22', - 'fuchsia': '#FF00FF', - 'gainsboro': '#DCDCDC', - 'ghostwhite': '#F8F8FF', - 'gold': '#FFD700', - 'goldenrod': '#DAA520', - 'gray': '#808080', - 'green': '#008000', - 'greenyellow': '#ADFF2F', - 'grey': '#808080', - 'honeydew': '#F0FFF0', - 'hotpink': '#FF69B4', - 'indianred': '#CD5C5C', - 'indigo': '#4B0082', - 'ivory': '#FFFFF0', - 'khaki': '#F0E68C', - 'lavender': '#E6E6FA', - 'lavenderblush': '#FFF0F5', - 'lawngreen': '#7CFC00', - 'lemonchiffon': '#FFFACD', - 'lightblue': '#ADD8E6', - 'lightcoral': '#F08080', - 'lightcyan': '#E0FFFF', - 'lightgoldenrodyellow': '#FAFAD2', - 'lightgray': '#D3D3D3', - 'lightgreen': '#90EE90', - 'lightgrey': '#D3D3D3', - 'lightpink': '#FFB6C1', - 'lightsalmon': '#FFA07A', - 'lightseagreen': '#20B2AA', - 'lightskyblue': '#87CEFA', - 'lightslategray': '#778899', - 'lightslategrey': '#778899', - 'lightsteelblue': '#B0C4DE', - 'lightyellow': '#FFFFE0', - 'lime': '#00FF00', - 'limegreen': '#32CD32', - 'linen': '#FAF0E6', - 'magenta': '#FF00FF', - 'maroon': '#800000', - 'mediumaquamarine': '#66CDAA', - 'mediumblue': '#0000CD', - 'mediumorchid': '#BA55D3', - 'mediumpurple': '#9370DB', - 'mediumseagreen': '#3CB371', - 'mediumslateblue': '#7B68EE', - 'mediumspringgreen': '#00FA9A', - 'mediumturquoise': '#48D1CC', - 'mediumvioletred': '#C71585', - 'midnightblue': '#191970', - 'mintcream': '#F5FFFA', - 'mistyrose': '#FFE4E1', - 'moccasin': '#FFE4B5', - 'navajowhite': '#FFDEAD', - 'navy': '#000080', - 'oldlace': '#FDF5E6', - 'olive': '#808000', - 'olivedrab': '#6B8E23', - 'orange': '#FFA500', - 'orangered': '#FF4500', - 'orchid': '#DA70D6', - 'palegoldenrod': '#EEE8AA', - 'palegreen': '#98FB98', - 'paleturquoise': '#AFEEEE', - 'palevioletred': '#DB7093', - 'papayawhip': '#FFEFD5', - 'peachpuff': '#FFDAB9', - 'peru': '#CD853F', - 'pink': '#FFC0CB', - 'plum': '#DDA0DD', - 'powderblue': '#B0E0E6', - 'purple': '#800080', - 'rebeccapurple': '#663399', - 'red': '#FF0000', - 'rosybrown': '#BC8F8F', - 'royalblue': '#4169E1', - 'saddlebrown': '#8B4513', - 'salmon': '#FA8072', - 'sandybrown': '#F4A460', - 'seagreen': '#2E8B57', - 'seashell': '#FFF5EE', - 'sienna': '#A0522D', - 'silver': '#C0C0C0', - 'skyblue': '#87CEEB', - 'slateblue': '#6A5ACD', - 'slategray': '#708090', - 'slategrey': '#708090', - 'snow': '#FFFAFA', - 'springgreen': '#00FF7F', - 'steelblue': '#4682B4', - 'tan': '#D2B48C', - 'teal': '#008080', - 'thistle': '#D8BFD8', - 'tomato': '#FF6347', - 'turquoise': '#40E0D0', - 'violet': '#EE82EE', - 'wheat': '#F5DEB3', - 'white': '#FFFFFF', - 'whitesmoke': '#F5F5F5', - 'yellow': '#FFFF00', - 'yellowgreen': '#9ACD32' -} - -WIKI_COLOR_NAME_TO_HEX ={ - 'air force blue (raf)': '#5d8aa8', - 'air force blue (usaf)': '#00308f', - 'air superiority blue': '#72a0c1', - 'alabama crimson': '#a32638', - 'alice blue': '#f0f8ff', - 'alizarin crimson': '#e32636', - 'alloy orange': '#c46210', - 'almond': '#efdecd', - 'amaranth': '#e52b50', - 'amber': '#ffbf00', - 'amber (sae/ece)': '#ff7e00', - 'american rose': '#ff033e', - 'amethyst': '#96c', - 'android green': '#a4c639', - 'anti-flash white': '#f2f3f4', - 'antique brass': '#cd9575', - 'antique fuchsia': '#915c83', - 'antique ruby': '#841b2d', - 'antique white': '#faebd7', - 'ao (english)': '#008000', - 'apple green': '#8db600', - 'apricot': '#fbceb1', - 'aqua': '#0ff', - 'aquamarine': '#7fffd4', - 'army green': '#4b5320', - 'arsenic': '#3b444b', - 'arylide yellow': '#e9d66b', - 'ash grey': '#b2beb5', - 'asparagus': '#87a96b', - 'atomic tangerine': '#f96', - 'auburn': '#a52a2a', - 'aureolin': '#fdee00', - 'aurometalsaurus': '#6e7f80', - 'avocado': '#568203', - 'azure': '#007fff', - 'azure mist/web': '#f0ffff', - 'baby blue': '#89cff0', - 'baby blue eyes': '#a1caf1', - 'baby pink': '#f4c2c2', - 'ball blue': '#21abcd', - 'banana mania': '#fae7b5', - 'banana yellow': '#ffe135', - 'barn red': '#7c0a02', - 'battleship grey': '#848482', - 'bazaar': '#98777b', - 'beau blue': '#bcd4e6', - 'beaver': '#9f8170', - 'beige': '#f5f5dc', - 'big dip o’ruby': '#9c2542', - 'bisque': '#ffe4c4', - 'bistre': '#3d2b1f', - 'bittersweet': '#fe6f5e', - 'bittersweet shimmer': '#bf4f51', - 'black': '#000', - 'black bean': '#3d0c02', - 'black leather jacket': '#253529', - 'black olive': '#3b3c36', - 'blanched almond': '#ffebcd', - 'blast-off bronze': '#a57164', - 'bleu de france': '#318ce7', - 'blizzard blue': '#ace5ee', - 'blond': '#faf0be', - 'blue': '#00f', - 'blue bell': '#a2a2d0', - 'blue (crayola)': '#1f75fe', - 'blue gray': '#69c', - 'blue-green': '#0d98ba', - 'blue (munsell)': '#0093af', - 'blue (ncs)': '#0087bd', - 'blue (pigment)': '#339', - 'blue (ryb)': '#0247fe', - 'blue sapphire': '#126180', - 'blue-violet': '#8a2be2', - 'blush': '#de5d83', - 'bole': '#79443b', - 'bondi blue': '#0095b6', - 'bone': '#e3dac9', - 'boston university red': '#c00', - 'bottle green': '#006a4e', - 'boysenberry': '#873260', - 'brandeis blue': '#0070ff', - 'brass': '#b5a642', - 'brick red': '#cb4154', - 'bright cerulean': '#1dacd6', - 'bright green': '#6f0', - 'bright lavender': '#bf94e4', - 'bright maroon': '#c32148', - 'bright pink': '#ff007f', - 'bright turquoise': '#08e8de', - 'bright ube': '#d19fe8', - 'brilliant lavender': '#f4bbff', - 'brilliant rose': '#ff55a3', - 'brink pink': '#fb607f', - 'british racing green': '#004225', - 'bronze': '#cd7f32', - 'brown (traditional)': '#964b00', - 'brown (web)': '#a52a2a', - 'bubble gum': '#ffc1cc', - 'bubbles': '#e7feff', - 'buff': '#f0dc82', - 'bulgarian rose': '#480607', - 'burgundy': '#800020', - 'burlywood': '#deb887', - 'burnt orange': '#c50', - 'burnt sienna': '#e97451', - 'burnt umber': '#8a3324', - 'byzantine': '#bd33a4', - 'byzantium': '#702963', - 'cadet': '#536872', - 'cadet blue': '#5f9ea0', - 'cadet grey': '#91a3b0', - 'cadmium green': '#006b3c', - 'cadmium orange': '#ed872d', - 'cadmium red': '#e30022', - 'cadmium yellow': '#fff600', - 'café au lait': '#a67b5b', - 'café noir': '#4b3621', - 'cal poly green': '#1e4d2b', - 'cambridge blue': '#a3c1ad', - 'camel': '#c19a6b', - 'cameo pink': '#efbbcc', - 'camouflage green': '#78866b', - 'canary yellow': '#ffef00', - 'candy apple red': '#ff0800', - 'candy pink': '#e4717a', - 'capri': '#00bfff', - 'caput mortuum': '#592720', - 'cardinal': '#c41e3a', - 'caribbean green': '#0c9', - 'carmine': '#960018', - 'carmine (m&p)': '#d70040', - 'carmine pink': '#eb4c42', - 'carmine red': '#ff0038', - 'carnation pink': '#ffa6c9', - 'carnelian': '#b31b1b', - 'carolina blue': '#99badd', - 'carrot orange': '#ed9121', - 'catalina blue': '#062a78', - 'ceil': '#92a1cf', - 'celadon': '#ace1af', - 'celadon blue': '#007ba7', - 'celadon green': '#2f847c', - 'celeste (colour)': '#b2ffff', - 'celestial blue': '#4997d0', - 'cerise': '#de3163', - 'cerise pink': '#ec3b83', - 'cerulean': '#007ba7', - 'cerulean blue': '#2a52be', - 'cerulean frost': '#6d9bc3', - 'cg blue': '#007aa5', - 'cg red': '#e03c31', - 'chamoisee': '#a0785a', - 'champagne': '#fad6a5', - 'charcoal': '#36454f', - 'charm pink': '#e68fac', - 'chartreuse (traditional)': '#dfff00', - 'chartreuse (web)': '#7fff00', - 'cherry': '#de3163', - 'cherry blossom pink': '#ffb7c5', - 'chestnut': '#cd5c5c', - 'china pink': '#de6fa1', - 'china rose': '#a8516e', - 'chinese red': '#aa381e', - 'chocolate (traditional)': '#7b3f00', - 'chocolate (web)': '#d2691e', - 'chrome yellow': '#ffa700', - 'cinereous': '#98817b', - 'cinnabar': '#e34234', - 'cinnamon': '#d2691e', - 'citrine': '#e4d00a', - 'classic rose': '#fbcce7', - 'cobalt': '#0047ab', - 'cocoa brown': '#d2691e', - 'coffee': '#6f4e37', - 'columbia blue': '#9bddff', - 'congo pink': '#f88379', - 'cool black': '#002e63', - 'cool grey': '#8c92ac', - 'copper': '#b87333', - 'copper (crayola)': '#da8a67', - 'copper penny': '#ad6f69', - 'copper red': '#cb6d51', - 'copper rose': '#966', - 'coquelicot': '#ff3800', - 'coral': '#ff7f50', - 'coral pink': '#f88379', - 'coral red': '#ff4040', - 'cordovan': '#893f45', - 'corn': '#fbec5d', - 'cornell red': '#b31b1b', - 'cornflower blue': '#6495ed', - 'cornsilk': '#fff8dc', - 'cosmic latte': '#fff8e7', - 'cotton candy': '#ffbcd9', - 'cream': '#fffdd0', - 'crimson': '#dc143c', - 'crimson glory': '#be0032', - 'cyan': '#0ff', - 'cyan (process)': '#00b7eb', - 'daffodil': '#ffff31', - 'dandelion': '#f0e130', - 'dark blue': '#00008b', - 'dark brown': '#654321', - 'dark byzantium': '#5d3954', - 'dark candy apple red': '#a40000', - 'dark cerulean': '#08457e', - 'dark chestnut': '#986960', - 'dark coral': '#cd5b45', - 'dark cyan': '#008b8b', - 'dark electric blue': '#536878', - 'dark goldenrod': '#b8860b', - 'dark gray': '#a9a9a9', - 'dark green': '#013220', - 'dark imperial blue': '#00416a', - 'dark jungle green': '#1a2421', - 'dark khaki': '#bdb76b', - 'dark lava': '#483c32', - 'dark lavender': '#734f96', - 'dark magenta': '#8b008b', - 'dark midnight blue': '#036', - 'dark olive green': '#556b2f', - 'dark orange': '#ff8c00', - 'dark orchid': '#9932cc', - 'dark pastel blue': '#779ecb', - 'dark pastel green': '#03c03c', - 'dark pastel purple': '#966fd6', - 'dark pastel red': '#c23b22', - 'dark pink': '#e75480', - 'dark powder blue': '#039', - 'dark raspberry': '#872657', - 'dark red': '#8b0000', - 'dark salmon': '#e9967a', - 'dark scarlet': '#560319', - 'dark sea green': '#8fbc8f', - 'dark sienna': '#3c1414', - 'dark slate blue': '#483d8b', - 'dark slate gray': '#2f4f4f', - 'dark spring green': '#177245', - 'dark tan': '#918151', - 'dark tangerine': '#ffa812', - 'dark taupe': '#483c32', - 'dark terra cotta': '#cc4e5c', - 'dark turquoise': '#00ced1', - 'dark violet': '#9400d3', - 'dark yellow': '#9b870c', - 'dartmouth green': '#00703c', - "davy's grey": '#555', - 'debian red': '#d70a53', - 'deep carmine': '#a9203e', - 'deep carmine pink': '#ef3038', - 'deep carrot orange': '#e9692c', - 'deep cerise': '#da3287', - 'deep champagne': '#fad6a5', - 'deep chestnut': '#b94e48', - 'deep coffee': '#704241', - 'deep fuchsia': '#c154c1', - 'deep jungle green': '#004b49', - 'deep lilac': '#95b', - 'deep magenta': '#c0c', - 'deep peach': '#ffcba4', - 'deep pink': '#ff1493', - 'deep ruby': '#843f5b', - 'deep saffron': '#f93', - 'deep sky blue': '#00bfff', - 'deep tuscan red': '#66424d', - 'denim': '#1560bd', - 'desert': '#c19a6b', - 'desert sand': '#edc9af', - 'dim gray': '#696969', - 'dodger blue': '#1e90ff', - 'dogwood rose': '#d71868', - 'dollar bill': '#85bb65', - 'drab': '#967117', - 'duke blue': '#00009c', - 'earth yellow': '#e1a95f', - 'ebony': '#555d50', - 'ecru': '#c2b280', - 'eggplant': '#614051', - 'eggshell': '#f0ead6', - 'egyptian blue': '#1034a6', - 'electric blue': '#7df9ff', - 'electric crimson': '#ff003f', - 'electric cyan': '#0ff', - 'electric green': '#0f0', - 'electric indigo': '#6f00ff', - 'electric lavender': '#f4bbff', - 'electric lime': '#cf0', - 'electric purple': '#bf00ff', - 'electric ultramarine': '#3f00ff', - 'electric violet': '#8f00ff', - 'electric yellow': '#ff0', - 'emerald': '#50c878', - 'english lavender': '#b48395', - 'eton blue': '#96c8a2', - 'fallow': '#c19a6b', - 'falu red': '#801818', - 'fandango': '#b53389', - 'fashion fuchsia': '#f400a1', - 'fawn': '#e5aa70', - 'feldgrau': '#4d5d53', - 'fern green': '#4f7942', - 'ferrari red': '#ff2800', - 'field drab': '#6c541e', - 'fire engine red': '#ce2029', - 'firebrick': '#b22222', - 'flame': '#e25822', - 'flamingo pink': '#fc8eac', - 'flavescent': '#f7e98e', - 'flax': '#eedc82', - 'floral white': '#fffaf0', - 'fluorescent orange': '#ffbf00', - 'fluorescent pink': '#ff1493', - 'fluorescent yellow': '#cf0', - 'folly': '#ff004f', - 'forest green (traditional)': '#014421', - 'forest green (web)': '#228b22', - 'french beige': '#a67b5b', - 'french blue': '#0072bb', - 'french lilac': '#86608e', - 'french lime': '#cf0', - 'french raspberry': '#c72c48', - 'french rose': '#f64a8a', - 'fuchsia': '#f0f', - 'fuchsia (crayola)': '#c154c1', - 'fuchsia pink': '#f7f', - 'fuchsia rose': '#c74375', - 'fulvous': '#e48400', - 'fuzzy wuzzy': '#c66', - 'gainsboro': '#dcdcdc', - 'gamboge': '#e49b0f', - 'ghost white': '#f8f8ff', - 'ginger': '#b06500', - 'glaucous': '#6082b6', - 'glitter': '#e6e8fa', - 'gold (metallic)': '#d4af37', - 'gold (web) (golden)': '#ffd700', - 'golden brown': '#996515', - 'golden poppy': '#fcc200', - 'golden yellow': '#ffdf00', - 'goldenrod': '#daa520', - 'granny smith apple': '#a8e4a0', - 'gray': '#808080', - 'gray-asparagus': '#465945', - 'gray (html/css gray)': '#808080', - 'gray (x11 gray)': '#bebebe', - 'green (color wheel) (x11 green)': '#0f0', - 'green (crayola)': '#1cac78', - 'green (html/css green)': '#008000', - 'green (munsell)': '#00a877', - 'green (ncs)': '#009f6b', - 'green (pigment)': '#00a550', - 'green (ryb)': '#66b032', - 'green-yellow': '#adff2f', - 'grullo': '#a99a86', - 'guppie green': '#00ff7f', - 'halayà úbe': '#663854', - 'han blue': '#446ccf', - 'han purple': '#5218fa', - 'hansa yellow': '#e9d66b', - 'harlequin': '#3fff00', - 'harvard crimson': '#c90016', - 'harvest gold': '#da9100', - 'heart gold': '#808000', - 'heliotrope': '#df73ff', - 'hollywood cerise': '#f400a1', - 'honeydew': '#f0fff0', - 'honolulu blue': '#007fbf', - "hooker's green": '#49796b', - 'hot magenta': '#ff1dce', - 'hot pink': '#ff69b4', - 'hunter green': '#355e3b', - 'iceberg': '#71a6d2', - 'icterine': '#fcf75e', - 'imperial blue': '#002395', - 'inchworm': '#b2ec5d', - 'india green': '#138808', - 'indian red': '#cd5c5c', - 'indian yellow': '#e3a857', - 'indigo': '#6f00ff', - 'indigo (dye)': '#00416a', - 'indigo (web)': '#4b0082', - 'international klein blue': '#002fa7', - 'international orange (aerospace)': '#ff4f00', - 'international orange (engineering)': '#ba160c', - 'international orange (golden gate bridge)': '#c0362c', - 'iris': '#5a4fcf', - 'isabelline': '#f4f0ec', - 'islamic green': '#009000', - 'ivory': '#fffff0', - 'jade': '#00a86b', - 'jasmine': '#f8de7e', - 'jasper': '#d73b3e', - 'jazzberry jam': '#a50b5e', - 'jet': '#343434', - 'jonquil': '#fada5e', - 'june bud': '#bdda57', - 'jungle green': '#29ab87', - 'kelly green': '#4cbb17', - 'kenyan copper': '#7c1c05', - 'khaki (html/css) (khaki)': '#c3b091', - 'khaki (x11) (light khaki)': '#f0e68c', - 'ku crimson': '#e8000d', - 'la salle green': '#087830', - 'languid lavender': '#d6cadd', - 'lapis lazuli': '#26619c', - 'laser lemon': '#fefe22', - 'laurel green': '#a9ba9d', - 'lava': '#cf1020', - 'lavender blue': '#ccf', - 'lavender blush': '#fff0f5', - 'lavender (floral)': '#b57edc', - 'lavender gray': '#c4c3d0', - 'lavender indigo': '#9457eb', - 'lavender magenta': '#ee82ee', - 'lavender mist': '#e6e6fa', - 'lavender pink': '#fbaed2', - 'lavender purple': '#967bb6', - 'lavender rose': '#fba0e3', - 'lavender (web)': '#e6e6fa', - 'lawn green': '#7cfc00', - 'lemon': '#fff700', - 'lemon chiffon': '#fffacd', - 'lemon lime': '#e3ff00', - 'licorice': '#1a1110', - 'light apricot': '#fdd5b1', - 'light blue': '#add8e6', - 'light brown': '#b5651d', - 'light carmine pink': '#e66771', - 'light coral': '#f08080', - 'light cornflower blue': '#93ccea', - 'light crimson': '#f56991', - 'light cyan': '#e0ffff', - 'light fuchsia pink': '#f984ef', - 'light goldenrod yellow': '#fafad2', - 'light gray': '#d3d3d3', - 'light green': '#90ee90', - 'light khaki': '#f0e68c', - 'light pastel purple': '#b19cd9', - 'light pink': '#ffb6c1', - 'light red ochre': '#e97451', - 'light salmon': '#ffa07a', - 'light salmon pink': '#f99', - 'light sea green': '#20b2aa', - 'light sky blue': '#87cefa', - 'light slate gray': '#789', - 'light taupe': '#b38b6d', - 'light thulian pink': '#e68fac', - 'light yellow': '#ffffe0', - 'lilac': '#c8a2c8', - 'lime (color wheel)': '#bfff00', - 'lime green': '#32cd32', - 'lime (web) (x11 green)': '#0f0', - 'limerick': '#9dc209', - 'lincoln green': '#195905', - 'linen': '#faf0e6', - 'lion': '#c19a6b', - 'little boy blue': '#6ca0dc', - 'liver': '#534b4f', - 'lust': '#e62020', - 'magenta': '#f0f', - 'magenta (dye)': '#ca1f7b', - 'magenta (process)': '#ff0090', - 'magic mint': '#aaf0d1', - 'magnolia': '#f8f4ff', - 'mahogany': '#c04000', - 'maize': '#fbec5d', - 'majorelle blue': '#6050dc', - 'malachite': '#0bda51', - 'manatee': '#979aaa', - 'mango tango': '#ff8243', - 'mantis': '#74c365', - 'mardi gras': '#880085', - 'maroon (crayola)': '#c32148', - 'maroon (html/css)': '#800000', - 'maroon (x11)': '#b03060', - 'mauve': '#e0b0ff', - 'mauve taupe': '#915f6d', - 'mauvelous': '#ef98aa', - 'maya blue': '#73c2fb', - 'meat brown': '#e5b73b', - 'medium aquamarine': '#6da', - 'medium blue': '#0000cd', - 'medium candy apple red': '#e2062c', - 'medium carmine': '#af4035', - 'medium champagne': '#f3e5ab', - 'medium electric blue': '#035096', - 'medium jungle green': '#1c352d', - 'medium lavender magenta': '#dda0dd', - 'medium orchid': '#ba55d3', - 'medium persian blue': '#0067a5', - 'medium purple': '#9370db', - 'medium red-violet': '#bb3385', - 'medium ruby': '#aa4069', - 'medium sea green': '#3cb371', - 'medium slate blue': '#7b68ee', - 'medium spring bud': '#c9dc87', - 'medium spring green': '#00fa9a', - 'medium taupe': '#674c47', - 'medium turquoise': '#48d1cc', - 'medium tuscan red': '#79443b', - 'medium vermilion': '#d9603b', - 'medium violet-red': '#c71585', - 'mellow apricot': '#f8b878', - 'mellow yellow': '#f8de7e', - 'melon': '#fdbcb4', - 'midnight blue': '#191970', - 'midnight green (eagle green)': '#004953', - 'mikado yellow': '#ffc40c', - 'mint': '#3eb489', - 'mint cream': '#f5fffa', - 'mint green': '#98ff98', - 'misty rose': '#ffe4e1', - 'moccasin': '#faebd7', - 'mode beige': '#967117', - 'moonstone blue': '#73a9c2', - 'mordant red 19': '#ae0c00', - 'moss green': '#addfad', - 'mountain meadow': '#30ba8f', - 'mountbatten pink': '#997a8d', - 'msu green': '#18453b', - 'mulberry': '#c54b8c', - 'mustard': '#ffdb58', - 'myrtle': '#21421e', - 'nadeshiko pink': '#f6adc6', - 'napier green': '#2a8000', - 'naples yellow': '#fada5e', - 'navajo white': '#ffdead', - 'navy blue': '#000080', - 'neon carrot': '#ffa343', - 'neon fuchsia': '#fe4164', - 'neon green': '#39ff14', - 'new york pink': '#d7837f', - 'non-photo blue': '#a4dded', - 'north texas green': '#059033', - 'ocean boat blue': '#0077be', - 'ochre': '#c72', - 'office green': '#008000', - 'old gold': '#cfb53b', - 'old lace': '#fdf5e6', - 'old lavender': '#796878', - 'old mauve': '#673147', - 'old rose': '#c08081', - 'olive': '#808000', - 'olive drab #7': '#3c341f', - 'olive drab (web) (olive drab #3)': '#6b8e23', - 'olivine': '#9ab973', - 'onyx': '#353839', - 'opera mauve': '#b784a7', - 'orange (color wheel)': '#ff7f00', - 'orange peel': '#ff9f00', - 'orange-red': '#ff4500', - 'orange (ryb)': '#fb9902', - 'orange (web color)': '#ffa500', - 'orchid': '#da70d6', - 'otter brown': '#654321', - 'ou crimson red': '#900', - 'outer space': '#414a4c', - 'outrageous orange': '#ff6e4a', - 'oxford blue': '#002147', - 'pakistan green': '#060', - 'palatinate blue': '#273be2', - 'palatinate purple': '#682860', - 'pale aqua': '#bcd4e6', - 'pale blue': '#afeeee', - 'pale brown': '#987654', - 'pale carmine': '#af4035', - 'pale cerulean': '#9bc4e2', - 'pale chestnut': '#ddadaf', - 'pale copper': '#da8a67', - 'pale cornflower blue': '#abcdef', - 'pale gold': '#e6be8a', - 'pale goldenrod': '#eee8aa', - 'pale green': '#98fb98', - 'pale lavender': '#dcd0ff', - 'pale magenta': '#f984e5', - 'pale pink': '#fadadd', - 'pale plum': '#dda0dd', - 'pale red-violet': '#db7093', - 'pale robin egg blue': '#96ded1', - 'pale silver': '#c9c0bb', - 'pale spring bud': '#ecebbd', - 'pale taupe': '#bc987e', - 'pale violet-red': '#db7093', - 'pansy purple': '#78184a', - 'papaya whip': '#ffefd5', - 'paris green': '#50c878', - 'pastel blue': '#aec6cf', - 'pastel brown': '#836953', - 'pastel gray': '#cfcfc4', - 'pastel green': '#7d7', - 'pastel magenta': '#f49ac2', - 'pastel orange': '#ffb347', - 'pastel pink': '#dea5a4', - 'pastel purple': '#b39eb5', - 'pastel red': '#ff6961', - 'pastel violet': '#cb99c9', - 'pastel yellow': '#fdfd96', - 'patriarch': '#800080', - "payne's grey": '#536878', - 'peach': '#ffe5b4', - 'peach (crayola)': '#ffcba4', - 'peach-orange': '#fc9', - 'peach puff': '#ffdab9', - 'peach-yellow': '#fadfad', - 'pear': '#d1e231', - 'pearl': '#eae0c8', - 'pearl aqua': '#88d8c0', - 'pearly purple': '#b768a2', - 'peridot': '#e6e200', - 'periwinkle': '#ccf', - 'persian blue': '#1c39bb', - 'persian green': '#00a693', - 'persian indigo': '#32127a', - 'persian orange': '#d99058', - 'persian pink': '#f77fbe', - 'persian plum': '#701c1c', - 'persian red': '#c33', - 'persian rose': '#fe28a2', - 'persimmon': '#ec5800', - 'peru': '#cd853f', - 'phlox': '#df00ff', - 'phthalo blue': '#000f89', - 'phthalo green': '#123524', - 'piggy pink': '#fddde6', - 'pine green': '#01796f', - 'pink': '#ffc0cb', - 'pink lace': '#ffddf4', - 'pink-orange': '#f96', - 'pink pearl': '#e7accf', - 'pink sherbet': '#f78fa7', - 'pistachio': '#93c572', - 'platinum': '#e5e4e2', - 'plum (traditional)': '#8e4585', - 'plum (web)': '#dda0dd', - 'portland orange': '#ff5a36', - 'powder blue (web)': '#b0e0e6', - 'princeton orange': '#ff8f00', - 'prune': '#701c1c', - 'prussian blue': '#003153', - 'psychedelic purple': '#df00ff', - 'puce': '#c89', - 'pumpkin': '#ff7518', - 'purple heart': '#69359c', - 'purple (html/css)': '#800080', - 'purple mountain majesty': '#9678b6', - 'purple (munsell)': '#9f00c5', - 'purple pizzazz': '#fe4eda', - 'purple taupe': '#50404d', - 'purple (x11)': '#a020f0', - 'quartz': '#51484f', - 'rackley': '#5d8aa8', - 'radical red': '#ff355e', - 'rajah': '#fbab60', - 'raspberry': '#e30b5d', - 'raspberry glace': '#915f6d', - 'raspberry pink': '#e25098', - 'raspberry rose': '#b3446c', - 'raw umber': '#826644', - 'razzle dazzle rose': '#f3c', - 'razzmatazz': '#e3256b', - 'red': '#f00', - 'red-brown': '#a52a2a', - 'red devil': '#860111', - 'red (munsell)': '#f2003c', - 'red (ncs)': '#c40233', - 'red-orange': '#ff5349', - 'red (pigment)': '#ed1c24', - 'red (ryb)': '#fe2712', - 'red-violet': '#c71585', - 'redwood': '#ab4e52', - 'regalia': '#522d80', - 'resolution blue': '#002387', - 'rich black': '#004040', - 'rich brilliant lavender': '#f1a7fe', - 'rich carmine': '#d70040', - 'rich electric blue': '#0892d0', - 'rich lavender': '#a76bcf', - 'rich lilac': '#b666d2', - 'rich maroon': '#b03060', - 'rifle green': '#414833', - 'robin egg blue': '#0cc', - 'rose': '#ff007f', - 'rose bonbon': '#f9429e', - 'rose ebony': '#674846', - 'rose gold': '#b76e79', - 'rose madder': '#e32636', - 'rose pink': '#f6c', - 'rose quartz': '#aa98a9', - 'rose taupe': '#905d5d', - 'rose vale': '#ab4e52', - 'rosewood': '#65000b', - 'rosso corsa': '#d40000', - 'rosy brown': '#bc8f8f', - 'royal azure': '#0038a8', - 'royal blue (traditional)': '#002366', - 'royal blue (web)': '#4169e1', - 'royal fuchsia': '#ca2c92', - 'royal purple': '#7851a9', - 'royal yellow': '#fada5e', - 'rubine red': '#d10056', - 'ruby': '#e0115f', - 'ruby red': '#9b111e', - 'ruddy': '#ff0028', - 'ruddy brown': '#bb6528', - 'ruddy pink': '#e18e96', - 'rufous': '#a81c07', - 'russet': '#80461b', - 'rust': '#b7410e', - 'rusty red': '#da2c43', - 'sacramento state green': '#00563f', - 'saddle brown': '#8b4513', - 'safety orange (blaze orange)': '#ff6700', - 'saffron': '#f4c430', - 'salmon': '#ff8c69', - 'salmon pink': '#ff91a4', - 'sand': '#c2b280', - 'sand dune': '#967117', - 'sandstorm': '#ecd540', - 'sandy brown': '#f4a460', - 'sandy taupe': '#967117', - 'sangria': '#92000a', - 'sap green': '#507d2a', - 'sapphire': '#0f52ba', - 'sapphire blue': '#0067a5', - 'satin sheen gold': '#cba135', - 'scarlet': '#ff2400', - 'scarlet (crayola)': '#fd0e35', - 'school bus yellow': '#ffd800', - "screamin' green": '#76ff7a', - 'sea blue': '#006994', - 'sea green': '#2e8b57', - 'seal brown': '#321414', - 'seashell': '#fff5ee', - 'selective yellow': '#ffba00', - 'sepia': '#704214', - 'shadow': '#8a795d', - 'shamrock green': '#009e60', - 'shocking pink': '#fc0fc0', - 'shocking pink (crayola)': '#ff6fff', - 'sienna': '#882d17', - 'silver': '#c0c0c0', - 'sinopia': '#cb410b', - 'skobeloff': '#007474', - 'sky blue': '#87ceeb', - 'sky magenta': '#cf71af', - 'slate blue': '#6a5acd', - 'slate gray': '#708090', - 'smalt (dark powder blue)': '#039', - 'smokey topaz': '#933d41', - 'smoky black': '#100c08', - 'snow': '#fffafa', - 'spiro disco ball': '#0fc0fc', - 'spring bud': '#a7fc00', - 'spring green': '#00ff7f', - "st. patrick's blue": '#23297a', - 'steel blue': '#4682b4', - 'stil de grain yellow': '#fada5e', - 'stizza': '#900', - 'stormcloud': '#4f666a', - 'straw': '#e4d96f', - 'sunglow': '#fc3', - 'sunset': '#fad6a5', - 'tan': '#d2b48c', - 'tangelo': '#f94d00', - 'tangerine': '#f28500', - 'tangerine yellow': '#fc0', - 'tango pink': '#e4717a', - 'taupe': '#483c32', - 'taupe gray': '#8b8589', - 'tea green': '#d0f0c0', - 'tea rose (orange)': '#f88379', - 'tea rose (rose)': '#f4c2c2', - 'teal': '#008080', - 'teal blue': '#367588', - 'teal green': '#00827f', - 'telemagenta': '#cf3476', - 'tenné (tawny)': '#cd5700', - 'terra cotta': '#e2725b', - 'thistle': '#d8bfd8', - 'thulian pink': '#de6fa1', - 'tickle me pink': '#fc89ac', - 'tiffany blue': '#0abab5', - "tiger's eye": '#e08d3c', - 'timberwolf': '#dbd7d2', - 'titanium yellow': '#eee600', - 'tomato': '#ff6347', - 'toolbox': '#746cc0', - 'topaz': '#ffc87c', - 'tractor red': '#fd0e35', - 'trolley grey': '#808080', - 'tropical rain forest': '#00755e', - 'true blue': '#0073cf', - 'tufts blue': '#417dc1', - 'tumbleweed': '#deaa88', - 'turkish rose': '#b57281', - 'turquoise': '#30d5c8', - 'turquoise blue': '#00ffef', - 'turquoise green': '#a0d6b4', - 'tuscan red': '#7c4848', - 'twilight lavender': '#8a496b', - 'tyrian purple': '#66023c', - 'ua blue': '#03a', - 'ua red': '#d9004c', - 'ube': '#8878c3', - 'ucla blue': '#536895', - 'ucla gold': '#ffb300', - 'ufo green': '#3cd070', - 'ultra pink': '#ff6fff', - 'ultramarine': '#120a8f', - 'ultramarine blue': '#4166f5', - 'umber': '#635147', - 'unbleached silk': '#ffddca', - 'united nations blue': '#5b92e5', - 'university of california gold': '#b78727', - 'unmellow yellow': '#ff6', - 'up forest green': '#014421', - 'up maroon': '#7b1113', - 'upsdell red': '#ae2029', - 'urobilin': '#e1ad21', - 'usafa blue': '#004f98', - 'usc cardinal': '#900', - 'usc gold': '#fc0', - 'utah crimson': '#d3003f', - 'vanilla': '#f3e5ab', - 'vegas gold': '#c5b358', - 'venetian red': '#c80815', - 'verdigris': '#43b3ae', - 'vermilion (cinnabar)': '#e34234', - 'vermilion (plochere)': '#d9603b', - 'veronica': '#a020f0', - 'violet': '#8f00ff', - 'violet-blue': '#324ab2', - 'violet (color wheel)': '#7f00ff', - 'violet (ryb)': '#8601af', - 'violet (web)': '#ee82ee', - 'viridian': '#40826d', - 'vivid auburn': '#922724', - 'vivid burgundy': '#9f1d35', - 'vivid cerise': '#da1d81', - 'vivid tangerine': '#ffa089', - 'vivid violet': '#9f00ff', - 'warm black': '#004242', - 'waterspout': '#a4f4f9', - 'wenge': '#645452', - 'wheat': '#f5deb3', - 'white': '#fff', - 'white smoke': '#f5f5f5', - 'wild blue yonder': '#a2add0', - 'wild strawberry': '#ff43a4', - 'wild watermelon': '#fc6c85', - 'wine': '#722f37', - 'wine dregs': '#673147', - 'wisteria': '#c9a0dc', - 'wood brown': '#c19a6b', - 'xanadu': '#738678', - 'yale blue': '#0f4d92', - 'yellow': '#ff0', - 'yellow-green': '#9acd32', - 'yellow (munsell)': '#efcc00', - 'yellow (ncs)': '#ffd300', - 'yellow orange': '#ffae42', - 'yellow (process)': '#ffef00', - 'yellow (ryb)': '#fefe33', - 'zaffre': '#0014a8', - 'zinnwaldite brown': '#2c1608' -} - -palettes = { - 'xkcd':XKCD_NAME_TO_HEX, - 'wiki_color': WIKI_COLOR_NAME_TO_HEX, - 'flux_sdxl': FLUX_SDXL_NAME_TO_HEX, - 'css4':CSS4_NAME_TO_HEX, - 'css3':CSS3_NAMES_TO_HEX, - 'html4':HTML4_NAMES_TO_HEX -} - -import torch -import re -from PIL import Image -from .imagefunc import Hex_to_RGB, AnyType, pil2tensor, tensor2pil, log, load_custom_size, find_best_match_by_similarity - -any = AnyType("*") - - -class LS_ColorName: - - def __init__(self): - self.NODE_NAME = 'ColorName' - - @classmethod - def INPUT_TYPES(self): - return { - "required": { - "color": ("STRING", {"default": "#000000", "forceInput":False},), - "palette": (list(palettes.keys()),), - }, - "optional": { - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("color_name",) - FUNCTION = 'get_color_name' - CATEGORY = '😺dzNodes/LayerColor' - - def get_color_name(self, color, palette): - - (r, g, b) = Hex_to_RGB(color) - - if palette == "flux_sdxl": - color_table = FLUX_SDXL_NAME_TO_HEX - elif palette == "wiki_color": - color_table = WIKI_COLOR_NAME_TO_HEX - elif palette == "xkcd": - color_table = XKCD_NAME_TO_HEX - elif palette == "css4": - color_table = CSS4_NAME_TO_HEX - elif palette == "css3": - color_table = CSS3_NAMES_TO_HEX - else: - color_table = HTML4_NAMES_TO_HEX - - min_colors = {} - for name, hex_code in color_table.items(): - r_c, g_c, b_c = Hex_to_RGB(hex_code) - rd = (r_c - r) ** 2 - gd = (g_c - g) ** 2 - bd = (b_c - b) ** 2 - min_colors[(rd + gd + bd)] = name - color_name = min_colors[min(min_colors.keys())] - - return (color_name,) - - -class LS_NameToColor: - - def __init__(self): - self.NODE_NAME = 'NameToColor' - - @classmethod - def INPUT_TYPES(self): - size_list = ['custom'] - size_list.extend(load_custom_size()) - return { - "required": { - "color_name": ("STRING", {"default": "white", "forceInput":False},), - "palette": (list(palettes.keys()),), - "in_palette_only": ("BOOLEAN", {"default": False}), # 仅在当前颜色表中查找 - "default_color": ("STRING", {"default": "#000000", "forceInput": False},), - "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}), - }, - "optional": { - "size_as": (any, {}), - } - } - - RETURN_TYPES = ("IMAGE", "STRING",) - RETURN_NAMES = ("image", "color",) - FUNCTION = 'name2color' - CATEGORY = '😺dzNodes/LayerColor' - - def name2color(self, color_name, palette, in_palette_only, default_color, size, custom_width, custom_height, size_as=None): - - if palette == "flux_sdxl": - color_table = FLUX_SDXL_NAME_TO_HEX - elif palette == "wiki_color": - color_table = WIKI_COLOR_NAME_TO_HEX - elif palette == "xkcd": - color_table = XKCD_NAME_TO_HEX - elif palette == "css4": - color_table = CSS4_NAME_TO_HEX - elif palette == "css3": - color_table = CSS3_NAMES_TO_HEX - else: - color_table = HTML4_NAMES_TO_HEX - - 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 - - color_name = color_name.lower() - print(f"color_name={color_name}") - - ret_color = "" - try: - ret_color = color_table[color_name] - except KeyError: - if not in_palette_only: - for table_name, table in palettes.items(): - try: - ret_color = table[color_name] - break - except KeyError: - pass - if ret_color != "": - log(f'{self.NODE_NAME}: "{color_name}" not in current color table, find it in "{table_name}".') - else: # 在全部色表中寻找最近似名称 - match_keys = {} - for table_name, table in palettes.items(): - match_key = find_best_match_by_similarity(color_name, list(table.keys())) - if match_key is not None: - match_keys[match_key] = table_name - - if match_keys == {}: - log(f'{self.NODE_NAME}: "{color_name}" not in color tables and not find any approximation, return default color.') - ret_color = default_color - else: - print(f"finded {len(match_keys)} keys:{match_keys}") - match_key = find_best_match_by_similarity(color_name, list(match_keys.keys())) - log(f'{self.NODE_NAME}: "{color_name}" not in color tables, return the approximation "{match_key}" in "{match_keys[match_key]}".') - ret_color = palettes[match_keys[match_key]][match_key] - - else: - log(f'{self.NODE_NAME}: "{color_name}" not in current color table, return default color.') - ret_color = default_color - - ret_image = Image.new('RGB', (width, height), color=ret_color) - - return (pil2tensor(ret_image), ret_color,) - - -NODE_CLASS_MAPPINGS = { - "LayerUtility: ColorName": LS_ColorName, - "LayerUtility: NameToColor": LS_NameToColor, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: ColorName": "LayerUtility: Color Name", - "LayerUtility: NameToColor": "LayerUtility: Name To Color", +# 预定义一组标准颜色名称和对应的 RGB 值 +FLUX_SDXL_NAME_TO_HEX = { + 'coral': '#FA7060', + 'gray': '#ADB0B0', + 'sepia': '#FBD396', + 'buff': '#F7D095', + 'peach': '#FEC6A5', + 'maroon': '#670106', + 'white': '#FCFCFB', + 'may green': '#35D042', + 'cocoa': '#A4411C', + 'carmine': '#F80718', + 'cyan': '#15E2E5', + 'celadon': '#98E9BC', + 'yinmn blue': '#0698E2', + 'indigo': '#001E5C', + 'nickel': '#A8A7A5', + 'dodgerblue': '#0081CE', + 'hot pink': '#FE539F', + 'navy blue': '#001B45', + 'blue': '#08B1E9', + 'sandy brown': '#FAC27E', + 'savoy blue': '#015EA2', + 'tan': '#FDC47B', + 'spring green': '#58FB29', + 'amber': '#F17701', + 'olive green': '#5B6404', + 'plum purple': '#66006F', + 'mulberry': '#BB0967', + 'eggplant': '#860F6E', + 'wisteria purple': '#D28FFA', + 'lemon chiffon': '#FEF672', + 'melon': '#FEB684', + 'yellow orange': '#FEB405', + 'aubergine': '#790055', + 'orange': '#FB7600', + 'amaranth': '#AD0025', + 'bisque': '#FED7AE', + 'ebony': '#05090C', + 'deep pink': '#F9588F', + 'burgundy': '#850018', + 'rust': '#E02801', + 'persimmon': '#F66F03', + 'prussian blue': '#0057C0', + 'brass': '#DD8E13', + 'purple': '#9319C7', + 'blue gray': '#5D9BC0', + 'puce': '#F86985', + 'caribbean green': '#06C56B', + 'burnt sienna': '#FA720B', + 'hunter green': '#197B29', + 'russet': '#BC4A05', + 'khaki': '#C49E6A', + 'lilac': '#E4BFF9', + 'jade': '#04B16F', + 'midnight blue': '#001D4C', + 'slate gray': '#464E53', + 'goldenrod': '#FECB05', + 'charcoal': '#0F1216', + 'silver': '#D1D2D2', + 'teal': '#14C5B0', + 'emerald green': '#00914A', + 'violet': '#A52DD7', + 'powder blue': '#92E5F9', + 'aquamarine': '#63F4ED', + 'claret': '#94001E', + 'honeydew': '#FEE66A', + 'cerulean': '#1BCFE2', + 'lavender': '#DABFF7', + 'cadmium yellow': '#FEDA03', + 'oxblood': '#88000C', + 'gold': '#E5B01D', + 'amethyst purple': '#9214B4', + 'rosegold': '#FEA577', + 'magenta': '#F10B86', + 'venetian red': '#CB0003', + 'ultramarine blue': '#005FC5', + 'mint green': '#9BFCC7', + 'persian green': '#26B84B', + 'tangerine orange': '#FE8403', + 'bronze': '#865C26', + 'onyx': '#1D1F28', + 'black': '#010101', + 'sky blue': '#69E2FC', + 'sea green': '#5AE0B7', + 'mauve': '#D76C77', + 'pacific blue': '#109FD7', + 'blue violet': '#5131E4', + 'royal blue': '#005CD6', + 'saffron': '#FEBC05', + 'yellow': '#FCDE00', + 'umber': '#F06212', + 'pink': '#FEABC7', + 'apricot': '#FEB171', + 'chocolate': '#6B2108', + 'linen': '#F4EFE4', + 'wine': '#930020', + 'plum': '#5D0051', + 'lime green': '#A2FE10', + 'blanched almond': '#FDE4C6', + 'sage': '#9FDDB0', + 'mahogany': '#9D1902', + 'cornflower blue': '#54C4FE', + 'turmeric': '#FEBB00', + 'tyrian purple': '#5E0071', + 'turquoise': '#2BF1DC', + 'taupe': '#DEBB95', + 'carnelian': '#FC3106', + 'blush': '#FEC0C3', + 'alice blue': '#8AE5FA', + 'pullman brown': '#51270B', + 'lapis lazuli': '#0038B0', + 'aureolin': '#FEC132', + 'saddlebrown': '#C76728', + 'orange red': '#FE4207', + 'maize': '#FECD17', + 'cobalt blue': '#0049CA', + 'chestnut': '#C85817', + 'cornsilk': '#FEE66B', + 'royal purple': '#7000B4', + 'copper': '#E7561C', + 'terra cotta': '#FC6029', + 'scarlet': '#D30000', + 'red': '#E10000', + 'cream': '#FCF0D3', + 'vermilion': '#E90001', + 'rebecca purple': '#B63DCD', + 'robin egg blue': '#8AEEEC', + 'vanilla': '#FEEFD0', + 'sienna': '#FC8D29', + 'cerise': '#FE4277', + 'alabaster': '#FAEEDC', + 'baby blue': '#A0EDFE', + 'beige': '#FAE1BB', + 'jazzberry jam': '#F3238A', + 'carnation pink': '#FEAEC3', + 'seafoam': '#A4F6E5', + 'ochre': '#F69402', + 'salmon': '#F97D4D', + 'viridian': '#02AB41', + 'sand': '#FED99E', + 'rufous': '#FE5111', + 'ivory': '#F8F0D3', + 'heliotrope': '#B36FCD', + 'antique white': '#F8F7EE', + 'slate blue': '#0D5984', + 'citrine': '#FEC83C', + 'ash gray': '#9FA5A8', + 'brown': '#99470C', + 'periwinkle': '#96BCFA', + 'green': '#37DF42', + 'caramel': '#FE9F36', + 'yellow green': '#D2FE14', + 'chartreuse': '#B9FE00', + 'crimson': '#C20000', + 'mustard': '#F9B800', + 'lemon yellow': '#FEF01B' +} + + +CSS3_NAMES_TO_HEX = { + "aliceblue": "#f0f8ff", + "antiquewhite": "#faebd7", + "aqua": "#00ffff", + "aquamarine": "#7fffd4", + "azure": "#f0ffff", + "beige": "#f5f5dc", + "bisque": "#ffe4c4", + "black": "#000000", + "blanchedalmond": "#ffebcd", + "blue": "#0000ff", + "blueviolet": "#8a2be2", + "brown": "#a52a2a", + "burlywood": "#deb887", + "cadetblue": "#5f9ea0", + "chartreuse": "#7fff00", + "chocolate": "#d2691e", + "coral": "#ff7f50", + "cornflowerblue": "#6495ed", + "cornsilk": "#fff8dc", + "crimson": "#dc143c", + "cyan": "#00ffff", + "darkblue": "#00008b", + "darkcyan": "#008b8b", + "darkgoldenrod": "#b8860b", + "darkgray": "#a9a9a9", + "darkgrey": "#a9a9a9", + "darkgreen": "#006400", + "darkkhaki": "#bdb76b", + "darkmagenta": "#8b008b", + "darkolivegreen": "#556b2f", + "darkorange": "#ff8c00", + "darkorchid": "#9932cc", + "darkred": "#8b0000", + "darksalmon": "#e9967a", + "darkseagreen": "#8fbc8f", + "darkslateblue": "#483d8b", + "darkslategray": "#2f4f4f", + "darkslategrey": "#2f4f4f", + "darkturquoise": "#00ced1", + "darkviolet": "#9400d3", + "deeppink": "#ff1493", + "deepskyblue": "#00bfff", + "dimgray": "#696969", + "dimgrey": "#696969", + "dodgerblue": "#1e90ff", + "firebrick": "#b22222", + "floralwhite": "#fffaf0", + "forestgreen": "#228b22", + "fuchsia": "#ff00ff", + "gainsboro": "#dcdcdc", + "ghostwhite": "#f8f8ff", + "gold": "#ffd700", + "goldenrod": "#daa520", + "gray": "#808080", + "grey": "#808080", + "green": "#008000", + "greenyellow": "#adff2f", + "honeydew": "#f0fff0", + "hotpink": "#ff69b4", + "indianred": "#cd5c5c", + "indigo": "#4b0082", + "ivory": "#fffff0", + "khaki": "#f0e68c", + "lavender": "#e6e6fa", + "lavenderblush": "#fff0f5", + "lawngreen": "#7cfc00", + "lemonchiffon": "#fffacd", + "lightblue": "#add8e6", + "lightcoral": "#f08080", + "lightcyan": "#e0ffff", + "lightgoldenrodyellow": "#fafad2", + "lightgray": "#d3d3d3", + "lightgrey": "#d3d3d3", + "lightgreen": "#90ee90", + "lightpink": "#ffb6c1", + "lightsalmon": "#ffa07a", + "lightseagreen": "#20b2aa", + "lightskyblue": "#87cefa", + "lightslategray": "#778899", + "lightslategrey": "#778899", + "lightsteelblue": "#b0c4de", + "lightyellow": "#ffffe0", + "lime": "#00ff00", + "limegreen": "#32cd32", + "linen": "#faf0e6", + "magenta": "#ff00ff", + "maroon": "#800000", + "mediumaquamarine": "#66cdaa", + "mediumblue": "#0000cd", + "mediumorchid": "#ba55d3", + "mediumpurple": "#9370db", + "mediumseagreen": "#3cb371", + "mediumslateblue": "#7b68ee", + "mediumspringgreen": "#00fa9a", + "mediumturquoise": "#48d1cc", + "mediumvioletred": "#c71585", + "midnightblue": "#191970", + "mintcream": "#f5fffa", + "mistyrose": "#ffe4e1", + "moccasin": "#ffe4b5", + "navajowhite": "#ffdead", + "navy": "#000080", + "oldlace": "#fdf5e6", + "olive": "#808000", + "olivedrab": "#6b8e23", + "orange": "#ffa500", + "orangered": "#ff4500", + "orchid": "#da70d6", + "palegoldenrod": "#eee8aa", + "palegreen": "#98fb98", + "paleturquoise": "#afeeee", + "palevioletred": "#db7093", + "papayawhip": "#ffefd5", + "peachpuff": "#ffdab9", + "peru": "#cd853f", + "pink": "#ffc0cb", + "plum": "#dda0dd", + "powderblue": "#b0e0e6", + "purple": "#800080", + "red": "#ff0000", + "rosybrown": "#bc8f8f", + "royalblue": "#4169e1", + "saddlebrown": "#8b4513", + "salmon": "#fa8072", + "sandybrown": "#f4a460", + "seagreen": "#2e8b57", + "seashell": "#fff5ee", + "sienna": "#a0522d", + "silver": "#c0c0c0", + "skyblue": "#87ceeb", + "slateblue": "#6a5acd", + "slategray": "#708090", + "slategrey": "#708090", + "snow": "#fffafa", + "springgreen": "#00ff7f", + "steelblue": "#4682b4", + "tan": "#d2b48c", + "teal": "#008080", + "thistle": "#d8bfd8", + "tomato": "#ff6347", + "turquoise": "#40e0d0", + "violet": "#ee82ee", + "wheat": "#f5deb3", + "white": "#ffffff", + "whitesmoke": "#f5f5f5", + "yellow": "#ffff00", + "yellowgreen": "#9acd32" +} + +XKCD_NAME_TO_HEX = { + "cloudy blue": "#acc2d9", + "dark pastel green": "#56ae57", + "dust": "#b2996e", + "electric lime": "#a8ff04", + "fresh green": "#69d84f", + "light eggplant": "#894585", + "nasty green": "#70b23f", + "really light blue": "#d4ffff", + "tea": "#65ab7c", + "warm purple": "#952e8f", + "yellowish tan": "#fcfc81", + "cement": "#a5a391", + "dark grass green": "#388004", + "dusty teal": "#4c9085", + "grey teal": "#5e9b8a", + "macaroni and cheese": "#efb435", + "pinkish tan": "#d99b82", + "spruce": "#0a5f38", + "strong blue": "#0c06f7", + "toxic green": "#61de2a", + "windows blue": "#3778bf", + "blue blue": "#2242c7", + "blue with a hint of purple": "#533cc6", + "booger": "#9bb53c", + "bright sea green": "#05ffa6", + "dark green blue": "#1f6357", + "deep turquoise": "#017374", + "green teal": "#0cb577", + "strong pink": "#ff0789", + "bland": "#afa88b", + "deep aqua": "#08787f", + "lavender pink": "#dd85d7", + "light moss green": "#a6c875", + "light seafoam green": "#a7ffb5", + "olive yellow": "#c2b709", + "pig pink": "#e78ea5", + "deep lilac": "#966ebd", + "desert": "#ccad60", + "dusty lavender": "#ac86a8", + "purpley grey": "#947e94", + "purply": "#983fb2", + "candy pink": "#ff63e9", + "light pastel green": "#b2fba5", + "boring green": "#63b365", + "kiwi green": "#8ee53f", + "light grey green": "#b7e1a1", + "orange pink": "#ff6f52", + "tea green": "#bdf8a3", + "very light brown": "#d3b683", + "egg shell": "#fffcc4", + "eggplant purple": "#430541", + "powder pink": "#ffb2d0", + "reddish grey": "#997570", + "baby shit brown": "#ad900d", + "liliac": "#c48efd", + "stormy blue": "#507b9c", + "ugly brown": "#7d7103", + "custard": "#fffd78", + "darkish pink": "#da467d", + "deep brown": "#410200", + "greenish beige": "#c9d179", + "manilla": "#fffa86", + "off blue": "#5684ae", + "battleship grey": "#6b7c85", + "browny green": "#6f6c0a", + "bruise": "#7e4071", + "kelley green": "#009337", + "sickly yellow": "#d0e429", + "sunny yellow": "#fff917", + "azul": "#1d5dec", + "darkgreen": "#054907", + "green/yellow": "#b5ce08", + "lichen": "#8fb67b", + "light light green": "#c8ffb0", + "pale gold": "#fdde6c", + "sun yellow": "#ffdf22", + "tan green": "#a9be70", + "burple": "#6832e3", + "butterscotch": "#fdb147", + "toupe": "#c7ac7d", + "dark cream": "#fff39a", + "indian red": "#850e04", + "light lavendar": "#efc0fe", + "poison green": "#40fd14", + "baby puke green": "#b6c406", + "bright yellow green": "#9dff00", + "charcoal grey": "#3c4142", + "squash": "#f2ab15", + "cinnamon": "#ac4f06", + "light pea green": "#c4fe82", + "radioactive green": "#2cfa1f", + "raw sienna": "#9a6200", + "baby purple": "#ca9bf7", + "cocoa": "#875f42", + "light royal blue": "#3a2efe", + "orangeish": "#fd8d49", + "rust brown": "#8b3103", + "sand brown": "#cba560", + "swamp": "#698339", + "tealish green": "#0cdc73", + "burnt siena": "#b75203", + "camo": "#7f8f4e", + "dusk blue": "#26538d", + "fern": "#63a950", + "old rose": "#c87f89", + "pale light green": "#b1fc99", + "peachy pink": "#ff9a8a", + "rosy pink": "#f6688e", + "light bluish green": "#76fda8", + "light bright green": "#53fe5c", + "light neon green": "#4efd54", + "light seafoam": "#a0febf", + "tiffany blue": "#7bf2da", + "washed out green": "#bcf5a6", + "browny orange": "#ca6b02", + "nice blue": "#107ab0", + "sapphire": "#2138ab", + "greyish teal": "#719f91", + "orangey yellow": "#fdb915", + "parchment": "#fefcaf", + "straw": "#fcf679", + "very dark brown": "#1d0200", + "terracota": "#cb6843", + "ugly blue": "#31668a", + "clear blue": "#247afd", + "creme": "#ffffb6", + "foam green": "#90fda9", + "grey/green": "#86a17d", + "light gold": "#fddc5c", + "seafoam blue": "#78d1b6", + "topaz": "#13bbaf", + "violet pink": "#fb5ffc", + "wintergreen": "#20f986", + "yellow tan": "#ffe36e", + "dark fuchsia": "#9d0759", + "indigo blue": "#3a18b1", + "light yellowish green": "#c2ff89", + "pale magenta": "#d767ad", + "rich purple": "#720058", + "sunflower yellow": "#ffda03", + "green/blue": "#01c08d", + "leather": "#ac7434", + "racing green": "#014600", + "vivid purple": "#9900fa", + "dark royal blue": "#02066f", + "hazel": "#8e7618", + "muted pink": "#d1768f", + "booger green": "#96b403", + "canary": "#fdff63", + "cool grey": "#95a3a6", + "dark taupe": "#7f684e", + "darkish purple": "#751973", + "true green": "#089404", + "coral pink": "#ff6163", + "dark sage": "#598556", + "dark slate blue": "#214761", + "flat blue": "#3c73a8", + "mushroom": "#ba9e88", + "rich blue": "#021bf9", + "dirty purple": "#734a65", + "greenblue": "#23c48b", + "icky green": "#8fae22", + "light khaki": "#e6f2a2", + "warm blue": "#4b57db", + "dark hot pink": "#d90166", + "deep sea blue": "#015482", + "carmine": "#9d0216", + "dark yellow green": "#728f02", + "pale peach": "#ffe5ad", + "plum purple": "#4e0550", + "golden rod": "#f9bc08", + "neon red": "#ff073a", + "old pink": "#c77986", + "very pale blue": "#d6fffe", + "blood orange": "#fe4b03", + "grapefruit": "#fd5956", + "sand yellow": "#fce166", + "clay brown": "#b2713d", + "dark blue grey": "#1f3b4d", + "flat green": "#699d4c", + "light green blue": "#56fca2", + "warm pink": "#fb5581", + "dodger blue": "#3e82fc", + "gross green": "#a0bf16", + "ice": "#d6fffa", + "metallic blue": "#4f738e", + "pale salmon": "#ffb19a", + "sap green": "#5c8b15", + "algae": "#54ac68", + "bluey grey": "#89a0b0", + "greeny grey": "#7ea07a", + "highlighter green": "#1bfc06", + "light light blue": "#cafffb", + "light mint": "#b6ffbb", + "raw umber": "#a75e09", + "vivid blue": "#152eff", + "deep lavender": "#8d5eb7", + "dull teal": "#5f9e8f", + "light greenish blue": "#63f7b4", + "mud green": "#606602", + "pinky": "#fc86aa", + "red wine": "#8c0034", + "shit green": "#758000", + "tan brown": "#ab7e4c", + "darkblue": "#030764", + "rosa": "#fe86a4", + "lipstick": "#d5174e", + "pale mauve": "#fed0fc", + "claret": "#680018", + "dandelion": "#fedf08", + "orangered": "#fe420f", + "poop green": "#6f7c00", + "ruby": "#ca0147", + "dark": "#1b2431", + "greenish turquoise": "#00fbb0", + "pastel red": "#db5856", + "piss yellow": "#ddd618", + "bright cyan": "#41fdfe", + "dark coral": "#cf524e", + "algae green": "#21c36f", + "darkish red": "#a90308", + "reddy brown": "#6e1005", + "blush pink": "#fe828c", + "camouflage green": "#4b6113", + "lawn green": "#4da409", + "putty": "#beae8a", + "vibrant blue": "#0339f8", + "dark sand": "#a88f59", + "purple/blue": "#5d21d0", + "saffron": "#feb209", + "twilight": "#4e518b", + "warm brown": "#964e02", + "bluegrey": "#85a3b2", + "bubble gum pink": "#ff69af", + "duck egg blue": "#c3fbf4", + "greenish cyan": "#2afeb7", + "petrol": "#005f6a", + "royal": "#0c1793", + "butter": "#ffff81", + "dusty orange": "#f0833a", + "off yellow": "#f1f33f", + "pale olive green": "#b1d27b", + "orangish": "#fc824a", + "leaf": "#71aa34", + "light blue grey": "#b7c9e2", + "dried blood": "#4b0101", + "lightish purple": "#a552e6", + "rusty red": "#af2f0d", + "lavender blue": "#8b88f8", + "light grass green": "#9af764", + "light mint green": "#a6fbb2", + "sunflower": "#ffc512", + "velvet": "#750851", + "brick orange": "#c14a09", + "lightish red": "#fe2f4a", + "pure blue": "#0203e2", + "twilight blue": "#0a437a", + "violet red": "#a50055", + "yellowy brown": "#ae8b0c", + "carnation": "#fd798f", + "muddy yellow": "#bfac05", + "dark seafoam green": "#3eaf76", + "deep rose": "#c74767", + "dusty red": "#b9484e", + "grey/blue": "#647d8e", + "lemon lime": "#bffe28", + "purple/pink": "#d725de", + "brown yellow": "#b29705", + "purple brown": "#673a3f", + "wisteria": "#a87dc2", + "banana yellow": "#fafe4b", + "lipstick red": "#c0022f", + "water blue": "#0e87cc", + "brown grey": "#8d8468", + "vibrant purple": "#ad03de", + "baby green": "#8cff9e", + "barf green": "#94ac02", + "eggshell blue": "#c4fff7", + "sandy yellow": "#fdee73", + "cool green": "#33b864", + "pale": "#fff9d0", + "blue/grey": "#758da3", + "hot magenta": "#f504c9", + "greyblue": "#77a1b5", + "purpley": "#8756e4", + "baby shit green": "#889717", + "brownish pink": "#c27e79", + "dark aquamarine": "#017371", + "diarrhea": "#9f8303", + "light mustard": "#f7d560", + "pale sky blue": "#bdf6fe", + "turtle green": "#75b84f", + "bright olive": "#9cbb04", + "dark grey blue": "#29465b", + "greeny brown": "#696006", + "lemon green": "#adf802", + "light periwinkle": "#c1c6fc", + "seaweed green": "#35ad6b", + "sunshine yellow": "#fffd37", + "ugly purple": "#a442a0", + "medium pink": "#f36196", + "puke brown": "#947706", + "very light pink": "#fff4f2", + "viridian": "#1e9167", + "bile": "#b5c306", + "faded yellow": "#feff7f", + "very pale green": "#cffdbc", + "vibrant green": "#0add08", + "bright lime": "#87fd05", + "spearmint": "#1ef876", + "light aquamarine": "#7bfdc7", + "light sage": "#bcecac", + "yellowgreen": "#bbf90f", + "baby poo": "#ab9004", + "dark seafoam": "#1fb57a", + "deep teal": "#00555a", + "heather": "#a484ac", + "rust orange": "#c45508", + "dirty blue": "#3f829d", + "fern green": "#548d44", + "bright lilac": "#c95efb", + "weird green": "#3ae57f", + "peacock blue": "#016795", + "avocado green": "#87a922", + "faded orange": "#f0944d", + "grape purple": "#5d1451", + "hot green": "#25ff29", + "lime yellow": "#d0fe1d", + "mango": "#ffa62b", + "shamrock": "#01b44c", + "bubblegum": "#ff6cb5", + "purplish brown": "#6b4247", + "vomit yellow": "#c7c10c", + "pale cyan": "#b7fffa", + "key lime": "#aeff6e", + "tomato red": "#ec2d01", + "lightgreen": "#76ff7b", + "merlot": "#730039", + "night blue": "#040348", + "purpleish pink": "#df4ec8", + "apple": "#6ecb3c", + "baby poop green": "#8f9805", + "green apple": "#5edc1f", + "heliotrope": "#d94ff5", + "yellow/green": "#c8fd3d", + "almost black": "#070d0d", + "cool blue": "#4984b8", + "leafy green": "#51b73b", + "mustard brown": "#ac7e04", + "dusk": "#4e5481", + "dull brown": "#876e4b", + "frog green": "#58bc08", + "vivid green": "#2fef10", + "bright light green": "#2dfe54", + "fluro green": "#0aff02", + "kiwi": "#9cef43", + "seaweed": "#18d17b", + "navy green": "#35530a", + "ultramarine blue": "#1805db", + "iris": "#6258c4", + "pastel orange": "#ff964f", + "yellowish orange": "#ffab0f", + "perrywinkle": "#8f8ce7", + "tealish": "#24bca8", + "dark plum": "#3f012c", + "pear": "#cbf85f", + "pinkish orange": "#ff724c", + "midnight purple": "#280137", + "light urple": "#b36ff6", + "dark mint": "#48c072", + "greenish tan": "#bccb7a", + "light burgundy": "#a8415b", + "turquoise blue": "#06b1c4", + "ugly pink": "#cd7584", + "sandy": "#f1da7a", + "electric pink": "#ff0490", + "muted purple": "#805b87", + "mid green": "#50a747", + "greyish": "#a8a495", + "neon yellow": "#cfff04", + "banana": "#ffff7e", + "carnation pink": "#ff7fa7", + "tomato": "#ef4026", + "sea": "#3c9992", + "muddy brown": "#886806", + "turquoise green": "#04f489", + "buff": "#fef69e", + "fawn": "#cfaf7b", + "muted blue": "#3b719f", + "pale rose": "#fdc1c5", + "dark mint green": "#20c073", + "amethyst": "#9b5fc0", + "blue/green": "#0f9b8e", + "chestnut": "#742802", + "sick green": "#9db92c", + "pea": "#a4bf20", + "rusty orange": "#cd5909", + "stone": "#ada587", + "rose red": "#be013c", + "pale aqua": "#b8ffeb", + "deep orange": "#dc4d01", + "earth": "#a2653e", + "mossy green": "#638b27", + "grassy green": "#419c03", + "pale lime green": "#b1ff65", + "light grey blue": "#9dbcd4", + "pale grey": "#fdfdfe", + "asparagus": "#77ab56", + "blueberry": "#464196", + "purple red": "#990147", + "pale lime": "#befd73", + "greenish teal": "#32bf84", + "caramel": "#af6f09", + "deep magenta": "#a0025c", + "light peach": "#ffd8b1", + "milk chocolate": "#7f4e1e", + "ocher": "#bf9b0c", + "off green": "#6ba353", + "purply pink": "#f075e6", + "lightblue": "#7bc8f6", + "dusky blue": "#475f94", + "golden": "#f5bf03", + "light beige": "#fffeb6", + "butter yellow": "#fffd74", + "dusky purple": "#895b7b", + "french blue": "#436bad", + "ugly yellow": "#d0c101", + "greeny yellow": "#c6f808", + "orangish red": "#f43605", + "shamrock green": "#02c14d", + "orangish brown": "#b25f03", + "tree green": "#2a7e19", + "deep violet": "#490648", + "gunmetal": "#536267", + "blue/purple": "#5a06ef", + "cherry": "#cf0234", + "sandy brown": "#c4a661", + "warm grey": "#978a84", + "dark indigo": "#1f0954", + "midnight": "#03012d", + "bluey green": "#2bb179", + "grey pink": "#c3909b", + "soft purple": "#a66fb5", + "blood": "#770001", + "brown red": "#922b05", + "medium grey": "#7d7f7c", + "berry": "#990f4b", + "poo": "#8f7303", + "purpley pink": "#c83cb9", + "light salmon": "#fea993", + "snot": "#acbb0d", + "easter purple": "#c071fe", + "light yellow green": "#ccfd7f", + "dark navy blue": "#00022e", + "drab": "#828344", + "light rose": "#ffc5cb", + "rouge": "#ab1239", + "purplish red": "#b0054b", + "slime green": "#99cc04", + "baby poop": "#937c00", + "irish green": "#019529", + "pink/purple": "#ef1de7", + "dark navy": "#000435", + "greeny blue": "#42b395", + "light plum": "#9d5783", + "pinkish grey": "#c8aca9", + "dirty orange": "#c87606", + "rust red": "#aa2704", + "pale lilac": "#e4cbff", + "orangey red": "#fa4224", + "primary blue": "#0804f9", + "kermit green": "#5cb200", + "brownish purple": "#76424e", + "murky green": "#6c7a0e", + "wheat": "#fbdd7e", + "very dark purple": "#2a0134", + "bottle green": "#044a05", + "watermelon": "#fd4659", + "deep sky blue": "#0d75f8", + "fire engine red": "#fe0002", + "yellow ochre": "#cb9d06", + "pumpkin orange": "#fb7d07", + "pale olive": "#b9cc81", + "light lilac": "#edc8ff", + "lightish green": "#61e160", + "carolina blue": "#8ab8fe", + "mulberry": "#920a4e", + "shocking pink": "#fe02a2", + "auburn": "#9a3001", + "bright lime green": "#65fe08", + "celadon": "#befdb7", + "pinkish brown": "#b17261", + "poo brown": "#885f01", + "bright sky blue": "#02ccfe", + "celery": "#c1fd95", + "dirt brown": "#836539", + "strawberry": "#fb2943", + "dark lime": "#84b701", + "copper": "#b66325", + "medium brown": "#7f5112", + "muted green": "#5fa052", + "robin's egg": "#6dedfd", + "bright aqua": "#0bf9ea", + "bright lavender": "#c760ff", + "ivory": "#ffffcb", + "very light purple": "#f6cefc", + "light navy": "#155084", + "pink red": "#f5054f", + "olive brown": "#645403", + "poop brown": "#7a5901", + "mustard green": "#a8b504", + "ocean green": "#3d9973", + "very dark blue": "#000133", + "dusty green": "#76a973", + "light navy blue": "#2e5a88", + "minty green": "#0bf77d", + "adobe": "#bd6c48", + "barney": "#ac1db8", + "jade green": "#2baf6a", + "bright light blue": "#26f7fd", + "light lime": "#aefd6c", + "dark khaki": "#9b8f55", + "orange yellow": "#ffad01", + "ocre": "#c69c04", + "maize": "#f4d054", + "faded pink": "#de9dac", + "british racing green": "#05480d", + "sandstone": "#c9ae74", + "mud brown": "#60460f", + "light sea green": "#98f6b0", + "robin egg blue": "#8af1fe", + "aqua marine": "#2ee8bb", + "dark sea green": "#11875d", + "soft pink": "#fdb0c0", + "orangey brown": "#b16002", + "cherry red": "#f7022a", + "burnt yellow": "#d5ab09", + "brownish grey": "#86775f", + "camel": "#c69f59", + "purplish grey": "#7a687f", + "marine": "#042e60", + "greyish pink": "#c88d94", + "pale turquoise": "#a5fbd5", + "pastel yellow": "#fffe71", + "bluey purple": "#6241c7", + "canary yellow": "#fffe40", + "faded red": "#d3494e", + "sepia": "#985e2b", + "coffee": "#a6814c", + "bright magenta": "#ff08e8", + "mocha": "#9d7651", + "ecru": "#feffca", + "purpleish": "#98568d", + "cranberry": "#9e003a", + "darkish green": "#287c37", + "brown orange": "#b96902", + "dusky rose": "#ba6873", + "melon": "#ff7855", + "sickly green": "#94b21c", + "silver": "#c5c9c7", + "purply blue": "#661aee", + "purpleish blue": "#6140ef", + "hospital green": "#9be5aa", + "shit brown": "#7b5804", + "mid blue": "#276ab3", + "amber": "#feb308", + "easter green": "#8cfd7e", + "soft blue": "#6488ea", + "cerulean blue": "#056eee", + "golden brown": "#b27a01", + "bright turquoise": "#0ffef9", + "red pink": "#fa2a55", + "red purple": "#820747", + "greyish brown": "#7a6a4f", + "vermillion": "#f4320c", + "russet": "#a13905", + "steel grey": "#6f828a", + "lighter purple": "#a55af4", + "bright violet": "#ad0afd", + "prussian blue": "#004577", + "slate green": "#658d6d", + "dirty pink": "#ca7b80", + "dark blue green": "#005249", + "pine": "#2b5d34", + "yellowy green": "#bff128", + "dark gold": "#b59410", + "bluish": "#2976bb", + "darkish blue": "#014182", + "dull red": "#bb3f3f", + "pinky red": "#fc2647", + "bronze": "#a87900", + "pale teal": "#82cbb2", + "military green": "#667c3e", + "barbie pink": "#fe46a5", + "bubblegum pink": "#fe83cc", + "pea soup green": "#94a617", + "dark mustard": "#a88905", + "shit": "#7f5f00", + "medium purple": "#9e43a2", + "very dark green": "#062e03", + "dirt": "#8a6e45", + "dusky pink": "#cc7a8b", + "red violet": "#9e0168", + "lemon yellow": "#fdff38", + "pistachio": "#c0fa8b", + "dull yellow": "#eedc5b", + "dark lime green": "#7ebd01", + "denim blue": "#3b5b92", + "teal blue": "#01889f", + "lightish blue": "#3d7afd", + "purpley blue": "#5f34e7", + "light indigo": "#6d5acf", + "swamp green": "#748500", + "brown green": "#706c11", + "dark maroon": "#3c0008", + "hot purple": "#cb00f5", + "dark forest green": "#002d04", + "faded blue": "#658cbb", + "drab green": "#749551", + "light lime green": "#b9ff66", + "snot green": "#9dc100", + "yellowish": "#faee66", + "light blue green": "#7efbb3", + "bordeaux": "#7b002c", + "light mauve": "#c292a1", + "ocean": "#017b92", + "marigold": "#fcc006", + "muddy green": "#657432", + "dull orange": "#d8863b", + "steel": "#738595", + "electric purple": "#aa23ff", + "fluorescent green": "#08ff08", + "yellowish brown": "#9b7a01", + "blush": "#f29e8e", + "soft green": "#6fc276", + "bright orange": "#ff5b00", + "lemon": "#fdff52", + "purple grey": "#866f85", + "acid green": "#8ffe09", + "pale lavender": "#eecffe", + "violet blue": "#510ac9", + "light forest green": "#4f9153", + "burnt red": "#9f2305", + "khaki green": "#728639", + "cerise": "#de0c62", + "faded purple": "#916e99", + "apricot": "#ffb16d", + "dark olive green": "#3c4d03", + "grey brown": "#7f7053", + "green grey": "#77926f", + "true blue": "#010fcc", + "pale violet": "#ceaefa", + "periwinkle blue": "#8f99fb", + "light sky blue": "#c6fcff", + "blurple": "#5539cc", + "green brown": "#544e03", + "bluegreen": "#017a79", + "bright teal": "#01f9c6", + "brownish yellow": "#c9b003", + "pea soup": "#929901", + "forest": "#0b5509", + "barney purple": "#a00498", + "ultramarine": "#2000b1", + "purplish": "#94568c", + "puke yellow": "#c2be0e", + "bluish grey": "#748b97", + "dark periwinkle": "#665fd1", + "dark lilac": "#9c6da5", + "reddish": "#c44240", + "light maroon": "#a24857", + "dusty purple": "#825f87", + "terra cotta": "#c9643b", + "avocado": "#90b134", + "marine blue": "#01386a", + "teal green": "#25a36f", + "slate grey": "#59656d", + "lighter green": "#75fd63", + "electric green": "#21fc0d", + "dusty blue": "#5a86ad", + "golden yellow": "#fec615", + "bright yellow": "#fffd01", + "light lavender": "#dfc5fe", + "umber": "#b26400", + "poop": "#7f5e00", + "dark peach": "#de7e5d", + "jungle green": "#048243", + "eggshell": "#ffffd4", + "denim": "#3b638c", + "yellow brown": "#b79400", + "dull purple": "#84597e", + "chocolate brown": "#411900", + "wine red": "#7b0323", + "neon blue": "#04d9ff", + "dirty green": "#667e2c", + "light tan": "#fbeeac", + "ice blue": "#d7fffe", + "cadet blue": "#4e7496", + "dark mauve": "#874c62", + "very light blue": "#d5ffff", + "grey purple": "#826d8c", + "pastel pink": "#ffbacd", + "very light green": "#d1ffbd", + "dark sky blue": "#448ee4", + "evergreen": "#05472a", + "dull pink": "#d5869d", + "aubergine": "#3d0734", + "mahogany": "#4a0100", + "reddish orange": "#f8481c", + "deep green": "#02590f", + "vomit green": "#89a203", + "purple pink": "#e03fd8", + "dusty pink": "#d58a94", + "faded green": "#7bb274", + "camo green": "#526525", + "pinky purple": "#c94cbe", + "pink purple": "#db4bda", + "brownish red": "#9e3623", + "dark rose": "#b5485d", + "mud": "#735c12", + "brownish": "#9c6d57", + "emerald green": "#028f1e", + "pale brown": "#b1916e", + "dull blue": "#49759c", + "burnt umber": "#a0450e", + "medium green": "#39ad48", + "clay": "#b66a50", + "light aqua": "#8cffdb", + "light olive green": "#a4be5c", + "brownish orange": "#cb7723", + "dark aqua": "#05696b", + "purplish pink": "#ce5dae", + "dark salmon": "#c85a53", + "greenish grey": "#96ae8d", + "jade": "#1fa774", + "ugly green": "#7a9703", + "dark beige": "#ac9362", + "emerald": "#01a049", + "pale red": "#d9544d", + "light magenta": "#fa5ff7", + "sky": "#82cafc", + "light cyan": "#acfffc", + "yellow orange": "#fcb001", + "reddish purple": "#910951", + "reddish pink": "#fe2c54", + "orchid": "#c875c4", + "dirty yellow": "#cdc50a", + "orange red": "#fd411e", + "deep red": "#9a0200", + "orange brown": "#be6400", + "cobalt blue": "#030aa7", + "neon pink": "#fe019a", + "rose pink": "#f7879a", + "greyish purple": "#887191", + "raspberry": "#b00149", + "aqua green": "#12e193", + "salmon pink": "#fe7b7c", + "tangerine": "#ff9408", + "brownish green": "#6a6e09", + "red brown": "#8b2e16", + "greenish brown": "#696112", + "pumpkin": "#e17701", + "pine green": "#0a481e", + "charcoal": "#343837", + "baby pink": "#ffb7ce", + "cornflower": "#6a79f7", + "blue violet": "#5d06e9", + "chocolate": "#3d1c02", + "greyish green": "#82a67d", + "scarlet": "#be0119", + "green yellow": "#c9ff27", + "dark olive": "#373e02", + "sienna": "#a9561e", + "pastel purple": "#caa0ff", + "terracotta": "#ca6641", + "aqua blue": "#02d8e9", + "sage green": "#88b378", + "blood red": "#980002", + "deep pink": "#cb0162", + "grass": "#5cac2d", + "moss": "#769958", + "pastel blue": "#a2bffe", + "bluish green": "#10a674", + "green blue": "#06b48b", + "dark tan": "#af884a", + "greenish blue": "#0b8b87", + "pale orange": "#ffa756", + "vomit": "#a2a415", + "forrest green": "#154406", + "dark lavender": "#856798", + "dark violet": "#34013f", + "purple blue": "#632de9", + "dark cyan": "#0a888a", + "olive drab": "#6f7632", + "pinkish": "#d46a7e", + "cobalt": "#1e488f", + "neon purple": "#bc13fe", + "light turquoise": "#7ef4cc", + "apple green": "#76cd26", + "dull green": "#74a662", + "wine": "#80013f", + "powder blue": "#b1d1fc", + "off white": "#ffffe4", + "electric blue": "#0652ff", + "dark turquoise": "#045c5a", + "blue purple": "#5729ce", + "azure": "#069af3", + "bright red": "#ff000d", + "pinkish red": "#f10c45", + "cornflower blue": "#5170d7", + "light olive": "#acbf69", + "grape": "#6c3461", + "greyish blue": "#5e819d", + "purplish blue": "#601ef9", + "yellowish green": "#b0dd16", + "greenish yellow": "#cdfd02", + "medium blue": "#2c6fbb", + "dusty rose": "#c0737a", + "light violet": "#d6b4fc", + "midnight blue": "#020035", + "bluish purple": "#703be7", + "red orange": "#fd3c06", + "dark magenta": "#960056", + "greenish": "#40a368", + "ocean blue": "#03719c", + "coral": "#fc5a50", + "cream": "#ffffc2", + "reddish brown": "#7f2b0a", + "burnt sienna": "#b04e0f", + "brick": "#a03623", + "sage": "#87ae73", + "grey green": "#789b73", + "white": "#ffffff", + "robin's egg blue": "#98eff9", + "moss green": "#658b38", + "steel blue": "#5a7d9a", + "eggplant": "#380835", + "light yellow": "#fffe7a", + "leaf green": "#5ca904", + "light grey": "#d8dcd6", + "puke": "#a5a502", + "pinkish purple": "#d648d7", + "sea blue": "#047495", + "pale purple": "#b790d4", + "slate blue": "#5b7c99", + "blue grey": "#607c8e", + "hunter green": "#0b4008", + "fuchsia": "#ed0dd9", + "crimson": "#8c000f", + "pale yellow": "#ffff84", + "ochre": "#bf9005", + "mustard yellow": "#d2bd0a", + "light red": "#ff474c", + "cerulean": "#0485d1", + "pale pink": "#ffcfdc", + "deep blue": "#040273", + "rust": "#a83c09", + "light teal": "#90e4c1", + "slate": "#516572", + "goldenrod": "#fac205", + "dark yellow": "#d5b60a", + "dark grey": "#363737", + "army green": "#4b5d16", + "grey blue": "#6b8ba4", + "seafoam": "#80f9ad", + "puce": "#a57e52", + "spring green": "#a9f971", + "dark orange": "#c65102", + "sand": "#e2ca76", + "pastel green": "#b0ff9d", + "mint": "#9ffeb0", + "light orange": "#fdaa48", + "bright pink": "#fe01b1", + "chartreuse": "#c1f80a", + "deep purple": "#36013f", + "dark brown": "#341c02", + "taupe": "#b9a281", + "pea green": "#8eab12", + "puke green": "#9aae07", + "kelly green": "#02ab2e", + "seafoam green": "#7af9ab", + "blue green": "#137e6d", + "khaki": "#aaa662", + "burgundy": "#610023", + "dark teal": "#014d4e", + "brick red": "#8f1402", + "royal purple": "#4b006e", + "plum": "#580f41", + "mint green": "#8fff9f", + "gold": "#dbb40c", + "baby blue": "#a2cffe", + "yellow green": "#c0fb2d", + "bright purple": "#be03fd", + "dark red": "#840000", + "pale blue": "#d0fefe", + "grass green": "#3f9b0b", + "navy": "#01153e", + "aquamarine": "#04d8b2", + "burnt orange": "#c04e01", + "neon green": "#0cff0c", + "bright blue": "#0165fc", + "rose": "#cf6275", + "light pink": "#ffd1df", + "mustard": "#ceb301", + "indigo": "#380282", + "lime": "#aaff32", + "sea green": "#53fca1", + "periwinkle": "#8e82fe", + "dark pink": "#cb416b", + "olive green": "#677a04", + "peach": "#ffb07c", + "pale green": "#c7fdb5", + "light brown": "#ad8150", + "hot pink": "#ff028d", + "black": "#000000", + "lilac": "#cea2fd", + "navy blue": "#001146", + "royal blue": "#0504aa", + "beige": "#e6daa6", + "salmon": "#ff796c", + "olive": "#6e750e", + "maroon": "#650021", + "bright green": "#01ff07", + "dark purple": "#35063e", + "mauve": "#ae7181", + "forest green": "#06470c", + "aqua": "#13eac9", + "cyan": "#00ffff", + "tan": "#d1b26f", + "dark blue": "#00035b", + "lavender": "#c79fef", + "turquoise": "#06c2ac", + "dark green": "#033500", + "violet": "#9a0eea", + "light purple": "#bf77f6", + "lime green": "#89fe05", + "grey": "#929591", + "sky blue": "#75bbfd", + "yellow": "#ffff14", + "magenta": "#c20078", + "light green": "#96f97b", + "orange": "#f97306", + "teal": "#029386", + "light blue": "#95d0fc", + "red": "#e50000", + "brown": "#653700", + "pink": "#ff81c0", + "blue": "#0343df", + "green": "#15b01a", + "purple": "#7e1e9c" +} + +HTML4_NAMES_TO_HEX = { + "aqua": "#00ffff", + "black": "#000000", + "blue": "#0000ff", + "fuchsia": "#ff00ff", + "green": "#008000", + "gray": "#808080", + "lime": "#00ff00", + "maroon": "#800000", + "navy": "#000080", + "olive": "#808000", + "purple": "#800080", + "red": "#ff0000", + "silver": "#c0c0c0", + "teal": "#008080", + "white": "#ffffff", + "yellow": "#ffff00" +} + +CSS4_NAME_TO_HEX = { + 'aliceblue': '#F0F8FF', + 'antiquewhite': '#FAEBD7', + 'aqua': '#00FFFF', + 'aquamarine': '#7FFFD4', + 'azure': '#F0FFFF', + 'beige': '#F5F5DC', + 'bisque': '#FFE4C4', + 'black': '#000000', + 'blanchedalmond': '#FFEBCD', + 'blue': '#0000FF', + 'blueviolet': '#8A2BE2', + 'brown': '#A52A2A', + 'burlywood': '#DEB887', + 'cadetblue': '#5F9EA0', + 'chartreuse': '#7FFF00', + 'chocolate': '#D2691E', + 'coral': '#FF7F50', + 'cornflowerblue': '#6495ED', + 'cornsilk': '#FFF8DC', + 'crimson': '#DC143C', + 'cyan': '#00FFFF', + 'darkblue': '#00008B', + 'darkcyan': '#008B8B', + 'darkgoldenrod': '#B8860B', + 'darkgray': '#A9A9A9', + 'darkgreen': '#006400', + 'darkgrey': '#A9A9A9', + 'darkkhaki': '#BDB76B', + 'darkmagenta': '#8B008B', + 'darkolivegreen': '#556B2F', + 'darkorange': '#FF8C00', + 'darkorchid': '#9932CC', + 'darkred': '#8B0000', + 'darksalmon': '#E9967A', + 'darkseagreen': '#8FBC8F', + 'darkslateblue': '#483D8B', + 'darkslategray': '#2F4F4F', + 'darkslategrey': '#2F4F4F', + 'darkturquoise': '#00CED1', + 'darkviolet': '#9400D3', + 'deeppink': '#FF1493', + 'deepskyblue': '#00BFFF', + 'dimgray': '#696969', + 'dimgrey': '#696969', + 'dodgerblue': '#1E90FF', + 'firebrick': '#B22222', + 'floralwhite': '#FFFAF0', + 'forestgreen': '#228B22', + 'fuchsia': '#FF00FF', + 'gainsboro': '#DCDCDC', + 'ghostwhite': '#F8F8FF', + 'gold': '#FFD700', + 'goldenrod': '#DAA520', + 'gray': '#808080', + 'green': '#008000', + 'greenyellow': '#ADFF2F', + 'grey': '#808080', + 'honeydew': '#F0FFF0', + 'hotpink': '#FF69B4', + 'indianred': '#CD5C5C', + 'indigo': '#4B0082', + 'ivory': '#FFFFF0', + 'khaki': '#F0E68C', + 'lavender': '#E6E6FA', + 'lavenderblush': '#FFF0F5', + 'lawngreen': '#7CFC00', + 'lemonchiffon': '#FFFACD', + 'lightblue': '#ADD8E6', + 'lightcoral': '#F08080', + 'lightcyan': '#E0FFFF', + 'lightgoldenrodyellow': '#FAFAD2', + 'lightgray': '#D3D3D3', + 'lightgreen': '#90EE90', + 'lightgrey': '#D3D3D3', + 'lightpink': '#FFB6C1', + 'lightsalmon': '#FFA07A', + 'lightseagreen': '#20B2AA', + 'lightskyblue': '#87CEFA', + 'lightslategray': '#778899', + 'lightslategrey': '#778899', + 'lightsteelblue': '#B0C4DE', + 'lightyellow': '#FFFFE0', + 'lime': '#00FF00', + 'limegreen': '#32CD32', + 'linen': '#FAF0E6', + 'magenta': '#FF00FF', + 'maroon': '#800000', + 'mediumaquamarine': '#66CDAA', + 'mediumblue': '#0000CD', + 'mediumorchid': '#BA55D3', + 'mediumpurple': '#9370DB', + 'mediumseagreen': '#3CB371', + 'mediumslateblue': '#7B68EE', + 'mediumspringgreen': '#00FA9A', + 'mediumturquoise': '#48D1CC', + 'mediumvioletred': '#C71585', + 'midnightblue': '#191970', + 'mintcream': '#F5FFFA', + 'mistyrose': '#FFE4E1', + 'moccasin': '#FFE4B5', + 'navajowhite': '#FFDEAD', + 'navy': '#000080', + 'oldlace': '#FDF5E6', + 'olive': '#808000', + 'olivedrab': '#6B8E23', + 'orange': '#FFA500', + 'orangered': '#FF4500', + 'orchid': '#DA70D6', + 'palegoldenrod': '#EEE8AA', + 'palegreen': '#98FB98', + 'paleturquoise': '#AFEEEE', + 'palevioletred': '#DB7093', + 'papayawhip': '#FFEFD5', + 'peachpuff': '#FFDAB9', + 'peru': '#CD853F', + 'pink': '#FFC0CB', + 'plum': '#DDA0DD', + 'powderblue': '#B0E0E6', + 'purple': '#800080', + 'rebeccapurple': '#663399', + 'red': '#FF0000', + 'rosybrown': '#BC8F8F', + 'royalblue': '#4169E1', + 'saddlebrown': '#8B4513', + 'salmon': '#FA8072', + 'sandybrown': '#F4A460', + 'seagreen': '#2E8B57', + 'seashell': '#FFF5EE', + 'sienna': '#A0522D', + 'silver': '#C0C0C0', + 'skyblue': '#87CEEB', + 'slateblue': '#6A5ACD', + 'slategray': '#708090', + 'slategrey': '#708090', + 'snow': '#FFFAFA', + 'springgreen': '#00FF7F', + 'steelblue': '#4682B4', + 'tan': '#D2B48C', + 'teal': '#008080', + 'thistle': '#D8BFD8', + 'tomato': '#FF6347', + 'turquoise': '#40E0D0', + 'violet': '#EE82EE', + 'wheat': '#F5DEB3', + 'white': '#FFFFFF', + 'whitesmoke': '#F5F5F5', + 'yellow': '#FFFF00', + 'yellowgreen': '#9ACD32' +} + +WIKI_COLOR_NAME_TO_HEX ={ + 'air force blue (raf)': '#5d8aa8', + 'air force blue (usaf)': '#00308f', + 'air superiority blue': '#72a0c1', + 'alabama crimson': '#a32638', + 'alice blue': '#f0f8ff', + 'alizarin crimson': '#e32636', + 'alloy orange': '#c46210', + 'almond': '#efdecd', + 'amaranth': '#e52b50', + 'amber': '#ffbf00', + 'amber (sae/ece)': '#ff7e00', + 'american rose': '#ff033e', + 'amethyst': '#96c', + 'android green': '#a4c639', + 'anti-flash white': '#f2f3f4', + 'antique brass': '#cd9575', + 'antique fuchsia': '#915c83', + 'antique ruby': '#841b2d', + 'antique white': '#faebd7', + 'ao (english)': '#008000', + 'apple green': '#8db600', + 'apricot': '#fbceb1', + 'aqua': '#0ff', + 'aquamarine': '#7fffd4', + 'army green': '#4b5320', + 'arsenic': '#3b444b', + 'arylide yellow': '#e9d66b', + 'ash grey': '#b2beb5', + 'asparagus': '#87a96b', + 'atomic tangerine': '#f96', + 'auburn': '#a52a2a', + 'aureolin': '#fdee00', + 'aurometalsaurus': '#6e7f80', + 'avocado': '#568203', + 'azure': '#007fff', + 'azure mist/web': '#f0ffff', + 'baby blue': '#89cff0', + 'baby blue eyes': '#a1caf1', + 'baby pink': '#f4c2c2', + 'ball blue': '#21abcd', + 'banana mania': '#fae7b5', + 'banana yellow': '#ffe135', + 'barn red': '#7c0a02', + 'battleship grey': '#848482', + 'bazaar': '#98777b', + 'beau blue': '#bcd4e6', + 'beaver': '#9f8170', + 'beige': '#f5f5dc', + 'big dip o’ruby': '#9c2542', + 'bisque': '#ffe4c4', + 'bistre': '#3d2b1f', + 'bittersweet': '#fe6f5e', + 'bittersweet shimmer': '#bf4f51', + 'black': '#000', + 'black bean': '#3d0c02', + 'black leather jacket': '#253529', + 'black olive': '#3b3c36', + 'blanched almond': '#ffebcd', + 'blast-off bronze': '#a57164', + 'bleu de france': '#318ce7', + 'blizzard blue': '#ace5ee', + 'blond': '#faf0be', + 'blue': '#00f', + 'blue bell': '#a2a2d0', + 'blue (crayola)': '#1f75fe', + 'blue gray': '#69c', + 'blue-green': '#0d98ba', + 'blue (munsell)': '#0093af', + 'blue (ncs)': '#0087bd', + 'blue (pigment)': '#339', + 'blue (ryb)': '#0247fe', + 'blue sapphire': '#126180', + 'blue-violet': '#8a2be2', + 'blush': '#de5d83', + 'bole': '#79443b', + 'bondi blue': '#0095b6', + 'bone': '#e3dac9', + 'boston university red': '#c00', + 'bottle green': '#006a4e', + 'boysenberry': '#873260', + 'brandeis blue': '#0070ff', + 'brass': '#b5a642', + 'brick red': '#cb4154', + 'bright cerulean': '#1dacd6', + 'bright green': '#6f0', + 'bright lavender': '#bf94e4', + 'bright maroon': '#c32148', + 'bright pink': '#ff007f', + 'bright turquoise': '#08e8de', + 'bright ube': '#d19fe8', + 'brilliant lavender': '#f4bbff', + 'brilliant rose': '#ff55a3', + 'brink pink': '#fb607f', + 'british racing green': '#004225', + 'bronze': '#cd7f32', + 'brown (traditional)': '#964b00', + 'brown (web)': '#a52a2a', + 'bubble gum': '#ffc1cc', + 'bubbles': '#e7feff', + 'buff': '#f0dc82', + 'bulgarian rose': '#480607', + 'burgundy': '#800020', + 'burlywood': '#deb887', + 'burnt orange': '#c50', + 'burnt sienna': '#e97451', + 'burnt umber': '#8a3324', + 'byzantine': '#bd33a4', + 'byzantium': '#702963', + 'cadet': '#536872', + 'cadet blue': '#5f9ea0', + 'cadet grey': '#91a3b0', + 'cadmium green': '#006b3c', + 'cadmium orange': '#ed872d', + 'cadmium red': '#e30022', + 'cadmium yellow': '#fff600', + 'café au lait': '#a67b5b', + 'café noir': '#4b3621', + 'cal poly green': '#1e4d2b', + 'cambridge blue': '#a3c1ad', + 'camel': '#c19a6b', + 'cameo pink': '#efbbcc', + 'camouflage green': '#78866b', + 'canary yellow': '#ffef00', + 'candy apple red': '#ff0800', + 'candy pink': '#e4717a', + 'capri': '#00bfff', + 'caput mortuum': '#592720', + 'cardinal': '#c41e3a', + 'caribbean green': '#0c9', + 'carmine': '#960018', + 'carmine (m&p)': '#d70040', + 'carmine pink': '#eb4c42', + 'carmine red': '#ff0038', + 'carnation pink': '#ffa6c9', + 'carnelian': '#b31b1b', + 'carolina blue': '#99badd', + 'carrot orange': '#ed9121', + 'catalina blue': '#062a78', + 'ceil': '#92a1cf', + 'celadon': '#ace1af', + 'celadon blue': '#007ba7', + 'celadon green': '#2f847c', + 'celeste (colour)': '#b2ffff', + 'celestial blue': '#4997d0', + 'cerise': '#de3163', + 'cerise pink': '#ec3b83', + 'cerulean': '#007ba7', + 'cerulean blue': '#2a52be', + 'cerulean frost': '#6d9bc3', + 'cg blue': '#007aa5', + 'cg red': '#e03c31', + 'chamoisee': '#a0785a', + 'champagne': '#fad6a5', + 'charcoal': '#36454f', + 'charm pink': '#e68fac', + 'chartreuse (traditional)': '#dfff00', + 'chartreuse (web)': '#7fff00', + 'cherry': '#de3163', + 'cherry blossom pink': '#ffb7c5', + 'chestnut': '#cd5c5c', + 'china pink': '#de6fa1', + 'china rose': '#a8516e', + 'chinese red': '#aa381e', + 'chocolate (traditional)': '#7b3f00', + 'chocolate (web)': '#d2691e', + 'chrome yellow': '#ffa700', + 'cinereous': '#98817b', + 'cinnabar': '#e34234', + 'cinnamon': '#d2691e', + 'citrine': '#e4d00a', + 'classic rose': '#fbcce7', + 'cobalt': '#0047ab', + 'cocoa brown': '#d2691e', + 'coffee': '#6f4e37', + 'columbia blue': '#9bddff', + 'congo pink': '#f88379', + 'cool black': '#002e63', + 'cool grey': '#8c92ac', + 'copper': '#b87333', + 'copper (crayola)': '#da8a67', + 'copper penny': '#ad6f69', + 'copper red': '#cb6d51', + 'copper rose': '#966', + 'coquelicot': '#ff3800', + 'coral': '#ff7f50', + 'coral pink': '#f88379', + 'coral red': '#ff4040', + 'cordovan': '#893f45', + 'corn': '#fbec5d', + 'cornell red': '#b31b1b', + 'cornflower blue': '#6495ed', + 'cornsilk': '#fff8dc', + 'cosmic latte': '#fff8e7', + 'cotton candy': '#ffbcd9', + 'cream': '#fffdd0', + 'crimson': '#dc143c', + 'crimson glory': '#be0032', + 'cyan': '#0ff', + 'cyan (process)': '#00b7eb', + 'daffodil': '#ffff31', + 'dandelion': '#f0e130', + 'dark blue': '#00008b', + 'dark brown': '#654321', + 'dark byzantium': '#5d3954', + 'dark candy apple red': '#a40000', + 'dark cerulean': '#08457e', + 'dark chestnut': '#986960', + 'dark coral': '#cd5b45', + 'dark cyan': '#008b8b', + 'dark electric blue': '#536878', + 'dark goldenrod': '#b8860b', + 'dark gray': '#a9a9a9', + 'dark green': '#013220', + 'dark imperial blue': '#00416a', + 'dark jungle green': '#1a2421', + 'dark khaki': '#bdb76b', + 'dark lava': '#483c32', + 'dark lavender': '#734f96', + 'dark magenta': '#8b008b', + 'dark midnight blue': '#036', + 'dark olive green': '#556b2f', + 'dark orange': '#ff8c00', + 'dark orchid': '#9932cc', + 'dark pastel blue': '#779ecb', + 'dark pastel green': '#03c03c', + 'dark pastel purple': '#966fd6', + 'dark pastel red': '#c23b22', + 'dark pink': '#e75480', + 'dark powder blue': '#039', + 'dark raspberry': '#872657', + 'dark red': '#8b0000', + 'dark salmon': '#e9967a', + 'dark scarlet': '#560319', + 'dark sea green': '#8fbc8f', + 'dark sienna': '#3c1414', + 'dark slate blue': '#483d8b', + 'dark slate gray': '#2f4f4f', + 'dark spring green': '#177245', + 'dark tan': '#918151', + 'dark tangerine': '#ffa812', + 'dark taupe': '#483c32', + 'dark terra cotta': '#cc4e5c', + 'dark turquoise': '#00ced1', + 'dark violet': '#9400d3', + 'dark yellow': '#9b870c', + 'dartmouth green': '#00703c', + "davy's grey": '#555', + 'debian red': '#d70a53', + 'deep carmine': '#a9203e', + 'deep carmine pink': '#ef3038', + 'deep carrot orange': '#e9692c', + 'deep cerise': '#da3287', + 'deep champagne': '#fad6a5', + 'deep chestnut': '#b94e48', + 'deep coffee': '#704241', + 'deep fuchsia': '#c154c1', + 'deep jungle green': '#004b49', + 'deep lilac': '#95b', + 'deep magenta': '#c0c', + 'deep peach': '#ffcba4', + 'deep pink': '#ff1493', + 'deep ruby': '#843f5b', + 'deep saffron': '#f93', + 'deep sky blue': '#00bfff', + 'deep tuscan red': '#66424d', + 'denim': '#1560bd', + 'desert': '#c19a6b', + 'desert sand': '#edc9af', + 'dim gray': '#696969', + 'dodger blue': '#1e90ff', + 'dogwood rose': '#d71868', + 'dollar bill': '#85bb65', + 'drab': '#967117', + 'duke blue': '#00009c', + 'earth yellow': '#e1a95f', + 'ebony': '#555d50', + 'ecru': '#c2b280', + 'eggplant': '#614051', + 'eggshell': '#f0ead6', + 'egyptian blue': '#1034a6', + 'electric blue': '#7df9ff', + 'electric crimson': '#ff003f', + 'electric cyan': '#0ff', + 'electric green': '#0f0', + 'electric indigo': '#6f00ff', + 'electric lavender': '#f4bbff', + 'electric lime': '#cf0', + 'electric purple': '#bf00ff', + 'electric ultramarine': '#3f00ff', + 'electric violet': '#8f00ff', + 'electric yellow': '#ff0', + 'emerald': '#50c878', + 'english lavender': '#b48395', + 'eton blue': '#96c8a2', + 'fallow': '#c19a6b', + 'falu red': '#801818', + 'fandango': '#b53389', + 'fashion fuchsia': '#f400a1', + 'fawn': '#e5aa70', + 'feldgrau': '#4d5d53', + 'fern green': '#4f7942', + 'ferrari red': '#ff2800', + 'field drab': '#6c541e', + 'fire engine red': '#ce2029', + 'firebrick': '#b22222', + 'flame': '#e25822', + 'flamingo pink': '#fc8eac', + 'flavescent': '#f7e98e', + 'flax': '#eedc82', + 'floral white': '#fffaf0', + 'fluorescent orange': '#ffbf00', + 'fluorescent pink': '#ff1493', + 'fluorescent yellow': '#cf0', + 'folly': '#ff004f', + 'forest green (traditional)': '#014421', + 'forest green (web)': '#228b22', + 'french beige': '#a67b5b', + 'french blue': '#0072bb', + 'french lilac': '#86608e', + 'french lime': '#cf0', + 'french raspberry': '#c72c48', + 'french rose': '#f64a8a', + 'fuchsia': '#f0f', + 'fuchsia (crayola)': '#c154c1', + 'fuchsia pink': '#f7f', + 'fuchsia rose': '#c74375', + 'fulvous': '#e48400', + 'fuzzy wuzzy': '#c66', + 'gainsboro': '#dcdcdc', + 'gamboge': '#e49b0f', + 'ghost white': '#f8f8ff', + 'ginger': '#b06500', + 'glaucous': '#6082b6', + 'glitter': '#e6e8fa', + 'gold (metallic)': '#d4af37', + 'gold (web) (golden)': '#ffd700', + 'golden brown': '#996515', + 'golden poppy': '#fcc200', + 'golden yellow': '#ffdf00', + 'goldenrod': '#daa520', + 'granny smith apple': '#a8e4a0', + 'gray': '#808080', + 'gray-asparagus': '#465945', + 'gray (html/css gray)': '#808080', + 'gray (x11 gray)': '#bebebe', + 'green (color wheel) (x11 green)': '#0f0', + 'green (crayola)': '#1cac78', + 'green (html/css green)': '#008000', + 'green (munsell)': '#00a877', + 'green (ncs)': '#009f6b', + 'green (pigment)': '#00a550', + 'green (ryb)': '#66b032', + 'green-yellow': '#adff2f', + 'grullo': '#a99a86', + 'guppie green': '#00ff7f', + 'halayà úbe': '#663854', + 'han blue': '#446ccf', + 'han purple': '#5218fa', + 'hansa yellow': '#e9d66b', + 'harlequin': '#3fff00', + 'harvard crimson': '#c90016', + 'harvest gold': '#da9100', + 'heart gold': '#808000', + 'heliotrope': '#df73ff', + 'hollywood cerise': '#f400a1', + 'honeydew': '#f0fff0', + 'honolulu blue': '#007fbf', + "hooker's green": '#49796b', + 'hot magenta': '#ff1dce', + 'hot pink': '#ff69b4', + 'hunter green': '#355e3b', + 'iceberg': '#71a6d2', + 'icterine': '#fcf75e', + 'imperial blue': '#002395', + 'inchworm': '#b2ec5d', + 'india green': '#138808', + 'indian red': '#cd5c5c', + 'indian yellow': '#e3a857', + 'indigo': '#6f00ff', + 'indigo (dye)': '#00416a', + 'indigo (web)': '#4b0082', + 'international klein blue': '#002fa7', + 'international orange (aerospace)': '#ff4f00', + 'international orange (engineering)': '#ba160c', + 'international orange (golden gate bridge)': '#c0362c', + 'iris': '#5a4fcf', + 'isabelline': '#f4f0ec', + 'islamic green': '#009000', + 'ivory': '#fffff0', + 'jade': '#00a86b', + 'jasmine': '#f8de7e', + 'jasper': '#d73b3e', + 'jazzberry jam': '#a50b5e', + 'jet': '#343434', + 'jonquil': '#fada5e', + 'june bud': '#bdda57', + 'jungle green': '#29ab87', + 'kelly green': '#4cbb17', + 'kenyan copper': '#7c1c05', + 'khaki (html/css) (khaki)': '#c3b091', + 'khaki (x11) (light khaki)': '#f0e68c', + 'ku crimson': '#e8000d', + 'la salle green': '#087830', + 'languid lavender': '#d6cadd', + 'lapis lazuli': '#26619c', + 'laser lemon': '#fefe22', + 'laurel green': '#a9ba9d', + 'lava': '#cf1020', + 'lavender blue': '#ccf', + 'lavender blush': '#fff0f5', + 'lavender (floral)': '#b57edc', + 'lavender gray': '#c4c3d0', + 'lavender indigo': '#9457eb', + 'lavender magenta': '#ee82ee', + 'lavender mist': '#e6e6fa', + 'lavender pink': '#fbaed2', + 'lavender purple': '#967bb6', + 'lavender rose': '#fba0e3', + 'lavender (web)': '#e6e6fa', + 'lawn green': '#7cfc00', + 'lemon': '#fff700', + 'lemon chiffon': '#fffacd', + 'lemon lime': '#e3ff00', + 'licorice': '#1a1110', + 'light apricot': '#fdd5b1', + 'light blue': '#add8e6', + 'light brown': '#b5651d', + 'light carmine pink': '#e66771', + 'light coral': '#f08080', + 'light cornflower blue': '#93ccea', + 'light crimson': '#f56991', + 'light cyan': '#e0ffff', + 'light fuchsia pink': '#f984ef', + 'light goldenrod yellow': '#fafad2', + 'light gray': '#d3d3d3', + 'light green': '#90ee90', + 'light khaki': '#f0e68c', + 'light pastel purple': '#b19cd9', + 'light pink': '#ffb6c1', + 'light red ochre': '#e97451', + 'light salmon': '#ffa07a', + 'light salmon pink': '#f99', + 'light sea green': '#20b2aa', + 'light sky blue': '#87cefa', + 'light slate gray': '#789', + 'light taupe': '#b38b6d', + 'light thulian pink': '#e68fac', + 'light yellow': '#ffffe0', + 'lilac': '#c8a2c8', + 'lime (color wheel)': '#bfff00', + 'lime green': '#32cd32', + 'lime (web) (x11 green)': '#0f0', + 'limerick': '#9dc209', + 'lincoln green': '#195905', + 'linen': '#faf0e6', + 'lion': '#c19a6b', + 'little boy blue': '#6ca0dc', + 'liver': '#534b4f', + 'lust': '#e62020', + 'magenta': '#f0f', + 'magenta (dye)': '#ca1f7b', + 'magenta (process)': '#ff0090', + 'magic mint': '#aaf0d1', + 'magnolia': '#f8f4ff', + 'mahogany': '#c04000', + 'maize': '#fbec5d', + 'majorelle blue': '#6050dc', + 'malachite': '#0bda51', + 'manatee': '#979aaa', + 'mango tango': '#ff8243', + 'mantis': '#74c365', + 'mardi gras': '#880085', + 'maroon (crayola)': '#c32148', + 'maroon (html/css)': '#800000', + 'maroon (x11)': '#b03060', + 'mauve': '#e0b0ff', + 'mauve taupe': '#915f6d', + 'mauvelous': '#ef98aa', + 'maya blue': '#73c2fb', + 'meat brown': '#e5b73b', + 'medium aquamarine': '#6da', + 'medium blue': '#0000cd', + 'medium candy apple red': '#e2062c', + 'medium carmine': '#af4035', + 'medium champagne': '#f3e5ab', + 'medium electric blue': '#035096', + 'medium jungle green': '#1c352d', + 'medium lavender magenta': '#dda0dd', + 'medium orchid': '#ba55d3', + 'medium persian blue': '#0067a5', + 'medium purple': '#9370db', + 'medium red-violet': '#bb3385', + 'medium ruby': '#aa4069', + 'medium sea green': '#3cb371', + 'medium slate blue': '#7b68ee', + 'medium spring bud': '#c9dc87', + 'medium spring green': '#00fa9a', + 'medium taupe': '#674c47', + 'medium turquoise': '#48d1cc', + 'medium tuscan red': '#79443b', + 'medium vermilion': '#d9603b', + 'medium violet-red': '#c71585', + 'mellow apricot': '#f8b878', + 'mellow yellow': '#f8de7e', + 'melon': '#fdbcb4', + 'midnight blue': '#191970', + 'midnight green (eagle green)': '#004953', + 'mikado yellow': '#ffc40c', + 'mint': '#3eb489', + 'mint cream': '#f5fffa', + 'mint green': '#98ff98', + 'misty rose': '#ffe4e1', + 'moccasin': '#faebd7', + 'mode beige': '#967117', + 'moonstone blue': '#73a9c2', + 'mordant red 19': '#ae0c00', + 'moss green': '#addfad', + 'mountain meadow': '#30ba8f', + 'mountbatten pink': '#997a8d', + 'msu green': '#18453b', + 'mulberry': '#c54b8c', + 'mustard': '#ffdb58', + 'myrtle': '#21421e', + 'nadeshiko pink': '#f6adc6', + 'napier green': '#2a8000', + 'naples yellow': '#fada5e', + 'navajo white': '#ffdead', + 'navy blue': '#000080', + 'neon carrot': '#ffa343', + 'neon fuchsia': '#fe4164', + 'neon green': '#39ff14', + 'new york pink': '#d7837f', + 'non-photo blue': '#a4dded', + 'north texas green': '#059033', + 'ocean boat blue': '#0077be', + 'ochre': '#c72', + 'office green': '#008000', + 'old gold': '#cfb53b', + 'old lace': '#fdf5e6', + 'old lavender': '#796878', + 'old mauve': '#673147', + 'old rose': '#c08081', + 'olive': '#808000', + 'olive drab #7': '#3c341f', + 'olive drab (web) (olive drab #3)': '#6b8e23', + 'olivine': '#9ab973', + 'onyx': '#353839', + 'opera mauve': '#b784a7', + 'orange (color wheel)': '#ff7f00', + 'orange peel': '#ff9f00', + 'orange-red': '#ff4500', + 'orange (ryb)': '#fb9902', + 'orange (web color)': '#ffa500', + 'orchid': '#da70d6', + 'otter brown': '#654321', + 'ou crimson red': '#900', + 'outer space': '#414a4c', + 'outrageous orange': '#ff6e4a', + 'oxford blue': '#002147', + 'pakistan green': '#060', + 'palatinate blue': '#273be2', + 'palatinate purple': '#682860', + 'pale aqua': '#bcd4e6', + 'pale blue': '#afeeee', + 'pale brown': '#987654', + 'pale carmine': '#af4035', + 'pale cerulean': '#9bc4e2', + 'pale chestnut': '#ddadaf', + 'pale copper': '#da8a67', + 'pale cornflower blue': '#abcdef', + 'pale gold': '#e6be8a', + 'pale goldenrod': '#eee8aa', + 'pale green': '#98fb98', + 'pale lavender': '#dcd0ff', + 'pale magenta': '#f984e5', + 'pale pink': '#fadadd', + 'pale plum': '#dda0dd', + 'pale red-violet': '#db7093', + 'pale robin egg blue': '#96ded1', + 'pale silver': '#c9c0bb', + 'pale spring bud': '#ecebbd', + 'pale taupe': '#bc987e', + 'pale violet-red': '#db7093', + 'pansy purple': '#78184a', + 'papaya whip': '#ffefd5', + 'paris green': '#50c878', + 'pastel blue': '#aec6cf', + 'pastel brown': '#836953', + 'pastel gray': '#cfcfc4', + 'pastel green': '#7d7', + 'pastel magenta': '#f49ac2', + 'pastel orange': '#ffb347', + 'pastel pink': '#dea5a4', + 'pastel purple': '#b39eb5', + 'pastel red': '#ff6961', + 'pastel violet': '#cb99c9', + 'pastel yellow': '#fdfd96', + 'patriarch': '#800080', + "payne's grey": '#536878', + 'peach': '#ffe5b4', + 'peach (crayola)': '#ffcba4', + 'peach-orange': '#fc9', + 'peach puff': '#ffdab9', + 'peach-yellow': '#fadfad', + 'pear': '#d1e231', + 'pearl': '#eae0c8', + 'pearl aqua': '#88d8c0', + 'pearly purple': '#b768a2', + 'peridot': '#e6e200', + 'periwinkle': '#ccf', + 'persian blue': '#1c39bb', + 'persian green': '#00a693', + 'persian indigo': '#32127a', + 'persian orange': '#d99058', + 'persian pink': '#f77fbe', + 'persian plum': '#701c1c', + 'persian red': '#c33', + 'persian rose': '#fe28a2', + 'persimmon': '#ec5800', + 'peru': '#cd853f', + 'phlox': '#df00ff', + 'phthalo blue': '#000f89', + 'phthalo green': '#123524', + 'piggy pink': '#fddde6', + 'pine green': '#01796f', + 'pink': '#ffc0cb', + 'pink lace': '#ffddf4', + 'pink-orange': '#f96', + 'pink pearl': '#e7accf', + 'pink sherbet': '#f78fa7', + 'pistachio': '#93c572', + 'platinum': '#e5e4e2', + 'plum (traditional)': '#8e4585', + 'plum (web)': '#dda0dd', + 'portland orange': '#ff5a36', + 'powder blue (web)': '#b0e0e6', + 'princeton orange': '#ff8f00', + 'prune': '#701c1c', + 'prussian blue': '#003153', + 'psychedelic purple': '#df00ff', + 'puce': '#c89', + 'pumpkin': '#ff7518', + 'purple heart': '#69359c', + 'purple (html/css)': '#800080', + 'purple mountain majesty': '#9678b6', + 'purple (munsell)': '#9f00c5', + 'purple pizzazz': '#fe4eda', + 'purple taupe': '#50404d', + 'purple (x11)': '#a020f0', + 'quartz': '#51484f', + 'rackley': '#5d8aa8', + 'radical red': '#ff355e', + 'rajah': '#fbab60', + 'raspberry': '#e30b5d', + 'raspberry glace': '#915f6d', + 'raspberry pink': '#e25098', + 'raspberry rose': '#b3446c', + 'raw umber': '#826644', + 'razzle dazzle rose': '#f3c', + 'razzmatazz': '#e3256b', + 'red': '#f00', + 'red-brown': '#a52a2a', + 'red devil': '#860111', + 'red (munsell)': '#f2003c', + 'red (ncs)': '#c40233', + 'red-orange': '#ff5349', + 'red (pigment)': '#ed1c24', + 'red (ryb)': '#fe2712', + 'red-violet': '#c71585', + 'redwood': '#ab4e52', + 'regalia': '#522d80', + 'resolution blue': '#002387', + 'rich black': '#004040', + 'rich brilliant lavender': '#f1a7fe', + 'rich carmine': '#d70040', + 'rich electric blue': '#0892d0', + 'rich lavender': '#a76bcf', + 'rich lilac': '#b666d2', + 'rich maroon': '#b03060', + 'rifle green': '#414833', + 'robin egg blue': '#0cc', + 'rose': '#ff007f', + 'rose bonbon': '#f9429e', + 'rose ebony': '#674846', + 'rose gold': '#b76e79', + 'rose madder': '#e32636', + 'rose pink': '#f6c', + 'rose quartz': '#aa98a9', + 'rose taupe': '#905d5d', + 'rose vale': '#ab4e52', + 'rosewood': '#65000b', + 'rosso corsa': '#d40000', + 'rosy brown': '#bc8f8f', + 'royal azure': '#0038a8', + 'royal blue (traditional)': '#002366', + 'royal blue (web)': '#4169e1', + 'royal fuchsia': '#ca2c92', + 'royal purple': '#7851a9', + 'royal yellow': '#fada5e', + 'rubine red': '#d10056', + 'ruby': '#e0115f', + 'ruby red': '#9b111e', + 'ruddy': '#ff0028', + 'ruddy brown': '#bb6528', + 'ruddy pink': '#e18e96', + 'rufous': '#a81c07', + 'russet': '#80461b', + 'rust': '#b7410e', + 'rusty red': '#da2c43', + 'sacramento state green': '#00563f', + 'saddle brown': '#8b4513', + 'safety orange (blaze orange)': '#ff6700', + 'saffron': '#f4c430', + 'salmon': '#ff8c69', + 'salmon pink': '#ff91a4', + 'sand': '#c2b280', + 'sand dune': '#967117', + 'sandstorm': '#ecd540', + 'sandy brown': '#f4a460', + 'sandy taupe': '#967117', + 'sangria': '#92000a', + 'sap green': '#507d2a', + 'sapphire': '#0f52ba', + 'sapphire blue': '#0067a5', + 'satin sheen gold': '#cba135', + 'scarlet': '#ff2400', + 'scarlet (crayola)': '#fd0e35', + 'school bus yellow': '#ffd800', + "screamin' green": '#76ff7a', + 'sea blue': '#006994', + 'sea green': '#2e8b57', + 'seal brown': '#321414', + 'seashell': '#fff5ee', + 'selective yellow': '#ffba00', + 'sepia': '#704214', + 'shadow': '#8a795d', + 'shamrock green': '#009e60', + 'shocking pink': '#fc0fc0', + 'shocking pink (crayola)': '#ff6fff', + 'sienna': '#882d17', + 'silver': '#c0c0c0', + 'sinopia': '#cb410b', + 'skobeloff': '#007474', + 'sky blue': '#87ceeb', + 'sky magenta': '#cf71af', + 'slate blue': '#6a5acd', + 'slate gray': '#708090', + 'smalt (dark powder blue)': '#039', + 'smokey topaz': '#933d41', + 'smoky black': '#100c08', + 'snow': '#fffafa', + 'spiro disco ball': '#0fc0fc', + 'spring bud': '#a7fc00', + 'spring green': '#00ff7f', + "st. patrick's blue": '#23297a', + 'steel blue': '#4682b4', + 'stil de grain yellow': '#fada5e', + 'stizza': '#900', + 'stormcloud': '#4f666a', + 'straw': '#e4d96f', + 'sunglow': '#fc3', + 'sunset': '#fad6a5', + 'tan': '#d2b48c', + 'tangelo': '#f94d00', + 'tangerine': '#f28500', + 'tangerine yellow': '#fc0', + 'tango pink': '#e4717a', + 'taupe': '#483c32', + 'taupe gray': '#8b8589', + 'tea green': '#d0f0c0', + 'tea rose (orange)': '#f88379', + 'tea rose (rose)': '#f4c2c2', + 'teal': '#008080', + 'teal blue': '#367588', + 'teal green': '#00827f', + 'telemagenta': '#cf3476', + 'tenné (tawny)': '#cd5700', + 'terra cotta': '#e2725b', + 'thistle': '#d8bfd8', + 'thulian pink': '#de6fa1', + 'tickle me pink': '#fc89ac', + 'tiffany blue': '#0abab5', + "tiger's eye": '#e08d3c', + 'timberwolf': '#dbd7d2', + 'titanium yellow': '#eee600', + 'tomato': '#ff6347', + 'toolbox': '#746cc0', + 'topaz': '#ffc87c', + 'tractor red': '#fd0e35', + 'trolley grey': '#808080', + 'tropical rain forest': '#00755e', + 'true blue': '#0073cf', + 'tufts blue': '#417dc1', + 'tumbleweed': '#deaa88', + 'turkish rose': '#b57281', + 'turquoise': '#30d5c8', + 'turquoise blue': '#00ffef', + 'turquoise green': '#a0d6b4', + 'tuscan red': '#7c4848', + 'twilight lavender': '#8a496b', + 'tyrian purple': '#66023c', + 'ua blue': '#03a', + 'ua red': '#d9004c', + 'ube': '#8878c3', + 'ucla blue': '#536895', + 'ucla gold': '#ffb300', + 'ufo green': '#3cd070', + 'ultra pink': '#ff6fff', + 'ultramarine': '#120a8f', + 'ultramarine blue': '#4166f5', + 'umber': '#635147', + 'unbleached silk': '#ffddca', + 'united nations blue': '#5b92e5', + 'university of california gold': '#b78727', + 'unmellow yellow': '#ff6', + 'up forest green': '#014421', + 'up maroon': '#7b1113', + 'upsdell red': '#ae2029', + 'urobilin': '#e1ad21', + 'usafa blue': '#004f98', + 'usc cardinal': '#900', + 'usc gold': '#fc0', + 'utah crimson': '#d3003f', + 'vanilla': '#f3e5ab', + 'vegas gold': '#c5b358', + 'venetian red': '#c80815', + 'verdigris': '#43b3ae', + 'vermilion (cinnabar)': '#e34234', + 'vermilion (plochere)': '#d9603b', + 'veronica': '#a020f0', + 'violet': '#8f00ff', + 'violet-blue': '#324ab2', + 'violet (color wheel)': '#7f00ff', + 'violet (ryb)': '#8601af', + 'violet (web)': '#ee82ee', + 'viridian': '#40826d', + 'vivid auburn': '#922724', + 'vivid burgundy': '#9f1d35', + 'vivid cerise': '#da1d81', + 'vivid tangerine': '#ffa089', + 'vivid violet': '#9f00ff', + 'warm black': '#004242', + 'waterspout': '#a4f4f9', + 'wenge': '#645452', + 'wheat': '#f5deb3', + 'white': '#fff', + 'white smoke': '#f5f5f5', + 'wild blue yonder': '#a2add0', + 'wild strawberry': '#ff43a4', + 'wild watermelon': '#fc6c85', + 'wine': '#722f37', + 'wine dregs': '#673147', + 'wisteria': '#c9a0dc', + 'wood brown': '#c19a6b', + 'xanadu': '#738678', + 'yale blue': '#0f4d92', + 'yellow': '#ff0', + 'yellow-green': '#9acd32', + 'yellow (munsell)': '#efcc00', + 'yellow (ncs)': '#ffd300', + 'yellow orange': '#ffae42', + 'yellow (process)': '#ffef00', + 'yellow (ryb)': '#fefe33', + 'zaffre': '#0014a8', + 'zinnwaldite brown': '#2c1608' +} + +palettes = { + 'xkcd':XKCD_NAME_TO_HEX, + 'wiki_color': WIKI_COLOR_NAME_TO_HEX, + 'flux_sdxl': FLUX_SDXL_NAME_TO_HEX, + 'css4':CSS4_NAME_TO_HEX, + 'css3':CSS3_NAMES_TO_HEX, + 'html4':HTML4_NAMES_TO_HEX +} + +import torch +import re +from PIL import Image +from .imagefunc import Hex_to_RGB, AnyType, pil2tensor, tensor2pil, log, load_custom_size, find_best_match_by_similarity + +any = AnyType("*") + + +class LS_ColorName: + + def __init__(self): + self.NODE_NAME = 'ColorName' + + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "color": ("STRING", {"default": "#000000", "forceInput":False},), + "palette": (list(palettes.keys()),), + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("color_name",) + FUNCTION = 'get_color_name' + CATEGORY = '😺dzNodes/LayerColor' + + def get_color_name(self, color, palette): + + (r, g, b) = Hex_to_RGB(color) + + if palette == "flux_sdxl": + color_table = FLUX_SDXL_NAME_TO_HEX + elif palette == "wiki_color": + color_table = WIKI_COLOR_NAME_TO_HEX + elif palette == "xkcd": + color_table = XKCD_NAME_TO_HEX + elif palette == "css4": + color_table = CSS4_NAME_TO_HEX + elif palette == "css3": + color_table = CSS3_NAMES_TO_HEX + else: + color_table = HTML4_NAMES_TO_HEX + + min_colors = {} + for name, hex_code in color_table.items(): + r_c, g_c, b_c = Hex_to_RGB(hex_code) + rd = (r_c - r) ** 2 + gd = (g_c - g) ** 2 + bd = (b_c - b) ** 2 + min_colors[(rd + gd + bd)] = name + color_name = min_colors[min(min_colors.keys())] + + return (color_name,) + + +class LS_NameToColor: + + def __init__(self): + self.NODE_NAME = 'NameToColor' + + @classmethod + def INPUT_TYPES(self): + size_list = ['custom'] + size_list.extend(load_custom_size()) + return { + "required": { + "color_name": ("STRING", {"default": "white", "forceInput":False},), + "palette": (list(palettes.keys()),), + "in_palette_only": ("BOOLEAN", {"default": False}), # 仅在当前颜色表中查找 + "default_color": ("STRING", {"default": "#000000", "forceInput": False},), + "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}), + }, + "optional": { + "size_as": (any, {}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING",) + RETURN_NAMES = ("image", "color",) + FUNCTION = 'name2color' + CATEGORY = '😺dzNodes/LayerColor' + + def name2color(self, color_name, palette, in_palette_only, default_color, size, custom_width, custom_height, size_as=None): + + if palette == "flux_sdxl": + color_table = FLUX_SDXL_NAME_TO_HEX + elif palette == "wiki_color": + color_table = WIKI_COLOR_NAME_TO_HEX + elif palette == "xkcd": + color_table = XKCD_NAME_TO_HEX + elif palette == "css4": + color_table = CSS4_NAME_TO_HEX + elif palette == "css3": + color_table = CSS3_NAMES_TO_HEX + else: + color_table = HTML4_NAMES_TO_HEX + + 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 + + color_name = color_name.lower() + print(f"color_name={color_name}") + + ret_color = "" + try: + ret_color = color_table[color_name] + except KeyError: + if not in_palette_only: + for table_name, table in palettes.items(): + try: + ret_color = table[color_name] + break + except KeyError: + pass + if ret_color != "": + log(f'{self.NODE_NAME}: "{color_name}" not in current color table, find it in "{table_name}".') + else: # 在全部色表中寻找最近似名称 + match_keys = {} + for table_name, table in palettes.items(): + match_key = find_best_match_by_similarity(color_name, list(table.keys())) + if match_key is not None: + match_keys[match_key] = table_name + + if match_keys == {}: + log(f'{self.NODE_NAME}: "{color_name}" not in color tables and not find any approximation, return default color.') + ret_color = default_color + else: + print(f"finded {len(match_keys)} keys:{match_keys}") + match_key = find_best_match_by_similarity(color_name, list(match_keys.keys())) + log(f'{self.NODE_NAME}: "{color_name}" not in color tables, return the approximation "{match_key}" in "{match_keys[match_key]}".') + ret_color = palettes[match_keys[match_key]][match_key] + + else: + log(f'{self.NODE_NAME}: "{color_name}" not in current color table, return default color.') + ret_color = default_color + + ret_image = Image.new('RGB', (width, height), color=ret_color) + + return (pil2tensor(ret_image), ret_color,) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ColorName": LS_ColorName, + "LayerUtility: NameToColor": LS_NameToColor, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ColorName": "LayerUtility: Color Name", + "LayerUtility: NameToColor": "LayerUtility: Name To Color", } \ No newline at end of file diff --git a/py/color_negative.py b/py/color_negative.py old mode 100644 new mode 100755 index 51e704a7..f9d1423a --- a/py/color_negative.py +++ b/py/color_negative.py @@ -1,101 +1,101 @@ -import torch -from .imagefunc import log, tensor2pil, pil2tensor -from .imagefunc import image_channel_merge, RGB2RGBA - - - - -negative_channel_list = ["RGB", "Mono", "R", "G", "B",] - - -def invert_specific_channel(image: torch.Tensor, channels_to_invert: list) -> torch.Tensor: - """ - 对图像中特定的通道进行颜色取反,其余通道保持不变。 - - 参数: - image (Tensor): 形状为 (B, H, W, 3),值在 [0.0, 1.0] 的 float 类型张量。 - channels_to_invert (list): 要取反的通道索引,例如 [0] 表示只取反 R 通道。 - - 返回: - Tensor: 修改后的图像张量。 - """ - - result = image.clone() - - for ch in channels_to_invert: - if ch < 0 or ch > 2: - raise ValueError(f"Invalid channel index: {ch}") - result[..., ch] = 1.0 - result[..., ch] - - return result - - -def rgb_to_grayscale(image: torch.Tensor) -> torch.Tensor: - """ - 将 RGB 图像转换为灰度图。 - - 参数: - image (Tensor): 形状为 (B, H, W, 3),值在 [0.0, 1.0] 的 float 类型张量。 - - 返回: - Tensor: 形状为 (B, H, W, 1) 的灰度图张量。 - """ - - # 定义加权系数 - weights = torch.tensor([0.2989, 0.5870, 0.1140], device=image.device).view(1, 1, 1, 3) - - # 加权求和 - grayscale = (image * weights).sum(dim=-1, keepdim=True) # 结果 shape: (B, H, W, 1) - - return grayscale.expand(-1,-1,-1,3) - -class LS_ColorNegative: - - def __init__(self): - self.NODE_NAME = 'ColorNegative' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), # - "negative_channel" : (negative_channel_list,), - }, - "optional": { - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = 'color_correct_negative' - CATEGORY = '😺dzNodes/LayerColor' - - def color_correct_negative(self, image, negative_channel,): - if image.shape[3] == 4: - rgb_image = image[..., :3] - else: - rgb_image = image - - if negative_channel == "RGB": - ret_image = invert_specific_channel(rgb_image, [0, 1, 2]) - elif negative_channel == "Mono": - mono_image = rgb_to_grayscale(rgb_image) - ret_image = invert_specific_channel(mono_image, [0, 1, 2]) - elif negative_channel == "R": - ret_image = invert_specific_channel(rgb_image, [0]) - elif negative_channel == "G": - ret_image = invert_specific_channel(rgb_image, [1]) - elif negative_channel == "B": - ret_image = invert_specific_channel(rgb_image, [2]) - - return (ret_image,) - - -NODE_CLASS_MAPPINGS = { - "LayerColor: Negative": LS_ColorNegative -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerColor: Negative": "LayerColor: Negative" +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import image_channel_merge, RGB2RGBA + + + + +negative_channel_list = ["RGB", "Mono", "R", "G", "B",] + + +def invert_specific_channel(image: torch.Tensor, channels_to_invert: list) -> torch.Tensor: + """ + 对图像中特定的通道进行颜色取反,其余通道保持不变。 + + 参数: + image (Tensor): 形状为 (B, H, W, 3),值在 [0.0, 1.0] 的 float 类型张量。 + channels_to_invert (list): 要取反的通道索引,例如 [0] 表示只取反 R 通道。 + + 返回: + Tensor: 修改后的图像张量。 + """ + + result = image.clone() + + for ch in channels_to_invert: + if ch < 0 or ch > 2: + raise ValueError(f"Invalid channel index: {ch}") + result[..., ch] = 1.0 - result[..., ch] + + return result + + +def rgb_to_grayscale(image: torch.Tensor) -> torch.Tensor: + """ + 将 RGB 图像转换为灰度图。 + + 参数: + image (Tensor): 形状为 (B, H, W, 3),值在 [0.0, 1.0] 的 float 类型张量。 + + 返回: + Tensor: 形状为 (B, H, W, 1) 的灰度图张量。 + """ + + # 定义加权系数 + weights = torch.tensor([0.2989, 0.5870, 0.1140], device=image.device).view(1, 1, 1, 3) + + # 加权求和 + grayscale = (image * weights).sum(dim=-1, keepdim=True) # 结果 shape: (B, H, W, 1) + + return grayscale.expand(-1,-1,-1,3) + +class LS_ColorNegative: + + def __init__(self): + self.NODE_NAME = 'ColorNegative' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "negative_channel" : (negative_channel_list,), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_negative' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_negative(self, image, negative_channel,): + if image.shape[3] == 4: + rgb_image = image[..., :3] + else: + rgb_image = image + + if negative_channel == "RGB": + ret_image = invert_specific_channel(rgb_image, [0, 1, 2]) + elif negative_channel == "Mono": + mono_image = rgb_to_grayscale(rgb_image) + ret_image = invert_specific_channel(mono_image, [0, 1, 2]) + elif negative_channel == "R": + ret_image = invert_specific_channel(rgb_image, [0]) + elif negative_channel == "G": + ret_image = invert_specific_channel(rgb_image, [1]) + elif negative_channel == "B": + ret_image = invert_specific_channel(rgb_image, [2]) + + return (ret_image,) + + +NODE_CLASS_MAPPINGS = { + "LayerColor: Negative": LS_ColorNegative +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: Negative": "LayerColor: Negative" } \ No newline at end of file diff --git a/py/color_overlay _v2.py b/py/color_overlay _v2.py old mode 100644 new mode 100755 index 6710dcd7..621144d9 --- a/py/color_overlay _v2.py +++ b/py/color_overlay _v2.py @@ -1,91 +1,91 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_overlay.py b/py/color_overlay.py old mode 100644 new mode 100755 diff --git a/py/color_picker.py b/py/color_picker.py old mode 100644 new mode 100755 diff --git a/py/color_to_HSVvalue.py b/py/color_to_HSVvalue.py old mode 100644 new mode 100755 index 207f99e6..5cdbb40c --- a/py/color_to_HSVvalue.py +++ b/py/color_to_HSVvalue.py @@ -1,43 +1,43 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_to_RGBvalue.py b/py/color_to_RGBvalue.py old mode 100644 new mode 100755 index 7694ecfa..34dcea85 --- a/py/color_to_RGBvalue.py +++ b/py/color_to_RGBvalue.py @@ -1,45 +1,45 @@ -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" +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" } \ No newline at end of file diff --git a/py/color_to_gray_value.py b/py/color_to_gray_value.py old mode 100644 new mode 100755 index 19e0c36f..dc22620f --- a/py/color_to_gray_value.py +++ b/py/color_to_gray_value.py @@ -1,37 +1,37 @@ -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" +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" } \ No newline at end of file diff --git a/py/create_gradient_mask.py b/py/create_gradient_mask.py old mode 100644 new mode 100755 index 03e5441e..9271d826 --- a/py/create_gradient_mask.py +++ b/py/create_gradient_mask.py @@ -1,125 +1,125 @@ -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" +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" } \ No newline at end of file diff --git a/py/crop_box_resolve.py b/py/crop_box_resolve.py old mode 100644 new mode 100755 index bca827e4..7c8655aa --- a/py/crop_box_resolve.py +++ b/py/crop_box_resolve.py @@ -1,43 +1,43 @@ - - - -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" + + + +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" } \ No newline at end of file diff --git a/py/crop_by_mask.py b/py/crop_by_mask.py old mode 100644 new mode 100755 diff --git a/py/crop_by_mask_v2.py b/py/crop_by_mask_v2.py old mode 100644 new mode 100755 index 9f994a20..8968af46 --- a/py/crop_by_mask_v2.py +++ b/py/crop_by_mask_v2.py @@ -1,116 +1,116 @@ -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" +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" } \ No newline at end of file diff --git a/py/crop_by_mask_v3.py b/py/crop_by_mask_v3.py old mode 100644 new mode 100755 index f4056151..7aca5280 --- a/py/crop_by_mask_v3.py +++ b/py/crop_by_mask_v3.py @@ -1,116 +1,116 @@ -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" +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" } \ No newline at end of file diff --git a/py/data_nodes.py b/py/data_nodes.py old mode 100644 new mode 100755 index e0f51064..4da2b90f --- a/py/data_nodes.py +++ b/py/data_nodes.py @@ -1,496 +1,496 @@ -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" +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" } \ No newline at end of file diff --git a/py/drop_shadow.py b/py/drop_shadow.py old mode 100644 new mode 100755 diff --git a/py/drop_shadow_v2.py b/py/drop_shadow_v2.py old mode 100644 new mode 100755 index e67ec8c7..9146e427 --- a/py/drop_shadow_v2.py +++ b/py/drop_shadow_v2.py @@ -1,111 +1,111 @@ -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" +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" } \ No newline at end of file diff --git a/py/drop_shadow_v3.py b/py/drop_shadow_v3.py old mode 100644 new mode 100755 index 53cd7385..5c3ebf76 --- a/py/drop_shadow_v3.py +++ b/py/drop_shadow_v3.py @@ -1,114 +1,114 @@ -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" +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" } \ No newline at end of file diff --git a/py/extend_canvas.py b/py/extend_canvas.py old mode 100644 new mode 100755 index c64c4c6e..c9f15487 --- a/py/extend_canvas.py +++ b/py/extend_canvas.py @@ -1,90 +1,90 @@ -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" +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" } \ No newline at end of file diff --git a/py/extend_canvas_v2.py b/py/extend_canvas_v2.py old mode 100644 new mode 100755 index 09759ef3..8fedea47 --- a/py/extend_canvas_v2.py +++ b/py/extend_canvas_v2.py @@ -1,97 +1,97 @@ -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" +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" } \ No newline at end of file diff --git a/py/film_post.py b/py/film_post.py old mode 100644 new mode 100755 index 2fdca4ab..815b5d33 --- a/py/film_post.py +++ b/py/film_post.py @@ -1,91 +1,91 @@ -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" +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" } \ No newline at end of file diff --git a/py/film_post_v2.py b/py/film_post_v2.py old mode 100644 new mode 100755 index de1e0ef2..1c3d04c6 --- a/py/film_post_v2.py +++ b/py/film_post_v2.py @@ -1,97 +1,97 @@ -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" +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" } \ No newline at end of file diff --git a/py/filmgrainer/__init__.py b/py/filmgrainer/__init__.py old mode 100644 new mode 100755 diff --git a/py/filmgrainer/filmgrainer.py b/py/filmgrainer/filmgrainer.py old mode 100644 new mode 100755 index 67092128..1e9838ca --- a/py/filmgrainer/filmgrainer.py +++ b/py/filmgrainer/filmgrainer.py @@ -1,117 +1,117 @@ -# 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) - +# 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 \ No newline at end of file diff --git a/py/filmgrainer/graingamma.py b/py/filmgrainer/graingamma.py old mode 100644 new mode 100755 index 0f3cc6e4..9f7d6bf0 --- a/py/filmgrainer/graingamma.py +++ b/py/filmgrainer/graingamma.py @@ -1,113 +1,113 @@ -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) +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() \ No newline at end of file diff --git a/py/filmgrainer/graingen.py b/py/filmgrainer/graingen.py old mode 100644 new mode 100755 index c3ea62ea..9e24ad29 --- a/py/filmgrainer/graingen.py +++ b/py/filmgrainer/graingen.py @@ -1,61 +1,61 @@ -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) +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]) \ No newline at end of file diff --git a/py/filmgrainer/processing.py b/py/filmgrainer/processing.py old mode 100644 new mode 100755 index 4c48e8e5..c3e3ac20 --- a/py/filmgrainer/processing.py +++ b/py/filmgrainer/processing.py @@ -1,32 +1,32 @@ -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 +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 \ No newline at end of file diff --git a/py/flux_kontext_image_scale.py b/py/flux_kontext_image_scale.py old mode 100644 new mode 100755 index d5c3c462..a0b8bfa7 --- a/py/flux_kontext_image_scale.py +++ b/py/flux_kontext_image_scale.py @@ -1,77 +1,77 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, fit_resize_image - - -PREFERED_KONTEXT_RESOLUTIONS = [ - (672, 1568), - (688, 1504), - (720, 1456), - (752, 1392), - (800, 1328), - (832, 1248), - (880, 1184), - (944, 1104), - (1024, 1024), - (1104, 944), - (1184, 880), - (1248, 832), - (1328, 800), - (1392, 752), - (1456, 720), - (1504, 688), - (1568, 672), -] - - -class LS_FluxKontextImageScale: - @classmethod - def INPUT_TYPES(s): - method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] - return {"required": {"image": ("IMAGE", ), - "method": (method_mode,), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "scale" - - CATEGORY = '😺dzNodes/LayerUtility' - DESCRIPTION = "This node resizes the image to one that is more optimal for flux kontext. For images with different aspect ratio, the scale will be adjusted appropriately to maintain all information" - - def scale(self, image, method): - ret_images = [] - - width = image.shape[2] - height = image.shape[1] - aspect_ratio = width / height - _, target_width, target_height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS) - # image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1) - - 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 img in image: - _image = torch.unsqueeze(img, 0) - _image = tensor2pil(img).convert('RGB') - resized_image = fit_resize_image(_image, target_width, target_height, 'fill', resize_sampler) - ret_images.append(pil2tensor(resized_image)) - - return (torch.cat(ret_images, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: FluxKontextImageScale": LS_FluxKontextImageScale -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: FluxKontextImageScale": "LayerUtility: Flux Kontext Image Scale" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, fit_resize_image + + +PREFERED_KONTEXT_RESOLUTIONS = [ + (672, 1568), + (688, 1504), + (720, 1456), + (752, 1392), + (800, 1328), + (832, 1248), + (880, 1184), + (944, 1104), + (1024, 1024), + (1104, 944), + (1184, 880), + (1248, 832), + (1328, 800), + (1392, 752), + (1456, 720), + (1504, 688), + (1568, 672), +] + + +class LS_FluxKontextImageScale: + @classmethod + def INPUT_TYPES(s): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return {"required": {"image": ("IMAGE", ), + "method": (method_mode,), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "scale" + + CATEGORY = '😺dzNodes/LayerUtility' + DESCRIPTION = "This node resizes the image to one that is more optimal for flux kontext. For images with different aspect ratio, the scale will be adjusted appropriately to maintain all information" + + def scale(self, image, method): + ret_images = [] + + width = image.shape[2] + height = image.shape[1] + aspect_ratio = width / height + _, target_width, target_height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS) + # image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1) + + 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 img in image: + _image = torch.unsqueeze(img, 0) + _image = tensor2pil(img).convert('RGB') + resized_image = fit_resize_image(_image, target_width, target_height, 'fill', resize_sampler) + ret_images.append(pil2tensor(resized_image)) + + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: FluxKontextImageScale": LS_FluxKontextImageScale +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: FluxKontextImageScale": "LayerUtility: Flux Kontext Image Scale" } \ No newline at end of file diff --git a/py/gaussian_blur.py b/py/gaussian_blur.py old mode 100644 new mode 100755 diff --git a/py/get_image_size.py b/py/get_image_size.py old mode 100644 new mode 100755 diff --git a/py/get_main_colors.py b/py/get_main_colors.py old mode 100644 new mode 100755 index 783a2569..d18dbbbd --- a/py/get_main_colors.py +++ b/py/get_main_colors.py @@ -1,236 +1,236 @@ -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", - +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", + } \ No newline at end of file diff --git a/py/gradient_image.py b/py/gradient_image.py old mode 100644 new mode 100755 diff --git a/py/gradient_image_v2.py b/py/gradient_image_v2.py old mode 100644 new mode 100755 index 9db41549..676c5e9d --- a/py/gradient_image_v2.py +++ b/py/gradient_image_v2.py @@ -1,71 +1,71 @@ -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" +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" } \ No newline at end of file diff --git a/py/gradient_map.py b/py/gradient_map.py old mode 100644 new mode 100755 index a8e07a1f..588163aa --- a/py/gradient_map.py +++ b/py/gradient_map.py @@ -1,86 +1,86 @@ -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" +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" } \ No newline at end of file diff --git a/py/gradient_overlay.py b/py/gradient_overlay.py old mode 100644 new mode 100755 diff --git a/py/gradient_overlay_v2.py b/py/gradient_overlay_v2.py old mode 100644 new mode 100755 index 90c38833..51ca7074 --- a/py/gradient_overlay_v2.py +++ b/py/gradient_overlay_v2.py @@ -1,102 +1,102 @@ -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" +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" } \ No newline at end of file diff --git a/py/halftone.py b/py/halftone.py old mode 100644 new mode 100755 index fa1f3d08..7fa7c717 --- a/py/halftone.py +++ b/py/halftone.py @@ -1,186 +1,186 @@ -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" +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" } \ No newline at end of file diff --git a/py/hdr_effects.py b/py/hdr_effects.py old mode 100644 new mode 100755 index 650e528d..cb47b702 --- a/py/hdr_effects.py +++ b/py/hdr_effects.py @@ -1,164 +1,164 @@ -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" +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" } \ No newline at end of file diff --git a/py/hl_frequency_detail_restore.py b/py/hl_frequency_detail_restore.py old mode 100644 new mode 100755 index 03750089..6612a051 --- a/py/hl_frequency_detail_restore.py +++ b/py/hl_frequency_detail_restore.py @@ -1,87 +1,87 @@ -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" +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" } \ No newline at end of file diff --git a/py/ic_mask.py b/py/ic_mask.py old mode 100644 new mode 100755 index b8976413..b87ea101 --- a/py/ic_mask.py +++ b/py/ic_mask.py @@ -1,244 +1,244 @@ -# 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", +# 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", } \ No newline at end of file diff --git a/py/image_blend.py b/py/image_blend.py old mode 100644 new mode 100755 diff --git a/py/image_blend_advance.py b/py/image_blend_advance.py old mode 100644 new mode 100755 diff --git a/py/image_blend_advance_v2.py b/py/image_blend_advance_v2.py old mode 100644 new mode 100755 index b291d96d..82657a34 --- a/py/image_blend_advance_v2.py +++ b/py/image_blend_advance_v2.py @@ -1,135 +1,135 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_blend_advance_v3.py b/py/image_blend_advance_v3.py old mode 100644 new mode 100755 index 1a92de3b..7ed2e5a8 --- a/py/image_blend_advance_v3.py +++ b/py/image_blend_advance_v3.py @@ -1,141 +1,141 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_blend_v2.py b/py/image_blend_v2.py old mode 100644 new mode 100755 index 8e94d63b..b9d259cc --- a/py/image_blend_v2.py +++ b/py/image_blend_v2.py @@ -1,91 +1,91 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_channel_merge.py b/py/image_channel_merge.py old mode 100644 new mode 100755 diff --git a/py/image_channel_split.py b/py/image_channel_split.py old mode 100644 new mode 100755 diff --git a/py/image_combine_alpha.py b/py/image_combine_alpha.py old mode 100644 new mode 100755 index 0de6f6d1..7aa097f4 --- a/py/image_combine_alpha.py +++ b/py/image_combine_alpha.py @@ -1,59 +1,59 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_hub.py b/py/image_hub.py old mode 100644 new mode 100755 index 32700411..eed96b71 --- a/py/image_hub.py +++ b/py/image_hub.py @@ -1,152 +1,152 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_mask_scale_as.py b/py/image_mask_scale_as.py old mode 100644 new mode 100755 diff --git a/py/image_opacity.py b/py/image_opacity.py old mode 100644 new mode 100755 diff --git a/py/image_reel.py b/py/image_reel.py old mode 100644 new mode 100755 index 936b46df..33aaa587 --- a/py/image_reel.py +++ b/py/image_reel.py @@ -1,224 +1,224 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_remove_alpha.py b/py/image_remove_alpha.py old mode 100644 new mode 100755 index d85b6f19..da76992c --- a/py/image_remove_alpha.py +++ b/py/image_remove_alpha.py @@ -1,64 +1,64 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_scale_by_aspect_ratio.py b/py/image_scale_by_aspect_ratio.py old mode 100644 new mode 100755 index bbb83e60..11d9e354 --- a/py/image_scale_by_aspect_ratio.py +++ b/py/image_scale_by_aspect_ratio.py @@ -1,158 +1,158 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_scale_by_aspect_ratio_v2.py b/py/image_scale_by_aspect_ratio_v2.py old mode 100644 new mode 100755 index c6e573c1..3b169fc6 --- a/py/image_scale_by_aspect_ratio_v2.py +++ b/py/image_scale_by_aspect_ratio_v2.py @@ -1,186 +1,186 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_scale_restore.py b/py/image_scale_restore.py old mode 100644 new mode 100755 diff --git a/py/image_scale_restore_v2.py b/py/image_scale_restore_v2.py old mode 100644 new mode 100755 index 3007676c..1b5d180f --- a/py/image_scale_restore_v2.py +++ b/py/image_scale_restore_v2.py @@ -1,132 +1,132 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_shift.py b/py/image_shift.py old mode 100644 new mode 100755 index f91d56f0..2ee76f1a --- a/py/image_shift.py +++ b/py/image_shift.py @@ -1,88 +1,88 @@ -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" +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" } \ No newline at end of file diff --git a/py/image_tagger_save.py b/py/image_tagger_save.py old mode 100644 new mode 100755 index 1d886476..167d540b --- a/py/image_tagger_save.py +++ b/py/image_tagger_save.py @@ -1,288 +1,288 @@ -import os.path -from pathlib import 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 } } - -class LSImageTaggerSave_V2: - def __init__(self): - self.output_dir = folder_paths.get_output_directory() - self.type = "output" - self.prefix_append = "" - self.compress_level = 4 - self.NODE_NAME = 'ImageTaggerSaveV2' - - @classmethod - def INPUT_TYPES(s): - return {"required": - {"image": ("IMAGE", ), - "tag_text": ("STRING", {"default": "", "forceInput":True}), - "custom_path": ("STRING", {"default": ""}), - "custom_filename": ("STRING", {"default": ""}), - "remove_custom_filename_ext": ("BOOLEAN", {"default": True}), - "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_v2" - OUTPUT_NODE = True - CATEGORY = '😺dzNodes/LayerUtility/SystemIO' - - def image_tagger_save_v2(self, image, tag_text, custom_path, custom_filename, remove_custom_filename_ext, - 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]) - - if custom_filename != "": - if remove_custom_filename_ext: - file = Path(custom_filename).stem - else: - file = custom_filename - - if timestamp == "millisecond": - file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}' - elif timestamp == "second": - file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S")}' - - else: - 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}' - - 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)) - - 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') - - if custom_filename == "": - # 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, - "LayerUtility: ImageTaggerSaveV2": LSImageTaggerSave_V2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save", - "LayerUtility: ImageTaggerSaveV2": "LayerUtility: Image Tagger Save V2", +import os.path +from pathlib import 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 } } + +class LSImageTaggerSave_V2: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + self.NODE_NAME = 'ImageTaggerSaveV2' + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"image": ("IMAGE", ), + "tag_text": ("STRING", {"default": "", "forceInput":True}), + "custom_path": ("STRING", {"default": ""}), + "custom_filename": ("STRING", {"default": ""}), + "remove_custom_filename_ext": ("BOOLEAN", {"default": True}), + "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_v2" + OUTPUT_NODE = True + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def image_tagger_save_v2(self, image, tag_text, custom_path, custom_filename, remove_custom_filename_ext, + 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]) + + if custom_filename != "": + if remove_custom_filename_ext: + file = Path(custom_filename).stem + else: + file = custom_filename + + if timestamp == "millisecond": + file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}' + elif timestamp == "second": + file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S")}' + + else: + 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}' + + 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)) + + 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') + + if custom_filename == "": + # 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, + "LayerUtility: ImageTaggerSaveV2": LSImageTaggerSave_V2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save", + "LayerUtility: ImageTaggerSaveV2": "LayerUtility: Image Tagger Save V2", } \ No newline at end of file diff --git a/py/image_to_mask.py b/py/image_to_mask.py old mode 100644 new mode 100755 index 5264b0d8..7c17e2f1 --- a/py/image_to_mask.py +++ b/py/image_to_mask.py @@ -1,111 +1,111 @@ -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" +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" } \ No newline at end of file diff --git a/py/imagefunc.py b/py/imagefunc.py old mode 100644 new mode 100755 diff --git a/py/inner_glow.py b/py/inner_glow.py old mode 100644 new mode 100755 diff --git a/py/inner_glow_v2.py b/py/inner_glow_v2.py old mode 100644 new mode 100755 index 9916bb49..58863874 --- a/py/inner_glow_v2.py +++ b/py/inner_glow_v2.py @@ -1,114 +1,114 @@ -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" +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" } \ No newline at end of file diff --git a/py/inner_shadow.py b/py/inner_shadow.py old mode 100644 new mode 100755 diff --git a/py/inner_shadow_v2.py b/py/inner_shadow_v2.py old mode 100644 new mode 100755 index 2e0108b0..76510cda --- a/py/inner_shadow_v2.py +++ b/py/inner_shadow_v2.py @@ -1,103 +1,103 @@ -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" +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" } \ No newline at end of file diff --git a/py/layer_image_transform.py b/py/layer_image_transform.py old mode 100644 new mode 100755 index e8ecb255..f763da0c --- a/py/layer_image_transform.py +++ b/py/layer_image_transform.py @@ -1,94 +1,94 @@ -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" +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" } \ No newline at end of file diff --git a/py/layer_mask_transform.py b/py/layer_mask_transform.py old mode 100644 new mode 100755 index 6b3ce60e..9a52972b --- a/py/layer_mask_transform.py +++ b/py/layer_mask_transform.py @@ -1,81 +1,81 @@ -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" +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" } \ No newline at end of file diff --git a/py/light_leak.py b/py/light_leak.py old mode 100644 new mode 100755 index 806f4993..c84c4d08 --- a/py/light_leak.py +++ b/py/light_leak.py @@ -1,85 +1,85 @@ -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" +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" } \ No newline at end of file diff --git a/py/load_images_from_path.py b/py/load_images_from_path.py old mode 100644 new mode 100755 index 4a7bf500..d980519b --- a/py/load_images_from_path.py +++ b/py/load_images_from_path.py @@ -1,110 +1,110 @@ -import os -from PIL import Image, ImageSequence, ImageOps -import torch -import numpy as np -import folder_paths -import node_helpers - -class LS_LoadImagesFromPath: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "path": ("STRING", {"placeholder": "c:/images", "images_path": []}), - }, - "optional": { - "image_load_cap": ("INT", {"default": 0, "min": 0, "max": 999999, "step": 1}), - "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT") - RETURN_NAMES = ("images", "masks", "file_name", "frame_count") - FUNCTION = "ls_load_images" - CATEGORY = '😺dzNodes/LayerUtility/SystemIO' - OUTPUT_IS_LIST = (True, True, True, False) - - - def ls_load_images(self, path: str, image_load_cap: int, select_every_nth: int): - load_images = [] - load_masks = [] - load_file_names = [] - load_frame_count = 0 - - - if os.path.isdir(path): - input_dir = os.path.normpath(path) - files = [ - os.path.join(input_dir, f) - for f in os.listdir(input_dir) - if os.path.isfile(os.path.join(input_dir, f)) - ] - - for i in range(len(files)): - - if i % select_every_nth != 0: - continue - - image_file = files[i] - image_path = folder_paths.get_annotated_filepath(image_file) - img = node_helpers.pillow(Image.open, image_path) - output_images = [] - output_masks = [] - w, h = None, None - - excluded_formats = ['MPO'] - - for i in ImageSequence.Iterator(img): - i = node_helpers.pillow(ImageOps.exif_transpose, i) - - if i.mode == 'I': - i = i.point(lambda i: i * (1 / 255)) - image = i.convert("RGB") - - if len(output_images) == 0: - w = image.size[0] - h = image.size[1] - - if image.size[0] != w or image.size[1] != h: - continue - - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image)[None,] - if 'A' in i.getbands(): - mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) - elif i.mode == 'P' and 'transparency' in i.info: - mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) - else: - mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") - output_images.append(image) - output_masks.append(mask.unsqueeze(0)) - - if len(output_images) > 1 and img.format not in excluded_formats: - output_image = torch.cat(output_images, dim=0) - output_mask = torch.cat(output_masks, dim=0) - else: - output_image = output_images[0] - output_mask = output_masks[0] - - load_images.append(output_image) - load_masks.append(output_mask) - load_file_names.append(os.path.basename(image_file)) - load_frame_count += 1 - if image_load_cap > 0 and load_frame_count >= image_load_cap: - break - - return (load_images, load_masks, load_file_names, load_frame_count) - - else: - raise Exception("directory is not valid: " + directory) - - -NODE_CLASS_MAPPINGS = { - "LayerUtility: LoadImagesFromPath": LS_LoadImagesFromPath, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: LoadImagesFromPath": "LayerUtility: Load Images From Path", +import os +from PIL import Image, ImageSequence, ImageOps +import torch +import numpy as np +import folder_paths +import node_helpers + +class LS_LoadImagesFromPath: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "path": ("STRING", {"placeholder": "c:/images", "images_path": []}), + }, + "optional": { + "image_load_cap": ("INT", {"default": 0, "min": 0, "max": 999999, "step": 1}), + "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}), + }, + } + + RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT") + RETURN_NAMES = ("images", "masks", "file_name", "frame_count") + FUNCTION = "ls_load_images" + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + OUTPUT_IS_LIST = (True, True, True, False) + + + def ls_load_images(self, path: str, image_load_cap: int, select_every_nth: int): + load_images = [] + load_masks = [] + load_file_names = [] + load_frame_count = 0 + + + if os.path.isdir(path): + input_dir = os.path.normpath(path) + files = [ + os.path.join(input_dir, f) + for f in os.listdir(input_dir) + if os.path.isfile(os.path.join(input_dir, f)) + ] + + for i in range(len(files)): + + if i % select_every_nth != 0: + continue + + image_file = files[i] + image_path = folder_paths.get_annotated_filepath(image_file) + img = node_helpers.pillow(Image.open, image_path) + output_images = [] + output_masks = [] + w, h = None, None + + excluded_formats = ['MPO'] + + for i in ImageSequence.Iterator(img): + i = node_helpers.pillow(ImageOps.exif_transpose, i) + + if i.mode == 'I': + i = i.point(lambda i: i * (1 / 255)) + image = i.convert("RGB") + + if len(output_images) == 0: + w = image.size[0] + h = image.size[1] + + if image.size[0] != w or image.size[1] != h: + continue + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + elif i.mode == 'P' and 'transparency' in i.info: + mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) + else: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + output_images.append(image) + output_masks.append(mask.unsqueeze(0)) + + if len(output_images) > 1 and img.format not in excluded_formats: + output_image = torch.cat(output_images, dim=0) + output_mask = torch.cat(output_masks, dim=0) + else: + output_image = output_images[0] + output_mask = output_masks[0] + + load_images.append(output_image) + load_masks.append(output_mask) + load_file_names.append(os.path.basename(image_file)) + load_frame_count += 1 + if image_load_cap > 0 and load_frame_count >= image_load_cap: + break + + return (load_images, load_masks, load_file_names, load_frame_count) + + else: + raise Exception("directory is not valid: " + directory) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: LoadImagesFromPath": LS_LoadImagesFromPath, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: LoadImagesFromPath": "LayerUtility: Load Images From Path", } \ No newline at end of file diff --git a/py/mask_box_detect.py b/py/mask_box_detect.py old mode 100644 new mode 100755 diff --git a/py/mask_by_color.py b/py/mask_by_color.py old mode 100644 new mode 100755 index af7ea5c5..ab20516e --- a/py/mask_by_color.py +++ b/py/mask_by_color.py @@ -1,84 +1,84 @@ -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" +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" } \ No newline at end of file diff --git a/py/mask_edge_shrink.py b/py/mask_edge_shrink.py old mode 100644 new mode 100755 diff --git a/py/mask_edge_ultra_detail.py b/py/mask_edge_ultra_detail.py old mode 100644 new mode 100755 index 244df20b..c48cc4c4 --- a/py/mask_edge_ultra_detail.py +++ b/py/mask_edge_ultra_detail.py @@ -1,80 +1,80 @@ -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", -} +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", +} diff --git a/py/mask_edge_ultra_detail_v2.py b/py/mask_edge_ultra_detail_v2.py old mode 100644 new mode 100755 index f289d6d9..ac14c14e --- a/py/mask_edge_ultra_detail_v2.py +++ b/py/mask_edge_ultra_detail_v2.py @@ -1,96 +1,96 @@ -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", -} +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", +} diff --git a/py/mask_gradient.py b/py/mask_gradient.py old mode 100644 new mode 100755 diff --git a/py/mask_grain.py b/py/mask_grain.py old mode 100644 new mode 100755 index 887aac70..1a2a6abd --- a/py/mask_grain.py +++ b/py/mask_grain.py @@ -1,63 +1,63 @@ -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" +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" } \ No newline at end of file diff --git a/py/mask_grow.py b/py/mask_grow.py old mode 100644 new mode 100755 diff --git a/py/mask_invert.py b/py/mask_invert.py old mode 100644 new mode 100755 diff --git a/py/mask_motion_blur.py b/py/mask_motion_blur.py old mode 100644 new mode 100755 diff --git a/py/mask_preview.py b/py/mask_preview.py old mode 100644 new mode 100755 diff --git a/py/mask_stroke.py b/py/mask_stroke.py old mode 100644 new mode 100755 diff --git a/py/motion_blur.py b/py/motion_blur.py old mode 100644 new mode 100755 diff --git a/py/nano_banana_image_scale.py b/py/nano_banana_image_scale.py old mode 100644 new mode 100755 index f0e55ac3..481bf07e --- a/py/nano_banana_image_scale.py +++ b/py/nano_banana_image_scale.py @@ -1,66 +1,66 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, fit_resize_image - - -PREFERED_BANANA_RESOLUTIONS = [ - (1024, 1024), - (1184, 864), - (864, 1184), - (832, 1248), - (1248, 832), - (768, 1344), - (1344, 768), -] - - -class LS_NanoBananaImageScale: - @classmethod - def INPUT_TYPES(s): - method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] - return {"required": {"image": ("IMAGE", ), - "method": (method_mode,), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "scale" - - CATEGORY = '😺dzNodes/LayerUtility' - DESCRIPTION = "This node resizes the image to one that is more optimal for nano-banana. For images with different aspect ratio, the scale will be adjusted appropriately to maintain all information" - - def scale(self, image, method): - ret_images = [] - - width = image.shape[2] - height = image.shape[1] - aspect_ratio = width / height - _, target_width, target_height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_BANANA_RESOLUTIONS) - - 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 img in image: - _image = torch.unsqueeze(img, 0) - _image = tensor2pil(img).convert('RGB') - resized_image = fit_resize_image(_image, target_width, target_height, 'fill', resize_sampler) - ret_images.append(pil2tensor(resized_image)) - - return (torch.cat(ret_images, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: NanoBananaImageScale": LS_NanoBananaImageScale -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: NanoBananaImageScale": "LayerUtility: Nano Banana Image Scale" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, fit_resize_image + + +PREFERED_BANANA_RESOLUTIONS = [ + (1024, 1024), + (1184, 864), + (864, 1184), + (832, 1248), + (1248, 832), + (768, 1344), + (1344, 768), +] + + +class LS_NanoBananaImageScale: + @classmethod + def INPUT_TYPES(s): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return {"required": {"image": ("IMAGE", ), + "method": (method_mode,), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "scale" + + CATEGORY = '😺dzNodes/LayerUtility' + DESCRIPTION = "This node resizes the image to one that is more optimal for nano-banana. For images with different aspect ratio, the scale will be adjusted appropriately to maintain all information" + + def scale(self, image, method): + ret_images = [] + + width = image.shape[2] + height = image.shape[1] + aspect_ratio = width / height + _, target_width, target_height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_BANANA_RESOLUTIONS) + + 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 img in image: + _image = torch.unsqueeze(img, 0) + _image = tensor2pil(img).convert('RGB') + resized_image = fit_resize_image(_image, target_width, target_height, 'fill', resize_sampler) + ret_images.append(pil2tensor(resized_image)) + + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: NanoBananaImageScale": LS_NanoBananaImageScale +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: NanoBananaImageScale": "LayerUtility: Nano Banana Image Scale" } \ No newline at end of file diff --git a/py/outer_glow.py b/py/outer_glow.py old mode 100644 new mode 100755 diff --git a/py/outer_glow_v2.py b/py/outer_glow_v2.py old mode 100644 new mode 100755 index 470b1060..18508255 --- a/py/outer_glow_v2.py +++ b/py/outer_glow_v2.py @@ -1,111 +1,111 @@ -import torch -import copy -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color, step_value, expand_mask, chop_image_v2, chop_mode_v2, BLEND_MODES - - -class OuterGlowV2: - - def __init__(self): - self.NODE_NAME = 'OuterGlowV2' - - @classmethod - def INPUT_TYPES(self): - - modes = copy.copy(BLEND_MODES) - chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"] - for i in chop_mode_list: - modes.pop(i) - chop_mode_list.extend(list(modes.keys())) - - return { - "required": { - "background_image": ("IMAGE", ), # - "layer_image": ("IMAGE",), # - "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask - "blend_mode": (chop_mode_list,), # 混合模式 - "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 - "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 - "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 - "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 - "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 - "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 - }, - "optional": { - "layer_mask": ("MASK",), # - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = 'outer_glow_v2' - CATEGORY = '😺dzNodes/LayerStyle' - - def outer_glow_v2(self, background_image, layer_image, - invert_mask, blend_mode, opacity, - brightness, glow_range, blur, light_color, glow_color, - layer_mask=None - ): - - b_images = [] - l_images = [] - l_masks = [] - ret_images = [] - - for b in background_image: - b_images.append(torch.unsqueeze(b, 0)) - for l in layer_image: - l_images.append(torch.unsqueeze(l, 0)) - m = tensor2pil(l) - if m.mode == 'RGBA': - l_masks.append(m.split()[-1]) - if layer_mask is not None: - if layer_mask.dim() == 2: - layer_mask = torch.unsqueeze(layer_mask, 0) - l_masks = [] - for m in layer_mask: - if invert_mask: - m = 1 - m - l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) - if len(l_masks) == 0: - log(f"Error: {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)) - blur_factor = blur / 20.0 - for i in range(max_batch): - background_image = b_images[i] if i < len(b_images) else b_images[-1] - layer_image = l_images[i] if i < len(l_images) else l_images[-1] - _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] - # preprocess - _canvas = tensor2pil(background_image).convert('RGB') - _layer = tensor2pil(layer_image).convert('RGB') - if _mask.size != _layer.size: - _mask = Image.new('L', _layer.size, 'white') - log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') - grow = glow_range - for x in range(brightness): - blur = int(grow * blur_factor) - _color = step_color(glow_color, light_color, brightness, x) - glow_mask = expand_mask(image2mask(_mask), grow, blur) #扩张,模糊 - # 合成glow - color_image = Image.new("RGB", _layer.size, color=_color) - alpha = tensor2pil(glow_mask).convert('L') - _glow = chop_image_v2(_canvas, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) - _canvas.paste(_glow.convert('RGB'), mask=alpha) - grow = grow - int(glow_range/brightness) - # 合成layer - _canvas.paste(_layer, mask=_mask) - - ret_images.append(pil2tensor(_canvas)) - - log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') - return (torch.cat(ret_images, dim=0),) - - -NODE_CLASS_MAPPINGS = { - "LayerStyle: OuterGlow V2": OuterGlowV2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerStyle: OuterGlow V2": "LayerStyle: OuterGlow V2" +import torch +import copy +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color, step_value, expand_mask, chop_image_v2, chop_mode_v2, BLEND_MODES + + +class OuterGlowV2: + + def __init__(self): + self.NODE_NAME = 'OuterGlowV2' + + @classmethod + def INPUT_TYPES(self): + + modes = copy.copy(BLEND_MODES) + chop_mode_list = ["screen", "linear dodge(add)", "color dodge", "lighten", "dodge", "hard light", "linear light"] + for i in chop_mode_list: + modes.pop(i) + chop_mode_list.extend(list(modes.keys())) + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode_list,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'outer_glow_v2' + CATEGORY = '😺dzNodes/LayerStyle' + + def outer_glow_v2(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {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)) + blur_factor = blur / 20.0 + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + grow = glow_range + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(_mask), grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(glow_mask).convert('L') + _glow = chop_image_v2(_canvas, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _canvas.paste(_glow.convert('RGB'), mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + + +NODE_CLASS_MAPPINGS = { + "LayerStyle: OuterGlow V2": OuterGlowV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: OuterGlow V2": "LayerStyle: OuterGlow V2" } \ No newline at end of file diff --git a/py/pixel_spread.py b/py/pixel_spread.py old mode 100644 new mode 100755 index 3760fb5d..a2b67fe8 --- a/py/pixel_spread.py +++ b/py/pixel_spread.py @@ -1,76 +1,76 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, expand_mask, pixel_spread - - - -class PixelSpread: - - def __init__(self): - self.NODE_NAME = 'PixelSpread' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), # - "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask - "mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), - }, - "optional": { - "mask": ("MASK",), # - } - } - - RETURN_TYPES = ("IMAGE", ) - RETURN_NAMES = ("image", ) - FUNCTION = 'pixel_spread' - CATEGORY = '😺dzNodes/LayerMask' - - def pixel_spread(self, image, invert_mask, mask_grow, mask=None): - - l_images = [] - l_masks = [] - ret_images = [] - - for l in image: - i = tensor2pil(torch.unsqueeze(l, 0)) - l_images.append(i) - if i.mode == 'RGBA': - l_masks.append(i.split()[-1]) - else: - l_masks.append(Image.new('L', i.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')) - 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] - if mask_grow != 0: - _mask = expand_mask(image2mask(_mask), mask_grow, 0) # 扩张,模糊 - _mask = mask2image(_mask) - - if _image.size != _mask.size: - log(f"Error: {self.NODE_NAME} skipped, because the mask is not match image.", message_type='error') - return (image,) - ret_image = pixel_spread(_image.convert('RGB'), _mask.convert('RGB')) - 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 = { - "LayerMask: PixelSpread": PixelSpread -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerMask: PixelSpread": "LayerMask: PixelSpread" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, expand_mask, pixel_spread + + + +class PixelSpread: + + def __init__(self): + self.NODE_NAME = 'PixelSpread' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask + "mask_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = 'pixel_spread' + CATEGORY = '😺dzNodes/LayerMask' + + def pixel_spread(self, image, invert_mask, mask_grow, mask=None): + + l_images = [] + l_masks = [] + ret_images = [] + + for l in image: + i = tensor2pil(torch.unsqueeze(l, 0)) + l_images.append(i) + if i.mode == 'RGBA': + l_masks.append(i.split()[-1]) + else: + l_masks.append(Image.new('L', i.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')) + 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] + if mask_grow != 0: + _mask = expand_mask(image2mask(_mask), mask_grow, 0) # 扩张,模糊 + _mask = mask2image(_mask) + + if _image.size != _mask.size: + log(f"Error: {self.NODE_NAME} skipped, because the mask is not match image.", message_type='error') + return (image,) + ret_image = pixel_spread(_image.convert('RGB'), _mask.convert('RGB')) + 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 = { + "LayerMask: PixelSpread": PixelSpread +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: PixelSpread": "LayerMask: PixelSpread" } \ No newline at end of file diff --git a/py/print_info.py b/py/print_info.py old mode 100644 new mode 100755 diff --git a/py/purge_vram.py b/py/purge_vram.py old mode 100644 new mode 100755 index 2777233e..8edea7ea --- a/py/purge_vram.py +++ b/py/purge_vram.py @@ -1,80 +1,80 @@ -import torch.cuda -import gc -import comfy.model_management -from .imagefunc import AnyType, clear_memory - -any = AnyType("*") - -class PurgeVRAM: - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "anything": (any, {}), - "purge_cache": ("BOOLEAN", {"default": True}), - "purge_models": ("BOOLEAN", {"default": True}), - }, - "optional": { - } - } - - RETURN_TYPES = () - FUNCTION = "purge_vram" - CATEGORY = '😺dzNodes/LayerUtility/SystemIO' - OUTPUT_NODE = True - - def purge_vram(self, anything, purge_cache, purge_models): - import torch.cuda - import gc - import comfy.model_management - clear_memory() - if purge_models: - comfy.model_management.unload_all_models() - comfy.model_management.soft_empty_cache() - return (None,) - -class PurgeVRAM_V2: - - def __init__(self): - self.NODE_NAME = 'PurgeVRAM V2' - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "anything": (any, {}), - "purge_cache": ("BOOLEAN", {"default": True}), - "purge_models": ("BOOLEAN", {"default": True}), - }, - "optional": { - } - } - - - RETURN_TYPES = (any,) - RETURN_NAMES = ("any",) - FUNCTION = "purge_vram_v2" - CATEGORY = '😺dzNodes/LayerUtility/SystemIO' - OUTPUT_NODE = True - - def purge_vram_v2(self, anything, purge_cache, purge_models): - clear_memory() - if purge_models: - comfy.model_management.unload_all_models() - comfy.model_management.soft_empty_cache() - return (anything,) - - -NODE_CLASS_MAPPINGS = { - "LayerUtility: PurgeVRAM": PurgeVRAM, - "LayerUtility: PurgeVRAM V2": PurgeVRAM_V2, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: PurgeVRAM": "LayerUtility: Purge VRAM", - "LayerUtility: PurgeVRAM V2": "LayerUtility: Purge VRAM V2", +import torch.cuda +import gc +import comfy.model_management +from .imagefunc import AnyType, clear_memory + +any = AnyType("*") + +class PurgeVRAM: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "anything": (any, {}), + "purge_cache": ("BOOLEAN", {"default": True}), + "purge_models": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = () + FUNCTION = "purge_vram" + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + OUTPUT_NODE = True + + def purge_vram(self, anything, purge_cache, purge_models): + import torch.cuda + import gc + import comfy.model_management + clear_memory() + if purge_models: + comfy.model_management.unload_all_models() + comfy.model_management.soft_empty_cache() + return (None,) + +class PurgeVRAM_V2: + + def __init__(self): + self.NODE_NAME = 'PurgeVRAM V2' + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "anything": (any, {}), + "purge_cache": ("BOOLEAN", {"default": True}), + "purge_models": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + + RETURN_TYPES = (any,) + RETURN_NAMES = ("any",) + FUNCTION = "purge_vram_v2" + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + OUTPUT_NODE = True + + def purge_vram_v2(self, anything, purge_cache, purge_models): + clear_memory() + if purge_models: + comfy.model_management.unload_all_models() + comfy.model_management.soft_empty_cache() + return (anything,) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: PurgeVRAM": PurgeVRAM, + "LayerUtility: PurgeVRAM V2": PurgeVRAM_V2, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: PurgeVRAM": "LayerUtility: Purge VRAM", + "LayerUtility: PurgeVRAM V2": "LayerUtility: Purge VRAM V2", } \ No newline at end of file diff --git a/py/random_generator.py b/py/random_generator.py old mode 100644 new mode 100755 index 2dded714..479646e3 --- a/py/random_generator.py +++ b/py/random_generator.py @@ -1,141 +1,141 @@ -from .imagefunc import AnyType -import random - -class LSRandomGenerator: - - def __init__(self): - self.NODE_NAME = 'RandomGenerator' - self.previous_seeds= set({}) - self.fixed_seed = 0 - pass - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "min_value": ("FLOAT", {"default": 0, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), - "max_value": ("FLOAT", {"default": 10, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), - "float_decimal_places": ("INT", {"default": 1, "min": 1, "max": 14, "step": 1}), - "fix_seed": ("BOOLEAN", {"default": False}), - }, - "optional": { - "image": ("IMAGE", ), - } - } - - RETURN_TYPES = ("INT", "FLOAT", "BOOLEAN",) - RETURN_NAMES = ("int", "float", "bool",) - FUNCTION = 'random_generator' - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def random_generator(self, min_value, max_value, float_decimal_places, fix_seed, image=None): - batch_size = 1 - if image is not None: - batch_size = image.shape[0] - ret_nunbers = [] - for i in range(batch_size): - new_seed = self.generate_unique_seed() - if fix_seed: - if self.fixed_seed == 0: - self.fixed_seed = new_seed - seed = self.fixed_seed - else: - seed = new_seed - random.seed(seed) - factor = random.uniform(3, 9) - random_float = random.uniform(min_value, max_value) / factor - random_float = round(random_float * factor, float_decimal_places) - random_int = int(random_float) - random_bool = random_int %2 == 0 - ret_nunbers.append((random_int, random_float, random_bool)) - - if len(ret_nunbers) > 1: - ret_ints = [item[0] for item in ret_nunbers] - ret_floats = [item[1] for item in ret_nunbers] - ret_bools = [item[2] for item in ret_nunbers] - return (ret_ints, ret_floats, ret_bools) - else: - return (ret_nunbers[0][0], ret_nunbers[0][1], ret_nunbers[0][2]) - - - def generate_unique_seed(self) -> int: - while True: - new_number = random.randint(0, int(1e14)) - if new_number not in self.previous_seeds: - self.previous_seeds.add(new_number) - return new_number - -class LS_RandomGeneratorV2: - - def __init__(self): - self.NODE_NAME = 'RandomGeneratorV2' - self.previous_seeds= set({}) - self.fixed_seed = 0 - pass - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "min_value": ("FLOAT", {"default": 0, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), - "max_value": ("FLOAT", {"default": 10, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), - "least": ("FLOAT", {"default": 0, "min": 0, "max": 1.0e14, "step": 0.01}), - "float_decimal_places": ("INT", {"default": 1, "min": 1, "max": 14, "step": 1}), - "seed":("INT", {"default": 0, "min": 0, "max": 1e14, "step": 1}), - - }, - "optional": { - "image": ("IMAGE",), - } - } - - RETURN_TYPES = ("INT", "FLOAT", "BOOLEAN",) - RETURN_NAMES = ("int", "float", "bool",) - # OUTPUT_IS_LIST = (True, True, True,) - FUNCTION = 'random_generator_v2' - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def random_generator_v2(self, min_value, max_value, least, float_decimal_places, seed, image=None): - batch_size = 1 - if image is not None: - batch_size = image.shape[0] - ret_nunbers = [] - for i in range(batch_size): - - random.seed(seed) - max_loop = 500 - i = 0 - while i < max_loop: - new_number = random.uniform(min_value, max_value) - if abs(new_number) - least >= 0 or least > max_value: - break - i += 1 - - # 转浮点 - factor = random.uniform(3, 9) - random_float = new_number / factor - random_float = round(random_float * factor, float_decimal_places) - random_int = int(random_float) - random_bool = random_int %2 == 0 - ret_nunbers.append((random_int, random_float, random_bool)) - - if len(ret_nunbers) > 1: - ret_ints = [item[0] for item in ret_nunbers] - ret_floats = [item[1] for item in ret_nunbers] - ret_bools = [item[2] for item in ret_nunbers] - return (ret_ints, ret_floats, ret_bools) - else: - return (ret_nunbers[0][0], ret_nunbers[0][1], ret_nunbers[0][2]) - - -NODE_CLASS_MAPPINGS = { - "LayerUtility: RandomGenerator": LSRandomGenerator, - "LayerUtility: RandomGeneratorV2": LS_RandomGeneratorV2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: RandomGenerator": "LayerUtility: Random Generator", - "LayerUtility: RandomGeneratorV2": "LayerUtility: Random Generator V2" +from .imagefunc import AnyType +import random + +class LSRandomGenerator: + + def __init__(self): + self.NODE_NAME = 'RandomGenerator' + self.previous_seeds= set({}) + self.fixed_seed = 0 + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "min_value": ("FLOAT", {"default": 0, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), + "max_value": ("FLOAT", {"default": 10, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), + "float_decimal_places": ("INT", {"default": 1, "min": 1, "max": 14, "step": 1}), + "fix_seed": ("BOOLEAN", {"default": False}), + }, + "optional": { + "image": ("IMAGE", ), + } + } + + RETURN_TYPES = ("INT", "FLOAT", "BOOLEAN",) + RETURN_NAMES = ("int", "float", "bool",) + FUNCTION = 'random_generator' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def random_generator(self, min_value, max_value, float_decimal_places, fix_seed, image=None): + batch_size = 1 + if image is not None: + batch_size = image.shape[0] + ret_nunbers = [] + for i in range(batch_size): + new_seed = self.generate_unique_seed() + if fix_seed: + if self.fixed_seed == 0: + self.fixed_seed = new_seed + seed = self.fixed_seed + else: + seed = new_seed + random.seed(seed) + factor = random.uniform(3, 9) + random_float = random.uniform(min_value, max_value) / factor + random_float = round(random_float * factor, float_decimal_places) + random_int = int(random_float) + random_bool = random_int %2 == 0 + ret_nunbers.append((random_int, random_float, random_bool)) + + if len(ret_nunbers) > 1: + ret_ints = [item[0] for item in ret_nunbers] + ret_floats = [item[1] for item in ret_nunbers] + ret_bools = [item[2] for item in ret_nunbers] + return (ret_ints, ret_floats, ret_bools) + else: + return (ret_nunbers[0][0], ret_nunbers[0][1], ret_nunbers[0][2]) + + + def generate_unique_seed(self) -> int: + while True: + new_number = random.randint(0, int(1e14)) + if new_number not in self.previous_seeds: + self.previous_seeds.add(new_number) + return new_number + +class LS_RandomGeneratorV2: + + def __init__(self): + self.NODE_NAME = 'RandomGeneratorV2' + self.previous_seeds= set({}) + self.fixed_seed = 0 + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "min_value": ("FLOAT", {"default": 0, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), + "max_value": ("FLOAT", {"default": 10, "min": -1.0e14, "max": 1.0e14, "step": 0.01}), + "least": ("FLOAT", {"default": 0, "min": 0, "max": 1.0e14, "step": 0.01}), + "float_decimal_places": ("INT", {"default": 1, "min": 1, "max": 14, "step": 1}), + "seed":("INT", {"default": 0, "min": 0, "max": 1e14, "step": 1}), + + }, + "optional": { + "image": ("IMAGE",), + } + } + + RETURN_TYPES = ("INT", "FLOAT", "BOOLEAN",) + RETURN_NAMES = ("int", "float", "bool",) + # OUTPUT_IS_LIST = (True, True, True,) + FUNCTION = 'random_generator_v2' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def random_generator_v2(self, min_value, max_value, least, float_decimal_places, seed, image=None): + batch_size = 1 + if image is not None: + batch_size = image.shape[0] + ret_nunbers = [] + for i in range(batch_size): + + random.seed(seed) + max_loop = 500 + i = 0 + while i < max_loop: + new_number = random.uniform(min_value, max_value) + if abs(new_number) - least >= 0 or least > max_value: + break + i += 1 + + # 转浮点 + factor = random.uniform(3, 9) + random_float = new_number / factor + random_float = round(random_float * factor, float_decimal_places) + random_int = int(random_float) + random_bool = random_int %2 == 0 + ret_nunbers.append((random_int, random_float, random_bool)) + + if len(ret_nunbers) > 1: + ret_ints = [item[0] for item in ret_nunbers] + ret_floats = [item[1] for item in ret_nunbers] + ret_bools = [item[2] for item in ret_nunbers] + return (ret_ints, ret_floats, ret_bools) + else: + return (ret_nunbers[0][0], ret_nunbers[0][1], ret_nunbers[0][2]) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: RandomGenerator": LSRandomGenerator, + "LayerUtility: RandomGeneratorV2": LS_RandomGeneratorV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: RandomGenerator": "LayerUtility: Random Generator", + "LayerUtility: RandomGeneratorV2": "LayerUtility: Random Generator V2" } \ No newline at end of file diff --git a/py/restore_crop_box.py b/py/restore_crop_box.py old mode 100644 new mode 100755 index 40cc07e7..311b6c0b --- a/py/restore_crop_box.py +++ b/py/restore_crop_box.py @@ -1,84 +1,84 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask - - - -class RestoreCropBox: - - def __init__(self): - self.NODE_NAME = 'RestoreCropBox' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "background_image": ("IMAGE", ), - "croped_image": ("IMAGE",), - "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask# - "crop_box": ("BOX",), - }, - "optional": { - "croped_mask": ("MASK",), - } - } - - RETURN_TYPES = ("IMAGE", "MASK", ) - RETURN_NAMES = ("image", "mask", ) - FUNCTION = 'restore_crop_box' - CATEGORY = '😺dzNodes/LayerUtility' - - def restore_crop_box(self, background_image, croped_image, invert_mask, crop_box, - croped_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 croped_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 croped_mask is not None: - if croped_mask.dim() == 2: - croped_mask = torch.unsqueeze(croped_mask, 0) - l_masks = [] - for m in croped_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] - croped_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(croped_image).convert('RGB') - - ret_mask = Image.new('L', size=_canvas.size, color='black') - _canvas.paste(_layer, box=tuple(crop_box), mask=_mask) - ret_mask.paste(_mask, box=tuple(crop_box)) - ret_images.append(pil2tensor(_canvas)) - 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: RestoreCropBox": RestoreCropBox -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: RestoreCropBox": "LayerUtility: RestoreCropBox" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask + + + +class RestoreCropBox: + + def __init__(self): + self.NODE_NAME = 'RestoreCropBox' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), + "croped_image": ("IMAGE",), + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask# + "crop_box": ("BOX",), + }, + "optional": { + "croped_mask": ("MASK",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = 'restore_crop_box' + CATEGORY = '😺dzNodes/LayerUtility' + + def restore_crop_box(self, background_image, croped_image, invert_mask, crop_box, + croped_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 croped_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 croped_mask is not None: + if croped_mask.dim() == 2: + croped_mask = torch.unsqueeze(croped_mask, 0) + l_masks = [] + for m in croped_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] + croped_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(croped_image).convert('RGB') + + ret_mask = Image.new('L', size=_canvas.size, color='black') + _canvas.paste(_layer, box=tuple(crop_box), mask=_mask) + ret_mask.paste(_mask, box=tuple(crop_box)) + ret_images.append(pil2tensor(_canvas)) + 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: RestoreCropBox": RestoreCropBox +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: RestoreCropBox": "LayerUtility: RestoreCropBox" } \ No newline at end of file diff --git a/py/rmbg_ultra.py b/py/rmbg_ultra.py old mode 100644 new mode 100755 index dc45ac7b..c29e410b --- a/py/rmbg_ultra.py +++ b/py/rmbg_ultra.py @@ -1,54 +1,54 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, RMBG, RGB2RGBA, mask_edge_detail - - -class RemBgUltra: - def __init__(self): - self.NODE_NAME = 'RemBgUltra' - - @classmethod - def INPUT_TYPES(cls): - - return { - "required": { - "image": ("IMAGE",), - "detail_range": ("INT", {"default": 8, "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}), - "process_detail": ("BOOLEAN", {"default": True}), - }, - "optional": { - } - } - - RETURN_TYPES = ("IMAGE", "MASK", ) - RETURN_NAMES = ("image", "mask", ) - FUNCTION = "rembg_ultra" - CATEGORY = '😺dzNodes/LayerMask' - - def rembg_ultra(self, image, detail_range, black_point, white_point, process_detail): - ret_images = [] - ret_masks = [] - - for i in image: - i = torch.unsqueeze(i, 0) - i = pil2tensor(tensor2pil(i).convert('RGB')) - orig_image = tensor2pil(i).convert('RGB') - _mask = RMBG(orig_image) - if process_detail: - _mask = tensor2pil(mask_edge_detail(i, pil2tensor(_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: RemBgUltra": RemBgUltra, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerMask: RemBgUltra": "LayerMask: RemBgUltra", -} +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, RMBG, RGB2RGBA, mask_edge_detail + + +class RemBgUltra: + def __init__(self): + self.NODE_NAME = 'RemBgUltra' + + @classmethod + def INPUT_TYPES(cls): + + return { + "required": { + "image": ("IMAGE",), + "detail_range": ("INT", {"default": 8, "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}), + "process_detail": ("BOOLEAN", {"default": True}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", ) + RETURN_NAMES = ("image", "mask", ) + FUNCTION = "rembg_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def rembg_ultra(self, image, detail_range, black_point, white_point, process_detail): + ret_images = [] + ret_masks = [] + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + _mask = RMBG(orig_image) + if process_detail: + _mask = tensor2pil(mask_edge_detail(i, pil2tensor(_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: RemBgUltra": RemBgUltra, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: RemBgUltra": "LayerMask: RemBgUltra", +} diff --git a/py/rmbg_ultra_v2.py b/py/rmbg_ultra_v2.py old mode 100644 new mode 100755 index 794831cc..3fbf1796 --- a/py/rmbg_ultra_v2.py +++ b/py/rmbg_ultra_v2.py @@ -1,82 +1,82 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, RMBG, RGB2RGBA, mask_edge_detail -from .imagefunc import guided_filter_alpha, histogram_remap, generate_VITMatte, generate_VITMatte_trimap - - - -class RmBgUltraV2: - def __init__(self): - self.NODE_NAME = 'RmBgUltra V2' - - @classmethod - def INPUT_TYPES(cls): - - method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] - device_list = ['cuda','cpu'] - return { - "required": { - "image": ("IMAGE",), - "detail_method": (method_list,), - "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), - "detail_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"}), - "process_detail": ("BOOLEAN", {"default": True}), - "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 = "rmbg_ultra_v2" - CATEGORY = '😺dzNodes/LayerMask' - - def rmbg_ultra_v2(self, image, detail_method, detail_erode, detail_dilate, - black_point, white_point, process_detail, device, max_megapixels): - ret_images = [] - ret_masks = [] - - if detail_method == 'VITMatte(local)': - local_files_only = True - else: - local_files_only = False - - for i in image: - i = torch.unsqueeze(i, 0) - i = pil2tensor(tensor2pil(i).convert('RGB')) - orig_image = tensor2pil(i).convert('RGB') - _mask = RMBG(orig_image) - _mask = pil2tensor(_mask) - - detail_range = detail_erode + detail_dilate - if process_detail: - if detail_method == 'GuidedFilter': - _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) - _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) - elif detail_method == 'PyMatting': - _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) - else: - _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) - else: - _mask = mask2image(_mask) - - 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: RmBgUltra V2": RmBgUltraV2, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerMask: RmBgUltra V2": "LayerMask: RmBgUltra V2", -} +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image, RMBG, RGB2RGBA, mask_edge_detail +from .imagefunc import guided_filter_alpha, histogram_remap, generate_VITMatte, generate_VITMatte_trimap + + + +class RmBgUltraV2: + def __init__(self): + self.NODE_NAME = 'RmBgUltra V2' + + @classmethod + def INPUT_TYPES(cls): + + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + device_list = ['cuda','cpu'] + return { + "required": { + "image": ("IMAGE",), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "detail_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"}), + "process_detail": ("BOOLEAN", {"default": True}), + "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 = "rmbg_ultra_v2" + CATEGORY = '😺dzNodes/LayerMask' + + def rmbg_ultra_v2(self, image, detail_method, detail_erode, detail_dilate, + black_point, white_point, process_detail, device, max_megapixels): + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + _mask = RMBG(orig_image) + _mask = pil2tensor(_mask) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) + else: + _mask = mask2image(_mask) + + 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: RmBgUltra V2": RmBgUltraV2, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: RmBgUltra V2": "LayerMask: RmBgUltra V2", +} diff --git a/py/rounded_rectangle.py b/py/rounded_rectangle.py old mode 100644 new mode 100755 index 8ac7243f..38c54533 --- a/py/rounded_rectangle.py +++ b/py/rounded_rectangle.py @@ -1,100 +1,100 @@ -import torch -from PIL import Image -from .imagefunc import log, pil2tensor, tensor2pil, image2mask, RGB2RGBA -from .imagefunc import draw_rounded_rectangle, gaussian_blur, mask_area, max_inscribed_rect, min_bounding_rect - - -class LS_RoundedRectangle: - - def __init__(self): - self.NODE_NAME = 'RoundedRectangle' - - @classmethod - def INPUT_TYPES(self): - detect_mode = ['mask_area', 'min_bounding_rect', 'max_inscribed_rect'] - return { - "required": { - "image": ("IMAGE",), - "rounded_rect_radius": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), - "anti_aliasing": ("INT", {"default": 2, "min": 0, "max": 16, "step": 1}), - "top": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "bottom": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "left": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "right": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "detect": (detect_mode,), - "obj_ext_top": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "obj_ext_bottom": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "obj_ext_left": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - "obj_ext_right": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), - }, - "optional": { - "object_mask": ("MASK",), - "crop_box": ("BOX",), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = 'rounded_rectangle' - CATEGORY = '😺dzNodes/LayerUtility' - - def rounded_rectangle(self, image, rounded_rect_radius, anti_aliasing, top, bottom, left, right, - detect, obj_ext_top, obj_ext_bottom, obj_ext_left, obj_ext_right, - object_mask=None, crop_box=None): - ret_images = [] - ret_masks = [] - - for index, img in enumerate(image): - orig_image = tensor2pil(torch.unsqueeze(img, 0)).convert('RGB') - width, height = orig_image.size - black_image = Image.new('L', (width, height), color="black") - - if crop_box is not None: - w = crop_box[2] - crop_box[0] - h = crop_box[3] - crop_box[1] - x1 = crop_box[0] - int(obj_ext_left * w * 0.01) - y1 = crop_box[1] - int(obj_ext_top * h * 0.01) - x2 = crop_box[2] + int(obj_ext_right * w * 0.01) - y2 = crop_box[3] + int(obj_ext_bottom * h * 0.01) - bbox = [(x1, y1, x2, y2)] - elif object_mask is not None: - if object_mask.dim() == 2: object_mask = torch.unsqueeze(object_mask, 0) - mask = object_mask[index] if index < len(object_mask) else object_mask[-1] - mask = tensor2pil(mask) - bluredmask = gaussian_blur(mask, 20).convert('L') - x = -10 - y = -10 - w = 4 - h = 4 - 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) - - x1 = x - int(obj_ext_left * w * 0.01) - y1 = y - int(obj_ext_top * h * 0.01) - x2 = x + w + int(obj_ext_right * w * 0.01) - y2 = y + h + int(obj_ext_bottom * h * 0.01) - bbox = [(x1, y1, x2, y2)] - else: - bbox = [(int(left * width * 0.01), - int(top * height * 0.01), - width - int(right * width * 0.01), - height - int(bottom * height * 0.01)) - ] - rect_mask = draw_rounded_rectangle(black_image, rounded_rect_radius, bbox, anti_aliasing, "white") - ret_image = RGB2RGBA(orig_image, rect_mask) - ret_images.append(pil2tensor(ret_image)) - ret_masks.append(image2mask(rect_mask)) - - return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: RoundedRectangle": LS_RoundedRectangle -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: RoundedRectangle": "LayerUtility: RoundedRectangle" +import torch +from PIL import Image +from .imagefunc import log, pil2tensor, tensor2pil, image2mask, RGB2RGBA +from .imagefunc import draw_rounded_rectangle, gaussian_blur, mask_area, max_inscribed_rect, min_bounding_rect + + +class LS_RoundedRectangle: + + def __init__(self): + self.NODE_NAME = 'RoundedRectangle' + + @classmethod + def INPUT_TYPES(self): + detect_mode = ['mask_area', 'min_bounding_rect', 'max_inscribed_rect'] + return { + "required": { + "image": ("IMAGE",), + "rounded_rect_radius": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), + "anti_aliasing": ("INT", {"default": 2, "min": 0, "max": 16, "step": 1}), + "top": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "bottom": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "left": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "right": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "detect": (detect_mode,), + "obj_ext_top": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "obj_ext_bottom": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "obj_ext_left": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + "obj_ext_right": ("FLOAT", {"default": 8, "min": -100, "max": 100, "step": 0.1}), + }, + "optional": { + "object_mask": ("MASK",), + "crop_box": ("BOX",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'rounded_rectangle' + CATEGORY = '😺dzNodes/LayerUtility' + + def rounded_rectangle(self, image, rounded_rect_radius, anti_aliasing, top, bottom, left, right, + detect, obj_ext_top, obj_ext_bottom, obj_ext_left, obj_ext_right, + object_mask=None, crop_box=None): + ret_images = [] + ret_masks = [] + + for index, img in enumerate(image): + orig_image = tensor2pil(torch.unsqueeze(img, 0)).convert('RGB') + width, height = orig_image.size + black_image = Image.new('L', (width, height), color="black") + + if crop_box is not None: + w = crop_box[2] - crop_box[0] + h = crop_box[3] - crop_box[1] + x1 = crop_box[0] - int(obj_ext_left * w * 0.01) + y1 = crop_box[1] - int(obj_ext_top * h * 0.01) + x2 = crop_box[2] + int(obj_ext_right * w * 0.01) + y2 = crop_box[3] + int(obj_ext_bottom * h * 0.01) + bbox = [(x1, y1, x2, y2)] + elif object_mask is not None: + if object_mask.dim() == 2: object_mask = torch.unsqueeze(object_mask, 0) + mask = object_mask[index] if index < len(object_mask) else object_mask[-1] + mask = tensor2pil(mask) + bluredmask = gaussian_blur(mask, 20).convert('L') + x = -10 + y = -10 + w = 4 + h = 4 + 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) + + x1 = x - int(obj_ext_left * w * 0.01) + y1 = y - int(obj_ext_top * h * 0.01) + x2 = x + w + int(obj_ext_right * w * 0.01) + y2 = y + h + int(obj_ext_bottom * h * 0.01) + bbox = [(x1, y1, x2, y2)] + else: + bbox = [(int(left * width * 0.01), + int(top * height * 0.01), + width - int(right * width * 0.01), + height - int(bottom * height * 0.01)) + ] + rect_mask = draw_rounded_rectangle(black_image, rounded_rect_radius, bbox, anti_aliasing, "white") + ret_image = RGB2RGBA(orig_image, rect_mask) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(rect_mask)) + + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: RoundedRectangle": LS_RoundedRectangle +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: RoundedRectangle": "LayerUtility: RoundedRectangle" } \ No newline at end of file diff --git a/py/segformer_ultra.py b/py/segformer_ultra.py old mode 100644 new mode 100755 index 62f7d5db..53085696 --- a/py/segformer_ultra.py +++ b/py/segformer_ultra.py @@ -1,944 +1,944 @@ -''' -原始代码来自 https://github.com/StartHua/Comfyui_segformer_b2_clothes -''' -import torch -import os -import numpy as np -from PIL import Image, ImageEnhance -from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation -import torch.nn as nn -import folder_paths -from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, RGB2RGBA -from .imagefunc import guided_filter_alpha, mask_edge_detail, histogram_remap, generate_VITMatte, generate_VITMatte_trimap - - -class SegformerPipeline: - def __init__(self): - self.model_name = '' - self.segment_label = [] - -SegPipeline = SegformerPipeline() - - -# 切割服装 -def get_segmentation_from_model(tensor_image, segformer_model): - - processor = segformer_model["processor"] - model = segformer_model["model"] - - cloth = tensor2pil(tensor_image) - - # 预处理和预测 - inputs = processor(images=cloth, return_tensors="pt") - outputs = model(**inputs) - logits = outputs.logits.cpu() - upsampled_logits = nn.functional.interpolate(logits, size=cloth.size[::-1], mode="bilinear", align_corners=False) - pred_seg = upsampled_logits.argmax(dim=1)[0].numpy() - return pred_seg,cloth - - -# 切割服装 -def get_segmentation(tensor_image, model_name='segformer_b2_clothes'): - cloth = tensor2pil(tensor_image) - model_folder_path = os.path.join(folder_paths.models_dir, model_name) - try: - model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[model_name][0][0]) - except: - pass - - processor = SegformerImageProcessor.from_pretrained(model_folder_path) - model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path) - # 预处理和预测 - inputs = processor(images=cloth, return_tensors="pt") - outputs = model(**inputs) - logits = outputs.logits.cpu() - upsampled_logits = nn.functional.interpolate(logits, size=cloth.size[::-1], mode="bilinear", align_corners=False) - pred_seg = upsampled_logits.argmax(dim=1)[0].numpy() - return pred_seg,cloth - - -class Segformer_B2_Clothes: - - def __init__(self): - self.NODE_NAME = 'SegformerB2ClothesUltra' - - - # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", 5: "Skirt", - # 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", 11: "Face", - # 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", 16: "Bag", 17: "Scarf" - - @classmethod - def INPUT_TYPES(cls): - method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] - device_list = ['cuda', 'cpu'] - return {"required": - { - "image": ("IMAGE",), - "face": ("BOOLEAN", {"default": False}), - "hair": ("BOOLEAN", {"default": False}), - "hat": ("BOOLEAN", {"default": False}), - "sunglass": ("BOOLEAN", {"default": False}), - "left_arm": ("BOOLEAN", {"default": False}), - "right_arm": ("BOOLEAN", {"default": False}), - "left_leg": ("BOOLEAN", {"default": False}), - "right_leg": ("BOOLEAN", {"default": False}), - "upper_clothes": ("BOOLEAN", {"default": False}), - "skirt": ("BOOLEAN", {"default": False}), - "pants": ("BOOLEAN", {"default": False}), - "dress": ("BOOLEAN", {"default": False}), - "belt": ("BOOLEAN", {"default": False}), - "shoe": ("BOOLEAN", {"default": False}), - "bag": ("BOOLEAN", {"default": False}), - "scarf": ("BOOLEAN", {"default": False}), - "detail_method": (method_list,), - "detail_erode": ("INT", {"default": 12, "min": 1, "max": 255, "step": 1}), - "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), - "black_point": ( - "FLOAT", {"default": 0.15, "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"}), - "process_detail": ("BOOLEAN", {"default": True}), - "device": (device_list,), - "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = "segformer_ultra" - CATEGORY = '😺dzNodes/LayerMask' - - def segformer_ultra(self, image, - face, hat, hair, sunglass, upper_clothes, skirt, pants, dress, belt, shoe, - left_leg, right_leg, left_arm, right_arm, bag, scarf, detail_method, - detail_erode, detail_dilate, black_point, white_point, process_detail, device, max_megapixels, - ): - - ret_images = [] - ret_masks = [] - - if detail_method == 'VITMatte(local)': - local_files_only = True - else: - local_files_only = False - - for i in image: - pred_seg, cloth = get_segmentation(i) - i = torch.unsqueeze(i, 0) - i = pil2tensor(tensor2pil(i).convert('RGB')) - orig_image = tensor2pil(i).convert('RGB') - - labels_to_keep = [0] - if not hat: - labels_to_keep.append(1) - if not hair: - labels_to_keep.append(2) - if not sunglass: - labels_to_keep.append(3) - if not upper_clothes: - labels_to_keep.append(4) - if not skirt: - labels_to_keep.append(5) - if not pants: - labels_to_keep.append(6) - if not dress: - labels_to_keep.append(7) - if not belt: - labels_to_keep.append(8) - if not shoe: - labels_to_keep.append(9) - labels_to_keep.append(10) - if not face: - labels_to_keep.append(11) - if not left_leg: - labels_to_keep.append(12) - if not right_leg: - labels_to_keep.append(13) - if not left_arm: - labels_to_keep.append(14) - if not right_arm: - labels_to_keep.append(15) - if not bag: - labels_to_keep.append(16) - if not scarf: - labels_to_keep.append(17) - - mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) - - # 创建agnostic-mask图像 - mask_image = Image.fromarray((1 - mask) * 255) - mask_image = mask_image.convert("L") - _mask = pil2tensor(mask_image) - - detail_range = detail_erode + detail_dilate - if process_detail: - if detail_method == 'GuidedFilter': - _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) - _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) - elif detail_method == 'PyMatting': - _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) - else: - _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) - else: - _mask = mask2image(_mask) - - 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),) - -class SegformerClothesPipelineLoader: - - def __init__(self): - self.NODE_NAME = 'SegformerClothesPipelineLoader' - pass - - # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", - # 5: "Skirt", 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", - # 11: "Face", 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", - # 17: "Scarf" - - @classmethod - def INPUT_TYPES(cls): - model_list = ['segformer_b3_clothes', 'segformer_b2_clothes'] - return {"required": - { "model": (model_list,), - "face": ("BOOLEAN", {"default": False, "label_on": "enabled(脸)", "label_off": "disabled(脸)"}), - "hair": ("BOOLEAN", {"default": False, "label_on": "enabled(头发)", "label_off": "disabled(头发)"}), - "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), - "sunglass": ("BOOLEAN", {"default": False, "label_on": "enabled(墨镜)", "label_off": "disabled(墨镜)"}), - "left_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(左臂)", "label_off": "disabled(左臂)"}), - "right_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(右臂)", "label_off": "disabled(右臂)"}), - "left_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(左腿)", "label_off": "disabled(左腿)"}), - "right_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(右腿)", "label_off": "disabled(右腿)"}), - "left_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(左鞋)", "label_off": "disabled(左鞋)"}), - "right_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(右鞋)", "label_off": "disabled(右鞋)"}), - "upper_clothes": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣)", "label_off": "disabled(上衣)"}), - "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(短裙)", "label_off": "disabled(短裙)"}), - "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), - "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), - "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(腰带)", "label_off": "disabled(腰带)"}), - "bag": ("BOOLEAN", {"default": False, "label_on": "enabled(背包)", "label_off": "disabled(背包)"}), - "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), - } - } - - RETURN_TYPES = ("SegPipeline",) - RETURN_NAMES = ("segformer_pipeline",) - FUNCTION = "segformer_clothes_pipeline_loader" - CATEGORY = '😺dzNodes/LayerMask' - - def segformer_clothes_pipeline_loader(self, model, - face, hat, hair, sunglass, - left_leg, right_leg, left_arm, right_arm, left_shoe, right_shoe, - upper_clothes, skirt, pants, dress, belt, bag, scarf, - ): - - pipeline = SegformerPipeline() - labels_to_keep = [0] - if not hat: - labels_to_keep.append(1) - if not hair: - labels_to_keep.append(2) - if not sunglass: - labels_to_keep.append(3) - if not upper_clothes: - labels_to_keep.append(4) - if not skirt: - labels_to_keep.append(5) - if not pants: - labels_to_keep.append(6) - if not dress: - labels_to_keep.append(7) - if not belt: - labels_to_keep.append(8) - if not left_shoe: - labels_to_keep.append(9) - if not right_shoe: - labels_to_keep.append(10) - if not face: - labels_to_keep.append(11) - if not left_leg: - labels_to_keep.append(12) - if not right_leg: - labels_to_keep.append(13) - if not left_arm: - labels_to_keep.append(14) - if not right_arm: - labels_to_keep.append(15) - if not bag: - labels_to_keep.append(16) - if not scarf: - labels_to_keep.append(17) - pipeline.segment_label = labels_to_keep - pipeline.model_name = model - return (pipeline,) - -class SegformerFashionPipelineLoader: - - def __init__(self): - self.NODE_NAME = 'SegformerFashionPipelineLoader' - pass - - @classmethod - def INPUT_TYPES(cls): - model_list = ['segformer_b3_fashion'] - return {"required": - { "model": (model_list,), - "shirt": ("BOOLEAN", {"default": False, "label_on": "enabled(衬衫、罩衫)", "label_off": "disabled(衬衫、罩衫)"}), - "top": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣、t恤)", "label_off": "disabled(上衣、t恤)"}), - "sweater": ("BOOLEAN", {"default": False, "label_on": "enabled(毛衣)", "label_off": "disabled(毛衣)"}), - "cardigan": ("BOOLEAN", {"default": False, "label_on": "enabled(开襟毛衫)", "label_off": "disabled(开襟毛衫)"}), - "jacket": ("BOOLEAN", {"default": False, "label_on": "enabled(夹克)", "label_off": "disabled(夹克)"}), - "vest": ("BOOLEAN", {"default": False, "label_on": "enabled(背心)", "label_off": "disabled(背心)"}), - "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), - "shorts": ("BOOLEAN", {"default": False, "label_on": "enabled(短裤)", "label_off": "disabled(短裤)"}), - "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(裙子)", "label_off": "disabled(裙子)"}), - "coat": ("BOOLEAN", {"default": False, "label_on": "enabled(外套)", "label_off": "disabled(外套)"}), - "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), - "jumpsuit": ("BOOLEAN", {"default": False, "label_on": "enabled(连身裤)", "label_off": "disabled(连身裤)"}), - "cape": ("BOOLEAN", {"default": False, "label_on": "enabled(斗篷)", "label_off": "disabled(斗篷)"}), - "glasses": ("BOOLEAN", {"default": False, "label_on": "enabled(眼镜)", "label_off": "disabled(眼镜)"}), - "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), - "hairaccessory": ("BOOLEAN", {"default": False, "label_on": "enabled(头带)", "label_off": "disabled(头带)"}), - "tie": ("BOOLEAN", {"default": False, "label_on": "enabled(领带)", "label_off": "disabled(领带)"}), - "glove": ("BOOLEAN", {"default": False, "label_on": "enabled(手套)", "label_off": "disabled(手套)"}), - "watch": ("BOOLEAN", {"default": False, "label_on": "enabled(手表)", "label_off": "disabled(手表)"}), - "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(皮带)", "label_off": "disabled(皮带)"}), - "legwarmer": ("BOOLEAN", {"default": False, "label_on": "enabled(腿套)", "label_off": "disabled(腿套)"}), - "tights": ("BOOLEAN", {"default": False, "label_on": "enabled(裤袜)","label_off": "disabled(裤袜)"}), - "sock": ("BOOLEAN", {"default": False, "label_on": "enabled(袜子)", "label_off": "disabled(袜子)"}), - "shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(鞋子)", "label_off": "disabled(鞋子)"}), - "bagwallet": ("BOOLEAN", {"default": False, "label_on": "enabled(手包)", "label_off": "disabled(手包)"}), - "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), - "umbrella": ("BOOLEAN", {"default": False, "label_on": "enabled(雨伞)", "label_off": "disabled(雨伞)"}), - "hood": ("BOOLEAN", {"default": False, "label_on": "enabled(兜帽)", "label_off": "disabled(兜帽)"}), - "collar": ("BOOLEAN", {"default": False, "label_on": "enabled(衣领)", "label_off": "disabled(衣领)"}), - "lapel": ("BOOLEAN", {"default": False, "label_on": "enabled(翻领)", "label_off": "disabled(翻领)"}), - "epaulette": ("BOOLEAN", {"default": False, "label_on": "enabled(肩章)", "label_off": "disabled(肩章)"}), - "sleeve": ("BOOLEAN", {"default": False, "label_on": "enabled(袖子)", "label_off": "disabled(袖子)"}), - "pocket": ("BOOLEAN", {"default": False, "label_on": "enabled(口袋)", "label_off": "disabled(口袋)"}), - "neckline": ("BOOLEAN", {"default": False, "label_on": "enabled(领口)", "label_off": "disabled(领口)"}), - "buckle": ("BOOLEAN", {"default": False, "label_on": "enabled(带扣)", "label_off": "disabled(带扣)"}), - "zipper": ("BOOLEAN", {"default": False, "label_on": "enabled(拉链)", "label_off": "disabled(拉链)"}), - "applique": ("BOOLEAN", {"default": False, "label_on": "enabled(贴花)", "label_off": "disabled(贴花)"}), - "bead": ("BOOLEAN", {"default": False, "label_on": "enabled(珠子)", "label_off": "disabled(珠子)"}), - "bow": ("BOOLEAN", {"default": False, "label_on": "enabled(蝴蝶结)", "label_off": "disabled(蝴蝶结)"}), - "flower": ("BOOLEAN", {"default": False, "label_on": "enabled(花)", "label_off": "disabled(花)"}), - "fringe": ("BOOLEAN", {"default": False, "label_on": "enabled(刘海)", "label_off": "disabled(刘海)"}), - "ribbon": ("BOOLEAN", {"default": False, "label_on": "enabled(丝带)", "label_off": "disabled(丝带)"}), - "rivet": ("BOOLEAN", {"default": False, "label_on": "enabled(铆钉)", "label_off": "disabled(铆钉)"}), - "ruffle": ("BOOLEAN", {"default": False, "label_on": "enabled(褶饰)", "label_off": "disabled(褶饰)"}), - "sequin": ("BOOLEAN", {"default": False, "label_on": "enabled(亮片)", "label_off": "disabled(亮片)"}), - "tassel": ("BOOLEAN", {"default": False, "label_on": "enabled(流苏)", "label_off": "disabled(流苏)"}), - } - } - - RETURN_TYPES = ("SegPipeline",) - RETURN_NAMES = ("segformer_pipeline",) - FUNCTION = "segformer_fashion_pipeline_loader" - CATEGORY = '😺dzNodes/LayerMask' - - def segformer_fashion_pipeline_loader(self, model, - shirt, top, sweater, cardigan, jacket, vest, pants, - shorts, skirt, coat, dress, jumpsuit, cape, glasses, - hat, hairaccessory, tie, glove, watch, belt, legwarmer, - tights, sock, shoe, bagwallet, scarf, umbrella, hood, - collar, lapel, epaulette, sleeve, pocket, neckline, - buckle, zipper, applique, bead, bow, flower, fringe, - ribbon, rivet, ruffle, sequin, tassel - ): - - pipeline = SegformerPipeline() - labels_to_keep = [0] - if not shirt: - labels_to_keep.append(1) - if not top: - labels_to_keep.append(2) - if not sweater: - labels_to_keep.append(3) - if not cardigan: - labels_to_keep.append(4) - if not jacket: - labels_to_keep.append(5) - if not vest: - labels_to_keep.append(6) - if not pants: - labels_to_keep.append(7) - if not shorts: - labels_to_keep.append(8) - if not skirt: - labels_to_keep.append(9) - if not coat: - labels_to_keep.append(10) - if not dress: - labels_to_keep.append(11) - if not jumpsuit: - labels_to_keep.append(12) - if not cape: - labels_to_keep.append(13) - if not glasses: - labels_to_keep.append(14) - if not hat: - labels_to_keep.append(15) - if not hairaccessory: - labels_to_keep.append(16) - if not tie: - labels_to_keep.append(17) - if not glove: - labels_to_keep.append(18) - if not watch: - labels_to_keep.append(19) - if not belt: - labels_to_keep.append(20) - if not legwarmer: - labels_to_keep.append(21) - if not tights: - labels_to_keep.append(22) - if not sock: - labels_to_keep.append(23) - if not shoe: - labels_to_keep.append(24) - if not bagwallet: - labels_to_keep.append(25) - if not scarf: - labels_to_keep.append(26) - if not umbrella: - labels_to_keep.append(27) - if not hood: - labels_to_keep.append(28) - if not collar: - labels_to_keep.append(29) - if not lapel: - labels_to_keep.append(30) - if not epaulette: - labels_to_keep.append(31) - if not sleeve: - labels_to_keep.append(32) - if not pocket: - labels_to_keep.append(33) - if not neckline: - labels_to_keep.append(34) - if not buckle: - labels_to_keep.append(35) - if not zipper: - labels_to_keep.append(36) - if not applique: - labels_to_keep.append(37) - if not bead: - labels_to_keep.append(38) - if not bow: - labels_to_keep.append(39) - if not flower: - labels_to_keep.append(40) - if not fringe: - labels_to_keep.append(41) - if not ribbon: - labels_to_keep.append(42) - if not rivet: - labels_to_keep.append(43) - if not ruffle: - labels_to_keep.append(44) - if not sequin: - labels_to_keep.append(45) - if not tassel: - labels_to_keep.append(46) - - pipeline.segment_label = labels_to_keep - pipeline.model_name = model - return (pipeline,) - -class SegformerUltraV2: - - def __init__(self): - self.NODE_NAME = 'SegformerUltraV2' - pass - - @classmethod - def INPUT_TYPES(cls): - method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] - device_list = ['cuda', 'cpu'] - return {"required": - { - "image": ("IMAGE",), - "segformer_pipeline": ("SegPipeline",), - "detail_method": (method_list,), - "detail_erode": ("INT", {"default": 8, "min": 1, "max": 255, "step": 1}), - "detail_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"}), - "process_detail": ("BOOLEAN", {"default": True}), - "device": (device_list,), - "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = "segformer_ultra_v2" - CATEGORY = '😺dzNodes/LayerMask' - - def segformer_ultra_v2(self, image, segformer_pipeline, - detail_method, detail_erode, detail_dilate, black_point, white_point, - process_detail, device, max_megapixels, - ): - model = segformer_pipeline.model_name - labels_to_keep = segformer_pipeline.segment_label - ret_images = [] - ret_masks = [] - - if detail_method == 'VITMatte(local)': - local_files_only = True - else: - local_files_only = False - - for i in image: - pred_seg, cloth = get_segmentation(i, model_name=model) - i = torch.unsqueeze(i, 0) - i = pil2tensor(tensor2pil(i).convert('RGB')) - orig_image = tensor2pil(i).convert('RGB') - - mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) - - # 创建agnostic-mask图像 - mask_image = Image.fromarray((1 - mask) * 255) - mask_image = mask_image.convert("L") - brightness_image = ImageEnhance.Brightness(mask_image) - mask_image = brightness_image.enhance(factor=1.08) - _mask = pil2tensor(mask_image) - - detail_range = detail_erode + detail_dilate - if process_detail: - if detail_method == 'GuidedFilter': - _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) - _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) - elif detail_method == 'PyMatting': - _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) - else: - _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) - else: - _mask = mask2image(_mask) - - 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),) - - -class LS_SegformerClothesSetting: - - def __init__(self): - self.NODE_NAME = 'SegformerClothesSetting' - pass - - # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", - # 5: "Skirt", 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", - # 11: "Face", 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", - # 17: "Scarf" - - @classmethod - def INPUT_TYPES(cls): - - return {"required": - { "face": ("BOOLEAN", {"default": False, "label_on": "enabled(脸)", "label_off": "disabled(脸)"}), - "hair": ("BOOLEAN", {"default": False, "label_on": "enabled(头发)", "label_off": "disabled(头发)"}), - "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), - "sunglass": ("BOOLEAN", {"default": False, "label_on": "enabled(墨镜)", "label_off": "disabled(墨镜)"}), - "left_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(左臂)", "label_off": "disabled(左臂)"}), - "right_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(右臂)", "label_off": "disabled(右臂)"}), - "left_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(左腿)", "label_off": "disabled(左腿)"}), - "right_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(右腿)", "label_off": "disabled(右腿)"}), - "left_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(左鞋)", "label_off": "disabled(左鞋)"}), - "right_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(右鞋)", "label_off": "disabled(右鞋)"}), - "upper_clothes": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣)", "label_off": "disabled(上衣)"}), - "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(短裙)", "label_off": "disabled(短裙)"}), - "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), - "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), - "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(腰带)", "label_off": "disabled(腰带)"}), - "bag": ("BOOLEAN", {"default": False, "label_on": "enabled(背包)", "label_off": "disabled(背包)"}), - "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), - } - } - - RETURN_TYPES = ("LS_SEGFORMER_SETTING",) - RETURN_NAMES = ("segformer_clothes_setting",) - FUNCTION = "run_segformer_clothes_setting" - CATEGORY = '😺dzNodes/LayerMask' - - def run_segformer_clothes_setting(self, face, hat, hair, sunglass, - left_leg, right_leg, left_arm, right_arm, left_shoe, right_shoe, - upper_clothes, skirt, pants, dress, belt, bag, scarf, - ): - - pipeline = SegformerPipeline() - labels_to_keep = [0] - if not hat: - labels_to_keep.append(1) - if not hair: - labels_to_keep.append(2) - if not sunglass: - labels_to_keep.append(3) - if not upper_clothes: - labels_to_keep.append(4) - if not skirt: - labels_to_keep.append(5) - if not pants: - labels_to_keep.append(6) - if not dress: - labels_to_keep.append(7) - if not belt: - labels_to_keep.append(8) - if not left_shoe: - labels_to_keep.append(9) - if not right_shoe: - labels_to_keep.append(10) - if not face: - labels_to_keep.append(11) - if not left_leg: - labels_to_keep.append(12) - if not right_leg: - labels_to_keep.append(13) - if not left_arm: - labels_to_keep.append(14) - if not right_arm: - labels_to_keep.append(15) - if not bag: - labels_to_keep.append(16) - if not scarf: - labels_to_keep.append(17) - - setting = {"labels_to_keep": labels_to_keep, "model_name": "segformer_b3_clothes"} - - return (setting,) - -class LS_SegformerFashionSetting: - - def __init__(self): - self.NODE_NAME = 'SegformerFashionSetting' - pass - - @classmethod - def INPUT_TYPES(cls): - return {"required": - { "shirt": ("BOOLEAN", {"default": False, "label_on": "enabled(衬衫、罩衫)", "label_off": "disabled(衬衫、罩衫)"}), - "top": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣、t恤)", "label_off": "disabled(上衣、t恤)"}), - "sweater": ("BOOLEAN", {"default": False, "label_on": "enabled(毛衣)", "label_off": "disabled(毛衣)"}), - "cardigan": ("BOOLEAN", {"default": False, "label_on": "enabled(开襟毛衫)", "label_off": "disabled(开襟毛衫)"}), - "jacket": ("BOOLEAN", {"default": False, "label_on": "enabled(夹克)", "label_off": "disabled(夹克)"}), - "vest": ("BOOLEAN", {"default": False, "label_on": "enabled(背心)", "label_off": "disabled(背心)"}), - "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), - "shorts": ("BOOLEAN", {"default": False, "label_on": "enabled(短裤)", "label_off": "disabled(短裤)"}), - "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(裙子)", "label_off": "disabled(裙子)"}), - "coat": ("BOOLEAN", {"default": False, "label_on": "enabled(外套)", "label_off": "disabled(外套)"}), - "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), - "jumpsuit": ("BOOLEAN", {"default": False, "label_on": "enabled(连身裤)", "label_off": "disabled(连身裤)"}), - "cape": ("BOOLEAN", {"default": False, "label_on": "enabled(斗篷)", "label_off": "disabled(斗篷)"}), - "glasses": ("BOOLEAN", {"default": False, "label_on": "enabled(眼镜)", "label_off": "disabled(眼镜)"}), - "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), - "hairaccessory": ("BOOLEAN", {"default": False, "label_on": "enabled(头带)", "label_off": "disabled(头带)"}), - "tie": ("BOOLEAN", {"default": False, "label_on": "enabled(领带)", "label_off": "disabled(领带)"}), - "glove": ("BOOLEAN", {"default": False, "label_on": "enabled(手套)", "label_off": "disabled(手套)"}), - "watch": ("BOOLEAN", {"default": False, "label_on": "enabled(手表)", "label_off": "disabled(手表)"}), - "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(皮带)", "label_off": "disabled(皮带)"}), - "legwarmer": ("BOOLEAN", {"default": False, "label_on": "enabled(腿套)", "label_off": "disabled(腿套)"}), - "tights": ("BOOLEAN", {"default": False, "label_on": "enabled(裤袜)","label_off": "disabled(裤袜)"}), - "sock": ("BOOLEAN", {"default": False, "label_on": "enabled(袜子)", "label_off": "disabled(袜子)"}), - "shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(鞋子)", "label_off": "disabled(鞋子)"}), - "bagwallet": ("BOOLEAN", {"default": False, "label_on": "enabled(手包)", "label_off": "disabled(手包)"}), - "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), - "umbrella": ("BOOLEAN", {"default": False, "label_on": "enabled(雨伞)", "label_off": "disabled(雨伞)"}), - "hood": ("BOOLEAN", {"default": False, "label_on": "enabled(兜帽)", "label_off": "disabled(兜帽)"}), - "collar": ("BOOLEAN", {"default": False, "label_on": "enabled(衣领)", "label_off": "disabled(衣领)"}), - "lapel": ("BOOLEAN", {"default": False, "label_on": "enabled(翻领)", "label_off": "disabled(翻领)"}), - "epaulette": ("BOOLEAN", {"default": False, "label_on": "enabled(肩章)", "label_off": "disabled(肩章)"}), - "sleeve": ("BOOLEAN", {"default": False, "label_on": "enabled(袖子)", "label_off": "disabled(袖子)"}), - "pocket": ("BOOLEAN", {"default": False, "label_on": "enabled(口袋)", "label_off": "disabled(口袋)"}), - "neckline": ("BOOLEAN", {"default": False, "label_on": "enabled(领口)", "label_off": "disabled(领口)"}), - "buckle": ("BOOLEAN", {"default": False, "label_on": "enabled(带扣)", "label_off": "disabled(带扣)"}), - "zipper": ("BOOLEAN", {"default": False, "label_on": "enabled(拉链)", "label_off": "disabled(拉链)"}), - "applique": ("BOOLEAN", {"default": False, "label_on": "enabled(贴花)", "label_off": "disabled(贴花)"}), - "bead": ("BOOLEAN", {"default": False, "label_on": "enabled(珠子)", "label_off": "disabled(珠子)"}), - "bow": ("BOOLEAN", {"default": False, "label_on": "enabled(蝴蝶结)", "label_off": "disabled(蝴蝶结)"}), - "flower": ("BOOLEAN", {"default": False, "label_on": "enabled(花)", "label_off": "disabled(花)"}), - "fringe": ("BOOLEAN", {"default": False, "label_on": "enabled(刘海)", "label_off": "disabled(刘海)"}), - "ribbon": ("BOOLEAN", {"default": False, "label_on": "enabled(丝带)", "label_off": "disabled(丝带)"}), - "rivet": ("BOOLEAN", {"default": False, "label_on": "enabled(铆钉)", "label_off": "disabled(铆钉)"}), - "ruffle": ("BOOLEAN", {"default": False, "label_on": "enabled(褶饰)", "label_off": "disabled(褶饰)"}), - "sequin": ("BOOLEAN", {"default": False, "label_on": "enabled(亮片)", "label_off": "disabled(亮片)"}), - "tassel": ("BOOLEAN", {"default": False, "label_on": "enabled(流苏)", "label_off": "disabled(流苏)"}), - } - } - - RETURN_TYPES = ("LS_SEGFORMER_SETTING",) - RETURN_NAMES = ("segformer_fashion_setting",) - FUNCTION = "run_segformer_fashion_setting" - CATEGORY = '😺dzNodes/LayerMask' - - def run_segformer_fashion_setting(self, shirt, top, sweater, cardigan, jacket, vest, pants, - shorts, skirt, coat, dress, jumpsuit, cape, glasses, - hat, hairaccessory, tie, glove, watch, belt, legwarmer, - tights, sock, shoe, bagwallet, scarf, umbrella, hood, - collar, lapel, epaulette, sleeve, pocket, neckline, - buckle, zipper, applique, bead, bow, flower, fringe, - ribbon, rivet, ruffle, sequin, tassel - ): - - pipeline = SegformerPipeline() - labels_to_keep = [0] - if not shirt: - labels_to_keep.append(1) - if not top: - labels_to_keep.append(2) - if not sweater: - labels_to_keep.append(3) - if not cardigan: - labels_to_keep.append(4) - if not jacket: - labels_to_keep.append(5) - if not vest: - labels_to_keep.append(6) - if not pants: - labels_to_keep.append(7) - if not shorts: - labels_to_keep.append(8) - if not skirt: - labels_to_keep.append(9) - if not coat: - labels_to_keep.append(10) - if not dress: - labels_to_keep.append(11) - if not jumpsuit: - labels_to_keep.append(12) - if not cape: - labels_to_keep.append(13) - if not glasses: - labels_to_keep.append(14) - if not hat: - labels_to_keep.append(15) - if not hairaccessory: - labels_to_keep.append(16) - if not tie: - labels_to_keep.append(17) - if not glove: - labels_to_keep.append(18) - if not watch: - labels_to_keep.append(19) - if not belt: - labels_to_keep.append(20) - if not legwarmer: - labels_to_keep.append(21) - if not tights: - labels_to_keep.append(22) - if not sock: - labels_to_keep.append(23) - if not shoe: - labels_to_keep.append(24) - if not bagwallet: - labels_to_keep.append(25) - if not scarf: - labels_to_keep.append(26) - if not umbrella: - labels_to_keep.append(27) - if not hood: - labels_to_keep.append(28) - if not collar: - labels_to_keep.append(29) - if not lapel: - labels_to_keep.append(30) - if not epaulette: - labels_to_keep.append(31) - if not sleeve: - labels_to_keep.append(32) - if not pocket: - labels_to_keep.append(33) - if not neckline: - labels_to_keep.append(34) - if not buckle: - labels_to_keep.append(35) - if not zipper: - labels_to_keep.append(36) - if not applique: - labels_to_keep.append(37) - if not bead: - labels_to_keep.append(38) - if not bow: - labels_to_keep.append(39) - if not flower: - labels_to_keep.append(40) - if not fringe: - labels_to_keep.append(41) - if not ribbon: - labels_to_keep.append(42) - if not rivet: - labels_to_keep.append(43) - if not ruffle: - labels_to_keep.append(44) - if not sequin: - labels_to_keep.append(45) - if not tassel: - labels_to_keep.append(46) - - setting = {"labels_to_keep":labels_to_keep, "model_name":"segformer_b3_fashion"} - - return (setting,) - -class LS_LoadSegformerModel: - - def __init__(self): - self.NODE_NAME = 'LoadSegformerModel' - pass - - @classmethod - def INPUT_TYPES(cls): - model_list = ['segformer_b3_clothes', 'segformer_b2_clothes', 'segformer_b3_fashion'] - device_list = ['cuda', 'cpu'] - return {"required": - { - "model_name": (model_list,), - "device": (device_list,), - } - } - - RETURN_TYPES = ("LS_SEGFORMER_MODEL", ) - RETURN_NAMES = ("segfromer_model", ) - FUNCTION = "load_segformer_model" - CATEGORY = '😺dzNodes/LayerMask' - - def load_segformer_model(self, model_name, device): - - model_folder_path = os.path.join(folder_paths.models_dir, model_name) - try: - model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[model_name][0][0]) - except: - pass - - processor = SegformerImageProcessor.from_pretrained(model_folder_path) - model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path) - - segfromer_model = {"processor":processor, "model":model, "device":device, "model_name":model_name} - - log(f"{self.NODE_NAME} Loaded Segformer Model {model_name}.", message_type='finish') - return (segfromer_model,) - -class LS_SegformerUltraV3: - - def __init__(self): - self.NODE_NAME = 'SegformerUltraV3' - pass - - @classmethod - def INPUT_TYPES(cls): - method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] - return {"required": - { - "image": ("IMAGE",), - "segformer_model": ("LS_SEGFORMER_MODEL",), - "segformer_setting": ("LS_SEGFORMER_SETTING",), - "detail_method": (method_list,), - "detail_erode": ("INT", {"default": 8, "min": 1, "max": 255, "step": 1}), - "detail_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"}), - "process_detail": ("BOOLEAN", {"default": True}), - "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = "segformer_ultra_v3" - CATEGORY = '😺dzNodes/LayerMask' - - def segformer_ultra_v3(self, image, segformer_model, segformer_setting, - detail_method, detail_erode, detail_dilate, black_point, white_point, - process_detail, max_megapixels, - ): - - device = segformer_model["device"] - model_name = segformer_model["model_name"] - - labels_to_keep = segformer_setting["labels_to_keep"] - labels_model_name = segformer_setting["model_name"] - - ret_images = [] - ret_masks = [] - - if model_name.rsplit('_', 1)[-1] != labels_model_name.rsplit('_', 1)[-1]: # 后缀不一致 - raise TypeError("Segformer Model and Segformer Setting are different.") - - if detail_method == 'VITMatte(local)': - local_files_only = True - else: - local_files_only = False - - for i in image: - pred_seg, cloth = get_segmentation_from_model(i, segformer_model) - i = torch.unsqueeze(i, 0) - i = pil2tensor(tensor2pil(i).convert('RGB')) - orig_image = tensor2pil(i).convert('RGB') - - mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) - - # 创建agnostic-mask图像 - mask_image = Image.fromarray((1 - mask) * 255) - mask_image = mask_image.convert("L") - brightness_image = ImageEnhance.Brightness(mask_image) - mask_image = brightness_image.enhance(factor=1.08) - _mask = pil2tensor(mask_image) - - detail_range = detail_erode + detail_dilate - if process_detail: - if detail_method == 'GuidedFilter': - _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) - _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) - elif detail_method == 'PyMatting': - _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) - else: - _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) - else: - _mask = mask2image(_mask) - - 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: SegformerB2ClothesUltra": Segformer_B2_Clothes, - "LayerMask: SegformerUltraV2": SegformerUltraV2, - "LayerMask: SegformerClothesPipelineLoader": SegformerClothesPipelineLoader, - "LayerMask: SegformerFashionPipelineLoader": SegformerFashionPipelineLoader, - "LayerMask: SegformerUltraV3": LS_SegformerUltraV3, - "LayerMask: SegformerClothesSetting": LS_SegformerClothesSetting, - "LayerMask: SegformerFashionSetting": LS_SegformerFashionSetting, - "LayerMask: LoadSegformerModel": LS_LoadSegformerModel, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerMask: SegformerB2ClothesUltra": "LayerMask: Segformer B2 Clothes Ultra", - "LayerMask: SegformerUltraV2": "LayerMask: Segformer Ultra V2", - "LayerMask: SegformerClothesPipelineLoader": "LayerMask: Segformer Clothes Pipeline", - "LayerMask: SegformerFashionPipelineLoader": "LayerMask: Segformer Fashion Pipeline", - "LayerMask: SegformerUltraV3": "LayerMask: Segformer Ultra V3", - "LayerMask: SegformerClothesSetting": "LayerMask: Segformer Clothes Setting", - "LayerMask: SegformerFashionSetting": "LayerMask: Segformer Fashion Setting", - "LayerMask: LoadSegformerModel": "LayerMask: Load Segformer Model", -} - +''' +原始代码来自 https://github.com/StartHua/Comfyui_segformer_b2_clothes +''' +import torch +import os +import numpy as np +from PIL import Image, ImageEnhance +from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation +import torch.nn as nn +import folder_paths +from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, RGB2RGBA +from .imagefunc import guided_filter_alpha, mask_edge_detail, histogram_remap, generate_VITMatte, generate_VITMatte_trimap + + +class SegformerPipeline: + def __init__(self): + self.model_name = '' + self.segment_label = [] + +SegPipeline = SegformerPipeline() + + +# 切割服装 +def get_segmentation_from_model(tensor_image, segformer_model): + + processor = segformer_model["processor"] + model = segformer_model["model"] + + cloth = tensor2pil(tensor_image) + + # 预处理和预测 + inputs = processor(images=cloth, return_tensors="pt") + outputs = model(**inputs) + logits = outputs.logits.cpu() + upsampled_logits = nn.functional.interpolate(logits, size=cloth.size[::-1], mode="bilinear", align_corners=False) + pred_seg = upsampled_logits.argmax(dim=1)[0].numpy() + return pred_seg,cloth + + +# 切割服装 +def get_segmentation(tensor_image, model_name='segformer_b2_clothes'): + cloth = tensor2pil(tensor_image) + model_folder_path = os.path.join(folder_paths.models_dir, model_name) + try: + model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[model_name][0][0]) + except: + pass + + processor = SegformerImageProcessor.from_pretrained(model_folder_path) + model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path) + # 预处理和预测 + inputs = processor(images=cloth, return_tensors="pt") + outputs = model(**inputs) + logits = outputs.logits.cpu() + upsampled_logits = nn.functional.interpolate(logits, size=cloth.size[::-1], mode="bilinear", align_corners=False) + pred_seg = upsampled_logits.argmax(dim=1)[0].numpy() + return pred_seg,cloth + + +class Segformer_B2_Clothes: + + def __init__(self): + self.NODE_NAME = 'SegformerB2ClothesUltra' + + + # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", 5: "Skirt", + # 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", 11: "Face", + # 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", 16: "Bag", 17: "Scarf" + + @classmethod + def INPUT_TYPES(cls): + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + device_list = ['cuda', 'cpu'] + return {"required": + { + "image": ("IMAGE",), + "face": ("BOOLEAN", {"default": False}), + "hair": ("BOOLEAN", {"default": False}), + "hat": ("BOOLEAN", {"default": False}), + "sunglass": ("BOOLEAN", {"default": False}), + "left_arm": ("BOOLEAN", {"default": False}), + "right_arm": ("BOOLEAN", {"default": False}), + "left_leg": ("BOOLEAN", {"default": False}), + "right_leg": ("BOOLEAN", {"default": False}), + "upper_clothes": ("BOOLEAN", {"default": False}), + "skirt": ("BOOLEAN", {"default": False}), + "pants": ("BOOLEAN", {"default": False}), + "dress": ("BOOLEAN", {"default": False}), + "belt": ("BOOLEAN", {"default": False}), + "shoe": ("BOOLEAN", {"default": False}), + "bag": ("BOOLEAN", {"default": False}), + "scarf": ("BOOLEAN", {"default": False}), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 12, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "black_point": ( + "FLOAT", {"default": 0.15, "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"}), + "process_detail": ("BOOLEAN", {"default": True}), + "device": (device_list,), + "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = "segformer_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_ultra(self, image, + face, hat, hair, sunglass, upper_clothes, skirt, pants, dress, belt, shoe, + left_leg, right_leg, left_arm, right_arm, bag, scarf, detail_method, + detail_erode, detail_dilate, black_point, white_point, process_detail, device, max_megapixels, + ): + + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + pred_seg, cloth = get_segmentation(i) + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + + labels_to_keep = [0] + if not hat: + labels_to_keep.append(1) + if not hair: + labels_to_keep.append(2) + if not sunglass: + labels_to_keep.append(3) + if not upper_clothes: + labels_to_keep.append(4) + if not skirt: + labels_to_keep.append(5) + if not pants: + labels_to_keep.append(6) + if not dress: + labels_to_keep.append(7) + if not belt: + labels_to_keep.append(8) + if not shoe: + labels_to_keep.append(9) + labels_to_keep.append(10) + if not face: + labels_to_keep.append(11) + if not left_leg: + labels_to_keep.append(12) + if not right_leg: + labels_to_keep.append(13) + if not left_arm: + labels_to_keep.append(14) + if not right_arm: + labels_to_keep.append(15) + if not bag: + labels_to_keep.append(16) + if not scarf: + labels_to_keep.append(17) + + mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) + + # 创建agnostic-mask图像 + mask_image = Image.fromarray((1 - mask) * 255) + mask_image = mask_image.convert("L") + _mask = pil2tensor(mask_image) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) + else: + _mask = mask2image(_mask) + + 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),) + +class SegformerClothesPipelineLoader: + + def __init__(self): + self.NODE_NAME = 'SegformerClothesPipelineLoader' + pass + + # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", + # 5: "Skirt", 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", + # 11: "Face", 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", + # 17: "Scarf" + + @classmethod + def INPUT_TYPES(cls): + model_list = ['segformer_b3_clothes', 'segformer_b2_clothes'] + return {"required": + { "model": (model_list,), + "face": ("BOOLEAN", {"default": False, "label_on": "enabled(脸)", "label_off": "disabled(脸)"}), + "hair": ("BOOLEAN", {"default": False, "label_on": "enabled(头发)", "label_off": "disabled(头发)"}), + "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), + "sunglass": ("BOOLEAN", {"default": False, "label_on": "enabled(墨镜)", "label_off": "disabled(墨镜)"}), + "left_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(左臂)", "label_off": "disabled(左臂)"}), + "right_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(右臂)", "label_off": "disabled(右臂)"}), + "left_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(左腿)", "label_off": "disabled(左腿)"}), + "right_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(右腿)", "label_off": "disabled(右腿)"}), + "left_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(左鞋)", "label_off": "disabled(左鞋)"}), + "right_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(右鞋)", "label_off": "disabled(右鞋)"}), + "upper_clothes": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣)", "label_off": "disabled(上衣)"}), + "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(短裙)", "label_off": "disabled(短裙)"}), + "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), + "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), + "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(腰带)", "label_off": "disabled(腰带)"}), + "bag": ("BOOLEAN", {"default": False, "label_on": "enabled(背包)", "label_off": "disabled(背包)"}), + "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), + } + } + + RETURN_TYPES = ("SegPipeline",) + RETURN_NAMES = ("segformer_pipeline",) + FUNCTION = "segformer_clothes_pipeline_loader" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_clothes_pipeline_loader(self, model, + face, hat, hair, sunglass, + left_leg, right_leg, left_arm, right_arm, left_shoe, right_shoe, + upper_clothes, skirt, pants, dress, belt, bag, scarf, + ): + + pipeline = SegformerPipeline() + labels_to_keep = [0] + if not hat: + labels_to_keep.append(1) + if not hair: + labels_to_keep.append(2) + if not sunglass: + labels_to_keep.append(3) + if not upper_clothes: + labels_to_keep.append(4) + if not skirt: + labels_to_keep.append(5) + if not pants: + labels_to_keep.append(6) + if not dress: + labels_to_keep.append(7) + if not belt: + labels_to_keep.append(8) + if not left_shoe: + labels_to_keep.append(9) + if not right_shoe: + labels_to_keep.append(10) + if not face: + labels_to_keep.append(11) + if not left_leg: + labels_to_keep.append(12) + if not right_leg: + labels_to_keep.append(13) + if not left_arm: + labels_to_keep.append(14) + if not right_arm: + labels_to_keep.append(15) + if not bag: + labels_to_keep.append(16) + if not scarf: + labels_to_keep.append(17) + pipeline.segment_label = labels_to_keep + pipeline.model_name = model + return (pipeline,) + +class SegformerFashionPipelineLoader: + + def __init__(self): + self.NODE_NAME = 'SegformerFashionPipelineLoader' + pass + + @classmethod + def INPUT_TYPES(cls): + model_list = ['segformer_b3_fashion'] + return {"required": + { "model": (model_list,), + "shirt": ("BOOLEAN", {"default": False, "label_on": "enabled(衬衫、罩衫)", "label_off": "disabled(衬衫、罩衫)"}), + "top": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣、t恤)", "label_off": "disabled(上衣、t恤)"}), + "sweater": ("BOOLEAN", {"default": False, "label_on": "enabled(毛衣)", "label_off": "disabled(毛衣)"}), + "cardigan": ("BOOLEAN", {"default": False, "label_on": "enabled(开襟毛衫)", "label_off": "disabled(开襟毛衫)"}), + "jacket": ("BOOLEAN", {"default": False, "label_on": "enabled(夹克)", "label_off": "disabled(夹克)"}), + "vest": ("BOOLEAN", {"default": False, "label_on": "enabled(背心)", "label_off": "disabled(背心)"}), + "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), + "shorts": ("BOOLEAN", {"default": False, "label_on": "enabled(短裤)", "label_off": "disabled(短裤)"}), + "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(裙子)", "label_off": "disabled(裙子)"}), + "coat": ("BOOLEAN", {"default": False, "label_on": "enabled(外套)", "label_off": "disabled(外套)"}), + "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), + "jumpsuit": ("BOOLEAN", {"default": False, "label_on": "enabled(连身裤)", "label_off": "disabled(连身裤)"}), + "cape": ("BOOLEAN", {"default": False, "label_on": "enabled(斗篷)", "label_off": "disabled(斗篷)"}), + "glasses": ("BOOLEAN", {"default": False, "label_on": "enabled(眼镜)", "label_off": "disabled(眼镜)"}), + "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), + "hairaccessory": ("BOOLEAN", {"default": False, "label_on": "enabled(头带)", "label_off": "disabled(头带)"}), + "tie": ("BOOLEAN", {"default": False, "label_on": "enabled(领带)", "label_off": "disabled(领带)"}), + "glove": ("BOOLEAN", {"default": False, "label_on": "enabled(手套)", "label_off": "disabled(手套)"}), + "watch": ("BOOLEAN", {"default": False, "label_on": "enabled(手表)", "label_off": "disabled(手表)"}), + "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(皮带)", "label_off": "disabled(皮带)"}), + "legwarmer": ("BOOLEAN", {"default": False, "label_on": "enabled(腿套)", "label_off": "disabled(腿套)"}), + "tights": ("BOOLEAN", {"default": False, "label_on": "enabled(裤袜)","label_off": "disabled(裤袜)"}), + "sock": ("BOOLEAN", {"default": False, "label_on": "enabled(袜子)", "label_off": "disabled(袜子)"}), + "shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(鞋子)", "label_off": "disabled(鞋子)"}), + "bagwallet": ("BOOLEAN", {"default": False, "label_on": "enabled(手包)", "label_off": "disabled(手包)"}), + "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), + "umbrella": ("BOOLEAN", {"default": False, "label_on": "enabled(雨伞)", "label_off": "disabled(雨伞)"}), + "hood": ("BOOLEAN", {"default": False, "label_on": "enabled(兜帽)", "label_off": "disabled(兜帽)"}), + "collar": ("BOOLEAN", {"default": False, "label_on": "enabled(衣领)", "label_off": "disabled(衣领)"}), + "lapel": ("BOOLEAN", {"default": False, "label_on": "enabled(翻领)", "label_off": "disabled(翻领)"}), + "epaulette": ("BOOLEAN", {"default": False, "label_on": "enabled(肩章)", "label_off": "disabled(肩章)"}), + "sleeve": ("BOOLEAN", {"default": False, "label_on": "enabled(袖子)", "label_off": "disabled(袖子)"}), + "pocket": ("BOOLEAN", {"default": False, "label_on": "enabled(口袋)", "label_off": "disabled(口袋)"}), + "neckline": ("BOOLEAN", {"default": False, "label_on": "enabled(领口)", "label_off": "disabled(领口)"}), + "buckle": ("BOOLEAN", {"default": False, "label_on": "enabled(带扣)", "label_off": "disabled(带扣)"}), + "zipper": ("BOOLEAN", {"default": False, "label_on": "enabled(拉链)", "label_off": "disabled(拉链)"}), + "applique": ("BOOLEAN", {"default": False, "label_on": "enabled(贴花)", "label_off": "disabled(贴花)"}), + "bead": ("BOOLEAN", {"default": False, "label_on": "enabled(珠子)", "label_off": "disabled(珠子)"}), + "bow": ("BOOLEAN", {"default": False, "label_on": "enabled(蝴蝶结)", "label_off": "disabled(蝴蝶结)"}), + "flower": ("BOOLEAN", {"default": False, "label_on": "enabled(花)", "label_off": "disabled(花)"}), + "fringe": ("BOOLEAN", {"default": False, "label_on": "enabled(刘海)", "label_off": "disabled(刘海)"}), + "ribbon": ("BOOLEAN", {"default": False, "label_on": "enabled(丝带)", "label_off": "disabled(丝带)"}), + "rivet": ("BOOLEAN", {"default": False, "label_on": "enabled(铆钉)", "label_off": "disabled(铆钉)"}), + "ruffle": ("BOOLEAN", {"default": False, "label_on": "enabled(褶饰)", "label_off": "disabled(褶饰)"}), + "sequin": ("BOOLEAN", {"default": False, "label_on": "enabled(亮片)", "label_off": "disabled(亮片)"}), + "tassel": ("BOOLEAN", {"default": False, "label_on": "enabled(流苏)", "label_off": "disabled(流苏)"}), + } + } + + RETURN_TYPES = ("SegPipeline",) + RETURN_NAMES = ("segformer_pipeline",) + FUNCTION = "segformer_fashion_pipeline_loader" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_fashion_pipeline_loader(self, model, + shirt, top, sweater, cardigan, jacket, vest, pants, + shorts, skirt, coat, dress, jumpsuit, cape, glasses, + hat, hairaccessory, tie, glove, watch, belt, legwarmer, + tights, sock, shoe, bagwallet, scarf, umbrella, hood, + collar, lapel, epaulette, sleeve, pocket, neckline, + buckle, zipper, applique, bead, bow, flower, fringe, + ribbon, rivet, ruffle, sequin, tassel + ): + + pipeline = SegformerPipeline() + labels_to_keep = [0] + if not shirt: + labels_to_keep.append(1) + if not top: + labels_to_keep.append(2) + if not sweater: + labels_to_keep.append(3) + if not cardigan: + labels_to_keep.append(4) + if not jacket: + labels_to_keep.append(5) + if not vest: + labels_to_keep.append(6) + if not pants: + labels_to_keep.append(7) + if not shorts: + labels_to_keep.append(8) + if not skirt: + labels_to_keep.append(9) + if not coat: + labels_to_keep.append(10) + if not dress: + labels_to_keep.append(11) + if not jumpsuit: + labels_to_keep.append(12) + if not cape: + labels_to_keep.append(13) + if not glasses: + labels_to_keep.append(14) + if not hat: + labels_to_keep.append(15) + if not hairaccessory: + labels_to_keep.append(16) + if not tie: + labels_to_keep.append(17) + if not glove: + labels_to_keep.append(18) + if not watch: + labels_to_keep.append(19) + if not belt: + labels_to_keep.append(20) + if not legwarmer: + labels_to_keep.append(21) + if not tights: + labels_to_keep.append(22) + if not sock: + labels_to_keep.append(23) + if not shoe: + labels_to_keep.append(24) + if not bagwallet: + labels_to_keep.append(25) + if not scarf: + labels_to_keep.append(26) + if not umbrella: + labels_to_keep.append(27) + if not hood: + labels_to_keep.append(28) + if not collar: + labels_to_keep.append(29) + if not lapel: + labels_to_keep.append(30) + if not epaulette: + labels_to_keep.append(31) + if not sleeve: + labels_to_keep.append(32) + if not pocket: + labels_to_keep.append(33) + if not neckline: + labels_to_keep.append(34) + if not buckle: + labels_to_keep.append(35) + if not zipper: + labels_to_keep.append(36) + if not applique: + labels_to_keep.append(37) + if not bead: + labels_to_keep.append(38) + if not bow: + labels_to_keep.append(39) + if not flower: + labels_to_keep.append(40) + if not fringe: + labels_to_keep.append(41) + if not ribbon: + labels_to_keep.append(42) + if not rivet: + labels_to_keep.append(43) + if not ruffle: + labels_to_keep.append(44) + if not sequin: + labels_to_keep.append(45) + if not tassel: + labels_to_keep.append(46) + + pipeline.segment_label = labels_to_keep + pipeline.model_name = model + return (pipeline,) + +class SegformerUltraV2: + + def __init__(self): + self.NODE_NAME = 'SegformerUltraV2' + pass + + @classmethod + def INPUT_TYPES(cls): + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + device_list = ['cuda', 'cpu'] + return {"required": + { + "image": ("IMAGE",), + "segformer_pipeline": ("SegPipeline",), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 8, "min": 1, "max": 255, "step": 1}), + "detail_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"}), + "process_detail": ("BOOLEAN", {"default": True}), + "device": (device_list,), + "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = "segformer_ultra_v2" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_ultra_v2(self, image, segformer_pipeline, + detail_method, detail_erode, detail_dilate, black_point, white_point, + process_detail, device, max_megapixels, + ): + model = segformer_pipeline.model_name + labels_to_keep = segformer_pipeline.segment_label + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + pred_seg, cloth = get_segmentation(i, model_name=model) + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + + mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) + + # 创建agnostic-mask图像 + mask_image = Image.fromarray((1 - mask) * 255) + mask_image = mask_image.convert("L") + brightness_image = ImageEnhance.Brightness(mask_image) + mask_image = brightness_image.enhance(factor=1.08) + _mask = pil2tensor(mask_image) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) + else: + _mask = mask2image(_mask) + + 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),) + + +class LS_SegformerClothesSetting: + + def __init__(self): + self.NODE_NAME = 'SegformerClothesSetting' + pass + + # Labels: 0: "Background", 1: "Hat", 2: "Hair", 3: "Sunglasses", 4: "Upper-clothes", + # 5: "Skirt", 6: "Pants", 7: "Dress", 8: "Belt", 9: "Left-shoe", 10: "Right-shoe", + # 11: "Face", 12: "Left-leg", 13: "Right-leg", 14: "Left-arm", 15: "Right-arm", + # 17: "Scarf" + + @classmethod + def INPUT_TYPES(cls): + + return {"required": + { "face": ("BOOLEAN", {"default": False, "label_on": "enabled(脸)", "label_off": "disabled(脸)"}), + "hair": ("BOOLEAN", {"default": False, "label_on": "enabled(头发)", "label_off": "disabled(头发)"}), + "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), + "sunglass": ("BOOLEAN", {"default": False, "label_on": "enabled(墨镜)", "label_off": "disabled(墨镜)"}), + "left_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(左臂)", "label_off": "disabled(左臂)"}), + "right_arm": ("BOOLEAN", {"default": False, "label_on": "enabled(右臂)", "label_off": "disabled(右臂)"}), + "left_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(左腿)", "label_off": "disabled(左腿)"}), + "right_leg": ("BOOLEAN", {"default": False, "label_on": "enabled(右腿)", "label_off": "disabled(右腿)"}), + "left_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(左鞋)", "label_off": "disabled(左鞋)"}), + "right_shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(右鞋)", "label_off": "disabled(右鞋)"}), + "upper_clothes": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣)", "label_off": "disabled(上衣)"}), + "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(短裙)", "label_off": "disabled(短裙)"}), + "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), + "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), + "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(腰带)", "label_off": "disabled(腰带)"}), + "bag": ("BOOLEAN", {"default": False, "label_on": "enabled(背包)", "label_off": "disabled(背包)"}), + "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), + } + } + + RETURN_TYPES = ("LS_SEGFORMER_SETTING",) + RETURN_NAMES = ("segformer_clothes_setting",) + FUNCTION = "run_segformer_clothes_setting" + CATEGORY = '😺dzNodes/LayerMask' + + def run_segformer_clothes_setting(self, face, hat, hair, sunglass, + left_leg, right_leg, left_arm, right_arm, left_shoe, right_shoe, + upper_clothes, skirt, pants, dress, belt, bag, scarf, + ): + + pipeline = SegformerPipeline() + labels_to_keep = [0] + if not hat: + labels_to_keep.append(1) + if not hair: + labels_to_keep.append(2) + if not sunglass: + labels_to_keep.append(3) + if not upper_clothes: + labels_to_keep.append(4) + if not skirt: + labels_to_keep.append(5) + if not pants: + labels_to_keep.append(6) + if not dress: + labels_to_keep.append(7) + if not belt: + labels_to_keep.append(8) + if not left_shoe: + labels_to_keep.append(9) + if not right_shoe: + labels_to_keep.append(10) + if not face: + labels_to_keep.append(11) + if not left_leg: + labels_to_keep.append(12) + if not right_leg: + labels_to_keep.append(13) + if not left_arm: + labels_to_keep.append(14) + if not right_arm: + labels_to_keep.append(15) + if not bag: + labels_to_keep.append(16) + if not scarf: + labels_to_keep.append(17) + + setting = {"labels_to_keep": labels_to_keep, "model_name": "segformer_b3_clothes"} + + return (setting,) + +class LS_SegformerFashionSetting: + + def __init__(self): + self.NODE_NAME = 'SegformerFashionSetting' + pass + + @classmethod + def INPUT_TYPES(cls): + return {"required": + { "shirt": ("BOOLEAN", {"default": False, "label_on": "enabled(衬衫、罩衫)", "label_off": "disabled(衬衫、罩衫)"}), + "top": ("BOOLEAN", {"default": False, "label_on": "enabled(上衣、t恤)", "label_off": "disabled(上衣、t恤)"}), + "sweater": ("BOOLEAN", {"default": False, "label_on": "enabled(毛衣)", "label_off": "disabled(毛衣)"}), + "cardigan": ("BOOLEAN", {"default": False, "label_on": "enabled(开襟毛衫)", "label_off": "disabled(开襟毛衫)"}), + "jacket": ("BOOLEAN", {"default": False, "label_on": "enabled(夹克)", "label_off": "disabled(夹克)"}), + "vest": ("BOOLEAN", {"default": False, "label_on": "enabled(背心)", "label_off": "disabled(背心)"}), + "pants": ("BOOLEAN", {"default": False, "label_on": "enabled(裤子)", "label_off": "disabled(裤子)"}), + "shorts": ("BOOLEAN", {"default": False, "label_on": "enabled(短裤)", "label_off": "disabled(短裤)"}), + "skirt": ("BOOLEAN", {"default": False, "label_on": "enabled(裙子)", "label_off": "disabled(裙子)"}), + "coat": ("BOOLEAN", {"default": False, "label_on": "enabled(外套)", "label_off": "disabled(外套)"}), + "dress": ("BOOLEAN", {"default": False, "label_on": "enabled(连衣裙)", "label_off": "disabled(连衣裙)"}), + "jumpsuit": ("BOOLEAN", {"default": False, "label_on": "enabled(连身裤)", "label_off": "disabled(连身裤)"}), + "cape": ("BOOLEAN", {"default": False, "label_on": "enabled(斗篷)", "label_off": "disabled(斗篷)"}), + "glasses": ("BOOLEAN", {"default": False, "label_on": "enabled(眼镜)", "label_off": "disabled(眼镜)"}), + "hat": ("BOOLEAN", {"default": False, "label_on": "enabled(帽子)", "label_off": "disabled(帽子)"}), + "hairaccessory": ("BOOLEAN", {"default": False, "label_on": "enabled(头带)", "label_off": "disabled(头带)"}), + "tie": ("BOOLEAN", {"default": False, "label_on": "enabled(领带)", "label_off": "disabled(领带)"}), + "glove": ("BOOLEAN", {"default": False, "label_on": "enabled(手套)", "label_off": "disabled(手套)"}), + "watch": ("BOOLEAN", {"default": False, "label_on": "enabled(手表)", "label_off": "disabled(手表)"}), + "belt": ("BOOLEAN", {"default": False, "label_on": "enabled(皮带)", "label_off": "disabled(皮带)"}), + "legwarmer": ("BOOLEAN", {"default": False, "label_on": "enabled(腿套)", "label_off": "disabled(腿套)"}), + "tights": ("BOOLEAN", {"default": False, "label_on": "enabled(裤袜)","label_off": "disabled(裤袜)"}), + "sock": ("BOOLEAN", {"default": False, "label_on": "enabled(袜子)", "label_off": "disabled(袜子)"}), + "shoe": ("BOOLEAN", {"default": False, "label_on": "enabled(鞋子)", "label_off": "disabled(鞋子)"}), + "bagwallet": ("BOOLEAN", {"default": False, "label_on": "enabled(手包)", "label_off": "disabled(手包)"}), + "scarf": ("BOOLEAN", {"default": False, "label_on": "enabled(围巾)", "label_off": "disabled(围巾)"}), + "umbrella": ("BOOLEAN", {"default": False, "label_on": "enabled(雨伞)", "label_off": "disabled(雨伞)"}), + "hood": ("BOOLEAN", {"default": False, "label_on": "enabled(兜帽)", "label_off": "disabled(兜帽)"}), + "collar": ("BOOLEAN", {"default": False, "label_on": "enabled(衣领)", "label_off": "disabled(衣领)"}), + "lapel": ("BOOLEAN", {"default": False, "label_on": "enabled(翻领)", "label_off": "disabled(翻领)"}), + "epaulette": ("BOOLEAN", {"default": False, "label_on": "enabled(肩章)", "label_off": "disabled(肩章)"}), + "sleeve": ("BOOLEAN", {"default": False, "label_on": "enabled(袖子)", "label_off": "disabled(袖子)"}), + "pocket": ("BOOLEAN", {"default": False, "label_on": "enabled(口袋)", "label_off": "disabled(口袋)"}), + "neckline": ("BOOLEAN", {"default": False, "label_on": "enabled(领口)", "label_off": "disabled(领口)"}), + "buckle": ("BOOLEAN", {"default": False, "label_on": "enabled(带扣)", "label_off": "disabled(带扣)"}), + "zipper": ("BOOLEAN", {"default": False, "label_on": "enabled(拉链)", "label_off": "disabled(拉链)"}), + "applique": ("BOOLEAN", {"default": False, "label_on": "enabled(贴花)", "label_off": "disabled(贴花)"}), + "bead": ("BOOLEAN", {"default": False, "label_on": "enabled(珠子)", "label_off": "disabled(珠子)"}), + "bow": ("BOOLEAN", {"default": False, "label_on": "enabled(蝴蝶结)", "label_off": "disabled(蝴蝶结)"}), + "flower": ("BOOLEAN", {"default": False, "label_on": "enabled(花)", "label_off": "disabled(花)"}), + "fringe": ("BOOLEAN", {"default": False, "label_on": "enabled(刘海)", "label_off": "disabled(刘海)"}), + "ribbon": ("BOOLEAN", {"default": False, "label_on": "enabled(丝带)", "label_off": "disabled(丝带)"}), + "rivet": ("BOOLEAN", {"default": False, "label_on": "enabled(铆钉)", "label_off": "disabled(铆钉)"}), + "ruffle": ("BOOLEAN", {"default": False, "label_on": "enabled(褶饰)", "label_off": "disabled(褶饰)"}), + "sequin": ("BOOLEAN", {"default": False, "label_on": "enabled(亮片)", "label_off": "disabled(亮片)"}), + "tassel": ("BOOLEAN", {"default": False, "label_on": "enabled(流苏)", "label_off": "disabled(流苏)"}), + } + } + + RETURN_TYPES = ("LS_SEGFORMER_SETTING",) + RETURN_NAMES = ("segformer_fashion_setting",) + FUNCTION = "run_segformer_fashion_setting" + CATEGORY = '😺dzNodes/LayerMask' + + def run_segformer_fashion_setting(self, shirt, top, sweater, cardigan, jacket, vest, pants, + shorts, skirt, coat, dress, jumpsuit, cape, glasses, + hat, hairaccessory, tie, glove, watch, belt, legwarmer, + tights, sock, shoe, bagwallet, scarf, umbrella, hood, + collar, lapel, epaulette, sleeve, pocket, neckline, + buckle, zipper, applique, bead, bow, flower, fringe, + ribbon, rivet, ruffle, sequin, tassel + ): + + pipeline = SegformerPipeline() + labels_to_keep = [0] + if not shirt: + labels_to_keep.append(1) + if not top: + labels_to_keep.append(2) + if not sweater: + labels_to_keep.append(3) + if not cardigan: + labels_to_keep.append(4) + if not jacket: + labels_to_keep.append(5) + if not vest: + labels_to_keep.append(6) + if not pants: + labels_to_keep.append(7) + if not shorts: + labels_to_keep.append(8) + if not skirt: + labels_to_keep.append(9) + if not coat: + labels_to_keep.append(10) + if not dress: + labels_to_keep.append(11) + if not jumpsuit: + labels_to_keep.append(12) + if not cape: + labels_to_keep.append(13) + if not glasses: + labels_to_keep.append(14) + if not hat: + labels_to_keep.append(15) + if not hairaccessory: + labels_to_keep.append(16) + if not tie: + labels_to_keep.append(17) + if not glove: + labels_to_keep.append(18) + if not watch: + labels_to_keep.append(19) + if not belt: + labels_to_keep.append(20) + if not legwarmer: + labels_to_keep.append(21) + if not tights: + labels_to_keep.append(22) + if not sock: + labels_to_keep.append(23) + if not shoe: + labels_to_keep.append(24) + if not bagwallet: + labels_to_keep.append(25) + if not scarf: + labels_to_keep.append(26) + if not umbrella: + labels_to_keep.append(27) + if not hood: + labels_to_keep.append(28) + if not collar: + labels_to_keep.append(29) + if not lapel: + labels_to_keep.append(30) + if not epaulette: + labels_to_keep.append(31) + if not sleeve: + labels_to_keep.append(32) + if not pocket: + labels_to_keep.append(33) + if not neckline: + labels_to_keep.append(34) + if not buckle: + labels_to_keep.append(35) + if not zipper: + labels_to_keep.append(36) + if not applique: + labels_to_keep.append(37) + if not bead: + labels_to_keep.append(38) + if not bow: + labels_to_keep.append(39) + if not flower: + labels_to_keep.append(40) + if not fringe: + labels_to_keep.append(41) + if not ribbon: + labels_to_keep.append(42) + if not rivet: + labels_to_keep.append(43) + if not ruffle: + labels_to_keep.append(44) + if not sequin: + labels_to_keep.append(45) + if not tassel: + labels_to_keep.append(46) + + setting = {"labels_to_keep":labels_to_keep, "model_name":"segformer_b3_fashion"} + + return (setting,) + +class LS_LoadSegformerModel: + + def __init__(self): + self.NODE_NAME = 'LoadSegformerModel' + pass + + @classmethod + def INPUT_TYPES(cls): + model_list = ['segformer_b3_clothes', 'segformer_b2_clothes', 'segformer_b3_fashion'] + device_list = ['cuda', 'cpu'] + return {"required": + { + "model_name": (model_list,), + "device": (device_list,), + } + } + + RETURN_TYPES = ("LS_SEGFORMER_MODEL", ) + RETURN_NAMES = ("segfromer_model", ) + FUNCTION = "load_segformer_model" + CATEGORY = '😺dzNodes/LayerMask' + + def load_segformer_model(self, model_name, device): + + model_folder_path = os.path.join(folder_paths.models_dir, model_name) + try: + model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[model_name][0][0]) + except: + pass + + processor = SegformerImageProcessor.from_pretrained(model_folder_path) + model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path) + + segfromer_model = {"processor":processor, "model":model, "device":device, "model_name":model_name} + + log(f"{self.NODE_NAME} Loaded Segformer Model {model_name}.", message_type='finish') + return (segfromer_model,) + +class LS_SegformerUltraV3: + + def __init__(self): + self.NODE_NAME = 'SegformerUltraV3' + pass + + @classmethod + def INPUT_TYPES(cls): + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + return {"required": + { + "image": ("IMAGE",), + "segformer_model": ("LS_SEGFORMER_MODEL",), + "segformer_setting": ("LS_SEGFORMER_SETTING",), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 8, "min": 1, "max": 255, "step": 1}), + "detail_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"}), + "process_detail": ("BOOLEAN", {"default": True}), + "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = "segformer_ultra_v3" + CATEGORY = '😺dzNodes/LayerMask' + + def segformer_ultra_v3(self, image, segformer_model, segformer_setting, + detail_method, detail_erode, detail_dilate, black_point, white_point, + process_detail, max_megapixels, + ): + + device = segformer_model["device"] + model_name = segformer_model["model_name"] + + labels_to_keep = segformer_setting["labels_to_keep"] + labels_model_name = segformer_setting["model_name"] + + ret_images = [] + ret_masks = [] + + if model_name.rsplit('_', 1)[-1] != labels_model_name.rsplit('_', 1)[-1]: # 后缀不一致 + raise TypeError("Segformer Model and Segformer Setting are different.") + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + for i in image: + pred_seg, cloth = get_segmentation_from_model(i, segformer_model) + i = torch.unsqueeze(i, 0) + i = pil2tensor(tensor2pil(i).convert('RGB')) + orig_image = tensor2pil(i).convert('RGB') + + mask = np.isin(pred_seg, labels_to_keep).astype(np.uint8) + + # 创建agnostic-mask图像 + mask_image = Image.fromarray((1 - mask) * 255) + mask_image = mask_image.convert("L") + brightness_image = ImageEnhance.Brightness(mask_image) + mask_image = brightness_image.enhance(factor=1.08) + _mask = pil2tensor(mask_image) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_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)) + else: + _mask = mask2image(_mask) + + 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: SegformerB2ClothesUltra": Segformer_B2_Clothes, + "LayerMask: SegformerUltraV2": SegformerUltraV2, + "LayerMask: SegformerClothesPipelineLoader": SegformerClothesPipelineLoader, + "LayerMask: SegformerFashionPipelineLoader": SegformerFashionPipelineLoader, + "LayerMask: SegformerUltraV3": LS_SegformerUltraV3, + "LayerMask: SegformerClothesSetting": LS_SegformerClothesSetting, + "LayerMask: SegformerFashionSetting": LS_SegformerFashionSetting, + "LayerMask: LoadSegformerModel": LS_LoadSegformerModel, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: SegformerB2ClothesUltra": "LayerMask: Segformer B2 Clothes Ultra", + "LayerMask: SegformerUltraV2": "LayerMask: Segformer Ultra V2", + "LayerMask: SegformerClothesPipelineLoader": "LayerMask: Segformer Clothes Pipeline", + "LayerMask: SegformerFashionPipelineLoader": "LayerMask: Segformer Fashion Pipeline", + "LayerMask: SegformerUltraV3": "LayerMask: Segformer Ultra V3", + "LayerMask: SegformerClothesSetting": "LayerMask: Segformer Clothes Setting", + "LayerMask: SegformerFashionSetting": "LayerMask: Segformer Fashion Setting", + "LayerMask: LoadSegformerModel": "LayerMask: Load Segformer Model", +} + diff --git a/py/shadow_highlight_mask.py b/py/shadow_highlight_mask.py old mode 100644 new mode 100755 index e20f46c7..37980a1e --- a/py/shadow_highlight_mask.py +++ b/py/shadow_highlight_mask.py @@ -1,185 +1,185 @@ -import torch -from PIL import Image, ImageChops -from .imagefunc import log, tensor2pil, pil2tensor, image2mask -from .imagefunc import get_gray_average, calculate_shadow_highlight_level, luminance_keyer - - -def norm_value(value): - if value < 0.01: - value = 0.01 - if value > 0.99: - value = 0.99 - return value - -class ShadowAndHighlightMask: - - def __init__(self): - self.NODE_NAME = 'Shadow & Highlight Mask' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), - "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_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 = ("MASK", "MASK") - RETURN_NAMES = ("shadow_mask", "highlight_mask") - FUNCTION = 'shadow_and_highlight_mask' - CATEGORY = '😺dzNodes/LayerMask' - - def shadow_and_highlight_mask(self, image, - shadow_level_offset, shadow_range, - highlight_level_offset, highlight_range, - mask=None - ): - - ret_shadow_masks = [] - ret_highlight_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: - 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) - 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) - - 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) - - black = Image.new('L', size=_image.size, color='black') - _mask = ImageChops.invert(_mask) - _shadow_mask.paste(black, mask=_mask) - _highlight_mask.paste(black, mask=_mask) - ret_shadow_masks.append(image2mask(_shadow_mask)) - ret_highlight_masks.append(image2mask(_highlight_mask)) - - log(f"{self.NODE_NAME} Processed {len(ret_shadow_masks)} image(s).", message_type='finish') - return (torch.cat(ret_shadow_masks, dim=0),torch.cat(ret_highlight_masks, dim=0),) - -class LS_ShadowAndHighlightMaskV2: - - def __init__(self): - self.NODE_NAME = 'Shadow Highlight Mask V2' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), - "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_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 = ("MASK", "MASK") - RETURN_NAMES = ("shadow_mask", "highlight_mask") - FUNCTION = 'shadow_and_highlight_mask_v2' - CATEGORY = '😺dzNodes/LayerMask' - - def shadow_and_highlight_mask_v2(self, image, - shadow_level_offset, shadow_range, - highlight_level_offset, highlight_range, - mask=None - ): - - ret_shadow_masks = [] - ret_highlight_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: - 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) - 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) - - 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) - - black = Image.new('L', size=_image.size, color='black') - _mask = ImageChops.invert(_mask) - _shadow_mask.paste(black, mask=_mask) - _highlight_mask.paste(black, mask=_mask) - ret_shadow_masks.append(image2mask(_shadow_mask)) - ret_highlight_masks.append(image2mask(_highlight_mask)) - - log(f"{self.NODE_NAME} Processed {len(ret_shadow_masks)} image(s).", message_type='finish') - return (torch.cat(ret_shadow_masks, dim=0),torch.cat(ret_highlight_masks, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerMask: Shadow & Highlight Mask": ShadowAndHighlightMask, - "LayerMask: ShadowHighlightMaskV2": LS_ShadowAndHighlightMaskV2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerMask: Shadow & Highlight Mask": "LayerMask: Shadow & Highlight Mask", - "LayerMask: ShadowHighlightMaskV2": "LayerMask: Shadow Highlight Mask V2" +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, image2mask +from .imagefunc import get_gray_average, calculate_shadow_highlight_level, luminance_keyer + + +def norm_value(value): + if value < 0.01: + value = 0.01 + if value > 0.99: + value = 0.99 + return value + +class ShadowAndHighlightMask: + + def __init__(self): + self.NODE_NAME = 'Shadow & Highlight Mask' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + "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_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 = ("MASK", "MASK") + RETURN_NAMES = ("shadow_mask", "highlight_mask") + FUNCTION = 'shadow_and_highlight_mask' + CATEGORY = '😺dzNodes/LayerMask' + + def shadow_and_highlight_mask(self, image, + shadow_level_offset, shadow_range, + highlight_level_offset, highlight_range, + mask=None + ): + + ret_shadow_masks = [] + ret_highlight_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: + 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) + 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) + + 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) + + black = Image.new('L', size=_image.size, color='black') + _mask = ImageChops.invert(_mask) + _shadow_mask.paste(black, mask=_mask) + _highlight_mask.paste(black, mask=_mask) + ret_shadow_masks.append(image2mask(_shadow_mask)) + ret_highlight_masks.append(image2mask(_highlight_mask)) + + log(f"{self.NODE_NAME} Processed {len(ret_shadow_masks)} image(s).", message_type='finish') + return (torch.cat(ret_shadow_masks, dim=0),torch.cat(ret_highlight_masks, dim=0),) + +class LS_ShadowAndHighlightMaskV2: + + def __init__(self): + self.NODE_NAME = 'Shadow Highlight Mask V2' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + "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_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 = ("MASK", "MASK") + RETURN_NAMES = ("shadow_mask", "highlight_mask") + FUNCTION = 'shadow_and_highlight_mask_v2' + CATEGORY = '😺dzNodes/LayerMask' + + def shadow_and_highlight_mask_v2(self, image, + shadow_level_offset, shadow_range, + highlight_level_offset, highlight_range, + mask=None + ): + + ret_shadow_masks = [] + ret_highlight_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: + 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) + 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) + + 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) + + black = Image.new('L', size=_image.size, color='black') + _mask = ImageChops.invert(_mask) + _shadow_mask.paste(black, mask=_mask) + _highlight_mask.paste(black, mask=_mask) + ret_shadow_masks.append(image2mask(_shadow_mask)) + ret_highlight_masks.append(image2mask(_highlight_mask)) + + log(f"{self.NODE_NAME} Processed {len(ret_shadow_masks)} image(s).", message_type='finish') + return (torch.cat(ret_shadow_masks, dim=0),torch.cat(ret_highlight_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerMask: Shadow & Highlight Mask": ShadowAndHighlightMask, + "LayerMask: ShadowHighlightMaskV2": LS_ShadowAndHighlightMaskV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: Shadow & Highlight Mask": "LayerMask: Shadow & Highlight Mask", + "LayerMask: ShadowHighlightMaskV2": "LayerMask: Shadow Highlight Mask V2" } \ No newline at end of file diff --git a/py/sharp_soft.py b/py/sharp_soft.py old mode 100644 new mode 100755 index ca63e15d..8dc3c50c --- a/py/sharp_soft.py +++ b/py/sharp_soft.py @@ -1,80 +1,80 @@ -import torch -import copy -import cv2 -import numpy as np -from PIL import Image -from .imagefunc import log - - - -class SharpAndSoft: - - def __init__(self): - self.NODE_NAME = 'Sharp & Soft' - - @classmethod - def INPUT_TYPES(self): - - enhance_list = ['very sharp', 'sharp', 'soft', 'very soft', 'None'] - - return { - "required": { - "images": ("IMAGE",), - "enhance": (enhance_list, ), - - }, - "optional": { - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = 'sharp_and_soft' - CATEGORY = '😺dzNodes/LayerFilter' - - def sharp_and_soft(self, images, enhance, ): - - if enhance == 'very sharp': - filter_radius = 1 - denoise = 0.6 - detail_mult = 2.8 - if enhance == 'sharp': - filter_radius = 3 - denoise = 0.12 - detail_mult = 1.8 - if enhance == 'soft': - filter_radius = 8 - denoise = 0.08 - detail_mult = 0.5 - if enhance == 'very soft': - filter_radius = 15 - denoise = 0.06 - detail_mult = 0.01 - else: - return (images,) - - d = int(filter_radius * 2) + 1 - s = 0.02 - n = denoise / 10 - dup = copy.deepcopy(images.cpu().numpy()) - - from cv2.ximgproc import guidedFilter - for index, image in enumerate(dup): - imgB = image - if denoise > 0.0: - imgB = cv2.bilateralFilter(image, d, n, d) - imgG = np.clip(guidedFilter(image, image, d, s), 0.001, 1) - details = (imgB / imgG - 1) * detail_mult + 1 - dup[index] = np.clip(details * imgG - imgB + image, 0, 1) - - log(f"{self.NODE_NAME} Processed {dup.shape[0]} image(s).", message_type='finish') - return (torch.from_numpy(dup),) - - -NODE_CLASS_MAPPINGS = { - "LayerFilter: Sharp & Soft": SharpAndSoft -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerFilter: Sharp & Soft": "LayerFilter: Sharp & Soft" +import torch +import copy +import cv2 +import numpy as np +from PIL import Image +from .imagefunc import log + + + +class SharpAndSoft: + + def __init__(self): + self.NODE_NAME = 'Sharp & Soft' + + @classmethod + def INPUT_TYPES(self): + + enhance_list = ['very sharp', 'sharp', 'soft', 'very soft', 'None'] + + return { + "required": { + "images": ("IMAGE",), + "enhance": (enhance_list, ), + + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'sharp_and_soft' + CATEGORY = '😺dzNodes/LayerFilter' + + def sharp_and_soft(self, images, enhance, ): + + if enhance == 'very sharp': + filter_radius = 1 + denoise = 0.6 + detail_mult = 2.8 + if enhance == 'sharp': + filter_radius = 3 + denoise = 0.12 + detail_mult = 1.8 + if enhance == 'soft': + filter_radius = 8 + denoise = 0.08 + detail_mult = 0.5 + if enhance == 'very soft': + filter_radius = 15 + denoise = 0.06 + detail_mult = 0.01 + else: + return (images,) + + d = int(filter_radius * 2) + 1 + s = 0.02 + n = denoise / 10 + dup = copy.deepcopy(images.cpu().numpy()) + + from cv2.ximgproc import guidedFilter + for index, image in enumerate(dup): + imgB = image + if denoise > 0.0: + imgB = cv2.bilateralFilter(image, d, n, d) + imgG = np.clip(guidedFilter(image, image, d, s), 0.001, 1) + details = (imgB / imgG - 1) * detail_mult + 1 + dup[index] = np.clip(details * imgG - imgB + image, 0, 1) + + log(f"{self.NODE_NAME} Processed {dup.shape[0]} image(s).", message_type='finish') + return (torch.from_numpy(dup),) + + +NODE_CLASS_MAPPINGS = { + "LayerFilter: Sharp & Soft": SharpAndSoft +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: Sharp & Soft": "LayerFilter: Sharp & Soft" } \ No newline at end of file diff --git a/py/simple_text_image.py b/py/simple_text_image.py old mode 100644 new mode 100755 index 1d9b6cce..f5607d6f --- a/py/simple_text_image.py +++ b/py/simple_text_image.py @@ -1,125 +1,125 @@ -import torch -import textwrap -import copy -from PIL import Image, ImageFont, ImageDraw -from typing import cast -from .imagefunc import AnyType, log, get_resource_dir, tensor2pil, pil2tensor, image2mask - - -any = AnyType("*") - -class SimpleTextImage: - - def __init__(self): - self.NODE_NAME = 'SimpleTextImage' - - @classmethod - def INPUT_TYPES(self): - - (_, FONT_DICT) = get_resource_dir() - FONT_LIST = list(FONT_DICT.keys()) - - return { - "required": { - "text": ("STRING",{"default": "text", "multiline": True}, - ), - "font_file": (FONT_LIST,), - "align": (["center", "left", "right"],), - "char_per_line": ("INT", {"default": 80, "min": 1, "max": 8096, "step": 1},), - "leading": ("INT",{"default": 8, "min": 0, "max": 8096, "step": 1},), - "font_size": ("INT",{"default": 72, "min": 1, "max": 2500, "step": 1},), - "text_color": ("STRING", {"default": "#FFFFFF"},), - "stroke_width": ("INT",{"default": 0, "min": 0, "max": 8096, "step": 1},), - "stroke_color": ("STRING",{"default": "#FF8000"},), - "x_offset": ("INT", {"default": 0, "min": 0, "max": 8096, "step": 1},), - "y_offset": ("INT", {"default": 0, "min": 0, "max": 8096, "step": 1},), - "width": ("INT", {"default": 512, "min": 1, "max": 8096, "step": 1},), - "height": ("INT", {"default": 512, "min": 1, "max": 8096, "step": 1},), - }, - "optional": { - "size_as": (any, {}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = 'simple_text_image' - CATEGORY = '😺dzNodes/LayerUtility' - - def simple_text_image(self, text, font_file, align, char_per_line, - leading, font_size, text_color, - stroke_width, stroke_color, x_offset, y_offset, - width, height, size_as=None - ): - - (_, FONT_DICT) = get_resource_dir() - FONT_LIST = list(FONT_DICT.keys()) - - ret_images = [] - ret_masks = [] - if size_as is not None: - if size_as.dim() == 2: - size_as_image = torch.unsqueeze(mask, 0) - if size_as.shape[0] > 0: - size_as_image = torch.unsqueeze(size_as[0], 0) - else: - size_as_image = copy.deepcopy(size_as) - width, height = tensor2pil(size_as_image).size - font_path = FONT_DICT.get(font_file) - (_, top, _, _) = ImageFont.truetype(font=font_path, size=font_size, encoding='unic').getbbox(text) - font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font_path, font_size)) - if char_per_line == 0: - char_per_line = int(width / font_size) - paragraphs = text.split('\n') - - img_height = height # line_height * len(lines) - img_width = width # max(font.getsize(line)[0] for line in lines) - - img = Image.new("RGBA", size=(img_width, img_height), color=(0, 0, 0, 0)) - draw = ImageDraw.Draw(img) - y_text = y_offset + stroke_width - for paragraph in paragraphs: - lines = textwrap.wrap(paragraph, width=char_per_line, expand_tabs=False, - replace_whitespace=False, drop_whitespace=False) - for line in lines: - width = font.getbbox(line)[2] - font.getbbox(line)[0] - height = font.getbbox(line)[3] - font.getbbox(line)[1] - # 根据 align 参数重新计算 x 坐标 - if align == "left": - x_text = x_offset - elif align == "center": - x_text = (img_width - width) // 2 - elif align == "right": - x_text = img_width - width - x_offset - else: - x_text = x_offset # 默认为左对齐 - - draw.text( - xy=(x_text, y_text), - text=line, - fill=text_color, - font=font, - stroke_width=stroke_width, - stroke_fill=stroke_color, - ) - y_text += height + leading - y_text += leading * 2 - - if size_as is not None: - for i in size_as: - ret_images.append(pil2tensor(img)) - ret_masks.append(image2mask(img.split()[3])) - else: - ret_images.append(pil2tensor(img)) - ret_masks.append(image2mask(img.split()[3])) - - log(f"{self.NODE_NAME} Processed.", message_type='finish') - return (torch.cat(ret_images, dim=0),torch.cat(ret_masks, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: SimpleTextImage": SimpleTextImage -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: SimpleTextImage": "LayerUtility: SimpleTextImage" +import torch +import textwrap +import copy +from PIL import Image, ImageFont, ImageDraw +from typing import cast +from .imagefunc import AnyType, log, get_resource_dir, tensor2pil, pil2tensor, image2mask + + +any = AnyType("*") + +class SimpleTextImage: + + def __init__(self): + self.NODE_NAME = 'SimpleTextImage' + + @classmethod + def INPUT_TYPES(self): + + (_, FONT_DICT) = get_resource_dir() + FONT_LIST = list(FONT_DICT.keys()) + + return { + "required": { + "text": ("STRING",{"default": "text", "multiline": True}, + ), + "font_file": (FONT_LIST,), + "align": (["center", "left", "right"],), + "char_per_line": ("INT", {"default": 80, "min": 1, "max": 8096, "step": 1},), + "leading": ("INT",{"default": 8, "min": 0, "max": 8096, "step": 1},), + "font_size": ("INT",{"default": 72, "min": 1, "max": 2500, "step": 1},), + "text_color": ("STRING", {"default": "#FFFFFF"},), + "stroke_width": ("INT",{"default": 0, "min": 0, "max": 8096, "step": 1},), + "stroke_color": ("STRING",{"default": "#FF8000"},), + "x_offset": ("INT", {"default": 0, "min": 0, "max": 8096, "step": 1},), + "y_offset": ("INT", {"default": 0, "min": 0, "max": 8096, "step": 1},), + "width": ("INT", {"default": 512, "min": 1, "max": 8096, "step": 1},), + "height": ("INT", {"default": 512, "min": 1, "max": 8096, "step": 1},), + }, + "optional": { + "size_as": (any, {}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'simple_text_image' + CATEGORY = '😺dzNodes/LayerUtility' + + def simple_text_image(self, text, font_file, align, char_per_line, + leading, font_size, text_color, + stroke_width, stroke_color, x_offset, y_offset, + width, height, size_as=None + ): + + (_, FONT_DICT) = get_resource_dir() + FONT_LIST = list(FONT_DICT.keys()) + + ret_images = [] + ret_masks = [] + if size_as is not None: + if size_as.dim() == 2: + size_as_image = torch.unsqueeze(mask, 0) + if size_as.shape[0] > 0: + size_as_image = torch.unsqueeze(size_as[0], 0) + else: + size_as_image = copy.deepcopy(size_as) + width, height = tensor2pil(size_as_image).size + font_path = FONT_DICT.get(font_file) + (_, top, _, _) = ImageFont.truetype(font=font_path, size=font_size, encoding='unic').getbbox(text) + font = cast(ImageFont.FreeTypeFont, ImageFont.truetype(font_path, font_size)) + if char_per_line == 0: + char_per_line = int(width / font_size) + paragraphs = text.split('\n') + + img_height = height # line_height * len(lines) + img_width = width # max(font.getsize(line)[0] for line in lines) + + img = Image.new("RGBA", size=(img_width, img_height), color=(0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + y_text = y_offset + stroke_width + for paragraph in paragraphs: + lines = textwrap.wrap(paragraph, width=char_per_line, expand_tabs=False, + replace_whitespace=False, drop_whitespace=False) + for line in lines: + width = font.getbbox(line)[2] - font.getbbox(line)[0] + height = font.getbbox(line)[3] - font.getbbox(line)[1] + # 根据 align 参数重新计算 x 坐标 + if align == "left": + x_text = x_offset + elif align == "center": + x_text = (img_width - width) // 2 + elif align == "right": + x_text = img_width - width - x_offset + else: + x_text = x_offset # 默认为左对齐 + + draw.text( + xy=(x_text, y_text), + text=line, + fill=text_color, + font=font, + stroke_width=stroke_width, + stroke_fill=stroke_color, + ) + y_text += height + leading + y_text += leading * 2 + + if size_as is not None: + for i in size_as: + ret_images.append(pil2tensor(img)) + ret_masks.append(image2mask(img.split()[3])) + else: + ret_images.append(pil2tensor(img)) + ret_masks.append(image2mask(img.split()[3])) + + log(f"{self.NODE_NAME} Processed.", message_type='finish') + return (torch.cat(ret_images, dim=0),torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: SimpleTextImage": SimpleTextImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: SimpleTextImage": "LayerUtility: SimpleTextImage" } \ No newline at end of file diff --git a/py/skin_beauty.py b/py/skin_beauty.py old mode 100644 new mode 100755 index 406dd561..0ab93262 --- a/py/skin_beauty.py +++ b/py/skin_beauty.py @@ -1,64 +1,64 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, gaussian_blur, chop_image -from .imagefunc import image_channel_split, gray_threshold, remove_background, get_image_bright_average, image_beauty - - -class SkinBeauty: - - def __init__(self): - self.NODE_NAME = 'SkinBeauty' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), - "smooth": ("INT", {"default": 20, "min": 1, "max": 64, "step": 1}), # 磨皮程度 - "threshold": ("INT", {"default": -10, "min": -255, "max": 255, "step": 1}), # 高光阈值 - "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 - }, - "optional": { - } - } - - RETURN_TYPES = ("IMAGE", "MASK") - RETURN_NAMES = ("image", "beauty_mask") - FUNCTION = 'skin_beauty' - CATEGORY = '😺dzNodes/LayerFilter' - - def skin_beauty(self, image, smooth, threshold, opacity - ): - - ret_images = [] - ret_masks = [] - for i in image: - i = torch.unsqueeze(i, 0) - _canvas = tensor2pil(i).convert('RGB') - _R, _, _, _ = image_channel_split(_canvas, mode='RGB') - _otsumask = gray_threshold(_R, otsu=True) - _removebkgd = remove_background(_R, _otsumask, '#000000') - auto_threshold = get_image_bright_average(_removebkgd) - 16 - light_mask = gray_threshold(_canvas, auto_threshold + threshold) - blur = int((_canvas.width + _canvas.height) / 2000 * smooth) - _image = image_beauty(_canvas, level=smooth) - _image = gaussian_blur(_image, blur) - _image = chop_image(_canvas, _image, 'normal', opacity) - light_mask = gaussian_blur(light_mask, blur).convert('L') - _canvas.paste(_image, mask=light_mask) - - ret_images.append(pil2tensor(_canvas)) - ret_masks.append(image2mask(light_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 = { - "LayerFilter: SkinBeauty": SkinBeauty -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerFilter: SkinBeauty": "LayerFilter: SkinBeauty" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, gaussian_blur, chop_image +from .imagefunc import image_channel_split, gray_threshold, remove_background, get_image_bright_average, image_beauty + + +class SkinBeauty: + + def __init__(self): + self.NODE_NAME = 'SkinBeauty' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + "smooth": ("INT", {"default": 20, "min": 1, "max": 64, "step": 1}), # 磨皮程度 + "threshold": ("INT", {"default": -10, "min": -255, "max": 255, "step": 1}), # 高光阈值 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + RETURN_NAMES = ("image", "beauty_mask") + FUNCTION = 'skin_beauty' + CATEGORY = '😺dzNodes/LayerFilter' + + def skin_beauty(self, image, smooth, threshold, opacity + ): + + ret_images = [] + ret_masks = [] + for i in image: + i = torch.unsqueeze(i, 0) + _canvas = tensor2pil(i).convert('RGB') + _R, _, _, _ = image_channel_split(_canvas, mode='RGB') + _otsumask = gray_threshold(_R, otsu=True) + _removebkgd = remove_background(_R, _otsumask, '#000000') + auto_threshold = get_image_bright_average(_removebkgd) - 16 + light_mask = gray_threshold(_canvas, auto_threshold + threshold) + blur = int((_canvas.width + _canvas.height) / 2000 * smooth) + _image = image_beauty(_canvas, level=smooth) + _image = gaussian_blur(_image, blur) + _image = chop_image(_canvas, _image, 'normal', opacity) + light_mask = gaussian_blur(light_mask, blur).convert('L') + _canvas.paste(_image, mask=light_mask) + + ret_images.append(pil2tensor(_canvas)) + ret_masks.append(image2mask(light_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 = { + "LayerFilter: SkinBeauty": SkinBeauty +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: SkinBeauty": "LayerFilter: SkinBeauty" } \ No newline at end of file diff --git a/py/soft_light.py b/py/soft_light.py old mode 100644 new mode 100755 diff --git a/py/stroke.py b/py/stroke.py old mode 100644 new mode 100755 diff --git a/py/stroke_v2.py b/py/stroke_v2.py old mode 100644 new mode 100755 index e452ac81..08bc8f18 --- a/py/stroke_v2.py +++ b/py/stroke_v2.py @@ -1,103 +1,103 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, subtract_mask, chop_image_v2, chop_mode_v2 - - - -class StrokeV2: - - def __init__(self): - self.NODE_NAME = 'StorkeV2' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "background_image": ("IMAGE", ), # - "layer_image": ("IMAGE",), # - "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask - "blend_mode": (chop_mode_v2,), # 混合模式 - "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 - "stroke_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), # 收缩值 - "stroke_width": ("INT", {"default": 8, "min": 0, "max": 999, "step": 1}), # 扩张值 - "blur": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), # 模糊 - "stroke_color": ("STRING", {"default": "#FF0000"}), # 描边颜色 - }, - "optional": { - "layer_mask": ("MASK",), # - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = 'stroke_v2' - CATEGORY = '😺dzNodes/LayerStyle' - - def stroke_v2(self, background_image, layer_image, - invert_mask, blend_mode, opacity, - stroke_grow, stroke_width, blur, stroke_color, - layer_mask=None - ): - - b_images = [] - l_images = [] - l_masks = [] - ret_images = [] - for b in background_image: - b_images.append(torch.unsqueeze(b, 0)) - for l in layer_image: - l_images.append(torch.unsqueeze(l, 0)) - m = tensor2pil(l) - if m.mode == 'RGBA': - l_masks.append(m.split()[-1]) - if layer_mask is not None: - if layer_mask.dim() == 2: - layer_mask = torch.unsqueeze(layer_mask, 0) - l_masks = [] - for m in layer_mask: - if invert_mask: - m = 1 - m - l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) - if len(l_masks) == 0: - log(f"Error: {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)) - - grow_offset = int(stroke_width / 2) - inner_stroke = stroke_grow - grow_offset - outer_stroke = inner_stroke + stroke_width - for i in range(max_batch): - background_image = b_images[i] if i < len(b_images) else b_images[-1] - layer_image = l_images[i] if i < len(l_images) else l_images[-1] - _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] - - # preprocess - _canvas = tensor2pil(background_image).convert('RGB') - _layer = tensor2pil(layer_image).convert('RGB') - - if _mask.size != _layer.size: - _mask = Image.new('L', _layer.size, 'white') - log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') - - inner_mask = expand_mask(image2mask(_mask), inner_stroke, blur) - outer_mask = expand_mask(image2mask(_mask), outer_stroke, blur) - stroke_mask = subtract_mask(outer_mask, inner_mask) - color_image = Image.new('RGB', size=_layer.size, color=stroke_color) - blend_image = chop_image_v2(_layer, color_image, blend_mode, opacity) - _canvas.paste(_layer, mask=_mask) - _canvas.paste(blend_image, mask=tensor2pil(stroke_mask)) - - ret_images.append(pil2tensor(_canvas)) - - log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') - return (torch.cat(ret_images, dim=0),) - -NODE_CLASS_MAPPINGS = { - "LayerStyle: Stroke V2": StrokeV2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerStyle: Stroke V2": "LayerStyle: Stroke V2" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, subtract_mask, chop_image_v2, chop_mode_v2 + + + +class StrokeV2: + + def __init__(self): + self.NODE_NAME = 'StorkeV2' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode_v2,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "stroke_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), # 收缩值 + "stroke_width": ("INT", {"default": 8, "min": 0, "max": 999, "step": 1}), # 扩张值 + "blur": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), # 模糊 + "stroke_color": ("STRING", {"default": "#FF0000"}), # 描边颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'stroke_v2' + CATEGORY = '😺dzNodes/LayerStyle' + + def stroke_v2(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + stroke_grow, stroke_width, blur, stroke_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {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)) + + grow_offset = int(stroke_width / 2) + inner_stroke = stroke_grow - grow_offset + outer_stroke = inner_stroke + stroke_width + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + inner_mask = expand_mask(image2mask(_mask), inner_stroke, blur) + outer_mask = expand_mask(image2mask(_mask), outer_stroke, blur) + stroke_mask = subtract_mask(outer_mask, inner_mask) + color_image = Image.new('RGB', size=_layer.size, color=stroke_color) + blend_image = chop_image_v2(_layer, color_image, blend_mode, opacity) + _canvas.paste(_layer, mask=_mask) + _canvas.paste(blend_image, mask=tensor2pil(stroke_mask)) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerStyle: Stroke V2": StrokeV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: Stroke V2": "LayerStyle: Stroke V2" } \ No newline at end of file diff --git a/py/text_image.py b/py/text_image.py old mode 100644 new mode 100755 index c31fc7fa..88ac8d10 --- a/py/text_image.py +++ b/py/text_image.py @@ -1,148 +1,148 @@ -import torch -import time -import random -from PIL import Image, ImageFont, ImageDraw -from .imagefunc import AnyType, log, tensor2pil, pil2tensor, image2mask, get_resource_dir, RGB2RGBA, random_numbers - - -any = AnyType("*") - -class TextImage: - - def __init__(self): - self.NODE_NAME = 'TextImage' - - @classmethod - def INPUT_TYPES(self): - - (_, FONT_DICT) = get_resource_dir() - FONT_LIST = list(FONT_DICT.keys()) - - layout_list = ['horizontal', 'vertical'] - random_seed = int(time.time()) - - return { - "required": { - "text": ("STRING", {"multiline": True, "default": "Text"}), - "font_file": (FONT_LIST,), - "spacing": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), - "leading": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), - "horizontal_border": ("FLOAT", {"default": 5, "min": -100, "max": 100, "step": 0.1}), # 左右距离百分比,横排为距左侧,竖排为距右侧 - "vertical_border": ("FLOAT", {"default": 5, "min": -100, "max": 100, "step": 0.1}), # 上距离百分比 - "scale": ("FLOAT", {"default": 80, "min": 0.1, "max": 999, "step": 0.01}), # 整体大小与画面长宽比,横排与宽比,竖排与高比 - "variation_range": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), # 随机大小和位置范围 - "variation_seed": ("INT", {"default": random_seed, "min": 0, "max": 999999999999, "step": 1}), # 随机种子 - "layout": (layout_list,), # 横排or竖排 - "width": ("INT", {"default": 512, "min": 4, "max": 999999, "step": 1}), - "height": ("INT", {"default": 512, "min": 4, "max": 999999, "step": 1}), - "text_color": ("STRING", {"default": "#FFA000"}), # 文字颜色 - "background_color": ("STRING", {"default": "#FFFFFF"}), # 背景颜色 - }, - "optional": { - "size_as": (any, {}), - } - } - - RETURN_TYPES = ("IMAGE", "MASK",) - RETURN_NAMES = ("image", "mask",) - FUNCTION = 'text_image' - CATEGORY = '😺dzNodes/LayerUtility' - - def text_image(self, text, font_file, spacing, leading, horizontal_border, vertical_border, scale, - variation_range, variation_seed, layout, width, height, text_color, background_color, - size_as=None - ): - - - (_, FONT_DICT) = get_resource_dir() - FONT_LIST = list(FONT_DICT.keys()) - # spacing -= 20 - # leading += 20 - # scale *= 0.7 - if size_as is not None: - width, height = tensor2pil(size_as).size - text_table = [] - max_char_in_line = 0 - total_char = 0 - spacing = int(spacing * scale / 100) - leading = int(leading * scale / 100) - lines = [] - text_lines = text.split("\n") - for l in text_lines: - if len(l) > 0: - lines.append(l) - total_char += len(l) - if len(l) > max_char_in_line: - max_char_in_line = len(l) - else: - lines.append(" ") - if layout == 'vertical': - char_horizontal_size = width // len(lines) - char_vertical_size = height // max_char_in_line - char_size = min(char_horizontal_size, char_vertical_size) - if char_size < 1: - char_size = 1 - start_x = width - int(width * horizontal_border/100) - char_size - else: - char_horizontal_size = width // max_char_in_line - char_vertical_size = height // len(lines) - char_size = min(char_horizontal_size, char_vertical_size) - if char_size < 1: - char_size = 1 - start_x = int(width * horizontal_border/100) - start_y = int(height * vertical_border/100) - - # calculate every char position and size to a table list - for i in range(len(lines)): - _x = start_x - _y = start_y - line_table = [] - line_random = random_numbers(total=len(lines[i]), - random_range=int(char_size * variation_range / 25), - seed=variation_seed, sum_of_numbers=0) - for j in range(0, len(lines[i])): - offset = int((char_size + line_random[j]) * variation_range / 250) - offset = int(offset * scale / 100) - font_size = char_size + line_random[j] - font_size = int(font_size * scale / 100) - if font_size < 4: - font_size = 4 - axis_x = _x + offset // 3 if random.random() > 0.5 else _x - offset // 3 - axis_y = _y + offset // 3 if random.random() > 0.5 else _y - offset // 3 - char_dict = {'char':lines[i][j], - 'axis':(axis_x, axis_y), - 'size':font_size} - line_table.append(char_dict) - if layout == 'vertical': - _y += char_size + line_random[j] + spacing - else: - _x += char_size + line_random[j] + spacing - if layout == 'vertical': - start_x -= leading * (i+1) + char_size - else: - start_y += leading * (i+1) + char_size - text_table.append(line_table) - - # draw char - _mask = Image.new('RGB', size=(width, height), color='black') - draw = ImageDraw.Draw(_mask) - for l in range(len(lines)): - for c in range(len(lines[l])): - font_path = FONT_DICT.get(font_file) - font_size = text_table[l][c].get('size') - font = ImageFont.truetype(font_path, font_size) - draw.text(text_table[l][c].get('axis'), text_table[l][c].get('char'), font=font, fill='white') - _canvas = Image.new('RGB', size=(width, height), color=background_color) - _color = Image.new('RGB', size=(width, height), color=text_color) - _canvas.paste(_color, mask=_mask.convert('L')) - _canvas = RGB2RGBA(_canvas, _mask) - log(f"{self.NODE_NAME} Processed.", message_type='finish') - return (pil2tensor(_canvas), image2mask(_mask),) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: TextImage": TextImage -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: TextImage": "LayerUtility: TextImage" +import torch +import time +import random +from PIL import Image, ImageFont, ImageDraw +from .imagefunc import AnyType, log, tensor2pil, pil2tensor, image2mask, get_resource_dir, RGB2RGBA, random_numbers + + +any = AnyType("*") + +class TextImage: + + def __init__(self): + self.NODE_NAME = 'TextImage' + + @classmethod + def INPUT_TYPES(self): + + (_, FONT_DICT) = get_resource_dir() + FONT_LIST = list(FONT_DICT.keys()) + + layout_list = ['horizontal', 'vertical'] + random_seed = int(time.time()) + + return { + "required": { + "text": ("STRING", {"multiline": True, "default": "Text"}), + "font_file": (FONT_LIST,), + "spacing": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), + "leading": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), + "horizontal_border": ("FLOAT", {"default": 5, "min": -100, "max": 100, "step": 0.1}), # 左右距离百分比,横排为距左侧,竖排为距右侧 + "vertical_border": ("FLOAT", {"default": 5, "min": -100, "max": 100, "step": 0.1}), # 上距离百分比 + "scale": ("FLOAT", {"default": 80, "min": 0.1, "max": 999, "step": 0.01}), # 整体大小与画面长宽比,横排与宽比,竖排与高比 + "variation_range": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}), # 随机大小和位置范围 + "variation_seed": ("INT", {"default": random_seed, "min": 0, "max": 999999999999, "step": 1}), # 随机种子 + "layout": (layout_list,), # 横排or竖排 + "width": ("INT", {"default": 512, "min": 4, "max": 999999, "step": 1}), + "height": ("INT", {"default": 512, "min": 4, "max": 999999, "step": 1}), + "text_color": ("STRING", {"default": "#FFA000"}), # 文字颜色 + "background_color": ("STRING", {"default": "#FFFFFF"}), # 背景颜色 + }, + "optional": { + "size_as": (any, {}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'text_image' + CATEGORY = '😺dzNodes/LayerUtility' + + def text_image(self, text, font_file, spacing, leading, horizontal_border, vertical_border, scale, + variation_range, variation_seed, layout, width, height, text_color, background_color, + size_as=None + ): + + + (_, FONT_DICT) = get_resource_dir() + FONT_LIST = list(FONT_DICT.keys()) + # spacing -= 20 + # leading += 20 + # scale *= 0.7 + if size_as is not None: + width, height = tensor2pil(size_as).size + text_table = [] + max_char_in_line = 0 + total_char = 0 + spacing = int(spacing * scale / 100) + leading = int(leading * scale / 100) + lines = [] + text_lines = text.split("\n") + for l in text_lines: + if len(l) > 0: + lines.append(l) + total_char += len(l) + if len(l) > max_char_in_line: + max_char_in_line = len(l) + else: + lines.append(" ") + if layout == 'vertical': + char_horizontal_size = width // len(lines) + char_vertical_size = height // max_char_in_line + char_size = min(char_horizontal_size, char_vertical_size) + if char_size < 1: + char_size = 1 + start_x = width - int(width * horizontal_border/100) - char_size + else: + char_horizontal_size = width // max_char_in_line + char_vertical_size = height // len(lines) + char_size = min(char_horizontal_size, char_vertical_size) + if char_size < 1: + char_size = 1 + start_x = int(width * horizontal_border/100) + start_y = int(height * vertical_border/100) + + # calculate every char position and size to a table list + for i in range(len(lines)): + _x = start_x + _y = start_y + line_table = [] + line_random = random_numbers(total=len(lines[i]), + random_range=int(char_size * variation_range / 25), + seed=variation_seed, sum_of_numbers=0) + for j in range(0, len(lines[i])): + offset = int((char_size + line_random[j]) * variation_range / 250) + offset = int(offset * scale / 100) + font_size = char_size + line_random[j] + font_size = int(font_size * scale / 100) + if font_size < 4: + font_size = 4 + axis_x = _x + offset // 3 if random.random() > 0.5 else _x - offset // 3 + axis_y = _y + offset // 3 if random.random() > 0.5 else _y - offset // 3 + char_dict = {'char':lines[i][j], + 'axis':(axis_x, axis_y), + 'size':font_size} + line_table.append(char_dict) + if layout == 'vertical': + _y += char_size + line_random[j] + spacing + else: + _x += char_size + line_random[j] + spacing + if layout == 'vertical': + start_x -= leading * (i+1) + char_size + else: + start_y += leading * (i+1) + char_size + text_table.append(line_table) + + # draw char + _mask = Image.new('RGB', size=(width, height), color='black') + draw = ImageDraw.Draw(_mask) + for l in range(len(lines)): + for c in range(len(lines[l])): + font_path = FONT_DICT.get(font_file) + font_size = text_table[l][c].get('size') + font = ImageFont.truetype(font_path, font_size) + draw.text(text_table[l][c].get('axis'), text_table[l][c].get('char'), font=font, fill='white') + _canvas = Image.new('RGB', size=(width, height), color=background_color) + _color = Image.new('RGB', size=(width, height), color=text_color) + _canvas.paste(_color, mask=_mask.convert('L')) + _canvas = RGB2RGBA(_canvas, _mask) + log(f"{self.NODE_NAME} Processed.", message_type='finish') + return (pil2tensor(_canvas), image2mask(_mask),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: TextImage": TextImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: TextImage": "LayerUtility: TextImage" } \ No newline at end of file diff --git a/py/text_image_v2.py b/py/text_image_v2.py old mode 100644 new mode 100755 diff --git a/py/text_join.py b/py/text_join.py old mode 100644 new mode 100755 index 5b50235e..d6bc4dd1 --- a/py/text_join.py +++ b/py/text_join.py @@ -1,95 +1,95 @@ - - - -class TextJoin: - - def __init__(self): - self.NODE_NAME = 'TextJoin' - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text_1": ("STRING", {"default": "", "multiline": False,"forceInput":False}), - - }, - "optional": { - "text_2": ("STRING", {"default": "", "multiline": False,"forceInput":False}), - "text_3": ("STRING", {"default": "", "multiline": False,"forceInput":False}), - "text_4": ("STRING", {"default": "", "multiline": False,"forceInput":False}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) - FUNCTION = "text_join" - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def text_join(self, text_1, text_2="", text_3="", text_4=""): - - texts = [] - if text_1 != "": - texts.append(text_1) - if text_2 != "": - texts.append(text_2) - if text_3 != "": - texts.append(text_3) - if text_4 != "": - texts.append(text_4) - if len(texts) > 0: - combined_text = ', '.join(texts) - return (combined_text.encode('unicode-escape').decode('unicode-escape'),) - else: - return ('',) - - -class LS_TextJoinV2: - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text_1": ("STRING", {"default": "", "multiline": False,"forceInput":True}), - "delimiter": ("STRING", {"default": ",", "multiline": False}), - }, - "optional": { - "text_2": ("STRING", {"default": "", "multiline": False,"forceInput":True}), - "text_3": ("STRING", {"default": "", "multiline": False,"forceInput":True}), - "text_4": ("STRING", {"default": "", "multiline": False,"forceInput":True}), - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) - FUNCTION = "text_join" - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def text_join(self, text_1, delimiter, text_2="", text_3="", text_4=""): - - texts = [] - if text_1 != "": - texts.append(text_1) - if text_2 != "": - texts.append(text_2) - if text_3 != "": - texts.append(text_3) - if text_4 != "": - texts.append(text_4) - if len(texts) > 0: - combined_text = delimiter.join(texts) - return (combined_text.encode('unicode-escape').decode('unicode-escape'),) - else: - return ('',) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: TextJoin": TextJoin, - "LayerUtility: TextJoinV2": LS_TextJoinV2 -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: TextJoin": "LayerUtility: TextJoin", - "LayerUtility: TextJoinV2": "LayerUtility: TextJoinV2" + + + +class TextJoin: + + def __init__(self): + self.NODE_NAME = 'TextJoin' + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_1": ("STRING", {"default": "", "multiline": False,"forceInput":False}), + + }, + "optional": { + "text_2": ("STRING", {"default": "", "multiline": False,"forceInput":False}), + "text_3": ("STRING", {"default": "", "multiline": False,"forceInput":False}), + "text_4": ("STRING", {"default": "", "multiline": False,"forceInput":False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "text_join" + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def text_join(self, text_1, text_2="", text_3="", text_4=""): + + texts = [] + if text_1 != "": + texts.append(text_1) + if text_2 != "": + texts.append(text_2) + if text_3 != "": + texts.append(text_3) + if text_4 != "": + texts.append(text_4) + if len(texts) > 0: + combined_text = ', '.join(texts) + return (combined_text.encode('unicode-escape').decode('unicode-escape'),) + else: + return ('',) + + +class LS_TextJoinV2: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text_1": ("STRING", {"default": "", "multiline": False,"forceInput":True}), + "delimiter": ("STRING", {"default": ",", "multiline": False}), + }, + "optional": { + "text_2": ("STRING", {"default": "", "multiline": False,"forceInput":True}), + "text_3": ("STRING", {"default": "", "multiline": False,"forceInput":True}), + "text_4": ("STRING", {"default": "", "multiline": False,"forceInput":True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "text_join" + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def text_join(self, text_1, delimiter, text_2="", text_3="", text_4=""): + + texts = [] + if text_1 != "": + texts.append(text_1) + if text_2 != "": + texts.append(text_2) + if text_3 != "": + texts.append(text_3) + if text_4 != "": + texts.append(text_4) + if len(texts) > 0: + combined_text = delimiter.join(texts) + return (combined_text.encode('unicode-escape').decode('unicode-escape'),) + else: + return ('',) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: TextJoin": TextJoin, + "LayerUtility: TextJoinV2": LS_TextJoinV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: TextJoin": "LayerUtility: TextJoin", + "LayerUtility: TextJoinV2": "LayerUtility: TextJoinV2" } \ No newline at end of file diff --git a/py/text_node.py b/py/text_node.py old mode 100644 new mode 100755 index 6bf0f48d..e1188f0a --- a/py/text_node.py +++ b/py/text_node.py @@ -1,86 +1,86 @@ -import os -import json -import random -from .imagefunc import AnyType, log, extract_all_numbers_from_str, extract_numbers, extract_substr_from_str -from .imagefunc import tokenize_string, find_best_match_by_similarity, remove_empty_lines, remove_duplicate_string -from .imagefunc import get_files, file_is_extension, is_contain_chinese - -any = AnyType("*") - -class LS_TextPreseter: - def __init__(self): - self.NODE_NAME = "TextPreseter" - @classmethod - def INPUT_TYPES(self): - return { - "required": - { - "title": ("STRING", {"default": "", "multiline": False}), - "content": ("STRING", {"default": '', "multiline": True}), - }, - "optional": { - "text_preset": ("LS_TEXT_PRESET", ), - } - } - - RETURN_TYPES = ("LS_TEXT_PRESET",) - RETURN_NAMES = ("text_preset",) - FUNCTION = 'text_preseter' - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def text_preseter(self, title, content, text_preset=None): - - if text_preset is None: - text_preset = {} - - if title: - text_preset[title] = content - - return (text_preset,) - -class LS_ChoiceTextPreset: - def __init__(self): - self.NODE_NAME = "ChoicePresetText" - @classmethod - def INPUT_TYPES(self): - return { - "required": - { "text_preset": ("LS_TEXT_PRESET", ), - "choice_title": ("STRING", {"default": '', "multiline": False}), - "random_choice": ("BOOLEAN", {"default": False}), - "default": ("INT", {"default": 0, "min": 0, "max": 1e4, "step": 1}), - "seed": ("INT", {"default": 0, "min": 0, "max": 1e18, "step": 1}), - }, - "optional": { - } - } - - RETURN_TYPES = ("STRING", "STRING",) - RETURN_NAMES = ("title", "content",) - FUNCTION = 'choice_preset_text' - CATEGORY = '😺dzNodes/LayerUtility/Data' - - def choice_preset_text(self, text_preset, choice_title, random_choice, default, seed): - keys = list(text_preset.keys()) - ret_key = keys[default] - ret_value = '' - - if choice_title in text_preset and not random_choice: - ret_key = choice_title - elif random_choice: - random.seed(seed) - ret_key = random.choice(list(text_preset.keys())) - - ret_value = text_preset[ret_key] - - return (ret_key, ret_value) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: ChoiceTextPreset": LS_ChoiceTextPreset, - "LayerUtility: TextPreseter": LS_TextPreseter, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: ChoiceTextPreset": "LayerUtility: Choice Text Preset", - "LayerUtility: TextPreseter": "LayerUtility: Text Preseter", +import os +import json +import random +from .imagefunc import AnyType, log, extract_all_numbers_from_str, extract_numbers, extract_substr_from_str +from .imagefunc import tokenize_string, find_best_match_by_similarity, remove_empty_lines, remove_duplicate_string +from .imagefunc import get_files, file_is_extension, is_contain_chinese + +any = AnyType("*") + +class LS_TextPreseter: + def __init__(self): + self.NODE_NAME = "TextPreseter" + @classmethod + def INPUT_TYPES(self): + return { + "required": + { + "title": ("STRING", {"default": "", "multiline": False}), + "content": ("STRING", {"default": '', "multiline": True}), + }, + "optional": { + "text_preset": ("LS_TEXT_PRESET", ), + } + } + + RETURN_TYPES = ("LS_TEXT_PRESET",) + RETURN_NAMES = ("text_preset",) + FUNCTION = 'text_preseter' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def text_preseter(self, title, content, text_preset=None): + + if text_preset is None: + text_preset = {} + + if title: + text_preset[title] = content + + return (text_preset,) + +class LS_ChoiceTextPreset: + def __init__(self): + self.NODE_NAME = "ChoicePresetText" + @classmethod + def INPUT_TYPES(self): + return { + "required": + { "text_preset": ("LS_TEXT_PRESET", ), + "choice_title": ("STRING", {"default": '', "multiline": False}), + "random_choice": ("BOOLEAN", {"default": False}), + "default": ("INT", {"default": 0, "min": 0, "max": 1e4, "step": 1}), + "seed": ("INT", {"default": 0, "min": 0, "max": 1e18, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING", "STRING",) + RETURN_NAMES = ("title", "content",) + FUNCTION = 'choice_preset_text' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def choice_preset_text(self, text_preset, choice_title, random_choice, default, seed): + keys = list(text_preset.keys()) + ret_key = keys[default] + ret_value = '' + + if choice_title in text_preset and not random_choice: + ret_key = choice_title + elif random_choice: + random.seed(seed) + ret_key = random.choice(list(text_preset.keys())) + + ret_value = text_preset[ret_key] + + return (ret_key, ret_value) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ChoiceTextPreset": LS_ChoiceTextPreset, + "LayerUtility: TextPreseter": LS_TextPreseter, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ChoiceTextPreset": "LayerUtility: Choice Text Preset", + "LayerUtility: TextPreseter": "LayerUtility: Text Preseter", } \ No newline at end of file diff --git a/py/vqa_prompt.py b/py/vqa_prompt.py old mode 100644 new mode 100755 index 6d8cce11..daed6e33 --- a/py/vqa_prompt.py +++ b/py/vqa_prompt.py @@ -1,134 +1,134 @@ -import os -import sys -import torch -import re -from transformers import pipeline -import folder_paths - -from .imagefunc import log, tensor2pil - -vqa_model_path = os.path.join(folder_paths.models_dir, 'VQA') - -vqa_model_repos = { - "blip-vqa-base": "Salesforce/blip-vqa-base", - "blip-vqa-capfilt-large": "Salesforce/blip-vqa-capfilt-large", -} - -def get_models(): - sub_dirs = [] - for filename in os.listdir(vqa_model_path): - if os.path.isdir(os.path.join(vqa_model_path, filename)): - sub_dirs.append(filename) - return sub_dirs - -class LS_LoadVQAModel: - - def __init__(self): - self.processor = None - self.model = None - self.model_name = "" - self.device = "" - self.precision = "" - - @classmethod - def INPUT_TYPES(s): - model_list = list(vqa_model_repos.keys()) - precision_list = ["fp16", "fp32"] - device_list = ['cuda','cpu'] - return { - "required": { - "model": (model_list,), - "precision": (precision_list,), - "device": (device_list,), - }, - } - - RETURN_TYPES = ("VQA_MODEL",) - RETURN_NAMES = ("vqa_model",) - FUNCTION = "load_vqa_model" - CATEGORY = '😺dzNodes/LayerUtility' - - def load_vqa_model(self, model, precision, device): - - if (model == self.model_name and precision == self.precision and device == self.device - and self.model is not None and self.processor is not None): - return ([self.processor, self.model, device, precision, self.model_name],) - - model_path = os.path.join(vqa_model_path, model) - from transformers import BlipProcessor,BlipForQuestionAnswering - - # if there is no local files, use repo id to auto-download the dependencies. - if not os.path.exists(model_path): - model_path = vqa_model_repos[model] - - vqa_processor = BlipProcessor.from_pretrained(model_path) - if precision == 'fp16': - vqa_model = BlipForQuestionAnswering.from_pretrained(model_path, torch_dtype=torch.float16).to(device) - else: - vqa_model = BlipForQuestionAnswering.from_pretrained(model_path).to(device) - - self.processor = vqa_processor - self.model = vqa_model - self.model_name = model - self.device = device - self.precision = precision - - return ([vqa_processor, vqa_model, device, precision, model],) - -class LS_VQA_Prompt: - - def __init__(self): - self.NODE_NAME = 'VQA Prompt' - - @classmethod - def INPUT_TYPES(cls): - default_question = "{age number} years old {ethnicity} {gender}, weared {garment color} {garment}, {eye color} eyes, {hair style} {hair color} hair, {background} background." - - return { - "required": { - "image": ("IMAGE",), - "vqa_model": ("VQA_MODEL",), - "question": ("STRING", {"default": default_question, "multiline": True, "dynamicPrompts": False}), - }, - "optional": { - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("text",) - OUTPUT_IS_LIST = (True,) - FUNCTION = "vqa_prompt" - CATEGORY = '😺dzNodes/LayerUtility' - - def vqa_prompt(self, image, vqa_model, question): - answers = [] - [vqa_processor, vqa_model, device, precision, model_name] = vqa_model - - for img in image: - _img = tensor2pil(img).convert("RGB") - final_answer = question - matches = re.findall(r'\{([^}]*)\}', question) - - for match in matches: - if precision == 'fp16': - inputs = vqa_processor(_img, match, return_tensors="pt").to(device, torch.float16) - else: - inputs = vqa_processor(_img, match, return_tensors="pt").to(device) - out = vqa_model.generate(**inputs) - match_answer = vqa_processor.decode(out[0], skip_special_tokens=True) - log(f'{self.NODE_NAME} Q:"{match}", A:"{match_answer}"') - final_answer = final_answer.replace("{" + match + "}", match_answer) - answers.append(final_answer) - - log(f"{self.NODE_NAME} Processed.", message_type='finish') - return (answers,) - -NODE_CLASS_MAPPINGS = { - "LayerUtility: VQAPrompt": LS_VQA_Prompt, - "LayerUtility: LoadVQAModel": LS_LoadVQAModel -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerUtility: VQAPrompt": "LayerUtility: VQA Prompt", - "LayerUtility: LoadVQAModel": "LayerUtility: Load VQA Model" -} +import os +import sys +import torch +import re +from transformers import pipeline +import folder_paths + +from .imagefunc import log, tensor2pil + +vqa_model_path = os.path.join(folder_paths.models_dir, 'VQA') + +vqa_model_repos = { + "blip-vqa-base": "Salesforce/blip-vqa-base", + "blip-vqa-capfilt-large": "Salesforce/blip-vqa-capfilt-large", +} + +def get_models(): + sub_dirs = [] + for filename in os.listdir(vqa_model_path): + if os.path.isdir(os.path.join(vqa_model_path, filename)): + sub_dirs.append(filename) + return sub_dirs + +class LS_LoadVQAModel: + + def __init__(self): + self.processor = None + self.model = None + self.model_name = "" + self.device = "" + self.precision = "" + + @classmethod + def INPUT_TYPES(s): + model_list = list(vqa_model_repos.keys()) + precision_list = ["fp16", "fp32"] + device_list = ['cuda','cpu'] + return { + "required": { + "model": (model_list,), + "precision": (precision_list,), + "device": (device_list,), + }, + } + + RETURN_TYPES = ("VQA_MODEL",) + RETURN_NAMES = ("vqa_model",) + FUNCTION = "load_vqa_model" + CATEGORY = '😺dzNodes/LayerUtility' + + def load_vqa_model(self, model, precision, device): + + if (model == self.model_name and precision == self.precision and device == self.device + and self.model is not None and self.processor is not None): + return ([self.processor, self.model, device, precision, self.model_name],) + + model_path = os.path.join(vqa_model_path, model) + from transformers import BlipProcessor,BlipForQuestionAnswering + + # if there is no local files, use repo id to auto-download the dependencies. + if not os.path.exists(model_path): + model_path = vqa_model_repos[model] + + vqa_processor = BlipProcessor.from_pretrained(model_path) + if precision == 'fp16': + vqa_model = BlipForQuestionAnswering.from_pretrained(model_path, torch_dtype=torch.float16).to(device) + else: + vqa_model = BlipForQuestionAnswering.from_pretrained(model_path).to(device) + + self.processor = vqa_processor + self.model = vqa_model + self.model_name = model + self.device = device + self.precision = precision + + return ([vqa_processor, vqa_model, device, precision, model],) + +class LS_VQA_Prompt: + + def __init__(self): + self.NODE_NAME = 'VQA Prompt' + + @classmethod + def INPUT_TYPES(cls): + default_question = "{age number} years old {ethnicity} {gender}, weared {garment color} {garment}, {eye color} eyes, {hair style} {hair color} hair, {background} background." + + return { + "required": { + "image": ("IMAGE",), + "vqa_model": ("VQA_MODEL",), + "question": ("STRING", {"default": default_question, "multiline": True, "dynamicPrompts": False}), + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + OUTPUT_IS_LIST = (True,) + FUNCTION = "vqa_prompt" + CATEGORY = '😺dzNodes/LayerUtility' + + def vqa_prompt(self, image, vqa_model, question): + answers = [] + [vqa_processor, vqa_model, device, precision, model_name] = vqa_model + + for img in image: + _img = tensor2pil(img).convert("RGB") + final_answer = question + matches = re.findall(r'\{([^}]*)\}', question) + + for match in matches: + if precision == 'fp16': + inputs = vqa_processor(_img, match, return_tensors="pt").to(device, torch.float16) + else: + inputs = vqa_processor(_img, match, return_tensors="pt").to(device) + out = vqa_model.generate(**inputs) + match_answer = vqa_processor.decode(out[0], skip_special_tokens=True) + log(f'{self.NODE_NAME} Q:"{match}", A:"{match_answer}"') + final_answer = final_answer.replace("{" + match + "}", match_answer) + answers.append(final_answer) + + log(f"{self.NODE_NAME} Processed.", message_type='finish') + return (answers,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: VQAPrompt": LS_VQA_Prompt, + "LayerUtility: LoadVQAModel": LS_LoadVQAModel +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: VQAPrompt": "LayerUtility: VQA Prompt", + "LayerUtility: LoadVQAModel": "LayerUtility: Load VQA Model" +} diff --git a/py/water_color.py b/py/water_color.py old mode 100644 new mode 100755 index 697a99b7..c7da2868 --- a/py/water_color.py +++ b/py/water_color.py @@ -1,52 +1,52 @@ -import torch -from PIL import Image -from .imagefunc import log, tensor2pil, pil2tensor, image_watercolor, chop_image - - - -class WaterColor: - - def __init__(self): - self.NODE_NAME = 'WaterColor' - - @classmethod - def INPUT_TYPES(self): - - return { - "required": { - "image": ("IMAGE", ), - "line_density": ("INT", {"default": 50, "min": 1, "max": 100, "step": 1}), # 透明度 - "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 - }, - "optional": { - } - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("image",) - FUNCTION = 'water_color' - CATEGORY = '😺dzNodes/LayerFilter' - - def water_color(self, image, line_density, opacity - ): - - ret_images = [] - - for i in image: - i = torch.unsqueeze(i, 0) - _canvas = tensor2pil(i).convert('RGB') - _image = image_watercolor(_canvas, level=101-line_density) - 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: WaterColor": WaterColor -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "LayerFilter: WaterColor": "LayerFilter: WaterColor" +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image_watercolor, chop_image + + + +class WaterColor: + + def __init__(self): + self.NODE_NAME = 'WaterColor' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + "line_density": ("INT", {"default": 50, "min": 1, "max": 100, "step": 1}), # 透明度 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'water_color' + CATEGORY = '😺dzNodes/LayerFilter' + + def water_color(self, image, line_density, opacity + ): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + _canvas = tensor2pil(i).convert('RGB') + _image = image_watercolor(_canvas, level=101-line_density) + 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: WaterColor": WaterColor +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: WaterColor": "LayerFilter: WaterColor" } \ No newline at end of file diff --git a/py/xy2percent.py b/py/xy2percent.py old mode 100644 new mode 100755