From 2a9ebc4ae5946b913e54529d20e0c26dcc4df113 Mon Sep 17 00:00:00 2001 From: Jordan Thompson Date: Sat, 30 Sep 2023 23:30:10 -0700 Subject: [PATCH] Refactor for ComfyUI Manager compatibility --- ...nditioningBlend.py => ConditioningBlend.py | 378 +++++++++--------- ...odeForInpaint.py => VAEEncodeForInpaint.py | 120 +++--- WAS_VividSharpen.py => VividSharpen.py | 202 +++++----- __init__.py | 38 ++ __pycache__/ConditioningBlend.cpython-310.pyc | Bin 0 -> 6106 bytes .../VAEEncodeForInpaint.cpython-310.pyc | Bin 0 -> 2251 bytes __pycache__/VividSharpen.cpython-310.pyc | Bin 0 -> 3722 bytes __pycache__/__init__.cpython-310.pyc | Bin 0 -> 935 bytes 8 files changed, 388 insertions(+), 350 deletions(-) rename WAS_ConditioningBlend.py => ConditioningBlend.py (97%) rename WAS_VAEEncodeForInpaint.py => VAEEncodeForInpaint.py (97%) rename WAS_VividSharpen.py => VividSharpen.py (96%) create mode 100644 __init__.py create mode 100644 __pycache__/ConditioningBlend.cpython-310.pyc create mode 100644 __pycache__/VAEEncodeForInpaint.cpython-310.pyc create mode 100644 __pycache__/VividSharpen.cpython-310.pyc create mode 100644 __pycache__/__init__.cpython-310.pyc diff --git a/WAS_ConditioningBlend.py b/ConditioningBlend.py similarity index 97% rename from WAS_ConditioningBlend.py rename to ConditioningBlend.py index 4279f7c..b76524d 100644 --- a/WAS_ConditioningBlend.py +++ b/ConditioningBlend.py @@ -1,189 +1,189 @@ -import torch -import math - -def normalize(latent, target_min=None, target_max=None): - """ - Normalize a tensor `latent` between `target_min` and `target_max`. - - Args: - latent (torch.Tensor): The input tensor to be normalized. - target_min (float, optional): The minimum value after normalization. - - When `None` min will be tensor min range value. - target_max (float, optional): The maximum value after normalization. - - When `None` max will be tensor max range value. - - Returns: - torch.Tensor: The normalized tensor - """ - min_val = latent.min() - max_val = latent.max() - - if target_min is None: - target_min = min_val - if target_max is None: - target_max = max_val - - normalized = (latent - min_val) / (max_val - min_val) - scaled = normalized * (target_max - target_min) + target_min - return scaled - -def slerp(a, b, t): - """ - Perform Spherical Linear Interpolation (SLERP) between two tensors. - - This function interpolates between two input tensors `a` and `b` using SLERP, - which is a method for smoothly transitioning between orientations or vectors - represented as tensors. - - Args: - a (tensor): The first input tensor. - b (tensor): The second input tensor. - t (float): The blending factor, a value between 0 and 1 that controls the interpolation. - - Returns: - tensor: The result of SLERP interpolation between `a` and `b`. - - Note: - SLERP provides a smooth, shortest-path interpolation between two orientations or vectors - represented as tensors. It's commonly used in applications like 3D graphics and robotics. - """ - if a.shape != b.shape: - raise ValueError("Input tensors a and b must have the same shape.") - - a = torch.nn.functional.normalize(a, dim=-1) - b = torch.nn.functional.normalize(b, dim=-1) - - dot_product = torch.sum(a * b, dim=-1).clamp(-1.0, 1.0) - angle = torch.acos(dot_product) - - slerp_result = ( - (a * torch.sin((1 - t) * angle) + b * torch.sin(t * angle)) / - torch.sin(angle) - ) - - slerp_result = normalize(slerp_result) - - return slerp_result - -def hslerp(a, b, t): - """ - Perform Hybrid Spherical Linear Interpolation (HSLERP) between two tensors. - - This function combines two input tensors `a` and `b` using HSLERP, which is a specialized - interpolation method for smooth transitions between orientations or colors. - - Args: - a (tensor): The first input tensor. - b (tensor): The second input tensor. - t (float): The blending factor, a value between 0 and 1 that controls the interpolation. - - Returns: - tensor: The result of HSLERP interpolation between `a` and `b`. - - Note: - HSLERP provides smooth transitions between orientations or colors, particularly useful - in applications like image processing and 3D graphics. - """ - if a.shape != b.shape: - raise ValueError("Input tensors a and b must have the same shape.") - - num_channels = a.size(1) - - interpolation_tensor = torch.zeros(1, num_channels, 1, 1, device=a.device, dtype=a.dtype) - interpolation_tensor[0, 0, 0, 0] = 1.0 - - result = (1 - t) * a + t * b - - if t < 0.5: - result += (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor - else: - result -= (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor - - return result - -import torch - -blending_modes = { - # Linearly combines the two input tensors a and b using the parameter t. - 'add': lambda a, b, t: (a * t + b * (1 - t)), - - # Interpolates between tensors a and b using normalized linear interpolation. - 'bislerp': lambda a, b, t: (a * (1 - t) + b * t), - - # Interpolates between tensors a and b using cosine interpolation. - 'cosine interp': lambda a, b, t: (a + b - (a - b) * torch.cos(t * torch.tensor(math.pi))) / 2, - - # Interpolates between tensors a and b using cubic interpolation. - 'cuberp': lambda a, b, t: a + (b - a) * (3 * t ** 2 - 2 * t ** 3), - - # Computes the absolute difference between tensors a and b, scaled by t. - 'difference': lambda a, b, t: (abs(a - b) * t), - - # Combines tensors a and b using an exclusion formula, scaled by t. - 'exclusion': lambda a, b, t: ((a + b - 2 * a * b) * t), - - # Interpolates between tensors a and b using normalized linear interpolation, - # with a twist when t is greater than or equal to 0.5. - 'hslerp': lambda a, b, t: (a * (1 - t) + b * t) if t < 0.5 else (a * t + b * (1 - t)), - - # Adds tensor b to tensor a, scaled by t. - 'inject': lambda a, b, t: (a + b * t), - - # Interpolates between tensors a and b using linear interpolation. - 'lerp': lambda a, b, t: (a * (1 - t) + b * t), - - # Generates random values and combines tensors a and b with random weights, scaled by t. - 'random': lambda a, b, t: (a + (torch.rand_like(b) * b - a) * t), - - # Interpolates between tensors a and b using spherical linear interpolation (SLERP). - 'slerp': lambda a, b, t: (a * (1 - t) + b * t), - - # Subtracts tensor b from tensor a, scaled by t. - 'subtract': lambda a, b, t: (a * t - b * t), -} - -class WAS_ConditioningBlend: - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "conditioning_a": ("CONDITIONING", ), - "conditioning_b": ("CONDITIONING", ), - "blending_mode": (list(blending_modes.keys()), ), - "blending_strength": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - - RETURN_TYPES = ("CONDITIONING",) - RETURN_NAMES = ("conditioning",) - FUNCTION = "combine" - - CATEGORY = "conditioning" - - def combine(self, conditioning_a, conditioning_b, blending_mode, blending_strength, seed): - - if seed > 0: - torch.manual_seed(seed) - - a = conditioning_a[0][0].clone() - b = conditioning_b[0][0].clone() - - pa = conditioning_a[0][1]["pooled_output"].clone() - pb = conditioning_b[0][1]["pooled_output"].clone() - - cond = normalize(blending_modes[blending_mode](a, b, 1 - blending_strength)) - pooled = normalize(blending_modes[blending_mode](pa, pb, 1 - blending_strength)) - - conditioning = [[cond, {"pooled_output": pooled}]] - - return (conditioning, ) - - -NODE_CLASS_MAPPINGS = { - "ConditioningBlend": WAS_ConditioningBlend, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "ConditioningBlend": "Conditioning (Blend)", -} +import torch +import math + +def normalize(latent, target_min=None, target_max=None): + """ + Normalize a tensor `latent` between `target_min` and `target_max`. + + Args: + latent (torch.Tensor): The input tensor to be normalized. + target_min (float, optional): The minimum value after normalization. + - When `None` min will be tensor min range value. + target_max (float, optional): The maximum value after normalization. + - When `None` max will be tensor max range value. + + Returns: + torch.Tensor: The normalized tensor + """ + min_val = latent.min() + max_val = latent.max() + + if target_min is None: + target_min = min_val + if target_max is None: + target_max = max_val + + normalized = (latent - min_val) / (max_val - min_val) + scaled = normalized * (target_max - target_min) + target_min + return scaled + +def slerp(a, b, t): + """ + Perform Spherical Linear Interpolation (SLERP) between two tensors. + + This function interpolates between two input tensors `a` and `b` using SLERP, + which is a method for smoothly transitioning between orientations or vectors + represented as tensors. + + Args: + a (tensor): The first input tensor. + b (tensor): The second input tensor. + t (float): The blending factor, a value between 0 and 1 that controls the interpolation. + + Returns: + tensor: The result of SLERP interpolation between `a` and `b`. + + Note: + SLERP provides a smooth, shortest-path interpolation between two orientations or vectors + represented as tensors. It's commonly used in applications like 3D graphics and robotics. + """ + if a.shape != b.shape: + raise ValueError("Input tensors a and b must have the same shape.") + + a = torch.nn.functional.normalize(a, dim=-1) + b = torch.nn.functional.normalize(b, dim=-1) + + dot_product = torch.sum(a * b, dim=-1).clamp(-1.0, 1.0) + angle = torch.acos(dot_product) + + slerp_result = ( + (a * torch.sin((1 - t) * angle) + b * torch.sin(t * angle)) / + torch.sin(angle) + ) + + slerp_result = normalize(slerp_result) + + return slerp_result + +def hslerp(a, b, t): + """ + Perform Hybrid Spherical Linear Interpolation (HSLERP) between two tensors. + + This function combines two input tensors `a` and `b` using HSLERP, which is a specialized + interpolation method for smooth transitions between orientations or colors. + + Args: + a (tensor): The first input tensor. + b (tensor): The second input tensor. + t (float): The blending factor, a value between 0 and 1 that controls the interpolation. + + Returns: + tensor: The result of HSLERP interpolation between `a` and `b`. + + Note: + HSLERP provides smooth transitions between orientations or colors, particularly useful + in applications like image processing and 3D graphics. + """ + if a.shape != b.shape: + raise ValueError("Input tensors a and b must have the same shape.") + + num_channels = a.size(1) + + interpolation_tensor = torch.zeros(1, num_channels, 1, 1, device=a.device, dtype=a.dtype) + interpolation_tensor[0, 0, 0, 0] = 1.0 + + result = (1 - t) * a + t * b + + if t < 0.5: + result += (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor + else: + result -= (torch.norm(b - a, dim=1, keepdim=True) / 6) * interpolation_tensor + + return result + +import torch + +blending_modes = { + # Linearly combines the two input tensors a and b using the parameter t. + 'add': lambda a, b, t: (a * t + b * (1 - t)), + + # Interpolates between tensors a and b using normalized linear interpolation. + 'bislerp': lambda a, b, t: (a * (1 - t) + b * t), + + # Interpolates between tensors a and b using cosine interpolation. + 'cosine interp': lambda a, b, t: (a + b - (a - b) * torch.cos(t * torch.tensor(math.pi))) / 2, + + # Interpolates between tensors a and b using cubic interpolation. + 'cuberp': lambda a, b, t: a + (b - a) * (3 * t ** 2 - 2 * t ** 3), + + # Computes the absolute difference between tensors a and b, scaled by t. + 'difference': lambda a, b, t: (abs(a - b) * t), + + # Combines tensors a and b using an exclusion formula, scaled by t. + 'exclusion': lambda a, b, t: ((a + b - 2 * a * b) * t), + + # Interpolates between tensors a and b using normalized linear interpolation, + # with a twist when t is greater than or equal to 0.5. + 'hslerp': lambda a, b, t: (a * (1 - t) + b * t) if t < 0.5 else (a * t + b * (1 - t)), + + # Adds tensor b to tensor a, scaled by t. + 'inject': lambda a, b, t: (a + b * t), + + # Interpolates between tensors a and b using linear interpolation. + 'lerp': lambda a, b, t: (a * (1 - t) + b * t), + + # Generates random values and combines tensors a and b with random weights, scaled by t. + 'random': lambda a, b, t: (a + (torch.rand_like(b) * b - a) * t), + + # Interpolates between tensors a and b using spherical linear interpolation (SLERP). + 'slerp': lambda a, b, t: (a * (1 - t) + b * t), + + # Subtracts tensor b from tensor a, scaled by t. + 'subtract': lambda a, b, t: (a * t - b * t), +} + +class WAS_ConditioningBlend: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "conditioning_a": ("CONDITIONING", ), + "conditioning_b": ("CONDITIONING", ), + "blending_mode": (list(blending_modes.keys()), ), + "blending_strength": ("FLOAT", {"default": 0.5, "min": -10.0, "max": 10.0, "step": 0.001}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + + RETURN_TYPES = ("CONDITIONING",) + RETURN_NAMES = ("conditioning",) + FUNCTION = "combine" + + CATEGORY = "conditioning" + + def combine(self, conditioning_a, conditioning_b, blending_mode, blending_strength, seed): + + if seed > 0: + torch.manual_seed(seed) + + a = conditioning_a[0][0].clone() + b = conditioning_b[0][0].clone() + + pa = conditioning_a[0][1]["pooled_output"].clone() + pb = conditioning_b[0][1]["pooled_output"].clone() + + cond = normalize(blending_modes[blending_mode](a, b, 1 - blending_strength)) + pooled = normalize(blending_modes[blending_mode](pa, pb, 1 - blending_strength)) + + conditioning = [[cond, {"pooled_output": pooled}]] + + return (conditioning, ) + + +NODE_CLASS_MAPPINGS = { + "ConditioningBlend": WAS_ConditioningBlend, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ConditioningBlend": "Conditioning (Blend)", +} diff --git a/WAS_VAEEncodeForInpaint.py b/VAEEncodeForInpaint.py similarity index 97% rename from WAS_VAEEncodeForInpaint.py rename to VAEEncodeForInpaint.py index 2d14e04..2989dbc 100644 --- a/WAS_VAEEncodeForInpaint.py +++ b/VAEEncodeForInpaint.py @@ -1,61 +1,61 @@ -# Provides demonstration of PR https://github.com/comfyanonymous/ComfyUI/pull/1574/commits/297d1cff422198806cda40e4b6d71a6e6aa05453 - -import torch - -class WAS_VAEEncodeForInpaint: - @classmethod - def INPUT_TYPES(s): - return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ), "mask_offset": ("INT", {"default": 6, "min": -128, "max": 128, "step": 1}),}} - RETURN_TYPES = ("LATENT",) - FUNCTION = "encode" - - CATEGORY = "latent/inpaint" - - def encode(self, vae, pixels, mask, mask_offset=6): - x = (pixels.shape[1] // 8) * 8 - y = (pixels.shape[2] // 8) * 8 - mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear") - - pixels = pixels.clone() - if pixels.shape[1] != x or pixels.shape[2] != y: - x_offset = (pixels.shape[1] % 8) // 2 - y_offset = (pixels.shape[2] % 8) // 2 - pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:] - mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset] - - mask_erosion = self.modify_mask(mask, mask_offset) - - m = (1.0 - mask_erosion.round()).squeeze(1) - for i in range(3): - pixels[:,:,:,i] -= 0.5 - pixels[:,:,:,i] *= m - pixels[:,:,:,i] += 0.5 - t = vae.encode(pixels) - - return ({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, ) - - def modify_mask(self, mask, modify_by): - if modify_by == 0: - return mask - if modify_by > 0: - kernel_size = 2 * modify_by + 1 - kernel_tensor = torch.ones((1, 1, kernel_size, kernel_size)) - padding = modify_by - modified_mask = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1) - else: - kernel_size = 2 * abs(modify_by) + 1 - kernel_tensor = torch.ones((1, 1, kernel_size, kernel_size)) - padding = abs(modify_by) - eroded_mask = torch.nn.functional.conv2d(1 - mask.round(), kernel_tensor, padding=padding) - modified_mask = torch.clamp(1 - eroded_mask, 0, 1) - return modified_mask - - - -NODE_CLASS_MAPPINGS = { - "VAEEncodeForInpaint (WAS)": WAS_VAEEncodeForInpaint, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "VAEEncodeForInpaint (WAS)": "Inpainting VAE Encode (WAS)", +# Provides demonstration of PR https://github.com/comfyanonymous/ComfyUI/pull/1574/commits/297d1cff422198806cda40e4b6d71a6e6aa05453 + +import torch + +class WAS_VAEEncodeForInpaint: + @classmethod + def INPUT_TYPES(s): + return {"required": { "pixels": ("IMAGE", ), "vae": ("VAE", ), "mask": ("MASK", ), "mask_offset": ("INT", {"default": 6, "min": -128, "max": 128, "step": 1}),}} + RETURN_TYPES = ("LATENT",) + FUNCTION = "encode" + + CATEGORY = "latent/inpaint" + + def encode(self, vae, pixels, mask, mask_offset=6): + x = (pixels.shape[1] // 8) * 8 + y = (pixels.shape[2] // 8) * 8 + mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(pixels.shape[1], pixels.shape[2]), mode="bilinear") + + pixels = pixels.clone() + if pixels.shape[1] != x or pixels.shape[2] != y: + x_offset = (pixels.shape[1] % 8) // 2 + y_offset = (pixels.shape[2] % 8) // 2 + pixels = pixels[:,x_offset:x + x_offset, y_offset:y + y_offset,:] + mask = mask[:,:,x_offset:x + x_offset, y_offset:y + y_offset] + + mask_erosion = self.modify_mask(mask, mask_offset) + + m = (1.0 - mask_erosion.round()).squeeze(1) + for i in range(3): + pixels[:,:,:,i] -= 0.5 + pixels[:,:,:,i] *= m + pixels[:,:,:,i] += 0.5 + t = vae.encode(pixels) + + return ({"samples":t, "noise_mask": (mask_erosion[:,:,:x,:y].round())}, ) + + def modify_mask(self, mask, modify_by): + if modify_by == 0: + return mask + if modify_by > 0: + kernel_size = 2 * modify_by + 1 + kernel_tensor = torch.ones((1, 1, kernel_size, kernel_size)) + padding = modify_by + modified_mask = torch.clamp(torch.nn.functional.conv2d(mask.round(), kernel_tensor, padding=padding), 0, 1) + else: + kernel_size = 2 * abs(modify_by) + 1 + kernel_tensor = torch.ones((1, 1, kernel_size, kernel_size)) + padding = abs(modify_by) + eroded_mask = torch.nn.functional.conv2d(1 - mask.round(), kernel_tensor, padding=padding) + modified_mask = torch.clamp(1 - eroded_mask, 0, 1) + return modified_mask + + + +NODE_CLASS_MAPPINGS = { + "VAEEncodeForInpaint (WAS)": WAS_VAEEncodeForInpaint, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VAEEncodeForInpaint (WAS)": "Inpainting VAE Encode (WAS)", } \ No newline at end of file diff --git a/WAS_VividSharpen.py b/VividSharpen.py similarity index 96% rename from WAS_VividSharpen.py rename to VividSharpen.py index db2bca7..7e5afc3 100644 --- a/WAS_VividSharpen.py +++ b/VividSharpen.py @@ -1,101 +1,101 @@ -import torch -import numpy as np -from PIL import Image, ImageOps, ImageFilter, ImageEnhance - -# Tensor to PIL -def tensor2pil(image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - -# PIL to Tensor -def pil2tensor(image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - -# Vivid Light and Overlay methods adopted from layeris (an overlooked gem) -# https://github.com/subwaymatch/layer-is-python -def vivid_light(A, B, opacity=1.0): - with np.errstate(divide='ignore', invalid='ignore'): - b = np.where(B > 0, 1 - (1 - A) / (2 * B), 0) - d = np.where(B < 1, A / (2 * (1 - B)), 1) - - result = np.clip(np.where(B <= 0.5, b, d), 0, 1) - return alpha_blend(A, result, opacity) - -def overlay(A, B, opacity=1.0): - B = rgb_float_if_hex(B) - d1 = (2 * A) * B - d2 = 1 - 2 * (1 - A) * (1 - B) - result = np.where(A <= 0.5, d1, d2) - return alpha_blend(A, result, opacity) - -def alpha_blend(base, blend, opacity): - if opacity < 1.0: - return base * (1.0 - opacity) + blend * opacity - return blend - -def hex_to_rgb_float(hex_string): - return np.array(list((int(hex_string.lstrip('#')[i:i + 2], 16) / 255) for i in (0, 2, 4))) - -def rgb_float_if_hex(blend_data): - if isinstance(blend_data, str): - return hex_to_rgb_float(blend_data) - return blend_data - -def vivid_sharpen(image, radius=5, strength=1.0): - original = image.copy() - sg = Image.new('RGB', original.size, (255, 255, 255)) - sg.paste(original, (0, 0)) - sg = ImageOps.invert(sg) - sg = sg.filter(ImageFilter.GaussianBlur(radius=radius)) - - original_data = np.array(original).astype(float) / 255.0 - sg_data = np.array(sg).astype(float) / 255.0 - - result_data = vivid_light(original_data, sg_data, 1.0) - result_data = overlay(original_data, result_data, 1.0) - - result_image = Image.fromarray((result_data * 255).astype('uint8')) - result_image = Image.blend(original, result_image, strength) - - return result_image - -class VividSharpen: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "images": ("IMAGE",), - "radius": ("FLOAT", {"default": 1.5, "min": 0.01, "max": 64.0, "step": 0.01}), - "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - RETURN_NAMES = ("images",) - - FUNCTION = "sharpen" - - CATEGORY = "image/postprocessing" - - def sharpen(self, images, radius, strength): - - results = [] - if images.size(0) > 1: - for image in images: - image = tensor2pil(image) - results.append(pil2tensor(vivid_sharpen(image, radius=radius, strength=strength))) - results = torch.cat(results, dim=0) - else: - results = pil2tensor(vivid_sharpen(tensor2pil(images), radius=radius, strength=strength)) - - return (results,) - -NODE_CLASS_MAPPINGS = { - "VividSharpen": VividSharpen, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "VividSharpen": "VividSharpen", -} +import torch +import numpy as np +from PIL import Image, ImageOps, ImageFilter, ImageEnhance + +# Tensor to PIL +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +# PIL to Tensor +def pil2tensor(image): + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + +# Vivid Light and Overlay methods adopted from layeris (an overlooked gem) +# https://github.com/subwaymatch/layer-is-python +def vivid_light(A, B, opacity=1.0): + with np.errstate(divide='ignore', invalid='ignore'): + b = np.where(B > 0, 1 - (1 - A) / (2 * B), 0) + d = np.where(B < 1, A / (2 * (1 - B)), 1) + + result = np.clip(np.where(B <= 0.5, b, d), 0, 1) + return alpha_blend(A, result, opacity) + +def overlay(A, B, opacity=1.0): + B = rgb_float_if_hex(B) + d1 = (2 * A) * B + d2 = 1 - 2 * (1 - A) * (1 - B) + result = np.where(A <= 0.5, d1, d2) + return alpha_blend(A, result, opacity) + +def alpha_blend(base, blend, opacity): + if opacity < 1.0: + return base * (1.0 - opacity) + blend * opacity + return blend + +def hex_to_rgb_float(hex_string): + return np.array(list((int(hex_string.lstrip('#')[i:i + 2], 16) / 255) for i in (0, 2, 4))) + +def rgb_float_if_hex(blend_data): + if isinstance(blend_data, str): + return hex_to_rgb_float(blend_data) + return blend_data + +def vivid_sharpen(image, radius=5, strength=1.0): + original = image.copy() + sg = Image.new('RGB', original.size, (255, 255, 255)) + sg.paste(original, (0, 0)) + sg = ImageOps.invert(sg) + sg = sg.filter(ImageFilter.GaussianBlur(radius=radius)) + + original_data = np.array(original).astype(float) / 255.0 + sg_data = np.array(sg).astype(float) / 255.0 + + result_data = vivid_light(original_data, sg_data, 1.0) + result_data = overlay(original_data, result_data, 1.0) + + result_image = Image.fromarray((result_data * 255).astype('uint8')) + result_image = Image.blend(original, result_image, strength) + + return result_image + +class VividSharpen: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "radius": ("FLOAT", {"default": 1.5, "min": 0.01, "max": 64.0, "step": 0.01}), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + + FUNCTION = "sharpen" + + CATEGORY = "image/postprocessing" + + def sharpen(self, images, radius, strength): + + results = [] + if images.size(0) > 1: + for image in images: + image = tensor2pil(image) + results.append(pil2tensor(vivid_sharpen(image, radius=radius, strength=strength))) + results = torch.cat(results, dim=0) + else: + results = pil2tensor(vivid_sharpen(tensor2pil(images), radius=radius, strength=strength)) + + return (results,) + +NODE_CLASS_MAPPINGS = { + "VividSharpen": VividSharpen, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VividSharpen": "VividSharpen", +} diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..159d650 --- /dev/null +++ b/__init__.py @@ -0,0 +1,38 @@ +import importlib +import time + +extras = [ + ".ConditioningBlend", + ".VAEEncodeForInpaint", + ".VividSharpen", +] + +NODE_CLASS_MAPPINGS = {} +NODE_DISPLAY_NAME_MAPPINGS = {} +module_timings = {} + +print("[\033[94m\033[1mWAS Extras\033[0m] Loading extra custom nodes...") + +for module_name in extras: + start_time = time.time() + + success = True + try: + module = importlib.import_module(module_name, package=__name__) + except Exception: + success = False + pass + + end_time = time.time() + timing = end_time - start_time + + module_timings[module.__file__] = (timing, success) + + NODE_CLASS_MAPPINGS.update(getattr(module, 'NODE_CLASS_MAPPINGS', {})) + NODE_DISPLAY_NAME_MAPPINGS.update(getattr(module, 'NODE_DISPLAY_NAME_MAPPINGS', {})) + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] + +print("[\033[94m\033[1mWAS Extras\033[0m] Import times for extras:") +for module, (timing, success) in module_timings.items(): + print(f" {timing:.1f} seconds{('' if success else ' (IMPORT FAILED)')}: {module}") diff --git a/__pycache__/ConditioningBlend.cpython-310.pyc b/__pycache__/ConditioningBlend.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b47c40929e29acd1ba4d415a9f025f3d7adcc243 GIT binary patch literal 6106 zcmds5&2QYs73Yv#E_bz3{Gr6M(iBWiiX%W-tCrlvNP+fP7==~jVxa#+d+fP~9tJ%W=%FxrX%Y027FB<5$fZ_aHiF#B z#Xb(_^MJE(}PSInE9#`~)q9+xdRP_0WwbjxU zjlaN8f#0e7`l`jJJj-XRWj@7E<85=ctkqw90y(ut+n`tel<}tTIsD^AkW_o5ZEITw z-VARuWtSu;|BrFpRb#$ zgd47V8OwvRxSL1H;%sKTvOr*NCpw$VGq z$VU|fT>gDZ^-0ZbMxmG>81FL*NrYh8{g=Hlnen!7!(Q| zF-gv}5hdO(x+()XeG%RE5lxUcGsrn7?jr2GI62>U6PWE#=Hxa{vk*b$5ms^*l2>DB zpx28+Sbq@1h`>y@-w)unkA`$0Lf2% zT22oT;dI^G0A%Pkc6%UQx9`n7*}+q1(%^n?9bt{MlqfOsZHAKgnoie>s9uOVKaS4-?2-kE?!3C z0pPMxIY&dk067e}u=DF&2lN;T0F3LY0l30`Pf)gD0m`f`lbb1k>LOrE!4mEI*!?Fp z?EJ_BxOGbtm(mi(ms6H%Qv`2iZhvm@%8Lrb-c}jIvi7t-@8o^zRre43A?(AuE#dQL zBH4#u6UyLnEkGiI4$k{5_{x}{b4IW$?t5)tWjS1-(B8;6fNUeUwu_wmW2PMi0JTH2 zpZ{;<%Jh2#x;zWI^1%gmWytk46n@U>y8>7;2wah2P-hS*qY`i(ibj6VMNWq)+g_|T zID(rIh{{k&Od#$Ar5ebUEbiU*+ny}(6q_V`!J0xTjE<(^AZt)5m^fdy=J@XhM^b4 za&nK~HnZJPT8D#?)m3P88EoPgKx!v#R@J9!T zHxeP8QX7tQ2Av~LY0KJUy`fa4!0)lKPwf4;XkP@%wz&Il0CryjGr&c>f!BeFX>ywQ z%WUG|lmIJ;Kog4@v}XKYFG`e!3PNDTG@`N>_K^cAKqPzZ5 zIB(uUJ_H>K()X?hAGyv?hcdizP=-Q>r$+T7K~ z<%N}ntBr-m6m@lsXSd<0vgz*oZprcCiLKegZGt>re8vf^ zw+=WqW6$Gt1LpGML{1bPsDCWYTi)H65Uw4>S(p|Zh#XsJEL~q|uH0CfU;ZToQvj(O zI@4K=9b*&B{I|Jt@8v@+Gcz()EMPF^sEw{gmh(hb+(2uaz<>DAT}H7P{2Cu=37}?* ziVE7&He+fBJ`Dq(#x{i;KwLafx%fs#ufBEE8F?Q&?=xCgNsI)ygQvCBO3MIDb4t5! z;98u-k>ljlXCnE(NaTOn8iO3I#TqCkfq{xJ@xw`nbq)EDzRyLVZ;Xdn$ zRSf(B|CrJT8^l5Z~R~`{T1yN?MvfR46K?ssRNFBwA3_V!0-=Xxu1vK(&HS7w@{g#J+R( z&TSH%rAkR3EAavPkUa7Y;2ZD>c-SW-p8Uo$NQ9ZQ9bD9iJ?-4?&fLu0%x`9+nHiVB z_|;vIKj#Sf9XHd%fX!Dh;}3xdB4~#+$qErn7)L}Hhpb5jmo%VFCb%%+Z3tQ?MQaH1 zq}D<@JWSy=3mrd^Ve&~Z3chkcTgwD888fiJ!z=^8 zK#)?l4Ov(K_$pa>7Y-$Ic0vC&xKKPw2o+T#-AZ_sRFS2l4 zKcL4@DXcxJ6osWV#Ow|cMMJ2C)8XGhw6hg-f>8RZ$N>Jq0pcA1`WU2O{BduHJz%{1 z?H&l(vES`=WUSpV3S#MDT@(x2jGy>Dsm&x(ttXm=p?2H-u$2T+=y!A;%3Z2ngc#Je zl4IdkCkiE$RwRPLP5^TS(qUG7Ci0 zJx%wuvo|hL?d(rhg{*R=iem6Z)2^n0rpdp>tKLB+Xy5|O_!^K|I!o_Thh2sDEWHU& zfz7f4y$;U}>e7$d!F%Ub;i%kN7`Dm4xZ5zZ%K8ZwfC#$7l$AnuvNc!B0;=FLAbgGt zjD#Fh=zRuY(7*~B@RSWqIAsaJYTGse2~asHPoe5wVna4gjDeL}b*R(?D&&x!uWQ6d zeIs39C(|~6rf*=QEjYL5u@9f=E}Auk-BT?hK}bWVKX1D5nlBKt?&eM!9|QuUXh*jyXNQt4@KlR-{?mEV|n9 zywLAT&(p5wfk?lD>w@R)_WjP7lZOz*aaSf!BB8E;1G=zYX>6?5Mm=3SUvJdPjp{?V z$I8&FS01iE%3!F_>#0AauF=j9ZqlCaF*-2L3g}tmI0vKZ~XJK#{vnxhyTFq2@+|biA zRQCXO+EOArN-iil<+O+KB@&nXBe~~Lr;T!mxaAnFlqJ7c?J>4Vx2oz@)vJ&9^?TKy zn5Zy(f3KVk|5jt{pVT@0+34IrN&kdOGRb4sE?VBU(6?f{C)%Rtw4Gk5UFx}QmosTg zvCGZ$DrB*`vjNZ+-K@ivJ=?WdB9#5Be$#9>?K$;!$Uq zX8oR@^ktYn`f{P=FTKoEkUsh%dLGHvMxX{^GLJ(mS(v1Kb!8C6C2XG3RX^gYj^;hZ zI2bgFxd>vcn4InLk$@7MJudki^-9t*>ad%N_B5(!xZ273scKbD#?ouzz3gkh6|Ix`>Bj z?*C^H8GC~J;JXX%7{ZvT&Ih$qD$$6d45d}kU+xr~iDa$bj9zF{FdfF(aWz+;(*^uE>TYBwuvZF9U{m}ezrfFbcV5)E z`Y8t9Q}ltO`~tHX%lQu9hN^R@i&VO8BX*#8QZmI+p~zXbEiw)xK;2^%K{@sz<%p)b zfF0HMsrmsbMEaEKKK6~*_|dw*5xz94P?w1117azu9}@i|s(y@0Tk74;JuIp$Yx z|9Pn5VDk*-QqrEsYkZbZqgEfIdqh<7FDk~TE`Z1()>Eb?Ap*}W5-vwuXJ=Ro6s&7C z)g1WDsSR6;$3Y5P8OxZv9lMSZ;Con*8u#UOF1Q(cFEnYMu9BWdRfOj6*}8G>T~FG# z*bZ#4k5NY!lONIIPigTdsD2CY{_4~=&)6RTeG>5u8L-hA_B-n=jI{&Un`2+Vr=M9; zKqlu~e#w~SjR{m2do~WIKqkzo3K6#P$$X6g_Y}`w8NU zASo9kkppJ-EC7%*Dgku38Fe%7Nf|~Juiz7iy2_}U+W_ubHUSWK<{09R#=w7Qtdvax zrb@E3V{g0J3DE97%|}z9-8vi9q{)Tb9`14yFz4lT6EFARDCMTM?=Rnkb;w8FRSm1K zA8EM30Y!m~hG~&HOtctwf}h4T(q@0KStL|D314WDM#z}X04Xz6WFNq1f*6MHt(#=8 z$Rl)ZIT)sC6eKs}p?c>XMbdE$d>AM*L9QrKsLOO1_2rfY|v2=9de8EKJDA+<_4y!tr^BM@vS(lA~>E}iH5Q4(doe~s1=;0V5T`tT|9 zV~+QZBVRYiQ7DiwBnG`BQQWV^I`#3EL>c!hU)vEuZMCUA6+RnADwOnuTDiNhyriAm zckV5$b+H+~|6K3>y8hSYe{Ahu-yd1MfsEqHa6LdW)OIgQPz5g`R~8Nobni0~$iUO+ zS(X~_P@iMNu^Q}7oF3Ogb)~ubV9j59xVqF@0Ix}H+v0 zTs=aAS7PBMTXl2#$Af;F4OG7q!o8Djhs-km2-go37^Blgip}{5ibR-OPlft0=eO8b z(+GE-J4n(FAj8URyh!jKA?SEzQ`!L5S2O1c00brwuPg$QbtsuQu@b4}9rt*y;WUe| zreuu}rw}qqf}7Yf>NQ-oL__q_#EW-;_oy`QH{;#vG!5IGAVdDXPkbc&yG^uP#8XO#>0P(Px6wO`L1Mb)sJX;U!l!LRl`-2pcneSuK0ehFNZPFHQ#?W z4B}!&wXmnIcH$sSdttWGm+DtEQoFyj_TYZAFz`XIxo~%>rM=q^nu}{I_u!I?3u{Zu z_wGM5cRb`HMgmGp>X)b}rp)C%fPiIJxF`z*zOZVRnTInFGD594&=mA3qcc^b0kXQb zS6A*Bq%^o_a8Oa+Fe)!pO;A%n-Umcc(lbe?umRmfpK9K_wd60}S!lKVy9=wUE6wGW zK4%7Rt+ZC}EIjl{$b-56CzJNB7RtOv@R43XMS{(8&-O&csd%32Ii6i{Ca1+E@m4tA G+y4RMW-4z0 literal 0 HcmV?d00001 diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a23a726aacd3b2d8ea475c3b94a22aa577732fb8 GIT binary patch literal 935 zcmZ`%%Wl&^6rHi1#Llx#gB0k-D?~OHMU@a$RS}Z3ij<}iDXnl)ja<)7aETxFIJEV) zYrddDYT5B8e8X&6@e2q6<2XfZn9;qX`#SeEeyNm4;(T3w2!{$n-xA^9qY2@qAof!* ziYT5TGkvj%r-(|Qu_-yolm#h*GLWV6HO2^}HagicbAWI09Wqs_m|Cm>I?F>23N-Uk zGKeMcr>mVN6k*hvwaG?e%SQ_%kiSP zWE?pk96Eewg#mc+^U?a~#iqMH+HjAXL%nkug%(>MJ#|m?o^MeBrbD9B?Kz8lSN8-O zGYsRffq5>l?Dy6bcKKTWpxq&@UUN7k`^~|i+us}VM+;@UI~??yZ%Mzo-}!5cpZz~= z*A4tI(j&(OrceAZ&Cj-CS=aSgHZ~@)sxz=fzl@`JMSs%W9~`_s)OVZRUZ>rNr7gYj zfNRbIXXc!8CBfp^MIS7b#mGYOuaH@GeL9~35oXaRGkSS*yJ z3mp^Ucuqu!5nOR~JFRjD3s|-(n2J=aN=jD2ig?AviX`DGE(%$}(l1p}aapQLwS~2Y IA7kbBAGf*+*8l(j literal 0 HcmV?d00001