From ca2c13e0ef2f7033a54dfcbe7f5791e71d4247bd Mon Sep 17 00:00:00 2001 From: Cyber Dick Lang <286878701@qq.com> Date: Mon, 23 Jun 2025 20:42:49 +0800 Subject: [PATCH] add reference code add reference code --- reference_code/add_grain.py | 50 + reference_code/any_rerouter.py | 35 + reference_code/batch_selector.py | 65 + reference_code/blend_if_mask.py | 104 + reference_code/blendmodes.py | 324 +++ reference_code/briarmbg.py | 455 +++ reference_code/channel_shake.py | 70 + reference_code/check_mask.py | 53 + reference_code/check_mask_v2.py | 60 + reference_code/color_adapter.py | 62 + reference_code/color_correct_HSV.py | 61 + reference_code/color_correct_LAB.py | 61 + reference_code/color_correct_LUTapply.py | 62 + reference_code/color_correct_RGB.py | 61 + reference_code/color_correct_YUV.py | 61 + reference_code/color_correct_auto_adjust.py | 115 + .../color_correct_auto_adjust_v2.py | 147 + .../color_correct_auto_brightness.py | 80 + .../color_correct_brightness_and_contrast.py | 114 + reference_code/color_correct_color_balance.py | 78 + .../color_correct_color_temperature.py | 61 + reference_code/color_correct_exposure.py | 63 + reference_code/color_correct_gamma.py | 52 + reference_code/color_correct_levels.py | 93 + .../color_correct_shadow_and_highlight.py | 247 ++ reference_code/color_image.py | 39 + reference_code/color_image_v2.py | 65 + reference_code/color_map.py | 57 + reference_code/color_name.py | 2481 ++++++++++++++++ reference_code/color_overlay _v2.py | 91 + reference_code/color_overlay.py | 91 + reference_code/color_picker.py | 40 + reference_code/color_to_HSVvalue.py | 43 + reference_code/color_to_RGBvalue.py | 45 + reference_code/color_to_gray_value.py | 37 + reference_code/create_gradient_mask.py | 125 + reference_code/crop_box_resolve.py | 43 + reference_code/crop_by_mask.py | 100 + reference_code/crop_by_mask_v2.py | 116 + reference_code/crop_by_mask_v3.py | 116 + reference_code/data_nodes.py | 496 ++++ reference_code/drop_shadow.py | 108 + reference_code/drop_shadow_v2.py | 111 + reference_code/drop_shadow_v3.py | 114 + reference_code/extend_canvas.py | 90 + reference_code/extend_canvas_v2.py | 97 + reference_code/film_post.py | 91 + reference_code/film_post_v2.py | 97 + reference_code/filmgrainer/__init__.py | 1 + reference_code/filmgrainer/filmgrainer.py | 117 + reference_code/filmgrainer/graingamma.py | 113 + reference_code/filmgrainer/graingen.py | 61 + reference_code/filmgrainer/processing.py | 32 + reference_code/gaussian_blur.py | 86 + reference_code/get_image_size.py | 39 + reference_code/get_main_colors.py | 236 ++ reference_code/gradient_image.py | 41 + reference_code/gradient_image_v2.py | 71 + reference_code/gradient_map.py | 86 + reference_code/gradient_overlay.py | 102 + reference_code/gradient_overlay_v2.py | 102 + reference_code/halftone.py | 186 ++ reference_code/hdr_effects.py | 164 ++ reference_code/hl_frequency_detail_restore.py | 87 + reference_code/ic_mask.py | 244 ++ reference_code/image_blend.py | 86 + reference_code/image_blend_advance.py | 135 + reference_code/image_blend_advance_v2.py | 135 + reference_code/image_blend_advance_v3.py | 141 + reference_code/image_blend_v2.py | 91 + reference_code/image_channel_merge.py | 72 + reference_code/image_channel_split.py | 53 + reference_code/image_combine_alpha.py | 59 + reference_code/image_hub.py | 152 + reference_code/image_mask_scale_as.py | 189 ++ reference_code/image_opacity.py | 87 + reference_code/image_reel.py | 224 ++ reference_code/image_remove_alpha.py | 64 + reference_code/image_scale_by_aspect_ratio.py | 158 + .../image_scale_by_aspect_ratio_v2.py | 186 ++ reference_code/image_scale_restore.py | 112 + reference_code/image_scale_restore_v2.py | 132 + reference_code/image_shift.py | 88 + reference_code/image_tagger_save.py | 142 + reference_code/image_to_mask.py | 111 + reference_code/imagefunc.py | 2553 +++++++++++++++++ reference_code/inner_glow.py | 107 + reference_code/inner_glow_v2.py | 114 + reference_code/inner_shadow.py | 102 + reference_code/inner_shadow_v2.py | 103 + reference_code/layer_image_transform.py | 94 + reference_code/layer_mask_transform.py | 81 + reference_code/light_leak.py | 85 + reference_code/mask_box_detect.py | 78 + reference_code/mask_by_color.py | 84 + reference_code/mask_edge_shrink.py | 81 + reference_code/mask_edge_ultra_detail.py | 80 + reference_code/mask_edge_ultra_detail_v2.py | 96 + reference_code/mask_gradient.py | 152 + reference_code/mask_grain.py | 63 + reference_code/mask_grow.py | 59 + reference_code/mask_invert.py | 49 + reference_code/mask_motion_blur.py | 58 + reference_code/mask_preview.py | 36 + reference_code/mask_stroke.py | 64 + reference_code/motion_blur.py | 49 + reference_code/outer_glow.py | 106 + reference_code/outer_glow_v2.py | 111 + reference_code/pixel_spread.py | 76 + reference_code/print_info.py | 57 + reference_code/purge_vram.py | 80 + reference_code/random_generator.py | 141 + reference_code/restore_crop_box.py | 84 + reference_code/rmbg_ultra.py | 54 + reference_code/rmbg_ultra_v2.py | 82 + reference_code/rounded_rectangle.py | 100 + reference_code/segformer_ultra.py | 944 ++++++ reference_code/shadow_highlight_mask.py | 185 ++ reference_code/sharp_soft.py | 80 + reference_code/simple_text_image.py | 125 + reference_code/skin_beauty.py | 64 + reference_code/soft_light.py | 63 + reference_code/stroke.py | 103 + reference_code/stroke_v2.py | 103 + reference_code/text_image.py | 148 + reference_code/text_image_v2.py | 150 + reference_code/text_join.py | 95 + reference_code/text_node.py | 86 + reference_code/vqa_prompt.py | 134 + reference_code/water_color.py | 52 + reference_code/xy2percent.py | 43 + 131 files changed, 18966 insertions(+) create mode 100644 reference_code/add_grain.py create mode 100644 reference_code/any_rerouter.py create mode 100644 reference_code/batch_selector.py create mode 100644 reference_code/blend_if_mask.py create mode 100644 reference_code/blendmodes.py create mode 100644 reference_code/briarmbg.py create mode 100644 reference_code/channel_shake.py create mode 100644 reference_code/check_mask.py create mode 100644 reference_code/check_mask_v2.py create mode 100644 reference_code/color_adapter.py create mode 100644 reference_code/color_correct_HSV.py create mode 100644 reference_code/color_correct_LAB.py create mode 100644 reference_code/color_correct_LUTapply.py create mode 100644 reference_code/color_correct_RGB.py create mode 100644 reference_code/color_correct_YUV.py create mode 100644 reference_code/color_correct_auto_adjust.py create mode 100644 reference_code/color_correct_auto_adjust_v2.py create mode 100644 reference_code/color_correct_auto_brightness.py create mode 100644 reference_code/color_correct_brightness_and_contrast.py create mode 100644 reference_code/color_correct_color_balance.py create mode 100644 reference_code/color_correct_color_temperature.py create mode 100644 reference_code/color_correct_exposure.py create mode 100644 reference_code/color_correct_gamma.py create mode 100644 reference_code/color_correct_levels.py create mode 100644 reference_code/color_correct_shadow_and_highlight.py create mode 100644 reference_code/color_image.py create mode 100644 reference_code/color_image_v2.py create mode 100644 reference_code/color_map.py create mode 100644 reference_code/color_name.py create mode 100644 reference_code/color_overlay _v2.py create mode 100644 reference_code/color_overlay.py create mode 100644 reference_code/color_picker.py create mode 100644 reference_code/color_to_HSVvalue.py create mode 100644 reference_code/color_to_RGBvalue.py create mode 100644 reference_code/color_to_gray_value.py create mode 100644 reference_code/create_gradient_mask.py create mode 100644 reference_code/crop_box_resolve.py create mode 100644 reference_code/crop_by_mask.py create mode 100644 reference_code/crop_by_mask_v2.py create mode 100644 reference_code/crop_by_mask_v3.py create mode 100644 reference_code/data_nodes.py create mode 100644 reference_code/drop_shadow.py create mode 100644 reference_code/drop_shadow_v2.py create mode 100644 reference_code/drop_shadow_v3.py create mode 100644 reference_code/extend_canvas.py create mode 100644 reference_code/extend_canvas_v2.py create mode 100644 reference_code/film_post.py create mode 100644 reference_code/film_post_v2.py create mode 100644 reference_code/filmgrainer/__init__.py create mode 100644 reference_code/filmgrainer/filmgrainer.py create mode 100644 reference_code/filmgrainer/graingamma.py create mode 100644 reference_code/filmgrainer/graingen.py create mode 100644 reference_code/filmgrainer/processing.py create mode 100644 reference_code/gaussian_blur.py create mode 100644 reference_code/get_image_size.py create mode 100644 reference_code/get_main_colors.py create mode 100644 reference_code/gradient_image.py create mode 100644 reference_code/gradient_image_v2.py create mode 100644 reference_code/gradient_map.py create mode 100644 reference_code/gradient_overlay.py create mode 100644 reference_code/gradient_overlay_v2.py create mode 100644 reference_code/halftone.py create mode 100644 reference_code/hdr_effects.py create mode 100644 reference_code/hl_frequency_detail_restore.py create mode 100644 reference_code/ic_mask.py create mode 100644 reference_code/image_blend.py create mode 100644 reference_code/image_blend_advance.py create mode 100644 reference_code/image_blend_advance_v2.py create mode 100644 reference_code/image_blend_advance_v3.py create mode 100644 reference_code/image_blend_v2.py create mode 100644 reference_code/image_channel_merge.py create mode 100644 reference_code/image_channel_split.py create mode 100644 reference_code/image_combine_alpha.py create mode 100644 reference_code/image_hub.py create mode 100644 reference_code/image_mask_scale_as.py create mode 100644 reference_code/image_opacity.py create mode 100644 reference_code/image_reel.py create mode 100644 reference_code/image_remove_alpha.py create mode 100644 reference_code/image_scale_by_aspect_ratio.py create mode 100644 reference_code/image_scale_by_aspect_ratio_v2.py create mode 100644 reference_code/image_scale_restore.py create mode 100644 reference_code/image_scale_restore_v2.py create mode 100644 reference_code/image_shift.py create mode 100644 reference_code/image_tagger_save.py create mode 100644 reference_code/image_to_mask.py create mode 100644 reference_code/imagefunc.py create mode 100644 reference_code/inner_glow.py create mode 100644 reference_code/inner_glow_v2.py create mode 100644 reference_code/inner_shadow.py create mode 100644 reference_code/inner_shadow_v2.py create mode 100644 reference_code/layer_image_transform.py create mode 100644 reference_code/layer_mask_transform.py create mode 100644 reference_code/light_leak.py create mode 100644 reference_code/mask_box_detect.py create mode 100644 reference_code/mask_by_color.py create mode 100644 reference_code/mask_edge_shrink.py create mode 100644 reference_code/mask_edge_ultra_detail.py create mode 100644 reference_code/mask_edge_ultra_detail_v2.py create mode 100644 reference_code/mask_gradient.py create mode 100644 reference_code/mask_grain.py create mode 100644 reference_code/mask_grow.py create mode 100644 reference_code/mask_invert.py create mode 100644 reference_code/mask_motion_blur.py create mode 100644 reference_code/mask_preview.py create mode 100644 reference_code/mask_stroke.py create mode 100644 reference_code/motion_blur.py create mode 100644 reference_code/outer_glow.py create mode 100644 reference_code/outer_glow_v2.py create mode 100644 reference_code/pixel_spread.py create mode 100644 reference_code/print_info.py create mode 100644 reference_code/purge_vram.py create mode 100644 reference_code/random_generator.py create mode 100644 reference_code/restore_crop_box.py create mode 100644 reference_code/rmbg_ultra.py create mode 100644 reference_code/rmbg_ultra_v2.py create mode 100644 reference_code/rounded_rectangle.py create mode 100644 reference_code/segformer_ultra.py create mode 100644 reference_code/shadow_highlight_mask.py create mode 100644 reference_code/sharp_soft.py create mode 100644 reference_code/simple_text_image.py create mode 100644 reference_code/skin_beauty.py create mode 100644 reference_code/soft_light.py create mode 100644 reference_code/stroke.py create mode 100644 reference_code/stroke_v2.py create mode 100644 reference_code/text_image.py create mode 100644 reference_code/text_image_v2.py create mode 100644 reference_code/text_join.py create mode 100644 reference_code/text_node.py create mode 100644 reference_code/vqa_prompt.py create mode 100644 reference_code/water_color.py create mode 100644 reference_code/xy2percent.py diff --git a/reference_code/add_grain.py b/reference_code/add_grain.py new file mode 100644 index 0000000..b7d4c10 --- /dev/null +++ b/reference_code/add_grain.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/any_rerouter.py b/reference_code/any_rerouter.py new file mode 100644 index 0000000..1e80079 --- /dev/null +++ b/reference_code/any_rerouter.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/batch_selector.py b/reference_code/batch_selector.py new file mode 100644 index 0000000..3d9ed66 --- /dev/null +++ b/reference_code/batch_selector.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/blend_if_mask.py b/reference_code/blend_if_mask.py new file mode 100644 index 0000000..c5eca0b --- /dev/null +++ b/reference_code/blend_if_mask.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/blendmodes.py b/reference_code/blendmodes.py new file mode 100644 index 0000000..fac31af --- /dev/null +++ b/reference_code/blendmodes.py @@ -0,0 +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 +} diff --git a/reference_code/briarmbg.py b/reference_code/briarmbg.py new file mode 100644 index 0000000..647bdc0 --- /dev/null +++ b/reference_code/briarmbg.py @@ -0,0 +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] + diff --git a/reference_code/channel_shake.py b/reference_code/channel_shake.py new file mode 100644 index 0000000..740b797 --- /dev/null +++ b/reference_code/channel_shake.py @@ -0,0 +1,70 @@ +import torch +import math +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import shift_image + + + + +class ChannelShake: + + def __init__(self): + self.NODE_NAME = 'ChannelShake' + + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGB', 'RBG', 'BGR', 'BRG', 'GBR', 'GRB'] + return { + "required": { + "image": ("IMAGE", ), # + "distance": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 距离 + "angle": ("FLOAT", {"default": 40, "min": -360, "max": 360, "step": 0.1}), # 角度 + "mode": (channel_mode,), # 模式 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'channel_shake' + CATEGORY = '😺dzNodes/LayerFilter' + + def channel_shake(self, image, distance, angle, mode, ): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + _canvas = tensor2pil(i).convert('RGB') + R, G, B = _canvas.split() + x = int(math.cos(angle) * distance) + y = int(math.sin(angle) * distance) + if mode.startswith('R'): + R = shift_image(R.convert('RGB'), -x, -y).convert('L') + if mode.startswith('G'): + G = shift_image(G.convert('RGB'), -x, -y).convert('L') + if mode.startswith('B'): + B = shift_image(B.convert('RGB'), -x, -y).convert('L') + if mode.endswith('R'): + R = shift_image(R.convert('RGB'), x, y).convert('L') + if mode.endswith('G'): + G = shift_image(G.convert('RGB'), x, y).convert('L') + if mode.endswith('B'): + B = shift_image(B.convert('RGB'), x, y).convert('L') + + ret_image = Image.merge('RGB', [R, G, B]) + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerFilter: ChannelShake": ChannelShake +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: ChannelShake": "LayerFilter: ChannelShake" +} \ No newline at end of file diff --git a/reference_code/check_mask.py b/reference_code/check_mask.py new file mode 100644 index 0000000..bfefaa6 --- /dev/null +++ b/reference_code/check_mask.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/check_mask_v2.py b/reference_code/check_mask_v2.py new file mode 100644 index 0000000..e9984dd --- /dev/null +++ b/reference_code/check_mask_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_adapter.py b/reference_code/color_adapter.py new file mode 100644 index 0000000..22a575b --- /dev/null +++ b/reference_code/color_adapter.py @@ -0,0 +1,62 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import color_adapter, chop_image, RGB2RGBA + + + +class ColorAdapter: + + def __init__(self): + self.NODE_NAME = 'ColorAdapter' + + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "color_ref_image": ("IMAGE", ), # + "opacity": ("INT", {"default": 75, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_adapter' + CATEGORY = '😺dzNodes/LayerColor' + + def color_adapter(self, image, color_ref_image, opacity): + ret_images = [] + + l_images = [] + r_images = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + for r in color_ref_image: + r_images.append(torch.unsqueeze(r, 0)) + for i in range(len(l_images)): + _image = l_images[i] + _ref = r_images[i] if len(ret_images) > i else r_images[-1] + + __image = tensor2pil(_image) + _canvas = __image.convert('RGB') + ret_image = color_adapter(_canvas, tensor2pil(_ref).convert('RGB')) + ret_image = chop_image(_canvas, ret_image, blend_mode='normal', opacity=opacity) + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: ColorAdapter": ColorAdapter +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: ColorAdapter": "LayerColor: ColorAdapter" +} \ No newline at end of file diff --git a/reference_code/color_correct_HSV.py b/reference_code/color_correct_HSV.py new file mode 100644 index 0000000..08d7e4b --- /dev/null +++ b/reference_code/color_correct_HSV.py @@ -0,0 +1,61 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import image_hue_offset, image_gray_offset, image_channel_merge, RGB2RGBA + + + +class ColorCorrectHSV: + + def __init__(self): + self.NODE_NAME = 'HSV' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "H": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "S": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "V": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_HSV' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_HSV(self, image, H, S, V): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i,0) + __image = tensor2pil(i) + _h, _s, _v = tensor2pil(i).convert('HSV').split() + if H != 0 : + _h = image_hue_offset(_h, H) + if S != 0 : + _s = image_gray_offset(_s, S) + if V != 0 : + _v = image_gray_offset(_v, V) + ret_image = image_channel_merge((_h, _s, _v), 'HSV') + + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: HSV": ColorCorrectHSV +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: HSV": "LayerColor: HSV" +} \ No newline at end of file diff --git a/reference_code/color_correct_LAB.py b/reference_code/color_correct_LAB.py new file mode 100644 index 0000000..27dcaec --- /dev/null +++ b/reference_code/color_correct_LAB.py @@ -0,0 +1,61 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA + + + +class ColorCorrectLAB: + + def __init__(self): + self.NODE_NAME = 'LAB' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "L": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "A": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "B": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_LAB' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_LAB(self, image, L, A, B): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + __image = tensor2pil(i) + _l, _a, _b = tensor2pil(i).convert('LAB').split() + if L != 0 : + _l = image_gray_offset(_l, L) + if A != 0 : + _a = image_gray_offset(_a, A) + if B != 0 : + _b = image_gray_offset(_b, B) + ret_image = image_channel_merge((_l, _a, _b), 'LAB') + + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: LAB": ColorCorrectLAB +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: LAB": "LayerColor: LAB" +} \ No newline at end of file diff --git a/reference_code/color_correct_LUTapply.py b/reference_code/color_correct_LUTapply.py new file mode 100644 index 0000000..2d1b3f6 --- /dev/null +++ b/reference_code/color_correct_LUTapply.py @@ -0,0 +1,62 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import get_resource_dir, apply_lut, RGB2RGBA + + + +class ColorCorrectLUTapply: + + def __init__(self): + self.NODE_NAME = 'LUT Apply' + + @classmethod + def INPUT_TYPES(self): + (LUT_DICT, _) = get_resource_dir() + LUT_LIST = list(LUT_DICT.keys()) + + color_space_list = ['linear', 'log'] + + return { + "required": { + "image": ("IMAGE", ), # + "LUT": (LUT_LIST,), # LUT文件 + "color_space": (color_space_list,), + "strength": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_LUTapply' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_LUTapply(self, image, LUT, color_space, strength): + + (LUT_DICT, _) = get_resource_dir() + ret_images = [] + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i) + + lut_file = LUT_DICT[LUT] + ret_image = apply_lut(_image, lut_file=lut_file, colorspace=color_space, strength=strength) + + if _image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, _image.split()[-1]) + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + + + + +NODE_CLASS_MAPPINGS = { + "LayerColor: LUT Apply": ColorCorrectLUTapply +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: LUT Apply": "LayerColor: LUT Apply" +} \ No newline at end of file diff --git a/reference_code/color_correct_RGB.py b/reference_code/color_correct_RGB.py new file mode 100644 index 0000000..0e32e6e --- /dev/null +++ b/reference_code/color_correct_RGB.py @@ -0,0 +1,61 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA + + + +class ColorCorrectRGB: + + def __init__(self): + self.NODE_NAME = 'RGB' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "R": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "G": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "B": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_RGB' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_RGB(self, image, R, G, B): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i,0) + __image = tensor2pil(i) + _r, _g, _b = tensor2pil(i).convert('RGB').split() + if R != 0 : + _r = image_gray_offset(_r, R) + if G != 0 : + _g = image_gray_offset(_g, G) + if B != 0 : + _b = image_gray_offset(_b, B) + ret_image = image_channel_merge((_r, _g, _b), 'RGB') + + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: RGB": ColorCorrectRGB +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: RGB": "LayerColor: RGB" +} \ No newline at end of file diff --git a/reference_code/color_correct_YUV.py b/reference_code/color_correct_YUV.py new file mode 100644 index 0000000..d0cd80c --- /dev/null +++ b/reference_code/color_correct_YUV.py @@ -0,0 +1,61 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import image_gray_offset, image_channel_merge, RGB2RGBA + + + +class ColorCorrectYUV: + + def __init__(self): + self.NODE_NAME = 'YUV' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "Y": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "U": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + "V": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_YUV' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_YUV(self, image, Y, U, V): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + __image = tensor2pil(i) + _y, _u, _v = tensor2pil(i).convert('YCbCr').split() + if Y != 0 : + _y = image_gray_offset(_y, Y) + if U != 0 : + _u = image_gray_offset(_u, U) + if V != 0 : + _v = image_gray_offset(_v, V) + ret_image = image_channel_merge((_y, _u, _v), 'YCbCr') + + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: YUV": ColorCorrectYUV +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: YUV": "LayerColor: YUV" +} \ No newline at end of file diff --git a/reference_code/color_correct_auto_adjust.py b/reference_code/color_correct_auto_adjust.py new file mode 100644 index 0000000..9a0de9c --- /dev/null +++ b/reference_code/color_correct_auto_adjust.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_auto_adjust_v2.py b/reference_code/color_correct_auto_adjust_v2.py new file mode 100644 index 0000000..796f437 --- /dev/null +++ b/reference_code/color_correct_auto_adjust_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_auto_brightness.py b/reference_code/color_correct_auto_brightness.py new file mode 100644 index 0000000..4580338 --- /dev/null +++ b/reference_code/color_correct_auto_brightness.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_brightness_and_contrast.py b/reference_code/color_correct_brightness_and_contrast.py new file mode 100644 index 0000000..1a48e10 --- /dev/null +++ b/reference_code/color_correct_brightness_and_contrast.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_color_balance.py b/reference_code/color_correct_color_balance.py new file mode 100644 index 0000000..8ae2971 --- /dev/null +++ b/reference_code/color_correct_color_balance.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_color_temperature.py b/reference_code/color_correct_color_temperature.py new file mode 100644 index 0000000..62b6e70 --- /dev/null +++ b/reference_code/color_correct_color_temperature.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_exposure.py b/reference_code/color_correct_exposure.py new file mode 100644 index 0000000..b51a5f8 --- /dev/null +++ b/reference_code/color_correct_exposure.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_gamma.py b/reference_code/color_correct_gamma.py new file mode 100644 index 0000000..dc8e588 --- /dev/null +++ b/reference_code/color_correct_gamma.py @@ -0,0 +1,52 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import gamma_trans, RGB2RGBA + + + +class ColorCorrectGamma: + + def __init__(self): + self.NODE_NAME = 'Gamma' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + "gamma": ("FLOAT", {"default": 1, "min": 0.1, "max": 10, "step": 0.01}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_correct_gamma' + CATEGORY = '😺dzNodes/LayerColor' + + def color_correct_gamma(self, image, gamma): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + __image = tensor2pil(i) + ret_image = gamma_trans(tensor2pil(i), gamma) + + if __image.mode == 'RGBA': + ret_image = RGB2RGBA(ret_image, __image.split()[-1]) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerColor: Gamma": ColorCorrectGamma +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerColor: Gamma": "LayerColor: Gamma" +} \ No newline at end of file diff --git a/reference_code/color_correct_levels.py b/reference_code/color_correct_levels.py new file mode 100644 index 0000000..ff91776 --- /dev/null +++ b/reference_code/color_correct_levels.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_correct_shadow_and_highlight.py b/reference_code/color_correct_shadow_and_highlight.py new file mode 100644 index 0000000..0eff8e7 --- /dev/null +++ b/reference_code/color_correct_shadow_and_highlight.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_image.py b/reference_code/color_image.py new file mode 100644 index 0000000..056ea8a --- /dev/null +++ b/reference_code/color_image.py @@ -0,0 +1,39 @@ +from PIL import Image +from .imagefunc import log, pil2tensor + + +class ColorImage: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}), + "height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}), + "color": ("STRING", {"default": "#000000"},), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = 'color_image' + CATEGORY = '😺dzNodes/LayerUtility' + + def color_image(self, width, height, color, ): + + ret_image = Image.new('RGB', (width, height), color=color) + return (pil2tensor(ret_image), ) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ColorImage": ColorImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ColorImage": "LayerUtility: ColorImage" +} \ No newline at end of file diff --git a/reference_code/color_image_v2.py b/reference_code/color_image_v2.py new file mode 100644 index 0000000..6d21453 --- /dev/null +++ b/reference_code/color_image_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_map.py b/reference_code/color_map.py new file mode 100644 index 0000000..fa67400 --- /dev/null +++ b/reference_code/color_map.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_name.py b/reference_code/color_name.py new file mode 100644 index 0000000..c3440d9 --- /dev/null +++ b/reference_code/color_name.py @@ -0,0 +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", +} \ No newline at end of file diff --git a/reference_code/color_overlay _v2.py b/reference_code/color_overlay _v2.py new file mode 100644 index 0000000..6710dcd --- /dev/null +++ b/reference_code/color_overlay _v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_overlay.py b/reference_code/color_overlay.py new file mode 100644 index 0000000..d6be6a5 --- /dev/null +++ b/reference_code/color_overlay.py @@ -0,0 +1,91 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor +from .imagefunc import chop_mode,chop_image + + + +class ColorOverlay: + + def __init__(self): + self.NODE_NAME = 'ColorOverlay' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "color": ("STRING", {"default": "#FFBF30"}), # 渐变开始颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'color_overlay' + CATEGORY = '😺dzNodes/LayerStyle' + + def color_overlay(self, background_image, layer_image, + invert_mask, blend_mode, opacity, color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + + max_batch = max(len(b_images), len(l_images), len(l_masks)) + _color = Image.new("RGB", tensor2pil(l_images[0]).size, color=color) + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + # 合成layer + _comp = chop_image(_layer, _color, blend_mode, opacity) + _canvas.paste(_comp, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerStyle: ColorOverlay": ColorOverlay +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: ColorOverlay": "LayerStyle: ColorOverlay" +} \ No newline at end of file diff --git a/reference_code/color_picker.py b/reference_code/color_picker.py new file mode 100644 index 0000000..bdcd2e9 --- /dev/null +++ b/reference_code/color_picker.py @@ -0,0 +1,40 @@ + +from .imagefunc import Hex_to_RGB + + +class ColorPicker: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + mode_list = ['HEX', 'DEC'] + return { + "required": { + "color": ("COLOR", {"default": "#FFFFFF"},), + "mode": (mode_list,), # 输出模式 + }, + "optional": { + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("value",) + FUNCTION = 'picker' + CATEGORY = '😺dzNodes/LayerUtility' + + def picker(self, color, mode): + ret = color + if mode == 'DEC': + ret = Hex_to_RGB(ret) + return (ret,) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ColorPicker": ColorPicker +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ColorPicker": "LayerUtility: ColorPicker" +} \ No newline at end of file diff --git a/reference_code/color_to_HSVvalue.py b/reference_code/color_to_HSVvalue.py new file mode 100644 index 0000000..207f99e --- /dev/null +++ b/reference_code/color_to_HSVvalue.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_to_RGBvalue.py b/reference_code/color_to_RGBvalue.py new file mode 100644 index 0000000..7694ecf --- /dev/null +++ b/reference_code/color_to_RGBvalue.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/color_to_gray_value.py b/reference_code/color_to_gray_value.py new file mode 100644 index 0000000..19e0c36 --- /dev/null +++ b/reference_code/color_to_gray_value.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/create_gradient_mask.py b/reference_code/create_gradient_mask.py new file mode 100644 index 0000000..03e5441 --- /dev/null +++ b/reference_code/create_gradient_mask.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/crop_box_resolve.py b/reference_code/crop_box_resolve.py new file mode 100644 index 0000000..bca827e --- /dev/null +++ b/reference_code/crop_box_resolve.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/crop_by_mask.py b/reference_code/crop_by_mask.py new file mode 100644 index 0000000..46ef54b --- /dev/null +++ b/reference_code/crop_by_mask.py @@ -0,0 +1,100 @@ +import torch + +from .imagefunc import log, tensor2pil, pil2tensor, mask2image, image2mask, gaussian_blur, min_bounding_rect, max_inscribed_rect, mask_area +from .imagefunc import num_round_up_to_multiple, draw_rect + + + +class CropByMask: + + def __init__(self): + self.NODE_NAME = 'CropByMask' + + @classmethod + def INPUT_TYPES(self): + detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area'] + return { + "required": { + "image": ("IMAGE", ), # + "mask_for_crop": ("MASK",), + "invert_mask": ("BOOLEAN", {"default": False}), # 反转mask# + "detect": (detect_mode,), + "top_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), + "bottom_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), + "left_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), + "right_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "IMAGE",) + RETURN_NAMES = ("croped_image", "croped_mask", "crop_box", "box_preview") + FUNCTION = 'crop_by_mask' + CATEGORY = '😺dzNodes/LayerUtility' + + def crop_by_mask(self, image, mask_for_crop, invert_mask, detect, + top_reserve, bottom_reserve, left_reserve, right_reserve + ): + + ret_images = [] + ret_masks = [] + l_images = [] + l_masks = [] + + + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + if mask_for_crop.dim() == 2: + mask_for_crop = torch.unsqueeze(mask_for_crop, 0) + # 如果有多张mask输入,使用第一张 + if mask_for_crop.shape[0] > 1: + log(f"Warning: Multiple mask inputs, using the first.", message_type='warning') + mask_for_crop = torch.unsqueeze(mask_for_crop[0], 0) + if invert_mask: + mask_for_crop = 1 - mask_for_crop + l_masks.append(tensor2pil(torch.unsqueeze(mask_for_crop, 0)).convert('L')) + + _mask = mask2image(mask_for_crop) + bluredmask = gaussian_blur(_mask, 20).convert('L') + x = 0 + y = 0 + width = 0 + height = 0 + if detect == "min_bounding_rect": + (x, y, width, height) = min_bounding_rect(bluredmask) + elif detect == "max_inscribed_rect": + (x, y, width, height) = max_inscribed_rect(bluredmask) + else: + (x, y, width, height) = mask_area(_mask) + + width = num_round_up_to_multiple(width, 8) + height = num_round_up_to_multiple(height, 8) + log(f"{self.NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}") + canvas_width, canvas_height = tensor2pil(torch.unsqueeze(image[0], 0)).convert('RGB').size + x1 = x - left_reserve if x - left_reserve > 0 else 0 + y1 = y - top_reserve if y - top_reserve > 0 else 0 + x2 = x + width + right_reserve if x + width + right_reserve < canvas_width else canvas_width + y2 = y + height + bottom_reserve if y + height + bottom_reserve < canvas_height else canvas_height + preview_image = tensor2pil(mask_for_crop).convert('RGB') + preview_image = draw_rect(preview_image, x, y, width, height, line_color="#F00000", line_width=(width+height)//100) + preview_image = draw_rect(preview_image, x1, y1, x2 - x1, y2 - y1, + line_color="#00F000", line_width=(width+height)//200) + crop_box = (x1, y1, x2, y2) + for i in range(len(l_images)): + _canvas = tensor2pil(l_images[i]).convert('RGB') + _mask = l_masks[0] + ret_images.append(pil2tensor(_canvas.crop(crop_box))) + ret_masks.append(image2mask(_mask.crop(crop_box))) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), list(crop_box), pil2tensor(preview_image),) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: CropByMask": CropByMask +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: CropByMask": "LayerUtility: CropByMask" +} \ No newline at end of file diff --git a/reference_code/crop_by_mask_v2.py b/reference_code/crop_by_mask_v2.py new file mode 100644 index 0000000..9f994a2 --- /dev/null +++ b/reference_code/crop_by_mask_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/crop_by_mask_v3.py b/reference_code/crop_by_mask_v3.py new file mode 100644 index 0000000..f405615 --- /dev/null +++ b/reference_code/crop_by_mask_v3.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/data_nodes.py b/reference_code/data_nodes.py new file mode 100644 index 0000000..e0f5106 --- /dev/null +++ b/reference_code/data_nodes.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/drop_shadow.py b/reference_code/drop_shadow.py new file mode 100644 index 0000000..5fb3215 --- /dev/null +++ b/reference_code/drop_shadow.py @@ -0,0 +1,108 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask2image +from .imagefunc import chop_image, chop_mode, shift_image, expand_mask + + + +class DropShadow: + + def __init__(self): + self.NODE_NAME = 'DropShadow' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度 + "distance_x": ("INT", {"default": 25, "min": -9999, "max": 9999, "step": 1}), # x_偏移 + "distance_y": ("INT", {"default": 25, "min": -9999, "max": 9999, "step": 1}), # y_偏移 + "grow": ("INT", {"default": 6, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 18, "min": 0, "max": 100, "step": 1}), # 模糊 + "shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'drop_shadow' + CATEGORY = '😺dzNodes/LayerStyle' + + def drop_shadow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, distance_x, distance_y, + grow, blur, shadow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + + max_batch = max(len(b_images), len(l_images), len(l_masks)) + distance_x = -distance_x + distance_y = -distance_y + shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color) + + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image) + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + if distance_x != 0 or distance_y != 0: + __mask = shift_image(_mask, distance_x, distance_y) # 位移 + shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊 + # 合成阴影 + alpha = tensor2pil(shadow_mask).convert('L') + _shadow = chop_image(_canvas, shadow_color, blend_mode, opacity) + _canvas.paste(_shadow, mask=alpha) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + + +NODE_CLASS_MAPPINGS = { + "LayerStyle: DropShadow": DropShadow +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: DropShadow": "LayerStyle: DropShadow" +} \ No newline at end of file diff --git a/reference_code/drop_shadow_v2.py b/reference_code/drop_shadow_v2.py new file mode 100644 index 0000000..e67ec8c --- /dev/null +++ b/reference_code/drop_shadow_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/drop_shadow_v3.py b/reference_code/drop_shadow_v3.py new file mode 100644 index 0000000..53cd738 --- /dev/null +++ b/reference_code/drop_shadow_v3.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/extend_canvas.py b/reference_code/extend_canvas.py new file mode 100644 index 0000000..c64c4c6 --- /dev/null +++ b/reference_code/extend_canvas.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/extend_canvas_v2.py b/reference_code/extend_canvas_v2.py new file mode 100644 index 0000000..09759ef --- /dev/null +++ b/reference_code/extend_canvas_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/film_post.py b/reference_code/film_post.py new file mode 100644 index 0000000..2fdca4a --- /dev/null +++ b/reference_code/film_post.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/film_post_v2.py b/reference_code/film_post_v2.py new file mode 100644 index 0000000..de1e0ef --- /dev/null +++ b/reference_code/film_post_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/filmgrainer/__init__.py b/reference_code/filmgrainer/__init__.py new file mode 100644 index 0000000..bb35ee1 --- /dev/null +++ b/reference_code/filmgrainer/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.2" \ No newline at end of file diff --git a/reference_code/filmgrainer/filmgrainer.py b/reference_code/filmgrainer/filmgrainer.py new file mode 100644 index 0000000..6709212 --- /dev/null +++ b/reference_code/filmgrainer/filmgrainer.py @@ -0,0 +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) + + return np.array(img).astype('float32') / 255.0 \ No newline at end of file diff --git a/reference_code/filmgrainer/graingamma.py b/reference_code/filmgrainer/graingamma.py new file mode 100644 index 0000000..0f3cc6e --- /dev/null +++ b/reference_code/filmgrainer/graingamma.py @@ -0,0 +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) + plt.show() \ No newline at end of file diff --git a/reference_code/filmgrainer/graingen.py b/reference_code/filmgrainer/graingen.py new file mode 100644 index 0000000..c3ea62e --- /dev/null +++ b/reference_code/filmgrainer/graingen.py @@ -0,0 +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) + out.save(sys.argv[1]) \ No newline at end of file diff --git a/reference_code/filmgrainer/processing.py b/reference_code/filmgrainer/processing.py new file mode 100644 index 0000000..4c48e8e --- /dev/null +++ b/reference_code/filmgrainer/processing.py @@ -0,0 +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 + return final_image \ No newline at end of file diff --git a/reference_code/gaussian_blur.py b/reference_code/gaussian_blur.py new file mode 100644 index 0000000..55556d7 --- /dev/null +++ b/reference_code/gaussian_blur.py @@ -0,0 +1,86 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur + + + +class GaussianBlur: + + def __init__(self): + self.NODE_NAME = 'GaussianBlur' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "blur": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'gaussian_blur' + CATEGORY = '😺dzNodes/LayerFilter' + + def gaussian_blur(self, image, blur): + + ret_images = [] + + for i in image: + _canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB') + + ret_images.append(pil2tensor(gaussian_blur(_canvas, blur))) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + + +class LS_GaussianBlurV2: + + def __init__(self): + self.NODE_NAME = 'GaussianBlurV2' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "blur": ("FLOAT", {"default": 20, "min": 0, "max": 1000, "step": 0.05}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'gaussian_blur_v2' + CATEGORY = '😺dzNodes/LayerFilter' + + def gaussian_blur_v2(self, image, blur): + + ret_images = [] + + if blur: + for i in image: + _canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB') + + ret_images.append(pil2tensor(gaussian_blur(_canvas, blur))) + else: + return (image,) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerFilter: GaussianBlur": GaussianBlur, + "LayerFilter: GaussianBlurV2": LS_GaussianBlurV2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: GaussianBlur": "LayerFilter: GaussianBlur", + "LayerFilter: GaussianBlurV2": "LayerFilter: Gaussian Blur V2" +} \ No newline at end of file diff --git a/reference_code/get_image_size.py b/reference_code/get_image_size.py new file mode 100644 index 0000000..f958086 --- /dev/null +++ b/reference_code/get_image_size.py @@ -0,0 +1,39 @@ +import torch +from .imagefunc import tensor2pil + +class GetImageSize: + + def __init__(self): + self.NODE_NAME = 'GetImageSize' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), + }, + "optional": { + } + } + + RETURN_TYPES = ("INT", "INT", "BOX") + RETURN_NAMES = ("width", "height", "original_size") + FUNCTION = 'get_image_size' + CATEGORY = '😺dzNodes/LayerUtility' + + def get_image_size(self, image,): + + if image.shape[0] > 0: + image = torch.unsqueeze(image[0], 0) + _image = tensor2pil(image) + + return (_image.width, _image.height, [_image.width, _image.height],) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: GetImageSize": GetImageSize +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: GetImageSize": "LayerUtility: GetImageSize" +} \ No newline at end of file diff --git a/reference_code/get_main_colors.py b/reference_code/get_main_colors.py new file mode 100644 index 0000000..783a256 --- /dev/null +++ b/reference_code/get_main_colors.py @@ -0,0 +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", + +} \ No newline at end of file diff --git a/reference_code/gradient_image.py b/reference_code/gradient_image.py new file mode 100644 index 0000000..715ff22 --- /dev/null +++ b/reference_code/gradient_image.py @@ -0,0 +1,41 @@ +import torch +from .imagefunc import gradient, pil2tensor + +class GradientImage: + + def __init__(self): + self.NODE_NAME = 'GradientImage' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}), + "height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}), + "angle": ("INT", {"default": 0, "min": -360, "max": 360, "step": 1}), + "start_color": ("STRING", {"default": "#FFFFFF"},), + "end_color": ("STRING", {"default": "#000000"},), + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", ) + RETURN_NAMES = ("image", ) + FUNCTION = 'gradient_image' + CATEGORY = '😺dzNodes/LayerUtility' + + def gradient_image(self, width, height, angle, start_color, end_color, ): + + ret_image = gradient(start_color, end_color, width, height, angle) + + return (pil2tensor(ret_image), ) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: GradientImage": GradientImage +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: GradientImage": "LayerUtility: GradientImage" +} \ No newline at end of file diff --git a/reference_code/gradient_image_v2.py b/reference_code/gradient_image_v2.py new file mode 100644 index 0000000..9db4154 --- /dev/null +++ b/reference_code/gradient_image_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/gradient_map.py b/reference_code/gradient_map.py new file mode 100644 index 0000000..a8e07a1 --- /dev/null +++ b/reference_code/gradient_map.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/gradient_overlay.py b/reference_code/gradient_overlay.py new file mode 100644 index 0000000..69b91bd --- /dev/null +++ b/reference_code/gradient_overlay.py @@ -0,0 +1,102 @@ +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, gradient, RGB_to_Hex, chop_image, chop_mode + + + +class GradientOverlay: + + def __init__(self): + self.NODE_NAME = 'GradientOverlay' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "start_color": ("STRING", {"default": "#FFBF30"}), # 渐变开始颜色 + "start_alpha": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "end_color": ("STRING", {"default": "#FE0000"}), # 渐变结束颜色 + "end_alpha": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "angle": ("INT", {"default": 0, "min": -180, "max": 180, "step": 1}), # 渐变角度 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'gradient_overlay' + CATEGORY = '😺dzNodes/LayerStyle' + + def gradient_overlay(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + start_color, start_alpha, end_color, end_alpha, angle, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + width, height = tensor2pil(l_images[0]).size + _gradient = gradient(start_color, end_color, width, height, float(angle)) + start_color = RGB_to_Hex((start_alpha, start_alpha, start_alpha)) + end_color = RGB_to_Hex((end_alpha, end_alpha, end_alpha)) + comp_alpha = gradient(start_color, end_color, width, height, float(angle)) + comp_alpha = ImageChops.invert(comp_alpha).convert('L') + + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + # 合成layer + _comp = chop_image(_layer, _gradient, blend_mode, opacity) + if start_alpha < 255 or end_alpha < 255: + _comp.paste(_layer, comp_alpha) + _canvas.paste(_comp, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerStyle: GradientOverlay": GradientOverlay +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: GradientOverlay": "LayerStyle: GradientOverlay" +} \ No newline at end of file diff --git a/reference_code/gradient_overlay_v2.py b/reference_code/gradient_overlay_v2.py new file mode 100644 index 0000000..90c3883 --- /dev/null +++ b/reference_code/gradient_overlay_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/halftone.py b/reference_code/halftone.py new file mode 100644 index 0000000..fa1f3d0 --- /dev/null +++ b/reference_code/halftone.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/hdr_effects.py b/reference_code/hdr_effects.py new file mode 100644 index 0000000..650e528 --- /dev/null +++ b/reference_code/hdr_effects.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/hl_frequency_detail_restore.py b/reference_code/hl_frequency_detail_restore.py new file mode 100644 index 0000000..0375008 --- /dev/null +++ b/reference_code/hl_frequency_detail_restore.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/ic_mask.py b/reference_code/ic_mask.py new file mode 100644 index 0000000..b897641 --- /dev/null +++ b/reference_code/ic_mask.py @@ -0,0 +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", +} \ No newline at end of file diff --git a/reference_code/image_blend.py b/reference_code/image_blend.py new file mode 100644 index 0000000..9f3f0b3 --- /dev/null +++ b/reference_code/image_blend.py @@ -0,0 +1,86 @@ +import torch +from PIL import Image +from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image, chop_mode + + + +class ImageBlend: + + def __init__(self): + self.NODE_NAME = 'ImageBlend' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'image_blend' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_blend(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + else: + l_masks.append(Image.new('L', m.size, 'white')) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + # 合成layer + _comp = chop_image(_canvas, _layer, blend_mode, opacity) + _canvas.paste(_comp, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageBlend": ImageBlend +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageBlend": "LayerUtility: ImageBlend" +} \ No newline at end of file diff --git a/reference_code/image_blend_advance.py b/reference_code/image_blend_advance.py new file mode 100644 index 0000000..165b67b --- /dev/null +++ b/reference_code/image_blend_advance.py @@ -0,0 +1,135 @@ +import torch +import copy +from PIL import Image +from .imagefunc import log, pil2tensor, tensor2pil, image2mask, mask2image, chop_image, chop_mode, image_rotate_extend_with_alpha + + + +class ImageBlendAdvance: + + def __init__(self): + self.NODE_NAME = 'ImageBlendAdvance' + + @classmethod + def INPUT_TYPES(self): + + mirror_mode = ['None', 'horizontal', 'vertical'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "x_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}), + "y_percent": ("FLOAT", {"default": 50, "min": -999, "max": 999, "step": 0.01}), + "mirror": (mirror_mode,), # 镜像翻转 + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}), + "transform_method": (method_mode,), + "anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}), + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK") + RETURN_NAMES = ("image", "mask") + FUNCTION = 'image_blend_advance' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_blend_advance(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + x_percent, y_percent, + mirror, scale, aspect_ratio, rotate, + transform_method, anti_aliasing, + layer_mask=None + ): + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + else: + l_masks.append(Image.new('L', m.size, 'white')) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(b_images), len(l_images), len(l_masks)) + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image) + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + orig_layer_width = _layer.width + orig_layer_height = _layer.height + _mask = _mask.convert("RGB") + + target_layer_width = int(orig_layer_width * scale) + target_layer_height = int(orig_layer_height * scale * aspect_ratio) + + # mirror + if mirror == 'horizontal': + _layer = _layer.transpose(Image.FLIP_LEFT_RIGHT) + _mask = _mask.transpose(Image.FLIP_LEFT_RIGHT) + elif mirror == 'vertical': + _layer = _layer.transpose(Image.FLIP_TOP_BOTTOM) + _mask = _mask.transpose(Image.FLIP_TOP_BOTTOM) + + # scale + _layer = _layer.resize((target_layer_width, target_layer_height)) + _mask = _mask.resize((target_layer_width, target_layer_height)) + # rotate + _layer, _mask, _ = image_rotate_extend_with_alpha(_layer, rotate, _mask, transform_method, anti_aliasing) + + # 处理位置 + x = int(_canvas.width * x_percent / 100 - _layer.width / 2) + y = int(_canvas.height * y_percent / 100 - _layer.height / 2) + + # composit layer + _comp = copy.copy(_canvas) + _compmask = Image.new("RGB", _comp.size, color='black') + _comp.paste(_layer, (x, y)) + _compmask.paste(_mask, (x, y)) + _compmask = _compmask.convert('L') + _comp = chop_image(_canvas, _comp, blend_mode, opacity) + + # composition background + _canvas.paste(_comp, mask=_compmask) + + ret_images.append(pil2tensor(_canvas)) + ret_masks.append(image2mask(_compmask)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageBlendAdvance": ImageBlendAdvance +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageBlendAdvance": "LayerUtility: ImageBlendAdvance" +} \ No newline at end of file diff --git a/reference_code/image_blend_advance_v2.py b/reference_code/image_blend_advance_v2.py new file mode 100644 index 0000000..b291d96 --- /dev/null +++ b/reference_code/image_blend_advance_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_blend_advance_v3.py b/reference_code/image_blend_advance_v3.py new file mode 100644 index 0000000..1a92de3 --- /dev/null +++ b/reference_code/image_blend_advance_v3.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_blend_v2.py b/reference_code/image_blend_v2.py new file mode 100644 index 0000000..8e94d63 --- /dev/null +++ b/reference_code/image_blend_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_channel_merge.py b/reference_code/image_channel_merge.py new file mode 100644 index 0000000..d216863 --- /dev/null +++ b/reference_code/image_channel_merge.py @@ -0,0 +1,72 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image_channel_merge + + + +class ImageChannelMerge: + + def __init__(self): + self.NODE_NAME = 'ImageChannelMerge' + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV'] + return { + "required": { + "channel_1": ("IMAGE", ), # + "channel_2": ("IMAGE",), # + "channel_3": ("IMAGE",), # + "mode": (channel_mode,), # 通道设置 + }, + "optional": { + "channel_4": ("IMAGE",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'image_channel_merge' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_channel_merge(self, channel_1, channel_2, channel_3, mode, channel_4=None): + + c1_images = [] + c2_images = [] + c3_images = [] + c4_images = [] + ret_images = [] + + width, height = tensor2pil(torch.unsqueeze(channel_1[0], 0)).size + for c in channel_1: + c1_images.append(torch.unsqueeze(c, 0)) + for c in channel_2: + c2_images.append(torch.unsqueeze(c, 0)) + for c in channel_3: + c3_images.append(torch.unsqueeze(c, 0)) + if channel_4 is not None: + for c in channel_4: + c4_images.append(torch.unsqueeze(c, 0)) + else: + c4_images.append(pil2tensor(Image.new('L', size=(width, height), color='white'))) + + max_batch = max(len(c1_images), len(c2_images), len(c3_images), len(c4_images)) + for i in range(max_batch): + c_1 = c1_images[i] if i < len(c1_images) else c1_images[-1] + c_2 = c2_images[i] if i < len(c2_images) else c2_images[-1] + c_3 = c3_images[i] if i < len(c3_images) else c3_images[-1] + c_4 = c4_images[i] if i < len(c4_images) else c4_images[-1] + ret_image = image_channel_merge((tensor2pil(c_1), tensor2pil(c_2), tensor2pil(c_3), tensor2pil(c_4)), mode) + + ret_images.append(pil2tensor(ret_image)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageChannelMerge": ImageChannelMerge +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageChannelMerge": "LayerUtility: ImageChannelMerge" +} \ No newline at end of file diff --git a/reference_code/image_channel_split.py b/reference_code/image_channel_split.py new file mode 100644 index 0000000..c47c9dd --- /dev/null +++ b/reference_code/image_channel_split.py @@ -0,0 +1,53 @@ +import torch +from .imagefunc import log, tensor2pil, pil2tensor, image_channel_split + + + +class ImageChannelSplit: + + def __init__(self): + self.NODE_NAME = 'ImageChannelSplit' + + @classmethod + def INPUT_TYPES(self): + channel_mode = ['RGBA', 'YCbCr', 'LAB', 'HSV'] + return { + "required": { + "image": ("IMAGE", ), # + "mode": (channel_mode,), # 通道设置 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE",) + RETURN_NAMES = ("channel_1", "channel_2", "channel_3", "channel_4",) + FUNCTION = 'image_channel_split' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_channel_split(self, image, mode): + + c1_images = [] + c2_images = [] + c3_images = [] + c4_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i).convert('RGBA') + channel1, channel2, channel3, channel4 = image_channel_split(_image, mode) + c1_images.append(pil2tensor(channel1)) + c2_images.append(pil2tensor(channel2)) + c3_images.append(pil2tensor(channel3)) + c4_images.append(pil2tensor(channel4)) + + log(f"{self.NODE_NAME} Processed {len(c1_images)} image(s).", message_type='finish') + return (torch.cat(c1_images, dim=0), torch.cat(c2_images, dim=0), torch.cat(c3_images, dim=0), torch.cat(c4_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageChannelSplit": ImageChannelSplit +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageChannelSplit": "LayerUtility: ImageChannelSplit" +} \ No newline at end of file diff --git a/reference_code/image_combine_alpha.py b/reference_code/image_combine_alpha.py new file mode 100644 index 0000000..0de6f6d --- /dev/null +++ b/reference_code/image_combine_alpha.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_hub.py b/reference_code/image_hub.py new file mode 100644 index 0000000..3270041 --- /dev/null +++ b/reference_code/image_hub.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_mask_scale_as.py b/reference_code/image_mask_scale_as.py new file mode 100644 index 0000000..7739a12 --- /dev/null +++ b/reference_code/image_mask_scale_as.py @@ -0,0 +1,189 @@ +import torch +from PIL import Image +from .imagefunc import AnyType, log, tensor2pil, pil2tensor, image2mask, fit_resize_image + + + +any = AnyType("*") + +class ImageMaskScaleAs: + + def __init__(self): + self.NODE_NAME = 'ImageMaskScaleAs' + + @classmethod + def INPUT_TYPES(self): + + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + + return { + "required": { + "scale_as": (any, {}), + "fit": (fit_mode,), + "method": (method_mode,), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "widht", "height",) + FUNCTION = 'image_mask_scale_as' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_mask_scale_as(self, scale_as, fit, method, + image=None, mask = None, + ): + if scale_as.shape[0] > 0: + _asimage = tensor2pil(scale_as[0]) + else: + _asimage = tensor2pil(scale_as) + target_width, target_height = _asimage.size + _mask = Image.new('L', size=_asimage.size, color='black') + _image = Image.new('RGB', size=_asimage.size, color='black') + orig_width = 4 + orig_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + ret_images = [] + ret_masks = [] + + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i).convert('RGB') + orig_width, orig_height = _image.size + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler) + ret_images.append(pil2tensor(_image)) + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + _mask = tensor2pil(m).convert('L') + orig_width, orig_height = _mask.size + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) >0: + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height],target_width, target_height,) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height],target_width, target_height,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + else: + log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.", message_type='error') + return (None, None, [orig_width, orig_height], 0, 0,) + + +class LS_ImageMaskScaleAsV2: + + def __init__(self): + self.NODE_NAME = 'ImageMaskScaleAsV2' + + @classmethod + def INPUT_TYPES(self): + + fit_mode = ['letterbox', 'crop', 'fill'] + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + + return { + "required": { + "scale_as": (any, {}), + "fit": (fit_mode,), + "method": (method_mode,), + "background_color": ("STRING", {"default": "#FFFFFF"},), + }, + "optional": { + "image": ("IMAGE",), # + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "widht", "height",) + FUNCTION = 'image_mask_scale_as_v2' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_mask_scale_as_v2(self, scale_as, fit, method, background_color, + image=None, mask=None, + ): + if scale_as.shape[0] > 0: + _asimage = tensor2pil(scale_as[0]) + else: + _asimage = tensor2pil(scale_as) + target_width, target_height = _asimage.size + _mask = Image.new('L', size=_asimage.size, color='black') + _image = Image.new('RGB', size=_asimage.size, color=background_color) + orig_width = 4 + orig_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + ret_images = [] + ret_masks = [] + + if image is not None: + for i in image: + i = torch.unsqueeze(i, 0) + _image = tensor2pil(i).convert('RGB') + orig_width, orig_height = _image.size + _image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler, background_color=background_color) + ret_images.append(pil2tensor(_image)) + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + for m in mask: + m = torch.unsqueeze(m, 0) + _mask = tensor2pil(m).convert('L') + orig_width, orig_height = _mask.size + _mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler, background_color=background_color).convert('L') + ret_masks.append(image2mask(_mask)) + if len(ret_images) > 0 and len(ret_masks) > 0: + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, + target_height,) + elif len(ret_images) > 0 and len(ret_masks) == 0: + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,) + elif len(ret_images) == 0 and len(ret_masks) > 0: + log(f"{self.NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish') + return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + else: + log(f"Error: {self.NODE_NAME} skipped, because the available image or mask is not found.", + message_type='error') + return (None, None, [orig_width, orig_height], 0, 0,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageMaskScaleAs": ImageMaskScaleAs, + "LayerUtility: ImageMaskScaleAsV2": LS_ImageMaskScaleAsV2, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageMaskScaleAs": "LayerUtility: Image Mask Scale As", + "LayerUtility: ImageMaskScaleAsV2": "LayerUtility: Image Mask Scale As V2", +} \ No newline at end of file diff --git a/reference_code/image_opacity.py b/reference_code/image_opacity.py new file mode 100644 index 0000000..8a40886 --- /dev/null +++ b/reference_code/image_opacity.py @@ -0,0 +1,87 @@ +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, image2mask + + + +class ImageOpacity: + + def __init__(self): + self.NODE_NAME = 'ImageOpacity' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + }, + "optional": { + "mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = 'image_opacity' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_opacity(self, image, opacity, invert_mask, + mask=None, + ): + + ret_images = [] + ret_masks = [] + l_images = [] + l_masks = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + else: + l_masks.append(Image.new('L', size=m.size, color='white')) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + for i in range(max_batch): + _image = l_images[i] if i < len(l_images) else l_images[-1] + _image = tensor2pil(_image) + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + if invert_mask: + _color = Image.new("L", _image.size, color=('white')) + _mask = ImageChops.invert(_mask) + else: + _color = Image.new("L", _image.size, color=('black')) + + alpha = 1 - opacity / 100.0 + ret_mask = Image.blend(_mask, _color, alpha) + R, G, B, = _image.convert('RGB').split() + if invert_mask: + ret_mask = ImageChops.invert(ret_mask) + ret_image = Image.merge('RGBA', (R, G, B, ret_mask)) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageOpacity": ImageOpacity +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageOpacity": "LayerUtility: ImageOpacity" +} \ No newline at end of file diff --git a/reference_code/image_reel.py b/reference_code/image_reel.py new file mode 100644 index 0000000..936b46d --- /dev/null +++ b/reference_code/image_reel.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_remove_alpha.py b/reference_code/image_remove_alpha.py new file mode 100644 index 0000000..d85b6f1 --- /dev/null +++ b/reference_code/image_remove_alpha.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_scale_by_aspect_ratio.py b/reference_code/image_scale_by_aspect_ratio.py new file mode 100644 index 0000000..bbb83e6 --- /dev/null +++ b/reference_code/image_scale_by_aspect_ratio.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_scale_by_aspect_ratio_v2.py b/reference_code/image_scale_by_aspect_ratio_v2.py new file mode 100644 index 0000000..c6e573c --- /dev/null +++ b/reference_code/image_scale_by_aspect_ratio_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_scale_restore.py b/reference_code/image_scale_restore.py new file mode 100644 index 0000000..12f15e2 --- /dev/null +++ b/reference_code/image_scale_restore.py @@ -0,0 +1,112 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask + + + +class ImageScaleRestore: + + def __init__(self): + self.NODE_NAME = 'ImageScaleRestore' + + @classmethod + def INPUT_TYPES(self): + method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest'] + return { + "required": { + "image": ("IMAGE", ), # + "scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}), + "method": (method_mode,), + "scale_by_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放 + "longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}), + }, + "optional": { + "mask": ("MASK",), # + "original_size": ("BOX",), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "BOX", "INT", "INT") + RETURN_NAMES = ("image", "mask", "original_size", "width", "height",) + FUNCTION = 'image_scale_restore' + CATEGORY = '😺dzNodes/LayerUtility' + + def image_scale_restore(self, image, scale, method, + scale_by_longest_side, longest_side, + mask = None, original_size = None + ): + + l_images = [] + l_masks = [] + ret_images = [] + ret_masks = [] + for l in image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + + if mask is not None: + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + l_masks = [] + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + max_batch = max(len(l_images), len(l_masks)) + + orig_width, orig_height = tensor2pil(l_images[0]).size + if original_size is not None: + target_width = original_size[0] + target_height = original_size[1] + else: + target_width = int(orig_width * scale) + target_height = int(orig_height * scale) + if scale_by_longest_side: + if orig_width > orig_height: + target_width = longest_side + target_height = int(target_width * orig_height / orig_width) + else: + target_height = longest_side + target_width = int(target_height * orig_width / orig_height) + if target_width < 4: + target_width = 4 + if target_height < 4: + target_height = 4 + resize_sampler = Image.LANCZOS + if method == "bicubic": + resize_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + elif method == "bilinear": + resize_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + elif method == "nearest": + resize_sampler = Image.NEAREST + + for i in range(max_batch): + + _image = l_images[i] if i < len(l_images) else l_images[-1] + + _canvas = tensor2pil(_image).convert('RGB') + ret_image = _canvas.resize((target_width, target_height), resize_sampler) + ret_mask = Image.new('L', size=ret_image.size, color='white') + if mask is not None: + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + ret_mask = _mask.resize((target_width, target_height), resize_sampler) + + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(ret_mask)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,) + + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageScaleRestore": ImageScaleRestore +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageScaleRestore": "LayerUtility: ImageScaleRestore" +} \ No newline at end of file diff --git a/reference_code/image_scale_restore_v2.py b/reference_code/image_scale_restore_v2.py new file mode 100644 index 0000000..3007676 --- /dev/null +++ b/reference_code/image_scale_restore_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_shift.py b/reference_code/image_shift.py new file mode 100644 index 0000000..f91d56f --- /dev/null +++ b/reference_code/image_shift.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/image_tagger_save.py b/reference_code/image_tagger_save.py new file mode 100644 index 0000000..9dc563e --- /dev/null +++ b/reference_code/image_tagger_save.py @@ -0,0 +1,142 @@ +import os.path +import shutil +from PIL import Image +from PIL.PngImagePlugin import PngInfo +import datetime +import torch +import numpy as np +import folder_paths +from .imagefunc import log, generate_random_name, remove_empty_lines + + + +class LSImageTaggerSave: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.compress_level = 4 + self.NODE_NAME = 'ImageTaggerSave' + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"image": ("IMAGE", ), + "tag_text": ("STRING", {"default": "", "forceInput":True}), + "custom_path": ("STRING", {"default": ""}), + "filename_prefix": ("STRING", {"default": "comfyui"}), + "timestamp": (["None", "second", "millisecond"],), + "format": (["png", "jpg"],), + "quality": ("INT", {"default": 80, "min": 10, "max": 100, "step": 1}), + "preview": ("BOOLEAN", {"default": True}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "image_tagger_save" + OUTPUT_NODE = True + CATEGORY = '😺dzNodes/LayerUtility/SystemIO' + + def image_tagger_save(self, image, tag_text, custom_path, filename_prefix, timestamp, format, quality, + preview, + prompt=None, extra_pnginfo=None): + + now = datetime.datetime.now() + custom_path = custom_path.replace("%date", now.strftime("%Y-%m-%d")) + custom_path = custom_path.replace("%time", now.strftime("%H-%M-%S")) + filename_prefix = filename_prefix.replace("%date", now.strftime("%Y-%m-%d")) + filename_prefix = filename_prefix.replace("%time", now.strftime("%H-%M-%S")) + filename_prefix += self.prefix_append + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, image[0].shape[1], image[0].shape[0]) + results = list() + temp_sub_dir = generate_random_name('_savepreview_', '_temp', 16) + temp_dir = os.path.join(folder_paths.get_temp_directory(), temp_sub_dir) + metadata = None + i = 255. * image[0].cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + + if timestamp == "millisecond": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}' + elif timestamp == "second": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}' + else: + file = f'{filename}_{counter:08}' + + preview_filename = "" + if custom_path != "": + if not os.path.exists(custom_path): + try: + os.makedirs(custom_path) + except Exception as e: + log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary folder.", + message_type='warning') + raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}") + else: + custom_path = folder_paths.get_output_directory() + + full_output_folder = os.path.normpath(custom_path) + # save preview image to temp_dir + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir) + try: + os.makedirs(temp_dir) + except Exception as e: + print(e) + log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary folder.", + message_type='warning') + try: + preview_filename = os.path.join(generate_random_name('saveimage_preview_', '_temp', 16) + '.png') + img.save(os.path.join(temp_dir, preview_filename)) + except Exception as e: + print(e) + log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary file.", message_type='warning') + + # check if file exists, change filename + while os.path.isfile(os.path.join(full_output_folder, f"{file}.{format}")): + counter += 1 + if timestamp == "millisecond": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}_{counter:08}' + elif timestamp == "second": + file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}_{counter:08}' + else: + file = f"{filename}_{counter:08}" + + image_file_name = os.path.join(full_output_folder, f"{file}.{format}") + tag_file_name = os.path.join(full_output_folder, f"{file}.txt") + + if format == "png": + img.save(image_file_name, pnginfo=metadata, compress_level= (100 - quality) // 10) + else: + if img.mode == "RGBA": + img = img.convert("RGB") + img.save(image_file_name, quality=quality) + with open(tag_file_name, "w", encoding="utf-8") as f: + f.write(remove_empty_lines(tag_text)) + log(f"{self.NODE_NAME} -> Saving image to {image_file_name}") + + if preview: + if custom_path == "": + results.append({ + "filename": f"{file}.{format}", + "subfolder": subfolder, + "type": self.type + }) + else: + results.append({ + "filename": preview_filename, + "subfolder": temp_sub_dir, + "type": "temp" + }) + + counter += 1 + + return { "ui": { "images": results } } + +NODE_CLASS_MAPPINGS = { + "LayerUtility: ImageTaggerSave": LSImageTaggerSave +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save" +} \ No newline at end of file diff --git a/reference_code/image_to_mask.py b/reference_code/image_to_mask.py new file mode 100644 index 0000000..5264b0d --- /dev/null +++ b/reference_code/image_to_mask.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/imagefunc.py b/reference_code/imagefunc.py new file mode 100644 index 0000000..9790341 --- /dev/null +++ b/reference_code/imagefunc.py @@ -0,0 +1,2553 @@ +"""Image process functions for ComfyUI nodes +by chflame https://github.com/chflame163 + +@author: chflame +@title: LayerStyle +@nickname: LayerStyle +@description: A set of nodes for ComfyUI that can composite layer and mask to achieve Photoshop like functionality. +""" + +import os +import sys +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +import pickle +import copy +import re +import json +import math +import glob +import numpy as np +import torch +import scipy.ndimage +import cv2 +import random +import time +from pathlib import Path +from tqdm import tqdm +from functools import lru_cache +from typing import Union, List +from PIL import Image, ImageFilter, ImageChops, ImageDraw, ImageOps, ImageEnhance, ImageFont +from skimage import img_as_float, img_as_ubyte +import torchvision.transforms.functional as TF +import torch.nn.functional as F +from transformers import AutoModel, AutoProcessor, StoppingCriteria, StoppingCriteriaList, AutoModelForCausalLM, AutoTokenizer +from colorsys import rgb_to_hsv +import folder_paths +import comfy.model_management +from .blendmodes import * + +def log(message:str, message_type:str='info'): + name = 'LayerStyle' + + if message_type == 'error': + message = '\033[1;41m' + message + '\033[m' + elif message_type == 'warning': + message = '\033[1;31m' + message + '\033[m' + elif message_type == 'finish': + message = '\033[1;32m' + message + '\033[m' + else: + message = '\033[1;33m' + message + '\033[m' + print(f"# 😺dzNodes: {name} -> {message}") + +try: + from cv2.ximgproc import guidedFilter +except ImportError as e: + # print(e) + log(f"Cannot import name 'guidedFilter' from 'cv2.ximgproc'" + f"\nA few nodes cannot works properly, while most nodes are not affected. Please REINSTALL package 'opencv-contrib-python'." + f"\nFor detail refer to \033[4mhttps://github.com/chflame163/ComfyUI_LayerStyle/issues/5\033[0m") + + + +'''warpper''' + +# create a wrapper function that can apply a function to multiple images in a batch while passing all other arguments to the function +def apply_to_batch(func): + def wrapper(self, image, *args, **kwargs): + images = [] + for img in image: + images.append(func(self, img, *args, **kwargs)) + batch_tensor = torch.cat(images, dim=0) + return (batch_tensor,) + return wrapper + + +'''pickle''' + + +def read_image(filename:str) -> Image: + return Image.open(filename) + +def pickle_to_file(obj:object, file_path:str): + with open(file_path, 'wb') as f: + pickle.dump(obj, f) + +def load_pickle(file_name:str) -> object: + with open(file_name, 'rb') as f: + obj = pickle.load(f) + return obj + +def load_light_leak_images() -> list: + file = os.path.join(folder_paths.models_dir, "layerstyle", "light_leak.pkl") + return load_pickle(file) + +def check_and_download_model(model_path, repo_id): + model_path = os.path.join(folder_paths.models_dir, model_path) + + if not os.path.exists(model_path): + print(f"Downloading {repo_id} model...") + from huggingface_hub import snapshot_download + snapshot_download(repo_id=repo_id, local_dir=model_path, ignore_patterns=["*.md", "*.txt", "onnx", ".git"]) + return model_path + +'''Converter''' + +def cv22ski(cv2_image:np.ndarray) -> np.array: + return img_as_float(cv2_image) + +def ski2cv2(ski:np.array) -> np.ndarray: + return img_as_ubyte(ski) + +def cv22pil(cv2_img:np.ndarray) -> Image: + cv2_img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB) + return Image.fromarray(cv2_img) + +def pil2cv2(pil_img:Image) -> np.array: + np_img_array = np.asarray(pil_img) + return cv2.cvtColor(np_img_array, cv2.COLOR_RGB2BGR) + +def pil2tensor(image:Image) -> torch.Tensor: + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + +def np2pil(np_image:np.ndarray) -> Image: + return Image.fromarray(np_image) + +def pil2np(pil_image:Image) -> np.array: + return np.ndarray(pil_image) + +def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor: + if isinstance(img_np, list): + return torch.cat([np2tensor(img) for img in img_np], dim=0) + return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0) + +def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]: + if len(tensor.shape) == 3: # Single image + return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) + else: # Batch of images + return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] + +def tensor2pil(t_image: torch.Tensor) -> Image: + return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +def tensor2cv2(image:torch.Tensor) -> np.array: + if image.dim() == 4: + image = image.squeeze() + npimage = image.numpy() + cv2image = np.uint8(npimage * 255 / npimage.max()) + return cv2.cvtColor(cv2image, cv2.COLOR_RGB2BGR) + +def image2mask(image:Image) -> torch.Tensor: + if image.mode == 'L': + return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) + else: + image = image.convert('RGB').split()[0] + return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) + +def mask2image(mask:torch.Tensor) -> Image: + masks = tensor2np(mask) + for m in masks: + _mask = Image.fromarray(m).convert("L") + _image = Image.new("RGBA", _mask.size, color='white') + _image = Image.composite( + _image, Image.new("RGBA", _mask.size, color='black'), _mask) + return _image + +'''Image Functions''' + +# 颜色加深 +def blend_color_burn(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = 1 - (1 - img_2) / (img_1 + 0.001) + mask_1 = img < 0 + mask_2 = img > 1 + img = img * (1 - mask_1) + img = img * (1 - mask_2) + mask_2 + return cv22pil(ski2cv2(img)) + +# 颜色减淡 +def blend_color_dodge(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_2 / (1.0 - img_1 + 0.001) + mask_2 = img > 1 + img = img * (1 - mask_2) + mask_2 + return cv22pil(ski2cv2(img)) + +# 线性加深 +def blend_linear_burn(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_1 + img_2 - 1 + mask_1 = img < 0 + img = img * (1 - mask_1) + return cv22pil(ski2cv2(img)) + +# 线性减淡 +def blend_linear_dodge(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_1 + img_2 + mask_2 = img > 1 + img = img * (1 - mask_2) + mask_2 + return cv22pil(ski2cv2(img)) + +# 变亮 +def blend_lighten(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_1 - img_2 + mask = img > 0 + img = img_1 * mask + img_2 * (1 - mask) + return cv22pil(ski2cv2(img)) + +# 变暗 +def blend_dark(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_1 - img_2 + mask = img < 0 + img = img_1 * mask + img_2 * (1 - mask) + return cv22pil(ski2cv2(img)) + +# 滤色 +def blend_screen(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = 1 - (1 - img_1) * (1 - img_2) + return cv22pil(ski2cv2(img)) + +# 叠加 +def blend_overlay(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + mask = img_2 < 0.5 + img = 2 * img_1 * img_2 * mask + (1 - mask) * (1 - 2 * (1 - img_1) * (1 - img_2)) + return cv22pil(ski2cv2(img)) + +# 柔光 +def blend_soft_light(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + mask = img_1 < 0.5 + T1 = (2 * img_1 - 1) * (img_2 - img_2 * img_2) + img_2 + T2 = (2 * img_1 - 1) * (np.sqrt(img_2) - img_2) + img_2 + img = T1 * mask + T2 * (1 - mask) + return cv22pil(ski2cv2(img)) + +# 强光 +def blend_hard_light(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + mask = img_1 < 0.5 + T1 = 2 * img_1 * img_2 + T2 = 1 - 2 * (1 - img_1) * (1 - img_2) + img = T1 * mask + T2 * (1 - mask) + return cv22pil(ski2cv2(img)) + +# 亮光 +def blend_vivid_light(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + mask = img_1 < 0.5 + T1 = 1 - (1 - img_2) / (2 * img_1 + 0.001) + T2 = img_2 / (2 * (1 - img_1) + 0.001) + mask_1 = T1 < 0 + mask_2 = T2 > 1 + T1 = T1 * (1 - mask_1) + T2 = T2 * (1 - mask_2) + mask_2 + img = T1 * mask + T2 * (1 - mask) + return cv22pil(ski2cv2(img)) + +# 点光 +def blend_pin_light(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + mask_1 = img_2 < (img_1 * 2 - 1) + mask_2 = img_2 > 2 * img_1 + T1 = 2 * img_1 - 1 + T2 = img_2 + T3 = 2 * img_1 + img = T1 * mask_1 + T2 * (1 - mask_1) * (1 - mask_2) + T3 * mask_2 + return cv22pil(ski2cv2(img)) + +# 线性光 +def blend_linear_light(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_2 + img_1 * 2 - 1 + mask_1 = img < 0 + mask_2 = img > 1 + img = img * (1 - mask_1) + img = img * (1 - mask_2) + mask_2 + return cv22pil(ski2cv2(img)) + +def blend_hard_mix(background_image:Image, layer_image:Image) -> Image: + img_1 = cv22ski(pil2cv2(background_image)) + img_2 = cv22ski(pil2cv2(layer_image)) + img = img_1 + img_2 + mask = img_1 + img_2 > 1 + img = img * (1 - mask) + mask + img = img * mask + return cv22pil(ski2cv2(img)) + +def shift_image(image:Image, distance_x:int, distance_y:int, background_color:str='#000000', cyclic:bool=False) -> Image: + width = image.width + height = image.height + ret_image = Image.new('RGB', size=(width, height), color=background_color) + for x in range(width): + for y in range(height): + if cyclic: + orig_x = x + distance_x + if orig_x > width-1 or orig_x < 0: + orig_x = abs(orig_x % width) + orig_y = y + distance_y + if orig_y > height-1 or orig_y < 0: + orig_y = abs(orig_y % height) + + pixel = image.getpixel((orig_x, orig_y)) + ret_image.putpixel((x, y), pixel) + else: + if x > -distance_x and y > -distance_y: # 防止回转 + if x + distance_x < width and y + distance_y < height: # 防止越界 + pixel = image.getpixel((x + distance_x, y + distance_y)) + ret_image.putpixel((x, y), pixel) + return ret_image + +def chop_image(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image: + ret_image = background_image + if blend_mode == 'normal': + ret_image = copy.deepcopy(layer_image) + if blend_mode == 'multply': + ret_image = ImageChops.multiply(background_image,layer_image) + if blend_mode == 'screen': + ret_image = ImageChops.screen(background_image, layer_image) + if blend_mode == 'add': + ret_image = ImageChops.add(background_image, layer_image, 1, 0) + if blend_mode == 'subtract': + ret_image = ImageChops.subtract(background_image, layer_image, 1, 0) + if blend_mode == 'difference': + ret_image = ImageChops.difference(background_image, layer_image) + if blend_mode == 'darker': + ret_image = ImageChops.darker(background_image, layer_image) + if blend_mode == 'lighter': + ret_image = ImageChops.lighter(background_image, layer_image) + if blend_mode == 'color_burn': + ret_image = blend_color_burn(background_image, layer_image) + if blend_mode == 'color_dodge': + ret_image = blend_color_dodge(background_image, layer_image) + if blend_mode == 'linear_burn': + ret_image = blend_linear_burn(background_image, layer_image) + if blend_mode == 'linear_dodge': + ret_image = blend_linear_dodge(background_image, layer_image) + if blend_mode == 'overlay': + ret_image = blend_overlay(background_image, layer_image) + if blend_mode == 'soft_light': + ret_image = blend_soft_light(background_image, layer_image) + if blend_mode == 'hard_light': + ret_image = blend_hard_light(background_image, layer_image) + if blend_mode == 'vivid_light': + ret_image = blend_vivid_light(background_image, layer_image) + if blend_mode == 'pin_light': + ret_image = blend_pin_light(background_image, layer_image) + if blend_mode == 'linear_light': + ret_image = blend_linear_light(background_image, layer_image) + if blend_mode == 'hard_mix': + ret_image = blend_hard_mix(background_image, layer_image) + # opacity + if opacity == 0: + ret_image = background_image + elif opacity < 100: + alpha = 1.0 - float(opacity) / 100 + ret_image = Image.blend(ret_image, background_image, alpha) + return ret_image + +def chop_image_v2(background_image:Image, layer_image:Image, blend_mode:str, opacity:int) -> Image: + + backdrop_prepped = np.asarray(background_image.convert('RGBA'), dtype=float) + source_prepped = np.asarray(layer_image.convert('RGBA'), dtype=float) + blended_np = BLEND_MODES[blend_mode](backdrop_prepped, source_prepped, opacity / 100) + + return Image.fromarray(np.uint8(blended_np)).convert('RGB') + +def remove_background(image:Image, mask:Image, color:str) -> Image: + width = image.width + height = image.height + ret_image = Image.new('RGB', size=(width, height), color=color) + ret_image.paste(image, mask=mask) + return ret_image + +def sharpen(image:Image) -> Image: + img = pil2cv2(image) + Laplace_kernel = np.array([[-1, -1, -1], + [-1, 9, -1], + [-1, -1, -1]], dtype=np.float32) + ret_image = cv2.filter2D(img, -1, Laplace_kernel) + return cv22pil(ret_image) + +def gaussian_blur(image:Image, radius:int) -> Image: + # image = image.convert("RGBA") + ret_image = image.filter(ImageFilter.GaussianBlur(radius=radius)) + return ret_image + +def motion_blur(image:Image, angle:int, blur:int) -> Image: + angle += 45 + blur *= 5 + image = np.array(pil2cv2(image)) + M = cv2.getRotationMatrix2D((blur / 2, blur / 2), angle, 1) + motion_blur_kernel = np.diag(np.ones(blur)) + motion_blur_kernel = cv2.warpAffine(motion_blur_kernel, M, (blur, blur)) + motion_blur_kernel = motion_blur_kernel / blur + blurred = cv2.filter2D(image, -1, motion_blur_kernel) + # convert to uint8 + cv2.normalize(blurred, blurred, 0, 255, cv2.NORM_MINMAX) + blurred = np.array(blurred, dtype=np.uint8) + ret_image = cv22pil(blurred) + return ret_image + +def __apply_vignette(image, vignette): + # If image needs to be normalized (0-1 range) + needs_normalization = image.max() > 1 + if needs_normalization: + image = image.astype(np.float32) / 255 + final_image = np.clip(image * vignette[..., np.newaxis], 0, 1) + if needs_normalization: + final_image = (final_image * 255).astype(np.uint8) + return final_image +def vignette_image(image:Image, intensity: float, center_x: float, center_y: float) -> Image: + image = pil2tensor(image) + _, height, width, _ = image.shape + # Generate the vignette for each image in the batch + # Create linear space but centered around the provided center point ratios + x = np.linspace(-1, 1, width) + y = np.linspace(-1, 1, height) + X, Y = np.meshgrid(x - (2 * center_x - 1), y - (2 * center_y - 1)) + # Calculate distances to the furthest corner + distances_to_corners = [ + np.sqrt((0 - center_x) ** 2 + (0 - center_y) ** 2), + np.sqrt((1 - center_x) ** 2 + (0 - center_y) ** 2), + np.sqrt((0 - center_x) ** 2 + (1 - center_y) ** 2), + np.sqrt((1 - center_x) ** 2 + (1 - center_y) ** 2) + ] + max_distance_to_corner = np.max(distances_to_corners) + radius = np.sqrt(X ** 2 + Y ** 2) + radius = radius / (max_distance_to_corner * np.sqrt(2)) # Normalize radius + opacity = np.clip(intensity, 0, 1) + vignette = 1 - radius * opacity + tensor_image = image.numpy() + # Apply vignette + vignette_image = __apply_vignette(tensor_image, vignette) + return tensor2pil(torch.from_numpy(vignette_image).unsqueeze(0)) + +def RGB2YCbCr(t): + YCbCr = t.detach().clone() + YCbCr[:,:,:,0] = 0.2123 * t[:,:,:,0] + 0.7152 * t[:,:,:,1] + 0.0722 * t[:,:,:,2] + YCbCr[:,:,:,1] = 0 - 0.1146 * t[:,:,:,0] - 0.3854 * t[:,:,:,1] + 0.5 * t[:,:,:,2] + YCbCr[:,:,:,2] = 0.5 * t[:,:,:,0] - 0.4542 * t[:,:,:,1] - 0.0458 * t[:,:,:,2] + return YCbCr + +def YCbCr2RGB(t): + RGB = t.detach().clone() + RGB[:,:,:,0] = t[:,:,:,0] + 1.5748 * t[:,:,:,2] + RGB[:,:,:,1] = t[:,:,:,0] - 0.1873 * t[:,:,:,1] - 0.4681 * t[:,:,:,2] + RGB[:,:,:,2] = t[:,:,:,0] + 1.8556 * t[:,:,:,1] + return RGB + +# gaussian blur a tensor image batch in format [B x H x W x C] on H/W (spatial, per-image, per-channel) +def cv_blur_tensor(images, dx, dy): + if min(dx, dy) > 100: + np_img = torch.nn.functional.interpolate(images.detach().clone().movedim(-1,1), scale_factor=0.1, mode='bilinear').movedim(1,-1).cpu().numpy() + for index, image in enumerate(np_img): + np_img[index] = cv2.GaussianBlur(image, (dx // 20 * 2 + 1, dy // 20 * 2 + 1), 0) + return torch.nn.functional.interpolate(torch.from_numpy(np_img).movedim(-1,1), size=(images.shape[1], images.shape[2]), mode='bilinear').movedim(1,-1) + else: + np_img = images.detach().clone().cpu().numpy() + for index, image in enumerate(np_img): + np_img[index] = cv2.GaussianBlur(image, (dx, dy), 0) + return torch.from_numpy(np_img) + +def image_add_grain(image:Image, scale:float=0.5, strength:float=0.5, saturation:float=0.7, toe:float=0.0, seed:int=0) -> Image: + + image = pil2tensor(image.convert("RGB")) + t = image.detach().clone() + torch.manual_seed(seed) + grain = torch.rand(t.shape[0], int(t.shape[1] // scale), int(t.shape[2] // scale), 3) + + YCbCr = RGB2YCbCr(grain) + YCbCr[:, :, :, 0] = cv_blur_tensor(YCbCr[:, :, :, 0], 3, 3) + YCbCr[:, :, :, 1] = cv_blur_tensor(YCbCr[:, :, :, 1], 15, 15) + YCbCr[:, :, :, 2] = cv_blur_tensor(YCbCr[:, :, :, 2], 11, 11) + + grain = (YCbCr2RGB(YCbCr) - 0.5) * strength + grain[:, :, :, 0] *= 2 + grain[:, :, :, 2] *= 3 + grain += 1 + grain = grain * saturation + grain[:, :, :, 1].unsqueeze(3).repeat(1, 1, 1, 3) * (1 - saturation) + + grain = torch.nn.functional.interpolate(grain.movedim(-1, 1), size=(t.shape[1], t.shape[2]), + mode='bilinear').movedim(1, -1) + t[:, :, :, :3] = torch.clip((1 - (1 - t[:, :, :, :3]) * grain) * (1 - toe) + toe, 0, 1) + return tensor2pil(t) + +def filmgrain_image(image:Image, scale:float, grain_power:float, + shadows:float, highs:float, grain_sat:float, + sharpen:int=1, grain_type:int=4, src_gamma:float=1.0, + gray_scale:bool=False, seed:int=0) -> Image: + # image = pil2tensor(image) + # grain_type, 1=fine, 2=fine simple, 3=coarse, 4=coarser + grain_type_index = 3 + + # Apply grain + from .filmgrainer import filmgrainer as fg + grain_image = fg.process(image, scale=scale, src_gamma=src_gamma, grain_power=grain_power, + shadows=shadows, highs=highs, grain_type=grain_type_index, + grain_sat=grain_sat, gray_scale=gray_scale, sharpen=sharpen, seed=seed) + return tensor2pil(torch.from_numpy(grain_image).unsqueeze(0)) + +def __apply_radialblur(image, blur_strength, radial_mask, focus_spread, steps): + from .filmgrainer import processing as processing_utils + needs_normalization = image.max() > 1 + if needs_normalization: + image = image.astype(np.float32) / 255 + blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread) + final_image = processing_utils.apply_blurred_images(image, blurred_images, radial_mask) + if needs_normalization: + final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8) + return final_image + +def radialblur_image(image:Image, blur_strength:float, center_x:float, center_y:float, focus_spread:float, steps:int=5) -> Image: + width, height = image.size + image = pil2tensor(image) + if image.dim() == 4: + image = image[0] + + # _, height, width, = image.shape + # Generate the vignette for each image in the batch + c_x, c_y = int(width * center_x), int(height * center_y) + # Calculate distances to all corners from the center + distances_to_corners = [ + np.sqrt((c_x - 0)**2 + (c_y - 0)**2), + np.sqrt((c_x - width)**2 + (c_y - 0)**2), + np.sqrt((c_x - 0)**2 + (c_y - height)**2), + np.sqrt((c_x - width)**2 + (c_y - height)**2) + ] + max_distance_to_corner = max(distances_to_corners) + # Create and adjust radial mask + X, Y = np.meshgrid(np.arange(width) - c_x, np.arange(height) - c_y) + radial_mask = np.sqrt(X**2 + Y**2) / max_distance_to_corner + tensor_image = image.numpy() + # Apply blur + blur_image = __apply_radialblur(tensor_image, blur_strength, radial_mask, focus_spread, steps) + return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0)) + +def __apply_depthblur(image, depth_map, blur_strength, focal_depth, focus_spread, steps): + from .filmgrainer import processing as processing_utils + # Normalize the input image if needed + needs_normalization = image.max() > 1 + if needs_normalization: + image = image.astype(np.float32) / 255 + # Normalize the depth map if needed + depth_map = depth_map.astype(np.float32) / 255 if depth_map.max() > 1 else depth_map + # Resize depth map to match the image dimensions + depth_map_resized = cv2.resize(depth_map, (image.shape[1], image.shape[0]), interpolation=cv2.INTER_LINEAR) + if len(depth_map_resized.shape) > 2: + depth_map_resized = cv2.cvtColor(depth_map_resized, cv2.COLOR_BGR2GRAY) + # Adjust the depth map based on the focal plane + depth_mask = np.abs(depth_map_resized - focal_depth) + depth_mask = np.clip(depth_mask / np.max(depth_mask), 0, 1) + # Generate blurred versions of the image + blurred_images = processing_utils.generate_blurred_images(image, blur_strength, steps, focus_spread) + # Use the adjusted depth map as a mask for applying blurred images + final_image = processing_utils.apply_blurred_images(image, blurred_images, depth_mask) + # Convert back to original range if the image was normalized + if needs_normalization: + final_image = np.clip(final_image * 255, 0, 255).astype(np.uint8) + return final_image + +def depthblur_image(image:Image, depth_map:Image, blur_strength:float, focal_depth:float, focus_spread:float, steps:int=5) -> Image: + width, height = image.size + image = pil2tensor(image) + depth_map = pil2tensor(depth_map) + if image.dim() == 4: + image = image[0] + if depth_map.dim() == 4: + depth_map = depth_map[0] + tensor_image = image.numpy() + tensor_image_depth = depth_map.numpy() + # Apply blur + blur_image = __apply_depthblur(tensor_image, tensor_image_depth, blur_strength, focal_depth, focus_spread, steps) + return tensor2pil(torch.from_numpy(blur_image).unsqueeze(0)) + +def fit_resize_image(image:Image, target_width:int, target_height:int, fit:str, resize_sampler:str, background_color:str = '#000000') -> Image: + image = image.convert('RGB') + orig_width, orig_height = image.size + if image is not None: + if fit == 'letterbox': + if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑 + fit_width = target_width + fit_height = int(target_width / orig_width * orig_height) + else: # 更瘦,左右留黑 + fit_height = target_height + fit_width = int(target_height / orig_height * orig_width) + fit_image = image.resize((fit_width, fit_height), resize_sampler) + ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color) + ret_image.paste(fit_image, box=((target_width - fit_width)//2, (target_height - fit_height)//2)) + elif fit == 'crop': + if orig_width / orig_height > target_width / target_height: # 更宽,裁左右 + fit_width = int(orig_height * target_width / target_height) + fit_image = image.crop( + ((orig_width - fit_width)//2, 0, (orig_width - fit_width)//2 + fit_width, orig_height)) + else: # 更瘦,裁上下 + fit_height = int(orig_width * target_height / target_width) + fit_image = image.crop( + (0, (orig_height-fit_height)//2, orig_width, (orig_height-fit_height)//2 + fit_height)) + ret_image = fit_image.resize((target_width, target_height), resize_sampler) + else: + ret_image = image.resize((target_width, target_height), resize_sampler) + return ret_image + +def __rotate_expand(image:Image, angle:float, SSAA:int=0, method:str="lanczos") -> Image: + images = pil2tensor(image) + expand = "true" + height, width = images[0, :, :, 0].shape + + def rotate_tensor(tensor): + resize_sampler = Image.LANCZOS + rotate_sampler = Image.BICUBIC + if method == "bicubic": + resize_sampler = Image.BICUBIC + rotate_sampler = Image.BICUBIC + elif method == "hamming": + resize_sampler = Image.HAMMING + rotate_sampler = Image.BILINEAR + elif method == "bilinear": + resize_sampler = Image.BILINEAR + rotate_sampler = Image.BILINEAR + elif method == "box": + resize_sampler = Image.BOX + rotate_sampler = Image.NEAREST + elif method == "nearest": + resize_sampler = Image.NEAREST + rotate_sampler = Image.NEAREST + img = tensor2pil(tensor) + if SSAA > 1: + img_us_scaled = img.resize((width * SSAA, height * SSAA), resize_sampler) + img_rotated = img_us_scaled.rotate(angle, rotate_sampler, expand == "true", fillcolor=(0, 0, 0, 0)) + img_down_scaled = img_rotated.resize((img_rotated.width // SSAA, img_rotated.height // SSAA), resize_sampler) + result = pil2tensor(img_down_scaled) + else: + img_rotated = img.rotate(angle, rotate_sampler, expand == "true", fillcolor=(0, 0, 0, 0)) + result = pil2tensor(img_rotated) + return result + + if angle == 0.0 or angle == 360.0: + return tensor2pil(images) + else: + rotated_tensor = torch.stack([rotate_tensor(images[i]) for i in range(len(images))]) + return tensor2pil(rotated_tensor).convert('RGB') + +def image_rotate_extend_with_alpha(image:Image, angle:float, alpha:Image=None, method:str="lanczos", SSAA:int=0) -> tuple: + _image = __rotate_expand(image.convert('RGB'), angle, SSAA, method) + if angle is not None: + _alpha = __rotate_expand(alpha.convert('RGB'), angle, SSAA, method) + ret_image = RGB2RGBA(_image, _alpha) + else: + ret_image = _image + return (_image, _alpha.convert('L'), ret_image) + +def create_box_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, scale:int=50) -> Image: + # scale is percent of border to center for the rectangle + if scale > 100: + scale = 100 + elif scale < 1: + scale = 1 + start_color = Hex_to_RGB(start_color_inhex) + end_color = Hex_to_RGB(end_color_inhex) + ret_image = Image.new("RGB", (width, height), start_color) + draw = ImageDraw.Draw(ret_image) + step = int(min(width, height) * scale / 100 / 2) + if step > 0: + for i in range(step): + R = int(start_color[0] * (step - i) / step + end_color[0] * i / step) + G = int(start_color[1] * (step - i) / step + end_color[1] * i / step) + B = int(start_color[2] * (step - i) / step + end_color[2] * i / step) + color = (R, G, B) + draw.rectangle((i, i, width - i, height - i), fill=color) + draw.rectangle((step, step, width - step, height - step), fill=end_color) + return ret_image + +def create_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, direction:str='bottom') -> Image: + # direction = one of top, bottom, left, right + start_color = Hex_to_RGB(start_color_inhex) + end_color = Hex_to_RGB(end_color_inhex) + ret_image = Image.new("RGB", (width, height), start_color) + draw = ImageDraw.Draw(ret_image) + if direction == 'bottom': + for i in range(height): + R = int(start_color[0] * (height - i) / height + end_color[0] * i / height) + G = int(start_color[1] * (height - i) / height + end_color[1] * i / height) + B = int(start_color[2] * (height - i) / height + end_color[2] * i / height) + color = (R, G, B) + draw.line((0, i, width, i), fill=color) + elif direction == 'top': + for i in range(height): + R = int(end_color[0] * (height - i) / height + start_color[0] * i / height) + G = int(end_color[1] * (height - i) / height + start_color[1] * i / height) + B = int(end_color[2] * (height - i) / height + start_color[2] * i / height) + color = (R, G, B) + draw.line((0, i, width, i), fill=color) + elif direction == 'right': + for i in range(width): + R = int(start_color[0] * (width - i) / width + end_color[0] * i / width) + G = int(start_color[1] * (width - i) / width + end_color[1] * i / width) + B = int(start_color[2] * (width - i) / width + end_color[2] * i / width) + color = (R, G, B) + draw.line((i, 0, i, height), fill=color) + elif direction == 'left': + for i in range(width): + R = int(end_color[0] * (width - i) / width + start_color[0] * i / width) + G = int(end_color[1] * (width - i) / width + start_color[1] * i / width) + B = int(end_color[2] * (width - i) / width + start_color[2] * i / width) + color = (R, G, B) + draw.line((i, 0, i, height), fill=color) + else: + log(f'A argument error of imagefunc.create_gradient(), ' + f'"direction=" must one of "top, bottom, left, right".', + message_type='error') + + return ret_image + +def gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, angle:float, ) -> Image: + radius = int((width + height) / 4) + g = create_gradient(start_color_inhex, end_color_inhex, radius, radius) + _canvas = Image.new('RGB', size=(radius, radius*3), color=start_color_inhex) + top = Image.new('RGB', size=(radius, radius), color=start_color_inhex) + bottom = Image.new('RGB', size=(radius, radius),color=end_color_inhex) + _canvas.paste(top, box=(0, 0, radius, radius)) + _canvas.paste(g, box=(0, radius, radius, radius * 2)) + _canvas.paste(bottom,box=(0, radius * 2, radius, radius * 3)) + _canvas = _canvas.resize((radius * 3, radius * 3)) + _canvas = __rotate_expand(_canvas,angle) + center = int(_canvas.width / 2) + _x = int(width / 3) + _y = int(height / 3) + ret_image = _canvas.crop((center - _x, center - _y, center + _x, center + _y)) + ret_image = ret_image.resize((width, height)) + return ret_image + +def draw_rounded_rectangle(image:Image, radius:int, bboxes:list, scale_factor:int=2, color:str="white") -> Image: + """ + 绘制圆角矩形图像。 + image:输入图片 + radius: 半径,100为纯椭圆 + bboxes: (x1,y1,x2,y2)列表 + scale_factor: 放大倍数 + :return: 绘制好的pillow图像 + """ + if scale_factor < 1 : scale_factor = 1 + + img = image.resize((image.width * scale_factor, image.height * scale_factor), Image.LANCZOS) + draw = ImageDraw.Draw(img) + + for (x1, y1, x2, y2) in bboxes: + r = radius * min(x2-x1, y2-y1) * 0.005 + x1, y1, x2, y2 = x1 * scale_factor, y1 * scale_factor, x2 * scale_factor, y2 * scale_factor + # 计算圆角矩形的四个角的圆弧 + draw.rounded_rectangle([x1, y1, x2, y2], radius=r * scale_factor, fill=color) + + img = img.filter(ImageFilter.SMOOTH_MORE) + img = img.resize((image.width, image.height), Image.LANCZOS) + + return img + +def draw_rect(image:Image, x:int, y:int, width:int, height:int, line_color:str, line_width:int, + box_color:str=None) -> Image: + draw = ImageDraw.Draw(image) + draw.rectangle((x, y, x + width, y + height), fill=box_color, outline=line_color, width=line_width, ) + return image + +def draw_border(image:Image, border_width:int, color:str='#FFFFFF') -> Image: + return ImageOps.expand(image, border=border_width, fill=color) + +# 对灰度图像进行直方图均衡化 +def normalize_gray(image:Image) -> Image: + if image.mode != 'L': + image = image.convert('L') + img = np.asarray(image) + balanced_img = img.copy() + hist, bins = np.histogram(img.reshape(-1), 256, (0, 256)) + bmin = np.min(np.where(hist > (hist.sum() * 0.0005))) + bmax = np.max(np.where(hist > (hist.sum() * 0.0005))) + balanced_img = np.clip(img, bmin, bmax) + balanced_img = ((balanced_img - bmin) / (bmax - bmin) * 255) + return Image.fromarray(balanced_img).convert('L') + +def remap_pixel(pixel:int, min_brightness:int, max_brightness:int) -> int: + return int((pixel - min_brightness) / (max_brightness - min_brightness) * 255) +def histogram_range(image:Image, black_point:int, black_range:int, white_point:int, white_range:int) -> Image: + + if image.mode != 'L': + image = image.convert('L') + + if black_point == 255: + black_point = 254 + if white_point == 0: + white_point = 1 + if black_point + black_range > 255: + black_range = 255 - black_point + if white_range > white_point: + white_range = white_point + + white_image = Image.new("L", size=image.size, color="white") + black_image = Image.new("L", size=image.size, color="black") + + if black_point == white_point: + return white_image + + + # draw white part + white_part = black_image + if white_point < 255 or white_range > 0: + for y in (range(image.height)): + for x in range(image.width): + pixel = image.getpixel((x, y)) + if pixel > white_point: # put white + white_part.putpixel((x, y), 255) + elif pixel > white_point - white_range: + pixel = remap_pixel(pixel, white_point - white_range, white_point) + white_part.putpixel((x, y), pixel) + white_part = ImageChops.invert(white_part) + + + # draw black part + black_part = black_image + if black_point > 0 or black_range > 0: + for y in (range(image.height)): + for x in range(image.width): + pixel = image.getpixel((x, y)) + if pixel < black_point: # put black + black_part.putpixel((x, y), 255) + elif pixel < black_point + black_range: + pixel = remap_pixel(pixel, black_point, black_point + black_range) + black_part.putpixel((x, y), 255 - pixel) + black_part = ImageChops.invert(black_part) + + ret_image = chop_image_v2(white_part, black_part, blend_mode='darken', opacity=100) + + return ret_image + +def histogram_equalization(image:Image, mask:Image=None, gamma_strength=0.5) -> Image: + + if image.mode != 'L': + image = image.convert('L') + + if mask is not None: + if mask.mode != 'L': + mask = mask.convert('L') + else: + mask = Image.new('L', size=image.size, color = 'white') + + # calculate Min/Max brightness pixel + min_brightness = 255 + max_brightness = 0 + average_brightness = 0 + total_pixel = 0 + for y in range(image.height): + for x in range(image.width): + if mask.getpixel((x, y)) == 0: + continue + else: + pixel = image.getpixel((x, y)) + if pixel < min_brightness: + min_brightness = pixel + if pixel > max_brightness: + max_brightness = pixel + average_brightness += pixel + total_pixel += 1 + if total_pixel == 0: + log(f"histogram_equalization: mask is not available, return orinianl image.") + return image + average_brightness = int(average_brightness / total_pixel) + + for y in range(image.height): + for x in range(image.width): + pixel = image.getpixel((x, y)) + image.putpixel((x, y), remap_pixel(pixel, min_brightness, max_brightness)) + + image = gamma_trans(image, (average_brightness - 127) / 127 * gamma_strength * 0.66 + 1) + + return image.convert('L') + +def adjust_levels(image:Image, input_black:int=0, input_white:int=255, midtones:float=1.0, + output_black:int=0, output_white:int=255) -> Image: + + if input_black == input_white or output_black == output_white: + return Image.new('RGB', size=image.size, color='gray') + + img = pil2cv2(image).astype(np.float64) + + if input_black > input_white: + input_black, input_white = input_white, input_black + if output_black > output_white: + output_black, output_white = output_white, output_black + + + # input_levels remap + if input_black > 0 or input_white < 255: + img = 255 * ((img - input_black) / (input_white - input_black)) + img[img < 0] = 0 + img[img > 255] = 255 + + # # mid_tone + if midtones != 1.0: + img = 255 * np.power(img / 255, 1.0 / midtones) + + img[img < 0] = 0 + img[img > 255] = 255 + + # output_levels remap + if output_black > 0 or output_white < 255: + img = (img / 255) * (output_white - output_black) + output_black + img[img < 0] = 0 + img[img > 255] = 255 + + img = img.astype(np.uint8) + return cv22pil(img) + +def get_image_color_tone(image:Image, mask:Image=None) -> str: + image = image.convert('RGB') + max_score = 0.0001 + dominant_color = (255, 255, 255) + if mask is not None: + if mask.mode != 'L': + mask = mask.convert('L') + canvas = Image.new('RGB', size=image.size, color='black') + canvas.paste(image, mask=mask) + image = canvas + + all_colors = image.getcolors(image.width * image.height) + for count, (r, g, b) in all_colors: + if mask is not None: + if r + g + b < 2: # 忽略黑色 + continue + saturation = rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)[1] + y = min(abs(r * 2104 + g * 4130 + b * 802 + 4096 + 131072) >> 13,235) + y = (y - 16.0) / (235 - 16) + score = (saturation+0.1)*count + if score > max_score: + max_score = score + dominant_color = (r, g, b) + ret_color = RGB_to_Hex(dominant_color) + return ret_color + +def get_image_color_average(image:Image, mask:Image=None) -> str: + image = image.convert('RGB') + width, height = image.size + total_red = 0 + total_green = 0 + total_blue = 0 + total_pixel =0 + for y in range(height): + for x in range(width): + if mask is not None: + if mask.mode != 'L': + mask = mask.convert('L') + if mask.getpixel((x, y)) <= 127: + continue + rgb = image.getpixel((x, y)) + total_red += rgb[0] + total_green += rgb[1] + total_blue += rgb[2] + total_pixel += 1 + + average_red = total_red // total_pixel + average_green = total_green // total_pixel + average_blue = total_blue // total_pixel + color = (average_red, average_green, average_blue) + ret_color = RGB_to_Hex(color) + return ret_color + +def get_gray_average(image:Image, mask:Image=None) -> int: + # image.mode = 'HSV', mask.mode = 'L' + image = image.convert('HSV') + + if mask is not None: + if mask.mode != 'L': + mask = mask.convert('L') + else: + mask = Image.new('L', size=image.size, color='white') + _, _, _v = image.convert('HSV').split() + _v = np.array(_v) + average_gray = _v[np.array(mask) > 16].mean() + # width, height = image.size + # total_gray = 0 + # valid_pixels = 0 + # for y in range(height): + # for x in range(width): + # if mask is not None: + # if mask.getpixel((x, y)) > 16: #mask亮度低于16的忽略不计 + # gray = _v.getpixel((x, y)) + # total_gray += gray + # valid_pixels += 1 + # else: + # gray = _v.getpixel((x, y)) + # total_gray += gray + # valid_pixels += 1 + # average_gray = total_gray // valid_pixels + return average_gray + +def calculate_shadow_highlight_level(gray:int) -> float: + range = 255 + shadow_exponent = 3 + highlight_exponent = 2 + shadow_ratio = gray ** shadow_exponent / range ** shadow_exponent + highlight_ratio = gray ** highlight_exponent / range ** highlight_exponent + shadow_level = shadow_ratio * 100 + (1 - shadow_ratio) * 32 + highlight_level = highlight_ratio * 100 + (1 - highlight_ratio) * 32 + return shadow_level, highlight_level + +def luminance_keyer(image:Image, low:float=0, high:float=1, gamma:float=1) -> Image: + image = pil2tensor(image) + t = image[:, :, :, :3].detach().clone() + alpha = 0.2126 * t[:, :, :, 0] + 0.7152 * t[:, :, :, 1] + 0.0722 * t[:, :, :, 2] + if low == high: + alpha = (alpha > high).to(t.dtype) + else: + alpha = (alpha - low) / (high - low) + if gamma != 1.0: + alpha = torch.pow(alpha, 1 / gamma) + alpha = torch.clamp(alpha, min=0, max=1).unsqueeze(3).repeat(1, 1, 1, 3) + return tensor2pil(alpha).convert('L') + +def get_image_bright_average(image:Image) -> int: + image = image.convert('L') + width, height = image.size + total_bright = 0 + pixels = 0 + for y in range(height): + for x in range(width): + b = image.getpixel((x, y)) + if b > 1: # 排除死黑 + pixels += 1 + total_bright += b + return int(total_bright / pixels) + +def image_channel_split(image:Image, mode = 'RGBA') -> tuple: + _image = image.convert('RGBA') + channel1 = Image.new('L', size=_image.size, color='black') + channel2 = Image.new('L', size=_image.size, color='black') + channel3 = Image.new('L', size=_image.size, color='black') + channel4 = Image.new('L', size=_image.size, color='black') + if mode == 'RGBA': + channel1, channel2, channel3, channel4 = _image.split() + if mode == 'RGB': + channel1, channel2, channel3 = _image.convert('RGB').split() + if mode == 'YCbCr': + channel1, channel2, channel3 = _image.convert('YCbCr').split() + if mode == 'LAB': + channel1, channel2, channel3 = _image.convert('LAB').split() + if mode == 'HSV': + channel1, channel2, channel3 = _image.convert('HSV').split() + return channel1, channel2, channel3, channel4 + +def image_channel_merge(channels:tuple, mode = 'RGB' ) -> Image: + channel1 = channels[0].convert('L') + channel2 = channels[1].convert('L') + channel3 = channels[2].convert('L') + channel4 = Image.new('L', size=channel1.size, color='white') + if mode == 'RGBA': + if len(channels) > 3: + channel4 = channels[3].convert('L') + ret_image = Image.merge('RGBA',[channel1, channel2, channel3, channel4]) + elif mode == 'RGB': + ret_image = Image.merge('RGB', [channel1, channel2, channel3]) + elif mode == 'YCbCr': + ret_image = Image.merge('YCbCr', [channel1, channel2, channel3]).convert('RGB') + elif mode == 'LAB': + ret_image = Image.merge('LAB', [channel1, channel2, channel3]).convert('RGB') + elif mode == 'HSV': + ret_image = Image.merge('HSV', [channel1, channel2, channel3]).convert('RGB') + return ret_image + +def image_gray_offset(image:Image, offset:int) -> Image: + image = image.convert('L') + image_array = np.array(image, dtype=np.int16) + image_array = np.clip(image_array + offset, 0, 255).astype(np.uint8) + ret_image = Image.fromarray(image_array, mode='L') + return ret_image + +def image_gray_ratio(image:Image, ratio:float) -> Image: + image = image.convert('L') + image_array = np.array(image, dtype=np.float32) + image_array = np.clip(image_array * ratio, 0, 255).astype(np.uint8) + ret_image = Image.fromarray(image_array, mode='L') + return ret_image + +def image_hue_offset(image:Image, offset:int) -> Image: + image = image.convert('L') + image_array = np.array(image, dtype=np.int16) + image_array = (image_array + offset) % 256 + image_array = image_array.astype(np.uint8) + ret_image = Image.fromarray(image_array, mode='L') + + return ret_image + +def gamma_trans(image:Image, gamma:float) -> Image: + cv2_image = pil2cv2(image) + gamma_table = [np.power(x/255.0,gamma)*255.0 for x in range(256)] + gamma_table = np.round(np.array(gamma_table)).astype(np.uint8) + _corrected = cv2.LUT(cv2_image,gamma_table) + return cv22pil(_corrected) + + +def read_LUT_IridasCube_encode_utf8(path: str): + from colour.utilities import as_float_array, as_int_scalar + from colour.io.luts.lut import LUT3x1D, LUT3D + title = re.sub("_|-|\\.", " ", os.path.splitext(os.path.basename(path))[0]) + domain_min, domain_max = np.array([0, 0, 0]), np.array([1, 1, 1]) + dimensions: int = 3 + size: int = 2 + data = [] + comments = [] + + with open(path, encoding='utf-8') as cube_file: + lines = cube_file.readlines() + for line in lines: + + line = line.strip() # noqa: PLW2901 + + if len(line) == 0: + continue + + if line.startswith("#"): + comments.append(line[1:].strip()) + continue + + tokens = line.split() + if tokens[0] == "TITLE": + title = " ".join(tokens[1:])[1:-1] + elif tokens[0] == "DOMAIN_MIN": + domain_min = as_float_array(tokens[1:]) + elif tokens[0] == "DOMAIN_MAX": + domain_max = as_float_array(tokens[1:]) + elif tokens[0] == "LUT_1D_SIZE": + dimensions = 2 + size = as_int_scalar(tokens[1]) + elif tokens[0] == "LUT_3D_SIZE": + dimensions = 3 + size = as_int_scalar(tokens[1]) + else: + data.append(tokens) + + table = as_float_array(data) + + LUT: LUT3x1D | LUT3D + if dimensions == 2: + LUT = LUT3x1D( + table, + title, + np.vstack([domain_min, domain_max]), + comments=comments, + ) + elif dimensions == 3: + # The lines of table data shall be in ascending index order, + # with the first component index (Red) changing most rapidly, + # and the last component index (Blue) changing least rapidly. + table = table.reshape([size, size, size, 3], order="F") + + LUT = LUT3D( + table, + title, + np.vstack([domain_min, domain_max]), + comments=comments, + ) + + return LUT + + +def apply_lut(image:Image, lut_file:str, colorspace:str, strength:int, clip_values:bool=True) -> Image: + """ + Apply a LUT to an image. + :param image: Image to apply the LUT to. + :param lut_file: LUT file to apply. + :param colorspace: Colorspace to convert the image to before applying the LUT. + :param clip_values: Clip the values of the LUT to the domain of the LUT. + :param strength: Strength of the LUT. + :return: Image with the LUT applied. + """ + log_colorspace = False + if colorspace == "log": + log_colorspace = True + + # from colour.io.luts.iridas_cube import read_LUT_IridasCube + + lut = read_LUT_IridasCube_encode_utf8(lut_file) + lut.name = lut_file + + if clip_values: + if lut.domain[0].max() == lut.domain[0].min() and lut.domain[1].max() == lut.domain[1].min(): + lut.table = np.clip(lut.table, lut.domain[0, 0], lut.domain[1, 0]) + else: + if len(lut.table.shape) == 2: # 3x1D + for dim in range(3): + lut.table[:, dim] = np.clip(lut.table[:, dim], lut.domain[0, dim], lut.domain[1, dim]) + else: # 3D + for dim in range(3): + lut.table[:, :, :, dim] = np.clip(lut.table[:, :, :, dim], lut.domain[0, dim], lut.domain[1, dim]) + + img = pil2tensor(image) + lut_img = img.numpy().copy() + is_non_default_domain = not np.array_equal(lut.domain, np.array([[0., 0., 0.], [1., 1., 1.]])) + dom_scale = None + if is_non_default_domain: + dom_scale = lut.domain[1] - lut.domain[0] + lut_img = lut_img * dom_scale + lut.domain[0] + if log_colorspace: + lut_img = lut_img ** (1/2.2) + lut_img = lut.apply(lut_img) + if log_colorspace: + lut_img = lut_img ** (2.2) + if is_non_default_domain: + lut_img = (lut_img - lut.domain[0]) / dom_scale + lut_img = torch.from_numpy(lut_img) + if strength < 100: + strength /= 100 + lut_img = strength * lut_img + (1 - strength) * img + + return tensor2pil(lut_img) + +def color_adapter(image:Image, ref_image:Image) -> Image: + image = pil2cv2(image) + ref_image = pil2cv2(ref_image) + image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) + image_mean, image_std = calculate_mean_std(image) + ref_image = cv2.cvtColor(ref_image, cv2.COLOR_BGR2LAB) + ref_image_mean, ref_image_std = calculate_mean_std(ref_image) + _image = ((image - image_mean) * (ref_image_std / image_std)) + ref_image_mean + np.putmask(_image, _image > 255, values=255) + np.putmask(_image, _image < 0, values=0) + ret_image = cv2.cvtColor(cv2.convertScaleAbs(_image), cv2.COLOR_LAB2BGR) + return cv22pil(ret_image) + +def calculate_mean_std(image:Image): + mean, std = cv2.meanStdDev(image) + mean = np.hstack(np.around(mean, decimals=2)) + std = np.hstack(np.around(std, decimals=2)) + return mean, std + +def image_watercolor(image:Image, level:int=50) -> Image: + img = pil2cv2(image) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + factor = (level / 128.0) ** 2 + sigmaS= int((image.width + image.height) / 5.0 * factor) + 1 + sigmaR = sigmaS / 32.0 * factor + 0.002 + img_color = cv2.stylization(img, sigma_s=sigmaS, sigma_r=sigmaR) + ret_image = cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB) + return cv22pil(ret_image) + + +def image_beauty(image:Image, level:int=50) -> Image: + img = pil2cv2(image) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + factor = (level / 50.0)**2 + d = int((image.width + image.height) / 256 * factor) + sigmaColor = max(1, float((image.width + image.height) / 256 * factor)) + sigmaSpace = max(1, float((image.width + image.height) / 160 * factor)) + img_bit = cv2.bilateralFilter(src=img, d=d, sigmaColor=sigmaColor, sigmaSpace=sigmaSpace) + ret_image = cv2.cvtColor(img_bit, cv2.COLOR_BGR2RGB) + return cv22pil(ret_image) + + +def pixel_spread(image:Image, mask:Image) -> Image: + from pymatting import estimate_foreground_ml + i1 = pil2tensor(image) + if mask.mode != 'RGB': + mask = mask.convert('RGB') + i_dup = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) + a_dup = copy.deepcopy(pil2tensor(mask).cpu().numpy().astype(np.float64)) + fg = copy.deepcopy(i1.cpu().numpy().astype(np.float64)) + + for index, img in enumerate(i_dup): + alpha = a_dup[index][:, :, 0] + fg[index], _ = estimate_foreground_ml(img, np.array(alpha), return_background=True) + + return tensor2pil(torch.from_numpy(fg.astype(np.float32))) + + +def watermark_image_size(image:Image) -> int: + size = int(math.sqrt(image.width * image.height * 0.015625) * 0.9) + return size + +def add_invisibal_watermark(image:Image, watermark_image:Image) -> Image: + """ + Adds an invisible watermark to an image. + """ + orig_image_mode = image.mode + temp_dir = os.path.join(folder_paths.get_temp_directory(), generate_random_name('_watermark_', '_temp', 16)) + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir) + image_dir = os.path.join(temp_dir, 'image') + wm_dir = os.path.join(temp_dir, 'wm') + result_dir = os.path.join(temp_dir, 'result') + + try: + os.makedirs(image_dir) + os.makedirs(wm_dir) + os.makedirs(result_dir) + except Exception as e: + # print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", message_type='error') + return (image,) + + image_file_name = os.path.join(generate_random_name('watermark_orig_', '_temp', 16) + '.png') + wm_file_name = os.path.join(generate_random_name('watermark_image_', '_temp', 16) + '.png') + output_file_name = os.path.join(generate_random_name('watermark_output_', '_temp', 16) + '.png') + + try: + if image.mode != "RGB": + image = image.convert("RGB") + image.save(os.path.join(image_dir, image_file_name)) + watermark_image.save(os.path.join(wm_dir, wm_file_name)) + except IOError as e: + # print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary file.", message_type='error') + return (image,) + + from blind_watermark import WaterMark + bwm1 = WaterMark(password_img=1, password_wm=1) + bwm1.read_img(os.path.join(image_dir, image_file_name)) + bwm1.read_wm(os.path.join(wm_dir, wm_file_name)) + output_image = os.path.join(result_dir, output_file_name) + bwm1.embed(output_image, compression_ratio=100) + + return Image.open(output_image).convert(orig_image_mode) + +def decode_watermark(image:Image, watermark_image_size:int=94) -> Image: + temp_dir = os.path.join(folder_paths.get_temp_directory(), generate_random_name('_watermark_', '_temp', 16)) + if os.path.isdir(temp_dir): + shutil.rmtree(temp_dir) + image_dir = os.path.join(temp_dir, 'decode_image') + result_dir = os.path.join(temp_dir, 'decode_result') + + try: + os.makedirs(image_dir) + os.makedirs(result_dir) + except Exception as e: + # print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.", message_type='error') + return (image,) + + image_file_name = os.path.join(generate_random_name('watermark_decode_', '_temp', 16) + '.png') + output_file_name = os.path.join(generate_random_name('watermark_decode_output_', '_temp', 16) + '.png') + + try: + image.save(os.path.join(image_dir, image_file_name)) + except IOError as e: + # print(e) + log(f"Error: {NODE_NAME} skipped, because unable to create temporary file.", message_type='error') + return (image,) + + from blind_watermark import WaterMark + bwm1 = WaterMark(password_img=1, password_wm=1) + decode_image = os.path.join(image_dir, image_file_name) + output_image = os.path.join(result_dir, output_file_name) + + try: + bwm1.extract(filename=decode_image, wm_shape=(watermark_image_size, watermark_image_size), + out_wm_name=os.path.join(output_image),) + ret_image = Image.open(output_image) + except Exception as e: + log(f"blind watermark extract fail, {e}") + ret_image = Image.new("RGB", (64, 64), color="black") + ret_image = normalize_gray(ret_image) + return ret_image + +# def generate_text_image(text:str, font_path:str, font_size:int, text_color:str="#FFFFFF", +# vertical:bool=True, stroke_width:int=1, stroke_color:str="#000000", +# spacing:int=0, leading:int=0) -> tuple: +# +# lines = text.split("\n") +# if vertical: +# layout = "vertical" +# else: +# layout = "horizontal" +# char_coordinates = [] +# if layout == "vertical": +# x = 0 +# y = 0 +# for i in range(len(lines)): +# line = lines[i] +# for char in line: +# char_coordinates.append((x, y)) +# y += font_size + spacing +# x += font_size + leading +# y = 0 +# else: +# x = 0 +# y = 0 +# for line in lines: +# for char in line: +# char_coordinates.append((x, y)) +# x += font_size + spacing +# y += font_size + leading +# x = 0 +# if layout == "vertical": +# width = (len(lines) * (font_size + spacing)) - spacing +# height = ((len(max(lines, key=len)) + 1) * (font_size + spacing)) + spacing +# else: +# width = (len(max(lines, key=len)) * (font_size + spacing)) - spacing +# height = ((len(lines) - 1) * (font_size + spacing)) + font_size +# +# image = Image.new('RGBA', size=(width, height), color=stroke_color) +# draw = ImageDraw.Draw(image) +# font = ImageFont.truetype(font_path, font_size) +# index = 0 +# for i, line in enumerate(lines): +# for j, char in enumerate(line): +# x, y = char_coordinates[index] +# if stroke_width > 0: +# draw.text((x - stroke_width, y), char, font=font, fill=stroke_color) +# draw.text((x + stroke_width, y), char, font=font, fill=stroke_color) +# draw.text((x, y - stroke_width), char, font=font, fill=stroke_color) +# draw.text((x, y + stroke_width), char, font=font, fill=stroke_color) +# draw.text((x, y), char, font=font, fill=text_color) +# index += 1 +# return (image.convert('RGB'), image.split()[3]) + +def generate_text_image(width:int, height:int, text:str, font_file:str, text_scale:float=1, font_color:str="#FFFFFF",) -> Image: + image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + font_size = int(width / len(text) * text_scale) + font = ImageFont.truetype(font_file, font_size) + bbox = draw.textbbox((0, 0), text, font=font) + text_width, text_height = bbox[2] - bbox[0], bbox[3] - bbox[1] + x = int((width - text_width) / 2) + y = int((height - text_height) / 2) - int(font_size / 2) + draw.text((x, y), text, font=font, fill=font_color) + return image + +'''Mask Functions''' + +def create_mask_from_color_cv2(image:Image, color:str, tolerance:int=0) -> Image: + (r, g, b) = Hex_to_RGB(color) + target_color = (b, g, r) + tolerance = 127 + int(tolerance * 1.28) + # tolerance = 255 - tolerance + # 将RGB颜色转换为HSV颜色空间 + image = pil2cv2(image) + hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + + # 定义目标颜色的HSV范围 + lower_color = np.array([max(target_color[0] - tolerance, 0), max(target_color[1] - tolerance, 0), max(target_color[2] - tolerance, 0)]) + upper_color = np.array([min(target_color[0] + tolerance, 255), min(target_color[1] + tolerance, 255), min(target_color[2] + tolerance, 255)]) + + # 创建掩码 + mask = cv2.inRange(hsv_image, lower_color, upper_color) + + return cv22pil(mask).convert("L") + +def create_mask_from_color_tensor(image:Image, color:str, tolerance:int=0) -> Image: + threshold = int(tolerance * 1.28) + (red, green, blue) = Hex_to_RGB(color) + image = pil2tensor(image).squeeze() + temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int) + color_value = torch.tensor([red, green, blue]) + lower_bound = (color_value - threshold).clamp(min=0) + upper_bound = (color_value + threshold).clamp(max=255) + lower_bound = lower_bound.view(1, 1, 1, 3) + upper_bound = upper_bound.view(1, 1, 1, 3) + mask = (temp >= lower_bound) & (temp <= upper_bound) + mask = mask.all(dim=-1) + mask = mask.float() + return tensor2pil(mask).convert("L") + +@lru_cache(maxsize=1, typed=False) +def load_RMBG_model(): + from .briarmbg import BriaRMBG + current_directory = os.path.dirname(os.path.abspath(__file__)) + device = "cuda" if torch.cuda.is_available() else "cpu" + net = BriaRMBG() + model_path = "" + try: + model_path = os.path.join(os.path.normpath(folder_paths.folder_names_and_paths['rmbg'][0][0]), "model.pth") + except: + pass + if not os.path.exists(model_path): + model_path = os.path.join(folder_paths.models_dir, "rmbg", "RMBG-1.4", "model.pth") + if not os.path.exists(model_path): + model_path = os.path.join(os.path.dirname(current_directory), "RMBG-1.4", "model.pth") + net.load_state_dict(torch.load(model_path, map_location=device, weights_only=True)) + net.to(device) + net.eval() + return net + + +def RMBG(image:Image) -> Image: + rmbgmodel = load_RMBG_model() + w, h = image.size + im_np = np.array(image.resize((1024, 1024), Image.BILINEAR)) + im_tensor = torch.tensor(im_np, dtype=torch.float32).permute(2, 0, 1) + im_tensor = torch.divide(torch.unsqueeze(im_tensor, 0), 255.0) + im_tensor = TF.normalize(im_tensor, [0.5, 0.5, 0.5], [1.0, 1.0, 1.0]) + if torch.cuda.is_available(): + im_tensor = im_tensor.cuda() + result = rmbgmodel(im_tensor) + result = torch.squeeze(F.interpolate(result[0][0], size=(h, w), mode='bilinear'), 0) + ma = torch.max(result) + mi = torch.min(result) + result = (result - mi) / (ma - mi) + im_array = (result * 255).cpu().data.numpy().astype(np.uint8) + _mask = torch.from_numpy(np.squeeze(im_array).astype(np.float32)) + return tensor2pil(_mask) + +def guided_filter_alpha(image:torch.Tensor, mask:torch.Tensor, filter_radius:int) -> torch.Tensor: + sigma = 0.15 + d = filter_radius + 1 + mask = pil2tensor(tensor2pil(mask).convert('RGB')) + if not bool(d % 2): + d += 1 + s = sigma / 10 + i_dup = copy.deepcopy(image.cpu().numpy()) + a_dup = copy.deepcopy(mask.cpu().numpy()) + for index, image in enumerate(i_dup): + alpha_work = a_dup[index] + i_dup[index] = guidedFilter(image, alpha_work, d, s) + return torch.from_numpy(i_dup) + +#pymatting edge detail +def mask_edge_detail(image:torch.Tensor, mask:torch.Tensor, detail_range:int=8, black_point:float=0.01, white_point:float=0.99) -> torch.Tensor: + from pymatting import fix_trimap, estimate_alpha_cf + d = detail_range * 5 + 1 + mask = pil2tensor(tensor2pil(mask).convert('RGB')) + if not bool(d % 2): + d += 1 + i_dup = copy.deepcopy(image.cpu().numpy().astype(np.float64)) + a_dup = copy.deepcopy(mask.cpu().numpy().astype(np.float64)) + for index, img in enumerate(i_dup): + trimap = a_dup[index][:, :, 0] # convert to single channel + if detail_range > 0: + trimap = cv2.GaussianBlur(trimap, (d, d), 0) + trimap = fix_trimap(trimap, black_point, white_point) + alpha = estimate_alpha_cf(img, trimap, laplacian_kwargs={"epsilon": 1e-6}, + cg_kwargs={"maxiter": 500}) + a_dup[index] = np.stack([alpha, alpha, alpha], axis=-1) # convert back to rgb + return torch.from_numpy(a_dup.astype(np.float32)) + +class VITMatteModel: + def __init__(self,model,processor): + self.model = model + self.processor = processor + +def load_VITMatte_model(model_name:str, local_files_only:bool=False) -> object: + model_name = "vitmatte" + model_repo = "hustvl/vitmatte-small-composition-1k" + model_path = check_and_download_model(model_name, model_repo) + from transformers import VitMatteImageProcessor, VitMatteForImageMatting + model = VitMatteForImageMatting.from_pretrained(model_path, local_files_only=local_files_only) + processor = VitMatteImageProcessor.from_pretrained(model_path, local_files_only=local_files_only) + vitmatte = VITMatteModel(model, processor) + return vitmatte + +def generate_VITMatte(image:Image, trimap:Image, local_files_only:bool=False, device:str="cpu", max_megapixels:float=2.0) -> Image: + if image.mode != 'RGB': + image = image.convert('RGB') + if trimap.mode != 'L': + trimap = trimap.convert('L') + max_megapixels *= 1048576 + width, height = image.size + ratio = width / height + target_width = math.sqrt(ratio * max_megapixels) + target_height = target_width / ratio + target_width = int(target_width) + target_height = int(target_height) + if width * height > max_megapixels: + image = image.resize((target_width, target_height), Image.BILINEAR) + trimap = trimap.resize((target_width, target_height), Image.BILINEAR) + # log(f"vitmatte image size {width}x{height} too large, resize to {target_width}x{target_height} for processing.") + model_name = "hustvl/vitmatte-small-composition-1k" + if device=="cpu": + device = torch.device('cpu') + else: + if torch.cuda.is_available(): + device = torch.device('cuda') + else: + log("vitmatte device is set to cuda, but not available, using cpu instead.") + device = torch.device('cpu') + vit_matte_model = load_VITMatte_model(model_name=model_name, local_files_only=local_files_only) + vit_matte_model.model.to(device) + # log(f"vitmatte processing, image size = {image.width}x{image.height}, device = {device}.") + inputs = vit_matte_model.processor(images=image, trimaps=trimap, return_tensors="pt") + with torch.no_grad(): + inputs = {k: v.to(device) for k, v in inputs.items()} + predictions = vit_matte_model.model(**inputs).alphas + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + mask = tensor2pil(predictions).convert('L') + mask = mask.crop( + (0, 0, image.width, image.height)) # remove padding that the prediction appends (works in 32px tiles) + if width * height > max_megapixels: + mask = mask.resize((width, height), Image.BILINEAR) + return mask + +def generate_VITMatte_trimap(mask:torch.Tensor, erode_kernel_size:int, dilate_kernel_size:int) -> Image: + def g_trimap(mask, erode_kernel_size=10, dilate_kernel_size=10): + erode_kernel = np.ones((erode_kernel_size, erode_kernel_size), np.uint8) + dilate_kernel = np.ones((dilate_kernel_size, dilate_kernel_size), np.uint8) + eroded = cv2.erode(mask, erode_kernel, iterations=5) + dilated = cv2.dilate(mask, dilate_kernel, iterations=5) + trimap = np.zeros_like(mask) + trimap[dilated == 255] = 128 + trimap[eroded == 255] = 255 + return trimap + + mask = mask.squeeze(0).cpu().detach().numpy().astype(np.uint8) * 255 + trimap = g_trimap(mask, erode_kernel_size, dilate_kernel_size).astype(np.float32) + trimap[trimap == 128] = 0.5 + trimap[trimap == 255] = 1 + trimap = torch.from_numpy(trimap).unsqueeze(0) + + return tensor2pil(trimap).convert('L') + + +def get_a_person_mask_generator_model_path() -> str: + model_folder_name = 'mediapipe' + model_name = 'selfie_multiclass_256x256.tflite' + + model_file_path = "" + try: + model_file_path = os.path.join(os.path.normpath(folder_paths.folder_names_and_paths[model_folder_name][0][0]), model_name) + except: + pass + if not os.path.exists(model_file_path): + model_file_path = os.path.join(folder_paths.models_dir, model_folder_name, model_name) + + if not os.path.exists(model_file_path): + import wget + model_url = f'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/{model_name}' + log(f"Downloading '{model_name}' model") + os.makedirs(os.path.dirname(model_file_path), exist_ok=True) + wget.download(model_url, model_file_path) + return model_file_path + +def mask_fix(images:torch.Tensor, radius:int, fill_holes:int, white_threshold:float, extra_clip:float) -> torch.Tensor: + d = radius * 2 + 1 + i_dup = copy.deepcopy(images.cpu().numpy()) + for index, image in enumerate(i_dup): + cleaned = cv2.bilateralFilter(image, 9, 0.05, 8) + alpha = np.clip((image - white_threshold) / (1 - white_threshold), 0, 1) + rgb = image * alpha + alpha = cv2.GaussianBlur(alpha, (d, d), 0) * 0.99 + np.average(alpha) * 0.01 + rgb = cv2.GaussianBlur(rgb, (d, d), 0) * 0.99 + np.average(rgb) * 0.01 + rgb = rgb / np.clip(alpha, 0.00001, 1) + rgb = rgb * extra_clip + cleaned = np.clip(cleaned / rgb, 0, 1) + if fill_holes > 0: + fD = fill_holes * 2 + 1 + gamma = cleaned * cleaned + kD = np.ones((fD, fD), np.uint8) + kE = np.ones((fD + 2, fD + 2), np.uint8) + gamma = cv2.dilate(gamma, kD, iterations=1) + gamma = cv2.erode(gamma, kE, iterations=1) + gamma = cv2.GaussianBlur(gamma, (fD, fD), 0) + cleaned = np.maximum(cleaned, gamma) + i_dup[index] = cleaned + return torch.from_numpy(i_dup) + +def histogram_remap(image:torch.Tensor, blackpoint:float, whitepoint:float) -> torch.Tensor: + bp = min(blackpoint, whitepoint - 0.001) + scale = 1 / (whitepoint - bp) + i_dup = copy.deepcopy(image.cpu().numpy()) + i_dup = np.clip((i_dup - bp) * scale, 0.0, 1.0) + return torch.from_numpy(i_dup) + +def expand_mask(mask:torch.Tensor, grow:int, blur:int) -> torch.Tensor: + # grow + c = 0 + kernel = np.array([[c, 1, c], + [1, 1, 1], + [c, 1, c]]) + growmask = mask.reshape((-1, mask.shape[-2], mask.shape[-1])) + out = [] + for m in growmask: + output = m.numpy() + for _ in range(abs(grow)): + if grow < 0: + output = scipy.ndimage.grey_erosion(output, footprint=kernel) + else: + output = scipy.ndimage.grey_dilation(output, footprint=kernel) + output = torch.from_numpy(output) + out.append(output) + # blur + for idx, tensor in enumerate(out): + pil_image = tensor2pil(tensor.cpu().detach()) + pil_image = pil_image.filter(ImageFilter.GaussianBlur(blur)) + out[idx] = pil2tensor(pil_image) + ret_mask = torch.cat(out, dim=0) + return ret_mask + +def mask_invert(mask:torch.Tensor) -> torch.Tensor: + return 1 - mask + +def subtract_mask(masks_a:torch.Tensor, masks_b:torch.Tensor) -> torch.Tensor: + return torch.clamp(masks_a - masks_b, 0, 255) + +def add_mask(masks_a:torch.Tensor, masks_b:torch.Tensor) -> torch.Tensor: + mask = chop_image(tensor2pil(masks_a), tensor2pil(masks_b), blend_mode='add', opacity=100) + return image2mask(mask) + +def RGB2RGBA(image:Image, mask:Image) -> Image: + (R, G, B) = image.convert('RGB').split() + return Image.merge('RGBA', (R, G, B, mask.convert('L'))) + +def mask_area(image:Image) -> tuple: + cv2_image = pil2cv2(image.convert('RGBA')) + gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 127, 255, 0) + locs = np.where(thresh == 255) + try: + x1 = np.min(locs[1]) + x2 = np.max(locs[1]) + y1 = np.min(locs[0]) + y2 = np.max(locs[0]) + except ValueError: + x1, y1, x2, y2 = -1, -1, 0, 0 + x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) + return (x1, y1, x2 - x1, y2 - y1) + +def min_bounding_rect(image:Image) -> tuple: + cv2_image = pil2cv2(image) + gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) + ret, thresh = cv2.threshold(gray, 127, 255, 0) + contours, _ = cv2.findContours(thresh, 1, 2) + x, y, width, height = 0, 0, 0, 0 + area = 0 + for contour in contours: + _x, _y, _w, _h = cv2.boundingRect(contour) + _area = _w * _h + if _area > area: + area = _area + x, y, width, height = _x, _y, _w, _h + return (x, y, width, height) + +def max_inscribed_rect(image:Image) -> tuple: + img = pil2cv2(image) + img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + ret, img_bin = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY) + contours, _ = cv2.findContours(img_bin, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) + contour = contours[0].reshape(len(contours[0]), 2) + rect = [] + for i in range(len(contour)): + x1, y1 = contour[i] + for j in range(len(contour)): + x2, y2 = contour[j] + area = abs(y2 - y1) * abs(x2 - x1) + rect.append(((x1, y1), (x2, y2), area)) + all_rect = sorted(rect, key=lambda x: x[2], reverse=True) + if all_rect: + best_rect_found = False + index_rect = 0 + nb_rect = len(all_rect) + while not best_rect_found and index_rect < nb_rect: + rect = all_rect[index_rect] + (x1, y1) = rect[0] + (x2, y2) = rect[1] + valid_rect = True + x = min(x1, x2) + while x < max(x1, x2) + 1 and valid_rect: + if any(img[y1, x]) == 0 or any(img[y2, x]) == 0: + valid_rect = False + x += 1 + y = min(y1, y2) + while y < max(y1, y2) + 1 and valid_rect: + if any(img[y, x1]) == 0 or any(img[y, x2]) == 0: + valid_rect = False + y += 1 + if valid_rect: + best_rect_found = True + index_rect += 1 + #较小的数值排前面 + x1, y1, x2, y2 = min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2) + return (x1, y1, x2 - x1, y2 - y1) + +def gray_threshold(image:Image, thresh:int=127, otsu:bool=False) -> Image: + cv2_image = pil2cv2(image) + gray = cv2.cvtColor(cv2_image, cv2.COLOR_BGR2GRAY) + if otsu: + _, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) + else: + _, thresh = cv2.threshold(gray, thresh, 255, cv2.THRESH_TOZERO) + return cv22pil(thresh).convert('L') + +def image_to_colormap(image:Image, index:int) -> Image: + return cv22pil(cv2.applyColorMap(pil2cv2(image), index)) + +# 检查mask有效区域面积比例 +def mask_white_area(mask:Image, white_point:int) -> float: + if mask.mode != 'L': + mask.convert('L') + white_pixels = 0 + for y in range(mask.height): + for x in range(mask.width): + mask.getpixel((x, y)) > 16 + if mask.getpixel((x, y)) > white_point: + white_pixels += 1 + return white_pixels / (mask.width * mask.height) + +'''Color Functions''' + +def color_balance(image:Image, shadows:list, midtones:list, highlights:list, + shadow_center:float=0.15, midtone_center:float=0.5, highlight_center:float=0.8, + shadow_max:float=0.1, midtone_max:float=0.3, highlight_max:float=0.2, + preserve_luminosity:bool=False) -> Image: + + img = pil2tensor(image) + # Create a copy of the img tensor + img_copy = img.clone() + + # Calculate the original luminance if preserve_luminosity is True + if preserve_luminosity: + original_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2] + + # Define the adjustment curves + def adjust(x, center, value, max_adjustment): + # Scale the adjustment value + value = value * max_adjustment + + # Define control points + points = torch.tensor([[0, 0], [center, center + value], [1, 1]]) + + # Create cubic spline + from scipy.interpolate import CubicSpline + cs = CubicSpline(points[:, 0], points[:, 1]) + + # Apply the cubic spline to the color channel + return torch.clamp(torch.from_numpy(cs(x)), 0, 1) + + # Apply the adjustments to each color channel + # shadows, midtones, highlights are lists of length 3 (for R, G, B channels) with values between -1 and 1 + for i, (s, m, h) in enumerate(zip(shadows, midtones, highlights)): + img_copy[..., i] = adjust(img_copy[..., i], shadow_center, s, shadow_max) + img_copy[..., i] = adjust(img_copy[..., i], midtone_center, m, midtone_max) + img_copy[..., i] = adjust(img_copy[..., i], highlight_center, h, highlight_max) + + # If preserve_luminosity is True, adjust the RGB values to match the original luminance + if preserve_luminosity: + current_luminance = 0.2126 * img_copy[..., 0] + 0.7152 * img_copy[..., 1] + 0.0722 * img_copy[..., 2] + img_copy *= (original_luminance / current_luminance).unsqueeze(-1) + + return tensor2pil(img_copy) + +def RGB_to_Hex(RGB:tuple) -> str: + color = '#' + for i in RGB: + num = int(i) + color += str(hex(num))[-2:].replace('x', '0').upper() + return color + +def Hex_to_RGB(inhex:str) -> tuple: + if not inhex.startswith('#'): + raise ValueError(f'Invalid Hex Code in {inhex}') + else: + if len(inhex) == 4: + inhex = "#" + "".join([char * 2 for char in inhex[1:]]) + rval = inhex[1:3] + gval = inhex[3:5] + bval = inhex[5:] + rgb = (int(rval, 16), int(gval, 16), int(bval, 16)) + return tuple(rgb) + +def RGB_to_HSV(RGB:tuple) -> list: + HSV = rgb_to_hsv(RGB[0] / 255.0, RGB[1] / 255.0, RGB[2] / 255.0) + return [int(x * 360) for x in HSV] + +def Hex_to_HSV_255level(inhex:str) -> list: + if not inhex.startswith('#'): + raise ValueError(f'Invalid Hex Code in {inhex}') + else: + if len(inhex) == 4: + inhex = "#" + "".join([char * 2 for char in inhex[1:]]) + rval = inhex[1:3] + gval = inhex[3:5] + bval = inhex[5:] + RGB = (int(rval, 16), int(gval, 16), int(bval, 16)) + HSV = rgb_to_hsv(RGB[0] / 255.0, RGB[1] / 255.0, RGB[2] / 255.0) + return [int(x * 255) for x in HSV] + +def HSV_255level_to_Hex(HSV: list) -> str: + if len(HSV) != 3 or any((not isinstance(v, int) or v < 0 or v > 255) for v in HSV): + raise ValueError('Invalid HSV values, each value should be an integer between 0 and 255') + + H, S, V = HSV + RGB = tuple(int(x * 255) for x in hsv_to_rgb(H / 255.0, S / 255.0, V / 255.0)) + + # Convert RGB values to hexadecimal format + hex_r = format(RGB[0], '02x') + hex_g = format(RGB[1], '02x') + hex_b = format(RGB[2], '02x') + + return '#' + hex_r + hex_g + hex_b + +# 返回补色色值 +def complementary_color(color: str) -> str: + color = Hex_to_RGB(color) + return RGB_to_Hex((255 - color[0], 255 - color[1], 255 - color[2])) + +# 返回颜色对应灰度值 +def rgb2gray(color:str)->int: + (r, g, b) = Hex_to_RGB(color) + return int((r * 299 + g * 587 + b * 114) / 1000) + +'''Value Functions''' +def is_valid_mask(tensor:torch.Tensor) -> bool: + return not bool(torch.all(tensor == 0).item()) + +def step_value(start_value, end_value, total_step, step) -> float: # 按当前步数在总步数中的位置返回比例值 + factor = step / total_step + return (end_value - start_value) * factor + start_value + +def step_color(start_color_inhex:str, end_color_inhex:str, total_step:int, step:int) -> str: # 按当前步数在总步数中的位置返回比例颜色 + start_color = tuple(Hex_to_RGB(start_color_inhex)) + end_color = tuple(Hex_to_RGB(end_color_inhex)) + start_R, start_G, start_B = start_color[0], start_color[1], start_color[2] + end_R, end_G, end_B = end_color[0], end_color[1], end_color[2] + ret_color = (int(step_value(start_R, end_R, total_step, step)), + int(step_value(start_G, end_G, total_step, step)), + int(step_value(start_B, end_B, total_step, step)), + ) + return RGB_to_Hex(ret_color) + +def has_letters(string:str) -> bool: + pattern = r'[a-zA-Z]' + match = re.search(pattern, string) + if match: + return True + else: + return False + + +def replace_case(old:str, new:str, text:str) -> str: + index = text.lower().find(old.lower()) + if index == -1: + return text + return replace_case(old, new, text[:index] + new + text[index + len(old):]) + +def random_numbers(total:int, random_range:int, seed:int=0, sum_of_numbers:int=0) -> list: + random.seed(seed) + numbers = [random.randint(-random_range//2, random_range//2) for _ in range(total - 1)] + avg = sum(numbers) // total + ret_list = [] + for i in numbers: + ret_list.append(i - avg) + ret_list.append((sum_of_numbers - sum(ret_list)) // 2) + return ret_list + +# 四舍五入取整数倍 +def num_round_to_multiple(number:int, multiple:int) -> int: + remainder = number % multiple + if remainder == 0 : + return number + else: + factor = int(number / multiple) + if number - factor * multiple > multiple / 2: + factor += 1 + return factor * multiple + +# 向上取整数倍 +def num_round_up_to_multiple(number: int, multiple: int) -> int: + remainder = number % multiple + if remainder == 0: + return number + else: + factor = (number + multiple - 1) // multiple # 向上取整的计算方式 + return factor * multiple + +def calculate_side_by_ratio(orig_width:int, orig_height:int, ratio:float, longest_side:int=0) -> int: + + if orig_width > orig_height: + if longest_side: + target_width = longest_side + else: + target_width = orig_width + target_height = int(target_width / ratio) + else: + if longest_side: + target_height = longest_side + else: + target_height = orig_height + target_width = int(target_height * ratio) + + if ratio < 1: + if 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) + + return target_width, target_height + +def generate_random_name(prefix:str, suffix:str, length:int) -> str: + name = ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(length)) + return prefix + name + suffix + +def check_image_file(file_name:str, interval:int) -> object: + while True: + if os.path.isfile(file_name): + try: + image = Image.open(file_name) + ret_image = copy.deepcopy(image) + image.close() + return ret_image + except Exception as e: + log(e) + return None + break + time.sleep(interval / 1000) + +# 判断字符串是否包含中文 +def is_contain_chinese(check_str:str) -> bool: + for ch in check_str: + if u'\u4e00' <= ch <= u'\u9fff': + return True + return False + +# 生成随机颜色 +def generate_random_color(): + """ + Generate a random color in hexadecimal format. + """ + # random.seed(int(time.time())) + return "#{:06x}".format(random.randint(0x101010, 0xFFFFFF)) + +# 提取字符串中的int数为列表 +def extract_numbers(string): + return [int(s) for s in re.findall(r'\d+', string)] + +# 提取字符串中的数值, 返回为列表 +def extract_all_numbers_from_str(string, checkint:bool=False): + # 定义浮点数的正则表达式模式 + number_pattern = r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?' + # 使用re.findall找到所有匹配的字符串 + matches = re.findall(number_pattern, string) + # 转换为浮点数 + numbers = [float(match) for match in matches] + number_list = [] + # 如果需要检查是否为整数,则将浮点数转换为整数 + if checkint: + for num in numbers: + int_num = int(num) + if math.isclose(num, int_num, rel_tol=1e-19): + number_list.append(int_num) + else: + number_list.append(num) + else: + number_list = numbers + + return number_list + + + +# 提取字符串中用"," ";" " "分开的字符串, 返回为列表 +def extract_substr_from_str(string) -> list: + return re.split(r'[,\s;,;]+', string) + +# lcs匹配算法,计算最长公共子序列 (LCS):子字符串顺序:以相同顺序出现,权重更高。额外字符惩罚:多余字符会降低相似度。 +def lcs_with_order(s1, s2): + """Calculate the length of the longest common subsequence (LCS) with the same order.""" + m, n = len(s1), len(s2) + dp = [[0] * (n + 1) for _ in range(m + 1)] + + for i in range(1, m + 1): + for j in range(1, n + 1): + if s1[i - 1] == s2[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + + return dp[m][n] + +# 使用正则表达式将字符串拆分为单词(token),对比的同时忽略大小写和非字母数字字符。 +def tokenize_string(s): + """Tokenize a string by splitting on non-alphanumeric characters and normalizing case.""" + return re.findall(r'\b\w+\b', s.lower()) + +# 在列表中找到字符串的最佳匹配 +def find_best_match_by_similarity(target, candidates): + """ + Find the best matching string based on substring order, extra character penalties, and tokenization. + + Parameters: + target (str): The target string. + candidates (list of str): List of candidate strings. + + Returns: + str: The best matching string. + """ + target_tokens = tokenize_string(target) + best_match = None + highest_score = float('-inf') + + for candidate in candidates: + candidate_tokens = tokenize_string(candidate) + + # Calculate LCS on tokens + target_str = ''.join(target_tokens) + candidate_str = ''.join(candidate_tokens) + lcs = lcs_with_order(target_str, candidate_str) + + # Calculate similarity score + match_ratio = lcs / len(target_str) # Ratio of matched characters + extra_char_penalty = len(candidate_str) - lcs # Penalty for extra characters + unmatched_tokens_penalty = len(set(candidate_tokens) - set(target_tokens)) # Penalty for unmatched tokens + score = match_ratio - 0.1 * extra_char_penalty - 0.2 * unmatched_tokens_penalty # Weighted score + + if score > highest_score: + highest_score = score + best_match = candidate + + return best_match + + +def clear_memory(): + import gc + # Cleanup + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.ipc_collect() + +def tensor_info(tensor:object) -> str: + value = '' + if isinstance(tensor, torch.Tensor): + value += f"\n Input dim = {tensor.dim()}, shape[0] = {tensor.shape[0]} \n" + for i in range(tensor.shape[0]): + t = tensor[i] + image = tensor2pil(t) + value += f'\n index {i}: Image.size = {image.size}, Image.mode = {image.mode}, dim = {t.dim()}, ' + for j in range(t.dim()): + value += f'shape[{j}] = {t.shape[j]}, ' + else: + value = f"tensor_info: Not tensor, type is {type(tensor)}" + return value + +# 去除空行 +def remove_empty_lines(text): + lines = text.split('\n') + non_empty_lines = [line for line in lines if line.strip() != ''] + return '\n'.join(non_empty_lines) + +# 去除重复的句子 +def remove_duplicate_string(text:str) -> str: + sentences = re.split(r'(?<=[:;,.!?])\s+', text) + unique_sentences = [] + seen = set() + for sentence in sentences: + if sentence not in seen: + seen.add(sentence) + unique_sentences.append(sentence) + return ' '.join(unique_sentences) + +files_for_uform_gen2_qwen = Path(os.path.join(folder_paths.models_dir, "LLavacheckpoints", "files_for_uform_gen2_qwen")) +class StopOnTokens(StoppingCriteria): + def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool: + stop_ids = [151645] # Define stop tokens as per your model's specifics + for stop_id in stop_ids: + if input_ids[0][-1] == stop_id: + return True + return False + +class UformGen2QwenChat: + + def __init__(self): + from huggingface_hub import snapshot_download + # self.model_path = snapshot_download("unum-cloud/uform-gen2-qwen-500m", + # local_dir=files_for_uform_gen2_qwen, + # force_download=False, # Set to True if you always want to download, regardless of local copy + # local_files_only=False, # Set to False to allow downloading if not available locally + # local_dir_use_symlinks="auto") # or set to True/False based on your symlink preference + self.model_path = files_for_uform_gen2_qwen + self.device = "cuda" if torch.cuda.is_available() else "cpu" + self.model = AutoModel.from_pretrained(self.model_path, trust_remote_code=True).to(self.device) + self.processor = AutoProcessor.from_pretrained(self.model_path, trust_remote_code=True) + + def chat_response(self, message, history, image_path): + stop = StopOnTokens() + messages = [{"role": "system", "content": "You are a helpful Assistant."}] + + for user_msg, assistant_msg in history: + messages.append({"role": "user", "content": user_msg}) + messages.append({"role": "assistant", "content": assistant_msg}) + + if len(messages) == 1: + message = f" {message}" + + messages.append({"role": "user", "content": message}) + + model_inputs = self.processor.tokenizer.apply_chat_template( + messages, + add_generation_prompt=True, + return_tensors="pt" + ) + + image = Image.open(image_path) # Load image using PIL + image_tensor = ( + self.processor.feature_extractor(image) + .unsqueeze(0) + ) + + attention_mask = torch.ones( + 1, model_inputs.shape[1] + self.processor.num_image_latents - 1 + ) + + model_inputs = { + "input_ids": model_inputs, + "images": image_tensor, + "attention_mask": attention_mask + } + + model_inputs = {k: v.to(self.device) for k, v in model_inputs.items()} + + with torch.inference_mode(): + output = self.model.generate( + **model_inputs, + max_new_tokens=512, + do_sample=True, + temperature=0.3, + repetition_penalty=1.2, + stopping_criteria=StoppingCriteriaList([stop]) + ) + + response_text = self.processor.tokenizer.decode(output[0], skip_special_tokens=True) + response_text = remove_duplicate_string(response_text) + return response_text + +'''CLASS''' + +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + def __eq__(self, __value: object) -> bool: + return True + def __ne__(self, __value: object) -> bool: + return False + + + +'''Load File''' + +def download_hg_model(model_id:str,exDir:str='') -> str: + # 下载本地 + model_checkpoint = os.path.join(folder_paths.models_dir, exDir, os.path.basename(model_id)) + if not os.path.exists(model_checkpoint): + from huggingface_hub import snapshot_download + snapshot_download(repo_id=model_id, local_dir=model_checkpoint, local_dir_use_symlinks=False) + return model_checkpoint + + +def get_files(model_path: str, file_ext_list:list) -> dict: + file_list = [] + for ext in file_ext_list: + file_list.extend(glob.glob(os.path.join(model_path, '*' + ext))) + files_dict = {} + for i in range(len(file_list)): + _, filename = os.path.split(file_list[i]) + files_dict[filename] = file_list[i] + return files_dict + +# def load_inference_prompt() -> str: +# inference_prompt_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "resource", +# "inference.prompt") +# ret_value = '' +# try: +# with open(inference_prompt_file, 'r') as f: +# ret_value = f.readlines() +# except Exception as e: +# log(f'Warning: {inference_prompt_file} ' + repr(e) + f", check it to be correct. ", message_type='warning') +# return ''.join(ret_value) + +def load_custom_size() -> list: + custom_size_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "custom_size.ini") + ret_value = ['1024 x 1024', + '768 x 512', + '512 x 768', + '1280 x 720', + '720 x 1280', + '1344 x 768', + '768 x 1344', + '1536 x 640', + '640 x 1536' + ] + try: + with open(custom_size_file, 'r') as f: + ini = f.readlines() + for line in ini: + if not line.startswith(f'#'): + ret_value.append(line.strip()) + except Exception as e: + pass + # log(f'Warning: {custom_size_file} not found' + f", use default size. ") + return ret_value + +def get_api_key(api_name:str) -> str: + api_key_ini_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), "api_key.ini") + ret_value = '' + try: + with open(api_key_ini_file, 'r') as f: + ini = f.readlines() + for line in ini: + if line.startswith(f'{api_name}='): + ret_value = line[line.find('=') + 1:].rstrip().lstrip() + break + except Exception as e: + log(f'Warning: {api_key_ini_file} ' + repr(e) + f", check it to be correct. ", message_type='warning') + remove_char = ['"', "'", '“', '”', '‘', '’'] + for i in remove_char: + if i in ret_value: + ret_value = ret_value.replace(i, '') + if len(ret_value) < 4: + log(f'Warning: Invalid API-key, Check the key in {api_key_ini_file}.', message_type='warning') + return ret_value + +# 判断文件名后缀是否包括在列表中(忽略大小写) +def file_is_extension(filename:str, ext_list:tuple) -> bool: + # 获取文件的真实后缀(包括点) + true_ext = os.path.splitext(filename)[1] + if true_ext.lower() in ext_list: + return True + return False + +# 遍历目录下包括子目录指定后缀文件,返回字典 +def collect_files(root_dir:str, suffixes:tuple, default_dir:str=""): + result = {} + for dirpath, _, filenames in os.walk(root_dir): + for file in filenames: + if file_is_extension(file, suffixes): + # 获取文件的完整路径作为 value + full_path = os.path.join(dirpath, file) + # 如果是default_dir 则去掉路径,使用文件名作为 key + if dirpath == default_dir: + relative_path = os.path.relpath(full_path, root_dir) + result.update({relative_path: full_path}) + else: + result.update({full_path: full_path}) + return result + + +def get_resource_dir() -> list: + default_lut_dir = [] + default_lut_dir.append(os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'lut')) + default_font_dir = [] + default_font_dir.append(os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), 'font')) + resource_dir_ini_file = os.path.join(os.path.dirname(os.path.dirname(os.path.normpath(__file__))), + "resource_dir.ini") + try: + with open(resource_dir_ini_file, 'r') as f: + ini = f.readlines() + for line in ini: + if line.startswith('LUT_dir='): + _ldir = line[line.find('=') + 1:].rstrip().lstrip() + for dir in extract_substr_from_str(_ldir) : + if os.path.exists(dir): + default_lut_dir.append(dir) + elif line.startswith('FONT_dir='): + _fdir = line[line.find('=') + 1:].rstrip().lstrip() + for dir in extract_substr_from_str(_fdir): + if os.path.exists(dir): + default_font_dir.append(dir) + except Exception as e: + pass + # log(f'Warning: {resource_dir_ini_file} not found' + f", default directory to be used. ") + + + LUT_DICT = {} + for dir in default_lut_dir: + LUT_DICT.update(collect_files(root_dir=dir, suffixes= ('.cube'), default_dir=default_lut_dir[0] )) # 后缀要小写 + LUT_LIST = list(LUT_DICT.keys()) + + FONT_DICT = {} + for dir in default_font_dir: + FONT_DICT.update(collect_files(root_dir=dir, suffixes=('.ttf', '.otf'), default_dir=default_font_dir[0])) # 后缀要小写 + FONT_LIST = list(FONT_DICT.keys()) + + return (LUT_DICT, FONT_DICT) + +# 规范bbox,保证x1 < x2, y1 < y2, 并返回int +def standardize_bbox(bboxes:list) -> list: + ret_bboxes = [] + for bbox in bboxes: + x1 = int(min(bbox[0], bbox[2])) + y1 = int(min(bbox[1], bbox[3])) + x2 = int(max(bbox[0], bbox[2])) + y2 = int(max(bbox[1], bbox[3])) + ret_bboxes.append([x1, y1, x2, y2]) + return ret_bboxes + +def draw_bounding_boxes(image: Image, bboxes: list, color: str = "#FF0000", line_width: int = 5) -> Image: + """ + Draw bounding boxes on the image using the coordinates provided in the bboxes dictionary. + """ + + (_, FONT_DICT) = get_resource_dir() + + font_size = 25 + font = ImageFont.truetype(list(FONT_DICT.items())[0][1], font_size) + + if len(bboxes) > 0: + draw = ImageDraw.Draw(image) + width, height = image.size + if line_width < 0: # auto line width + line_width = (image.width + image.height) // 1000 + + for index, box in enumerate(bboxes): + random_color = generate_random_color() + if color != "random": + random_color = color + xmin = min(box[0], box[2]) + xmax = max(box[0], box[2]) + ymin = min(box[1], box[3]) + ymax = max(box[1], box[3]) + draw.rectangle([xmin, ymin, xmax, ymax], outline=random_color, width=line_width) + draw.text((xmin, ymin - font_size*1.2), str(index), font=font, fill=random_color) + + return image + +def draw_bbox(image: Image, bbox: tuple, color: str = "#FF0000", line_width: int = 5, title: str = "", font_size: int = 10) -> Image: + """ + Draw bounding boxes on the image using the coordinates provided in the bboxes dictionary. + """ + + (_, FONT_DICT) = get_resource_dir() + + font = ImageFont.truetype(list(FONT_DICT.items())[0][1], font_size) + + draw = ImageDraw.Draw(image) + width, height = image.size + if line_width < 0: # auto line width + line_width = (image.width + image.height) // 1000 + + random_color = generate_random_color() + if color != "random": + random_color = color + xmin = min(bbox[0], bbox[2]) + xmax = max(bbox[0], bbox[2]) + ymin = min(bbox[1], bbox[3]) + ymax = max(bbox[1], bbox[3]) + draw.rectangle([xmin, ymin, xmax, ymax], outline=random_color, width=line_width) + if title != "": + draw.text((xmin, ymin - font_size*1.2), title, font=font, fill=random_color) + + return image + + + +'''Constant''' + +chop_mode = [ + 'normal', + 'multply', + 'screen', + 'add', + 'subtract', + 'difference', + 'darker', + 'lighter', + 'color_burn', + 'color_dodge', + 'linear_burn', + 'linear_dodge', + 'overlay', + 'soft_light', + 'hard_light', + 'vivid_light', + 'pin_light', + 'linear_light', + 'hard_mix' + ] + +# Blend Mode from Virtuoso Pack https://github.com/chrisfreilich/virtuoso-nodes +chop_mode_v2 = list(BLEND_MODES.keys()) + +gemini_generate_config = { + "temperature": 0, + "top_p": 1, + "top_k": 1, + "max_output_tokens": 400 +} + +gemini_safety_settings = [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "threshold": "BLOCK_NONE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "threshold": "BLOCK_NONE" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "threshold": "BLOCK_NONE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "threshold": "BLOCK_NONE" + } +] + +minicpm_llama3_v25_prompts = """ + # MISSION + You are an imagine generator for a slide deck tool. You will be given the text or description of a slide and you'll generate a few image descriptions that will be fed to an AI image generator. It will need to have a particular format (seen below). You will also be given some examples below. Think metaphorically and symbolically. + + # FORMAT + The format should follow this general pattern: + +
, , , , , + + It's not strictly required, as you'll see below, you can pick and choose various aspects, but this is the general order of operations + + # EXAMPLES + + a Shakespeare stage play, yellow mist, atmospheric, set design by Michel Crête, Aerial acrobatics design by André Simard, hyperrealistic, 4K, Octane render, unreal engine + + The Moon Knight dissolving into swirling sand, volumetric dust, cinematic lighting, close up portrait + + ethereal Bohemian Waxwing bird, Bombycilla garrulus :: intricate details, ornate, detailed illustration, octane render :: Johanna Rupprecht style, William Morris style :: trending on artstation + + steampunk cat, octane render, hyper realistic + + Hyper detailed movie still that fuses the iconic tea party scene from Alice in Wonderland showing the hatter and an adult alice. a wooden table is filled with teacups and cannabis plants. The scene is surrounded by flying weed. Some playcards flying around in the air. Captured with a Hasselblad medium format camera + + venice in a carnival picture 3, in the style of fantastical compositions, colorful, eye-catching compositions, symmetrical arrangements, navy and aquamarine, distinctive noses, gothic references, spiral group –style expressive + + Beautiful and terrifying Egyptian mummy, flirting and vamping with the viewer, rotting and decaying climbing out of a sarcophagus lunging at the viewer, symmetrical full body Portrait photo, elegant, highly detailed, soft ambient lighting, rule of thirds, professional photo HD Photography, film, sony, portray, kodak Polaroid 3200dpi scan medium format film Portra 800, vibrantly colored portrait photo by Joel – Peter Witkin + Diane Arbus + Rhiannon + Mike Tang, fashion shoot + + A grandmotherly Fate sits on a cozy cosmic throne knitting with mirrored threads of time, the solar system spins like clockwork behind her as she knits the futures of people together like an endless collage of destiny, maximilism, cinematic quality, sharp – focus, intricate details + + A cloud with several airplanes flying around on top, in the style of detailed fantasy art, nightcore, quiet moments captured in paint, radiant clusters, i cant believe how beautiful this is, detailed character design, dark cyan and light crimson + + An incredibly detailed close up macro beauty photo of an Asian model, hands holding a bouquet of pink roses, surrounded by scary crows from hell. Shot on a Hasselblad medium format camera with a 100mm lens. Unmistakable to a photograph. Cinematic lighting. Photographed by Tim Walker, trending on 500px + + Game-Art | An island with different geographical properties and multiple small cities floating in space ::10 Island | Floating island in space – waterfalls over the edge of the island falling into space – island fragments floating around the edge of the island, Mountain Ranges – Deserts – Snowy Landscapes – Small Villages – one larger city ::8 Environment | Galaxy – in deep space – other universes can be seen in the distance ::2 Style | Unreal Engine 5 – 8K UHD – Highly Detailed – Game-Art + + a warrior sitting on a giant creature and riding it in the water, with wings spread wide in the water, camera positioned just above the water to capture this beautiful scene, surface showing intricate details of the creature’s scales, fins, and wings, majesty, Hero rides on the creature in the water, digitally enhanced, enhanced graphics, straight, sharp focus, bright lighting, closeup, cinematic, Bronze, Azure, blue, ultra highly detailed, 18k, sharp focus, bright photo with rich colors, full coverage of a scene, straight view shot + + A real photographic landscape painting with incomparable reality,Super wide,Ominous sky,Sailing boat,Wooden boat,Lotus,Huge waves,Starry night,Harry potter,Volumetric lighting,Clearing,Realistic,James gurney,artstation + + Tiger monster with monstera plant over him, back alley in Bangkok, art by Otomo Katsuhiro crossover Yayoi Kusama and Hayao Miyazaki + + An elderly Italian woman with wrinkles, sitting in a local cafe filled with plants and wood decorations, looking out the window, wearing a white top with light purple linen blazer, natural afternoon light shining through the window + + # OUTPUT + Your output should just be an plain list of descriptions. No numbers, no extraneous labels, no hyphens. + Create only one prompt. + """ diff --git a/reference_code/inner_glow.py b/reference_code/inner_glow.py new file mode 100644 index 0000000..066bbfa --- /dev/null +++ b/reference_code/inner_glow.py @@ -0,0 +1,107 @@ +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color, expand_mask, mask_invert, chop_mode, chop_image, step_value + + +class InnerGlow: + + def __init__(self): + self.NODE_NAME = 'InnerGlow' + + @classmethod + def INPUT_TYPES(self): + chop_mode = ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract','difference','darker', + 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', + 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix'] + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_glow' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_glow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + blur_factor = blur / 20.0 + grow = glow_range + inner_mask = _mask + for x in range(brightness): + blur = int(grow * blur_factor) + _color = step_color(glow_color, light_color, brightness, x) + glow_mask = expand_mask(image2mask(inner_mask), -grow, blur) #扩张,模糊 + # 合成glow + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(mask_invert(glow_mask)).convert('L') + _glow = chop_image(_layer, color_image, blend_mode, int(step_value(1, opacity, brightness, x))) + _layer.paste(_glow, mask=alpha) + grow = grow - int(glow_range/brightness) + # 合成layer + _layer.paste(_canvas, mask=ImageChops.invert(_mask)) + ret_images.append(pil2tensor(_layer)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + + +NODE_CLASS_MAPPINGS = { + "LayerStyle: InnerGlow": InnerGlow +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: InnerGlow": "LayerStyle: InnerGlow" +} \ No newline at end of file diff --git a/reference_code/inner_glow_v2.py b/reference_code/inner_glow_v2.py new file mode 100644 index 0000000..9916bb4 --- /dev/null +++ b/reference_code/inner_glow_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/inner_shadow.py b/reference_code/inner_shadow.py new file mode 100644 index 0000000..7b6f60f --- /dev/null +++ b/reference_code/inner_shadow.py @@ -0,0 +1,102 @@ +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, shift_image, expand_mask, chop_image, chop_mode + + +class InnerShadow: + + def __init__(self): + self.NODE_NAME = 'InnerShadow' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 50, "min": 0, "max": 100, "step": 1}), # 透明度 + "distance_x": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # x_偏移 + "distance_y": ("INT", {"default": 5, "min": -9999, "max": 9999, "step": 1}), # y_偏移 + "grow": ("INT", {"default": 2, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 15, "min": 0, "max": 100, "step": 1}), # 模糊 + "shadow_color": ("STRING", {"default": "#000000"}), # 背景颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'inner_shadow' + CATEGORY = '😺dzNodes/LayerStyle' + + def inner_shadow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, distance_x, distance_y, + grow, blur, shadow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + distance_x = -distance_x + distance_y = -distance_y + shadow_color = Image.new("RGB", tensor2pil(l_images[0]).size, color=shadow_color) + for i in range(max_batch): + background_image = b_images[i] if i < len(b_images) else b_images[-1] + layer_image = l_images[i] if i < len(l_images) else l_images[-1] + _mask = l_masks[i] if i < len(l_masks) else l_masks[-1] + # preprocess + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + if _mask.size != _layer.size: + _mask = Image.new('L', _layer.size, 'white') + log(f"Warning: {self.NODE_NAME} mask mismatch, dropped!", message_type='warning') + + if distance_x != 0 or distance_y != 0: + __mask = shift_image(_mask, distance_x, distance_y) # 位移 + shadow_mask = expand_mask(image2mask(__mask), grow, blur) #扩张,模糊 + # 合成阴影 + alpha = tensor2pil(shadow_mask).convert('L') + _shadow = chop_image(_layer, shadow_color, blend_mode, opacity) + _layer.paste(_shadow, mask=ImageChops.invert(alpha)) + # 合成layer + _canvas.paste(_layer, mask=_mask) + + ret_images.append(pil2tensor(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerStyle: InnerShadow": InnerShadow +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: InnerShadow": "LayerStyle: InnerShadow" +} \ No newline at end of file diff --git a/reference_code/inner_shadow_v2.py b/reference_code/inner_shadow_v2.py new file mode 100644 index 0000000..2e0108b --- /dev/null +++ b/reference_code/inner_shadow_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/layer_image_transform.py b/reference_code/layer_image_transform.py new file mode 100644 index 0000000..e8ecb25 --- /dev/null +++ b/reference_code/layer_image_transform.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/layer_mask_transform.py b/reference_code/layer_mask_transform.py new file mode 100644 index 0000000..6b3ce60 --- /dev/null +++ b/reference_code/layer_mask_transform.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/light_leak.py b/reference_code/light_leak.py new file mode 100644 index 0000000..806f499 --- /dev/null +++ b/reference_code/light_leak.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/mask_box_detect.py b/reference_code/mask_box_detect.py new file mode 100644 index 0000000..d19efb6 --- /dev/null +++ b/reference_code/mask_box_detect.py @@ -0,0 +1,78 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur, mask2image +from .imagefunc import min_bounding_rect, max_inscribed_rect, mask_area, draw_rect + + +class MaskBoxDetect: + + def __init__(self): + self.NODE_NAME = 'MaskBoxDetect' + + @classmethod + def INPUT_TYPES(self): + detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area'] + return { + "required": { + "mask": ("MASK", ), + "detect": (detect_mode,), # 探测类型:最小外接矩形/最大内接矩形 + "x_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # x轴修正 + "y_adjust": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), # y轴修正 + "scale_adjust": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100, "step": 0.01}), # 比例修正 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE", "FLOAT", "FLOAT", "INT", "INT", "INT", "INT",) + RETURN_NAMES = ("box_preview", "x_percent", "y_percent", "width", "height", "x", "y",) + FUNCTION = 'mask_box_detect' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_box_detect(self,mask, detect, x_adjust, y_adjust, scale_adjust): + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + if mask.shape[0] > 0: + mask = torch.unsqueeze(mask[0], 0) + + _mask = mask2image(mask).convert('RGB') + + _mask = gaussian_blur(_mask, 20).convert('L') + x = 0 + y = 0 + width = 0 + height = 0 + + if detect == "min_bounding_rect": + (x, y, width, height) = min_bounding_rect(_mask) + elif detect == "max_inscribed_rect": + (x, y, width, height) = max_inscribed_rect(_mask) + else: + (x, y, width, height) = mask_area(_mask) + log(f"{self.NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}") + _width = width + _height = height + if scale_adjust != 1.0: + _width = int(width * scale_adjust) + _height = int(height * scale_adjust) + x = x - int((_width - width) / 2) + y = y - int((_height - height) / 2) + x += x_adjust + y += y_adjust + x_percent = (x + _width / 2) / _mask.width * 100 + y_percent = (y + _height / 2) / _mask.height * 100 + preview_image = tensor2pil(mask).convert('RGB') + preview_image = draw_rect(preview_image, x - x_adjust, y - y_adjust, width, height, line_color="#F00000", line_width=int(preview_image.height / 60)) + preview_image = draw_rect(preview_image, x, y, width, height, line_color="#00F000", line_width=int(preview_image.height / 40)) + log(f"{self.NODE_NAME} Processed.", message_type='finish') + return ( pil2tensor(preview_image), round(x_percent, 2), round(y_percent, 2), _width, _height, x, y,) + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskBoxDetect": MaskBoxDetect +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskBoxDetect": "LayerMask: MaskBoxDetect" +} \ No newline at end of file diff --git a/reference_code/mask_by_color.py b/reference_code/mask_by_color.py new file mode 100644 index 0000000..af7ea5c --- /dev/null +++ b/reference_code/mask_by_color.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/mask_edge_shrink.py b/reference_code/mask_edge_shrink.py new file mode 100644 index 0000000..2f10670 --- /dev/null +++ b/reference_code/mask_edge_shrink.py @@ -0,0 +1,81 @@ +import torch +from PIL import Image, ImageChops +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, mask_invert, step_color, expand_mask, step_value, chop_image + + + +class MaskEdgeShrink: + + def __init__(self): + self.NODE_NAME = 'MaskEdgeShrink' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "shrink_level": ("INT", {"default": 4, "min": 0, "max": 16, "step": 1}), + "soft": ("INT", {"default": 6, "min": 0, "max": 64, "step": 1}), + "edge_shrink": ("INT", {"default": 1, "min": 0, "max": 999, "step": 1}), + "edge_reserve": ("INT", {"default": 25, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_edge_shrink' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_edge_shrink(self, mask, invert_mask, shrink_level, soft, edge_shrink, edge_reserve): + + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + glow_range = shrink_level * soft + blur = 12 + + for i in range(len(l_masks)): + _mask = l_masks[i] + _canvas = Image.new('RGB', size=_mask.size, color='black') + _layer = Image.new('RGB', size=_mask.size, color='white') + loop_grow = glow_range + inner_mask = _mask + for x in range(shrink_level): + _color = step_color('#FFFFFF', '#000000', shrink_level, x) + glow_mask = expand_mask(image2mask(inner_mask), -loop_grow, blur / (x+0.1)) #扩张,模糊 + # 合成 + color_image = Image.new("RGB", _layer.size, color=_color) + alpha = tensor2pil(mask_invert(glow_mask)).convert('L') + _glow = chop_image(_layer, color_image, 'subtract', int(step_value(1, 100, shrink_level, x))) + _layer.paste(_glow, mask=alpha) + loop_grow = loop_grow - int(glow_range / shrink_level) + # 合成layer + _edge = tensor2pil(expand_mask(image2mask(_mask), -edge_shrink, 0)).convert('RGB') + _layer = chop_image(_layer, _edge, 'normal', edge_reserve) + _layer.paste(_canvas, mask=ImageChops.invert(_mask)) + + ret_masks.append(image2mask(_layer)) + + log(f"{self.NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) + + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskEdgeShrink": MaskEdgeShrink +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskEdgeShrink": "LayerMask: MaskEdgeShrink" +} \ No newline at end of file diff --git a/reference_code/mask_edge_ultra_detail.py b/reference_code/mask_edge_ultra_detail.py new file mode 100644 index 0000000..244df20 --- /dev/null +++ b/reference_code/mask_edge_ultra_detail.py @@ -0,0 +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", +} diff --git a/reference_code/mask_edge_ultra_detail_v2.py b/reference_code/mask_edge_ultra_detail_v2.py new file mode 100644 index 0000000..f289d6d --- /dev/null +++ b/reference_code/mask_edge_ultra_detail_v2.py @@ -0,0 +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", +} diff --git a/reference_code/mask_gradient.py b/reference_code/mask_gradient.py new file mode 100644 index 0000000..4bfddc4 --- /dev/null +++ b/reference_code/mask_gradient.py @@ -0,0 +1,152 @@ +import torch +import copy +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, chop_image, gradient, mask_area + + + +class MaskGradient: + + def __init__(self): + self.NODE_NAME = 'MaskGradient' + + @classmethod + def INPUT_TYPES(self): + side = ['top', 'bottom', 'left', 'right'] + return { + "required": { + "mask": ("MASK",), + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "gradient_side": (side,), + "gradient_scale": ("INT", {"default": 100, "min": 1, "max": 9999, "step": 1}), + "gradient_offset": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}), + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_gradient' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_gradient(self, mask, invert_mask, gradient_side, gradient_scale, gradient_offset, opacity, ): + + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + l_masks = [] + ret_masks = [] + + for m in mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + _canvas = copy.copy(_mask) + width = _mask.width + height = _mask.height + _gradient = gradient('#000000', '#FFFFFF', + 1024, 1024, 0) + # (box_x, box_y, box_width, box_height) = min_bounding_rect(_mask) + (box_x, box_y, box_width, box_height) = mask_area(_mask) + log(f"{self.NODE_NAME}: Box detected. x={box_x},y={box_y},width={box_width},height={box_height}") + if box_width < 1 or box_height < 1: + log(f"Error: {self.NODE_NAME} skipped, because the mask is does'nt have valid area", message_type='error') + return (mask,) + + if gradient_side == 'top': + boxsize = (width, box_height) + _gradient = _gradient.transpose(Image.FLIP_TOP_BOTTOM) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((width, int(box_height * gradient_scale / 100))) + _box.paste(_gradient, box = (0, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(0, gradient_offset)) + _box.paste(_boxwhite, box = (0, gradient_offset - _box.height)) + _gradient = _box + if gradient_offset > box_height: + _gradient = Image.new('RGB', size = boxsize, color = 'white') + _canvas.paste(_black, box = (0, box_y), mask = _gradient.convert('L')) + elif gradient_side == 'bottom': + boxsize = (width, box_height) + _gradient = _gradient.resize((width, box_height)) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((width, int(box_height * gradient_scale / 100))) + _box.paste(_gradient, box = (0, box_height - _gradient.height)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(0, gradient_offset)) + _box.paste(_boxwhite, box = (0, gradient_offset + _box.height)) + _gradient = _box + if gradient_offset < -box_height: + _gradient = Image.new('RGB', size=boxsize, color='white') + _canvas.paste(_black, box = (0, box_y + 1), mask = _gradient.convert('L')) + elif gradient_side == 'left': + boxsize = (box_width, height) + _gradient = _gradient.transpose(Image.ROTATE_270) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((int(box_width * gradient_scale / 100), height)) + _box.paste(_gradient, box = (0, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(gradient_offset, 0)) + _box.paste(_boxwhite, box = (gradient_offset - _box.width, 0)) + _gradient = _box + if gradient_offset > box_width: + _gradient = Image.new('RGB', size=boxsize, color='white') + _canvas.paste(_black, box = (box_x, 0), mask = _gradient.convert('L')) + elif gradient_side == 'right': + boxsize = (box_width, height) + _gradient = _gradient.transpose(Image.ROTATE_90) + _gradient = _gradient.resize(boxsize) + _black = Image.new('RGB', size = boxsize, color = 'black') + if gradient_scale != 100: + _box = Image.new('RGB', size = boxsize, color = 'black') + _gradient = _gradient.resize((int(box_width * gradient_scale / 100), height)) + _box.paste(_gradient, box = (box_width - _gradient.width, 0)) + _gradient = _box + if gradient_offset != 0: + _box = Image.new('RGB', size = boxsize, color = 'black') + _boxwhite = Image.new('RGB', size = boxsize, color = 'white') + _box.paste(_gradient, box=(gradient_offset, 0)) + _box.paste(_boxwhite, box = (gradient_offset + _box.width, 0)) + _gradient = _box + if gradient_offset < -box_width: + _gradient = Image.new('RGB', size=boxsize, color='white') + + _canvas.paste(_black, box = (box_x + 1, 0), mask = _gradient.convert('L')) + # opacity + if opacity < 100: + _canvas = chop_image(_mask, _canvas, 'normal', opacity) + ret_masks.append(image2mask(_canvas)) + + log(f"{self.NODE_NAME} Processed {len(ret_masks)} mask(s).", message_type='finish') + return (torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskGradient": MaskGradient +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskGradient": "LayerMask: MaskGradient" +} \ No newline at end of file diff --git a/reference_code/mask_grain.py b/reference_code/mask_grain.py new file mode 100644 index 0000000..887aac7 --- /dev/null +++ b/reference_code/mask_grain.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/mask_grow.py b/reference_code/mask_grow.py new file mode 100644 index 0000000..2443f3d --- /dev/null +++ b/reference_code/mask_grow.py @@ -0,0 +1,59 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, image2mask, expand_mask + + + +class MaskGrow: + + def __init__(self): + self.NODE_NAME = 'MaskGrow' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "grow": ("INT", {"default": 4, "min": -999, "max": 999, "step": 1}), + "blur": ("INT", {"default": 4, "min": 0, "max": 999, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_grow' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_grow(self, mask, invert_mask, grow, blur,): + + 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 i in range(len(l_masks)): + + _mask = l_masks[i] + ret_masks.append(expand_mask(image2mask(_mask), grow, blur) ) + + 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: MaskGrow": MaskGrow +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskGrow": "LayerMask: MaskGrow" +} \ No newline at end of file diff --git a/reference_code/mask_invert.py b/reference_code/mask_invert.py new file mode 100644 index 0000000..5f11356 --- /dev/null +++ b/reference_code/mask_invert.py @@ -0,0 +1,49 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, image2mask, mask_invert + + + +class MaskInvert: + + def __init__(self): + self.NODE_NAME = 'MaskInvert' + + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "mask": ("MASK", ), # + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_invert' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_invert(self,mask): + l_masks = [] + ret_masks = [] + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + + for m in mask: + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + + for i in range(len(l_masks)): + _mask = l_masks[i] + ret_masks.append(mask_invert(image2mask(_mask))) + + return (torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskInvert": MaskInvert +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskInvert": "LayerMask: MaskInvert" +} \ No newline at end of file diff --git a/reference_code/mask_motion_blur.py b/reference_code/mask_motion_blur.py new file mode 100644 index 0000000..8bc7f13 --- /dev/null +++ b/reference_code/mask_motion_blur.py @@ -0,0 +1,58 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, image2mask, motion_blur + + + +class MaskMotionBlur: + + def __init__(self): + self.NODE_NAME = 'MaskMotionBlur' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK",), + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blur": ("INT", {"default": 20, "min": 1, "max": 9999, "step": 1}), + "angle": ("FLOAT", {"default": 0, "min": -360, "max": 360, "step": 0.1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_motion_blur' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_motion_blur(self, mask, invert_mask, blur, angle,): + + 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 i in range(len(l_masks)): + _mask = l_masks[i] + _blurimage = motion_blur(_mask, angle, blur) + ret_masks.append(image2mask(_blurimage)) + + 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: MaskMotionBlur": MaskMotionBlur +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskMotionBlur": "LayerMask: MaskMotionBlur" +} \ No newline at end of file diff --git a/reference_code/mask_preview.py b/reference_code/mask_preview.py new file mode 100644 index 0000000..67e13d2 --- /dev/null +++ b/reference_code/mask_preview.py @@ -0,0 +1,36 @@ +import torch +import random +from nodes import SaveImage +import folder_paths + + +class MaskPreview(SaveImage): + def __init__(self): + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz1234567890") for x in range(5)) + self.compress_level = 4 + + @classmethod + def INPUT_TYPES(self): + return { + "required": {"mask": ("MASK",), }, + } + + FUNCTION = "mask_preview" + CATEGORY = '😺dzNodes/LayerMask' + OUTPUT_NODE = True + + def mask_preview(self, mask): + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3) + return self.save_images(preview, "MaskPreview") + +NODE_CLASS_MAPPINGS = { + "LayerMask: MaskPreview": MaskPreview +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskPreview": "LayerMask: MaskPreview" +} \ No newline at end of file diff --git a/reference_code/mask_stroke.py b/reference_code/mask_stroke.py new file mode 100644 index 0000000..391e4b5 --- /dev/null +++ b/reference_code/mask_stroke.py @@ -0,0 +1,64 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, image2mask, expand_mask, subtract_mask + + + +class MaskStroke: + + def __init__(self): + self.NODE_NAME = 'MaskStroke' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "mask": ("MASK", ), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "stroke_grow": ("INT", {"default": 0, "min": -999, "max": 999, "step": 1}), # 收缩值 + "stroke_width": ("INT", {"default": 20, "min": 0, "max": 999, "step": 1}), # 扩张值 + "blur": ("INT", {"default": 6, "min": 0, "max": 100, "step": 1}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("MASK",) + RETURN_NAMES = ("mask",) + FUNCTION = 'mask_stroke' + CATEGORY = '😺dzNodes/LayerMask' + + def mask_stroke(self, mask, invert_mask, stroke_grow, stroke_width, blur,): + + 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 i in range(len(l_masks)): + _mask = l_masks[i] + grow_offset = int(stroke_width / 2) + inner_stroke = stroke_grow - grow_offset + outer_stroke = inner_stroke + stroke_width + 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) + ret_masks.append(stroke_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: MaskStroke": MaskStroke +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: MaskStroke": "LayerMask: MaskStroke" +} \ No newline at end of file diff --git a/reference_code/motion_blur.py b/reference_code/motion_blur.py new file mode 100644 index 0000000..cb7e046 --- /dev/null +++ b/reference_code/motion_blur.py @@ -0,0 +1,49 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, motion_blur + + + +class MotionBlur: + + def __init__(self): + self.NODE_NAME = 'MotionBlur' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "angle": ("INT", {"default": 0, "min": -90, "max": 90, "step": 1}), # 角度 + "blur": ("INT", {"default": 20, "min": 1, "max": 999, "step": 1}), # 模糊 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'motion_blur' + CATEGORY = '😺dzNodes/LayerFilter' + + def motion_blur(self, image, angle, blur): + + ret_images = [] + + for i in image: + + _canvas = tensor2pil(torch.unsqueeze(i, 0)).convert('RGB') + + ret_images.append(pil2tensor(motion_blur(_canvas, angle, blur))) + + 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: MotionBlur": MotionBlur +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: MotionBlur": "LayerFilter: MotionBlur" +} \ No newline at end of file diff --git a/reference_code/outer_glow.py b/reference_code/outer_glow.py new file mode 100644 index 0000000..6807c7c --- /dev/null +++ b/reference_code/outer_glow.py @@ -0,0 +1,106 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, step_color,step_value, expand_mask, chop_image, chop_mode + + +class OuterGlow: + + def __init__(self): + self.NODE_NAME = 'OuterGlow' + + @classmethod + def INPUT_TYPES(self): + chop_mode = ['screen', 'add', 'lighter', 'normal', 'multply', 'subtract', 'difference', 'darker', + 'color_burn', 'color_dodge', 'linear_burn', 'linear_dodge', 'overlay', + 'soft_light', 'hard_light', 'vivid_light', 'pin_light', 'linear_light', 'hard_mix'] + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "brightness": ("INT", {"default": 5, "min": 2, "max": 20, "step": 1}), # 迭代 + "glow_range": ("INT", {"default": 48, "min": -9999, "max": 9999, "step": 1}), # 扩张 + "blur": ("INT", {"default": 25, "min": 0, "max": 9999, "step": 1}), # 扩张 + "light_color": ("STRING", {"default": "#FFBF30"}), # 光源中心颜色 + "glow_color": ("STRING", {"default": "#FE0000"}), # 辉光外围颜色 + }, + "optional": { + "layer_mask": ("MASK",), # + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'outer_glow' + CATEGORY = '😺dzNodes/LayerStyle' + + def outer_glow(self, background_image, layer_image, + invert_mask, blend_mode, opacity, + brightness, glow_range, blur, light_color, glow_color, + layer_mask=None + ): + + b_images = [] + l_images = [] + l_masks = [] + ret_images = [] + + for b in background_image: + b_images.append(torch.unsqueeze(b, 0)) + for l in layer_image: + l_images.append(torch.unsqueeze(l, 0)) + m = tensor2pil(l) + if m.mode == 'RGBA': + l_masks.append(m.split()[-1]) + if layer_mask is not None: + if layer_mask.dim() == 2: + layer_mask = torch.unsqueeze(layer_mask, 0) + l_masks = [] + for m in layer_mask: + if invert_mask: + m = 1 - m + l_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L')) + if len(l_masks) == 0: + log(f"Error: {self.NODE_NAME} skipped, because the available mask is not found.", message_type='error') + return (background_image,) + max_batch = max(len(b_images), len(l_images), len(l_masks)) + 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(_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": OuterGlow +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: OuterGlow": "LayerStyle: OuterGlow" +} \ No newline at end of file diff --git a/reference_code/outer_glow_v2.py b/reference_code/outer_glow_v2.py new file mode 100644 index 0000000..470b106 --- /dev/null +++ b/reference_code/outer_glow_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/pixel_spread.py b/reference_code/pixel_spread.py new file mode 100644 index 0000000..3760fb5 --- /dev/null +++ b/reference_code/pixel_spread.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/print_info.py b/reference_code/print_info.py new file mode 100644 index 0000000..dab4a87 --- /dev/null +++ b/reference_code/print_info.py @@ -0,0 +1,57 @@ +import torch +from .imagefunc import log, tensor2pil +from .imagefunc import AnyType + +any = AnyType("*") + +class PrintInfo: + + @classmethod + def INPUT_TYPES(cls): # pylint: disable = invalid-name, missing-function-docstring + return { + "required": { + "anything": (any, {}), + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "print_info" + CATEGORY = '😺dzNodes/LayerUtility/Data' + OUTPUT_NODE = True + + def print_info(self, anything=None): + value = f'PrintInfo:\nInput type = {type(anything)}' + if isinstance(anything, torch.Tensor): + # value += f"\n Input dim = {anything.dim()}, shape[0] = {anything.shape[0]}" + for i in range(anything.shape[0]): + t = anything[i] + image = tensor2pil(t) + value += f'index {i}: size={image.size}, mode="{image.mode}", dim={t.dim()}; ' + # for j in range(t.dim()): + # value += f'shape[{j}] = {t.shape[j]}, ' + # value += f'\n {t} \n' + elif isinstance(anything, str): + value = f"{value}{anything.encode('unicode-escape').decode('unicode-escape')}" + elif anything is not None: + try: + value = value + json.dumps(anything) + "\n" + except Exception: + try: + value = value + str(anything) + "\n" + except Exception: + value = 'source exists, but could not be serialized.' + else: + value = 'source does not exist.' + + log(value) + + return (value,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: PrintInfo": PrintInfo +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: PrintInfo": "LayerUtility: PrintInfo" +} \ No newline at end of file diff --git a/reference_code/purge_vram.py b/reference_code/purge_vram.py new file mode 100644 index 0000000..2777233 --- /dev/null +++ b/reference_code/purge_vram.py @@ -0,0 +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", +} \ No newline at end of file diff --git a/reference_code/random_generator.py b/reference_code/random_generator.py new file mode 100644 index 0000000..2dded71 --- /dev/null +++ b/reference_code/random_generator.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/restore_crop_box.py b/reference_code/restore_crop_box.py new file mode 100644 index 0000000..40cc07e --- /dev/null +++ b/reference_code/restore_crop_box.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/rmbg_ultra.py b/reference_code/rmbg_ultra.py new file mode 100644 index 0000000..dc45ac7 --- /dev/null +++ b/reference_code/rmbg_ultra.py @@ -0,0 +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", +} diff --git a/reference_code/rmbg_ultra_v2.py b/reference_code/rmbg_ultra_v2.py new file mode 100644 index 0000000..794831c --- /dev/null +++ b/reference_code/rmbg_ultra_v2.py @@ -0,0 +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", +} diff --git a/reference_code/rounded_rectangle.py b/reference_code/rounded_rectangle.py new file mode 100644 index 0000000..8ac7243 --- /dev/null +++ b/reference_code/rounded_rectangle.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/segformer_ultra.py b/reference_code/segformer_ultra.py new file mode 100644 index 0000000..62f7d5d --- /dev/null +++ b/reference_code/segformer_ultra.py @@ -0,0 +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", +} + diff --git a/reference_code/shadow_highlight_mask.py b/reference_code/shadow_highlight_mask.py new file mode 100644 index 0000000..e20f46c --- /dev/null +++ b/reference_code/shadow_highlight_mask.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/sharp_soft.py b/reference_code/sharp_soft.py new file mode 100644 index 0000000..ca63e15 --- /dev/null +++ b/reference_code/sharp_soft.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/simple_text_image.py b/reference_code/simple_text_image.py new file mode 100644 index 0000000..1d9b6cc --- /dev/null +++ b/reference_code/simple_text_image.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/skin_beauty.py b/reference_code/skin_beauty.py new file mode 100644 index 0000000..406dd56 --- /dev/null +++ b/reference_code/skin_beauty.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/soft_light.py b/reference_code/soft_light.py new file mode 100644 index 0000000..64372af --- /dev/null +++ b/reference_code/soft_light.py @@ -0,0 +1,63 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, gaussian_blur, chop_image +from .imagefunc import gray_threshold, remove_background, get_image_bright_average + + + +class SoftLight: + + def __init__(self): + self.NODE_NAME = 'SoftLight' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "image": ("IMAGE", ), # + "soft": ("FLOAT", {"default": 1, "min": 0.2, "max": 10, "step": 0.01}), # 模糊 + "threshold": ("INT", {"default": -10, "min": -255, "max": 255, "step": 1}), # 高光阈值 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + }, + "optional": { + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = 'soft_light' + CATEGORY = '😺dzNodes/LayerFilter' + + def soft_light(self, image, soft, threshold, opacity,): + + ret_images = [] + + for i in image: + i = torch.unsqueeze(i, 0) + blend_mode = 'screen' + _canvas = tensor2pil(i).convert('RGB') + blur = int((_canvas.width + _canvas.height) / 200 * soft) + _otsumask = gray_threshold(_canvas, otsu=True) + _removebkgd = remove_background(_canvas, _otsumask, '#000000').convert('L') + auto_threshold = get_image_bright_average(_removebkgd) + light_mask = gray_threshold(_canvas, auto_threshold + threshold) + highlight_mask = gray_threshold(_canvas, auto_threshold + (255 - auto_threshold) // 2 + threshold // 2) + blurimage = gaussian_blur(_canvas, soft).convert('RGB') + light = chop_image(_canvas, blurimage, blend_mode=blend_mode, opacity=opacity) + highlight = chop_image(light, blurimage, blend_mode=blend_mode, opacity=opacity) + _canvas.paste(highlight, mask=gaussian_blur(light_mask, blur * 2).convert('L')) + _canvas.paste(highlight, mask=gaussian_blur(highlight_mask, blur).convert('L')) + + 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: SoftLight": SoftLight +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerFilter: SoftLight": "LayerFilter: SoftLight" +} \ No newline at end of file diff --git a/reference_code/stroke.py b/reference_code/stroke.py new file mode 100644 index 0000000..5d67a6e --- /dev/null +++ b/reference_code/stroke.py @@ -0,0 +1,103 @@ +import torch +from PIL import Image +from .imagefunc import log, tensor2pil, pil2tensor, image2mask, expand_mask, subtract_mask, chop_image, chop_mode + + + +class Stroke: + + def __init__(self): + self.NODE_NAME = 'Storke' + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "invert_mask": ("BOOLEAN", {"default": True}), # 反转mask + "blend_mode": (chop_mode,), # 混合模式 + "opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}), # 透明度 + "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' + CATEGORY = '😺dzNodes/LayerStyle' + + def stroke(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(_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": Stroke +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerStyle: Stroke": "LayerStyle: Stroke" +} \ No newline at end of file diff --git a/reference_code/stroke_v2.py b/reference_code/stroke_v2.py new file mode 100644 index 0000000..e452ac8 --- /dev/null +++ b/reference_code/stroke_v2.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/text_image.py b/reference_code/text_image.py new file mode 100644 index 0000000..c31fc7f --- /dev/null +++ b/reference_code/text_image.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/text_image_v2.py b/reference_code/text_image_v2.py new file mode 100644 index 0000000..658a7fe --- /dev/null +++ b/reference_code/text_image_v2.py @@ -0,0 +1,150 @@ +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_v2: + + def __init__(self): + self.NODE_NAME = 'TextImageV2' + + @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()) + + if size_as is not None: + width, height = tensor2pil(size_as).size + text_table = [] + max_char_in_line = 0 + total_char = 0 + 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) + else: + char_horizontal_size = width // max_char_in_line + char_vertical_size = height // len(lines) + char_size = min(char_horizontal_size, char_vertical_size) + + # Adjust char_size based on scale + char_size = int(char_size * scale / 100) + char_size_h = int(char_size/width*height) + spacing = int(spacing * scale / 100) + leading = int(leading * scale / 100) + + # Calculate the starting position based on the center + text_width = char_size * max_char_in_line + spacing * (max_char_in_line - 1) + text_height = char_size * len(lines) + leading * (len(lines) - 1) + + # Adjust start_y to control the vertical position of the text center + start_x = int((width - text_width) * horizontal_border / 100) + + # Ensure text does not go out of bounds and smoothly transitions + start_y = int((height - text_height) * vertical_border / 100-(char_size_h-char_size)) + + # 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) + font_size = char_size + line_random[j] + 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 V2": TextImage_v2 +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: TextImage V2": "LayerUtility: TextImage V2" +} diff --git a/reference_code/text_join.py b/reference_code/text_join.py new file mode 100644 index 0000000..5b50235 --- /dev/null +++ b/reference_code/text_join.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/text_node.py b/reference_code/text_node.py new file mode 100644 index 0000000..6bf0f48 --- /dev/null +++ b/reference_code/text_node.py @@ -0,0 +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", +} \ No newline at end of file diff --git a/reference_code/vqa_prompt.py b/reference_code/vqa_prompt.py new file mode 100644 index 0000000..6d8cce1 --- /dev/null +++ b/reference_code/vqa_prompt.py @@ -0,0 +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" +} diff --git a/reference_code/water_color.py b/reference_code/water_color.py new file mode 100644 index 0000000..697a99b --- /dev/null +++ b/reference_code/water_color.py @@ -0,0 +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" +} \ No newline at end of file diff --git a/reference_code/xy2percent.py b/reference_code/xy2percent.py new file mode 100644 index 0000000..997a272 --- /dev/null +++ b/reference_code/xy2percent.py @@ -0,0 +1,43 @@ + +from .imagefunc import tensor2pil + +class XYtoPercent: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "background_image": ("IMAGE", ), # + "layer_image": ("IMAGE",), # + "x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + "y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}), + }, + "optional": { + } + } + + RETURN_TYPES = ("FLOAT", "FLOAT",) + RETURN_NAMES = ("x_percent", "y_percent",) + FUNCTION = 'xy_to_percent' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def xy_to_percent(self, background_image, layer_image, x, y,): + + _canvas = tensor2pil(background_image).convert('RGB') + _layer = tensor2pil(layer_image).convert('RGB') + x_percent = (x + _layer.width / 2) / _canvas.width * 100.0 + y_percent = (y + _layer.height / 2) / _canvas.height * 100.0 + + return (x_percent, y_percent,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: XY to Percent": XYtoPercent +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: XY to Percent": "LayerUtility: XY to Percent" +} \ No newline at end of file