diff --git a/WAS_Node_Suite.py b/WAS_Node_Suite.py index 6ba4bca..43c7b81 100644 --- a/WAS_Node_Suite.py +++ b/WAS_Node_Suite.py @@ -2,35 +2,43 @@ # # Copyright 2023 Jordan Thompson (WASasquatch) # -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to -# deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to +# deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. # -# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import torch, os, sys, subprocess, random, hashlib, json, time, requests +import cv2 +import nodes +import comfy.utils +import comfy.sd +import comfy.samplers +from typing import Optional +import torch +import os +import sys +import subprocess +import random +import hashlib +import json +import time +import requests import numpy as np from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops from PIL.PngImagePlugin import PngInfo from urllib.request import urlopen from io import BytesIO -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) +sys.path.insert(0, os.path.join( + os.path.dirname(os.path.realpath(__file__)), "comfy")) sys.path.append('../ComfyUI') -import comfy.samplers -import comfy.sd -import comfy.utils - -import nodes - -import cv2 # GLOBALS MIDAS_INSTALLED = False @@ -38,49 +46,66 @@ MIDAS_INSTALLED = False #! FUNCTIONS # Freeze PIP modules -def packages(): - import sys, subprocess + + +def packages() -> list[str]: + import sys + import subprocess return [r.decode().split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).split()] - + # Tensor to PIL -def tensor2pil(image): + + +def tensor2pil(image: torch.Tensor) -> Image.Image: return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - + # Convert PIL to Tensor -def pil2tensor(image): + + +def pil2tensor(image: Image.Image) -> torch.Tensor: return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - + # PIL Hex -def pil2hex(image): - return hashlib.sha256(np.array(tensor2pil(image)).astype(np.uint16).tobytes()).hexdigest().hex(); - + + +def pil2hex(image: torch.Tensor) -> str: + return hashlib.sha256(np.array(tensor2pil(image)).astype(np.uint16).tobytes()).hexdigest() + # Median Filter + + def medianFilter(img, diameter, sigmaColor, sigmaSpace): import cv2 as cv - diameter = int(diameter); sigmaColor = int(sigmaColor); sigmaSpace = int(sigmaSpace) + diameter = int(diameter) + sigmaColor = int(sigmaColor) + sigmaSpace = int(sigmaSpace) img = img.convert('RGB') img = cv.cvtColor(np.array(img), cv.COLOR_RGB2BGR) img = cv.bilateralFilter(img, diameter, sigmaColor, sigmaSpace) img = cv.cvtColor(np.array(img), cv.COLOR_BGR2RGB) return Image.fromarray(img).convert('RGB') + # INSTALLATION CLEANUP # Delete legacy nodes -legacy_was_nodes = ['fDOF_WAS.py','Image_Blank_WAS.py','Image_Blend_WAS.py','Image_Canny_Filter_WAS.py', 'Canny_Filter_WAS.py','Image_Combine_WAS.py','Image_Edge_Detection_WAS.py', 'Image_Film_Grain_WAS.py', 'Image_Filters_WAS.py', 'Image_Flip_WAS.py','Image_Nova_Filter_WAS.py','Image_Rotate_WAS.py','Image_Style_Filter_WAS.py','Latent_Noise_Injection_WAS.py','Latent_Upscale_WAS.py','MiDaS_Depth_Approx_WAS.py','NSP_CLIPTextEncoder.py','Samplers_WAS.py'] +legacy_was_nodes = ['fDOF_WAS.py', 'Image_Blank_WAS.py', 'Image_Blend_WAS.py', 'Image_Canny_Filter_WAS.py', 'Canny_Filter_WAS.py', 'Image_Combine_WAS.py', 'Image_Edge_Detection_WAS.py', 'Image_Film_Grain_WAS.py', 'Image_Filters_WAS.py', + 'Image_Flip_WAS.py', 'Image_Nova_Filter_WAS.py', 'Image_Rotate_WAS.py', 'Image_Style_Filter_WAS.py', 'Latent_Noise_Injection_WAS.py', 'Latent_Upscale_WAS.py', 'MiDaS_Depth_Approx_WAS.py', 'NSP_CLIPTextEncoder.py', 'Samplers_WAS.py'] legacy_was_nodes_found = [] f_disp = False +node_path_dir = os.getcwd()+'/ComfyUI/custom_nodes/' for f in legacy_was_nodes: - node_path_dir = os.getcwd()+'/ComfyUI/custom_nodes/' file = f'{node_path_dir}{f}' if os.path.exists(file): - import zipfile if not f_disp: - print('\033[34mWAS Node Suite:\033[0m Found legacy nodes. Archiving legacy nodes...') + print( + '\033[34mWAS Node Suite:\033[0m Found legacy nodes. Archiving legacy nodes...') f_disp = True legacy_was_nodes_found.append(file) if legacy_was_nodes_found: + import zipfile from os.path import basename - archive = zipfile.ZipFile(f'{node_path_dir}WAS_Legacy_Nodes_Backup_{round(time.time())}.zip', "w") + archive = zipfile.ZipFile( + f'{node_path_dir}WAS_Legacy_Nodes_Backup_{round(time.time())}.zip', "w") for f in legacy_was_nodes_found: archive.write(f, basename(f)) try: @@ -95,6 +120,7 @@ if f_disp: # IMAGE FILTER ADJUSTMENTS + class WAS_Image_Filters: def __init__(self): pass @@ -120,62 +146,64 @@ class WAS_Image_Filters: CATEGORY = "WAS Suite/Image" def image_filters(self, image, brightness, contrast, saturation, sharpness, blur, gaussian_blur, edge_enhance): - + pil_image = None - + # Apply NP Adjustments if brightness > 0.0 or brightness < 0.0: # Apply brightness image = np.clip(image + brightness, 0.0, 1.0) - + if contrast > 1.0 or contrast < 1.0: # Apply contrast image = np.clip(image * contrast, 0.0, 1.0) - + # Apply PIL Adjustments if saturation > 1.0 or saturation < 1.0: - #PIL Image + # PIL Image pil_image = tensor2pil(image) # Apply saturation pil_image = ImageEnhance.Color(pil_image).enhance(saturation) - + if sharpness > 1.0 or sharpness < 1.0: # Assign or create PIL Image pil_image = pil_image if pil_image else tensor2pil(image) # Apply sharpness pil_image = ImageEnhance.Sharpness(pil_image).enhance(sharpness) - + if blur > 0: # Assign or create PIL Image pil_image = pil_image if pil_image else tensor2pil(image) # Apply blur for _ in range(blur): pil_image = pil_image.filter(ImageFilter.BLUR) - + if gaussian_blur > 0.0: # Assign or create PIL Image pil_image = pil_image if pil_image else tensor2pil(image) # Apply Gaussian blur - pil_image = pil_image.filter(ImageFilter.GaussianBlur(radius = gaussian_blur)) - + pil_image = pil_image.filter( + ImageFilter.GaussianBlur(radius=gaussian_blur)) + if edge_enhance > 0.0: # Assign or create PIL Image pil_image = pil_image if pil_image else tensor2pil(image) # Edge Enhancement edge_enhanced_img = pil_image.filter(ImageFilter.EDGE_ENHANCE_MORE) # Blend Mask - blend_mask = Image.new(mode = "L", size = pil_image.size, color = (round(edge_enhance * 255))) + blend_mask = Image.new( + mode="L", size=pil_image.size, color=(round(edge_enhance * 255))) # Composite Original and Enhanced Version - pil_image = Image.composite(edge_enhanced_img, pil_image, blend_mask) + pil_image = Image.composite( + edge_enhanced_img, pil_image, blend_mask) # Clean-up del blend_mask, edge_enhanced_img - - # Output image - out_image = ( pil2tensor(pil_image) if pil_image else image ) - return ( out_image, ) - - + # Output image + out_image = (pil2tensor(pil_image) if pil_image else image) + + return (out_image, ) + # IMAGE STYLE FILTER @@ -228,21 +256,22 @@ class WAS_Image_Style_Filter: CATEGORY = "WAS Suite/Image" def image_style_filter(self, image, style): - + # Install Pilgram if 'pilgram' not in packages(): print("\033[34mWAS NS:\033[0m Installing Pilgram...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'pilgram']) - + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'pilgram']) + # Import Pilgram module import pilgram - + # Convert image to PIL image = tensor2pil(image) - + # WAS Filters WFilter = self.WAS_Filter_Class() - + # Apply blending match style: case "1977": @@ -255,8 +284,8 @@ class WAS_Image_Style_Filter: out_image = pilgram.brooklyn(image) case "clarendon": out_image = pilgram.clarendon(image) - case "cyberpunk": - out_image = WFilter.cyberpunk(image) + # case "cyberpunk": + # out_image = WFilter.cyberpunk(image) case "earlybird": out_image = pilgram.earlybird(image) case "fairy tale": @@ -303,24 +332,23 @@ class WAS_Image_Style_Filter: out_image = pilgram.xpro2(image) case _: out_image = image - + out_image = out_image.convert("RGB") - return ( torch.from_numpy(np.array(out_image).astype(np.float32) / 255.0).unsqueeze(0), ) - - + return (torch.from_numpy(np.array(out_image).astype(np.float32) / 255.0).unsqueeze(0), ) + # IN-HOUSE WAS FILTERS - + # HDR Effect - + class WAS_Filter_Class(): - + # Sparkle - Fairy Tale Filter - + def sparkle(self, image): - + import pilgram - + image = image.convert('RGBA') contrast_enhancer = ImageEnhance.Contrast(image) image = contrast_enhancer.enhance(1.25) @@ -358,14 +386,14 @@ class WAS_Image_Style_Filter: draw.point((x, y), fill=(r, g, b, 255)) particles2 = particles2.filter(ImageFilter.GaussianBlur(radius=1)) particles2.putalpha(128) - + image = pilgram.css.blending.color_dodge(image, particles) image = pilgram.css.blending.lighten(image, particles2) - #image = Image.alpha_composite(image, particles) + # image = Image.alpha_composite(image, particles) return image - - + + # COMBINE NODE class WAS_Image_Blending_Mode: @@ -404,19 +432,20 @@ class WAS_Image_Blending_Mode: CATEGORY = "WAS Suite/Image" def image_blending_mode(self, image_a, image_b, mode='add', blend_percentage=1.0): - + # Install Pilgram if 'pilgram' not in packages(): print("\033[34mWAS NS:\033[0m Installing Pilgram...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'pilgram']) - + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'pilgram']) + # Import Pilgram module import pilgram - + # Convert images to PIL img_a = tensor2pil(image_a) img_b = tensor2pil(image_b) - + # Apply blending match mode: case "color": @@ -449,20 +478,20 @@ class WAS_Image_Blending_Mode: out_image = pilgram.css.blending.soft_light(img_a, img_b) case _: out_image = img_a - + out_image = out_image.convert("RGB") - + # Blend image - blend_mask = Image.new(mode = "L", size = img_a.size, color = (round(blend_percentage * 255))) + blend_mask = Image.new(mode="L", size=img_a.size, + color=(round(blend_percentage * 255))) blend_mask = ImageOps.invert(blend_mask) out_image = Image.composite(img_a, out_image, blend_mask) - return ( pil2tensor(out_image), ) - + return (pil2tensor(out_image), ) + - # IMAGE BLEND NODE - + class WAS_Image_Blend: def __init__(self): pass @@ -483,20 +512,20 @@ class WAS_Image_Blend: CATEGORY = "WAS Suite/Image" def image_blend(self, image_a, image_b, blend_percentage): - + # Convert images to PIL img_a = tensor2pil(image_a) img_b = tensor2pil(image_b) # Blend image - blend_mask = Image.new(mode = "L", size = img_a.size, color = (round(blend_percentage * 255))) + blend_mask = Image.new(mode="L", size=img_a.size, + color=(round(blend_percentage * 255))) blend_mask = ImageOps.invert(blend_mask) img_result = Image.composite(img_a, img_b, blend_mask) - + del img_a, img_b, blend_mask - return ( pil2tensor(img_result), ) - + return (pil2tensor(img_result), ) # IMAGE TRANSPOSE @@ -524,10 +553,10 @@ class WAS_Image_Transpose: CATEGORY = "WAS Suite/Image" - def image_transpose(self, image, image_overlay, width, height, X, Y, rotation): - return ( pil2tensor(self.apply_transpose_image(tensor2pil(image), tensor2pil(image_overlay), (int(width), int(height)), (int(X), int(Y)), int(rotation))), ) - - def apply_transpose_image(self, base_image, transpose_image, size, location, rotation): + def image_transpose(self, image: torch.Tensor, image_overlay: torch.Tensor, width: int, height: int, X: int, Y: int, rotation: int): + return (pil2tensor(self.apply_transpose_image(tensor2pil(image), tensor2pil(image_overlay), (int(width), int(height)), (int(X), int(Y)), int(rotation))), ) + + def apply_transpose_image(self, base_image: Image.Image, transpose_image: Image.Image, size: tuple[int, int], location: tuple[int, int], rotation: int) -> Image.Image: # Resize the base image to the desired size transpose_image = transpose_image.resize(size) @@ -536,11 +565,10 @@ class WAS_Image_Transpose: transpose_image = transpose_image.rotate(rotation, expand=True) # Paste the transposed image onto the image - result_image = base_image.paste(transpose_image, location, transpose_image) + base_image.paste(transpose_image, location, transpose_image) # Return the resulting image - return result_image - + return base_image # IMAGE RESCALE @@ -554,8 +582,8 @@ class WAS_Image_Rescale: return { "required": { "image": ("IMAGE",), - "mode": (["rescale","resize"],), - "resampling": (["lanczos","nearest","bilinear","bicubic"],), + "mode": (["rescale", "resize"],), + "resampling": (["lanczos", "nearest", "bilinear", "bicubic"],), "rescale_factor": ("FLOAT", {"default": 2, "min": 0.01, "max": 16.0, "step": 0.01}), "resize_width": ("INT", {"default": 1024, "min": 1, "max": 48000, "step": 1}), "resize_height": ("INT", {"default": 1536, "min": 1, "max": 48000, "step": 1}), @@ -567,21 +595,23 @@ class WAS_Image_Rescale: CATEGORY = "WAS Suite/Image" - def image_rescale(self, image, mode="rescale", resampling="lanczos", rescale_factor=2, resize_width=1024, resize_height=1024): - return ( pil2tensor(self.apply_resize_image(tensor2pil(image), mode, rescale_factor, resize_width, resize_height, resampling)), ) - - def apply_resize_image(self, image, mode='scale', factor=None, width=None, height=None, resample='bicubic'): + def image_rescale(self, image: torch.Tensor, mode="rescale", resampling="lanczos", rescale_factor=2, resize_width=1024, resize_height=1024): + return (pil2tensor(self.apply_resize_image(tensor2pil(image), mode, rescale_factor, resize_width, resize_height, resampling)), ) + + def apply_resize_image(self, image: Image.Image, mode='scale', factor: int = 2, width: int = 1024, height: int = 1024, resample='bicubic'): # Get the current width and height of the image current_width, current_height = image.size - + # Calculate the new width and height based on the given mode and parameters if mode == 'rescale': - new_width, new_height = int(current_width * factor), int(current_height * factor) + new_width, new_height = int( + current_width * factor), int(current_height * factor) else: new_width = width if width % 8 == 0 else width + (8 - width % 8) - new_height = height if height % 8 == 0 else height + (8 - height % 8) - + new_height = height if height % 8 == 0 else height + \ + (8 - height % 8) + # Define a dictionary of resampling filters resample_filters = { 'nearest': 0, @@ -589,10 +619,11 @@ class WAS_Image_Rescale: 'bicubic': 3, 'lanczos': 1 } - + # Resize the image using the given resampling filter - resized_image = image.resize((new_width, new_height), resample=Image.Resampling(resample_filters(resample))) - + resized_image = image.resize( + (new_width, new_height), resample=Image.Resampling(resample_filters[resample])) + return resized_image @@ -606,7 +637,7 @@ class WAS_Load_Image_Batch: def INPUT_TYPES(cls): return { "required": { - "mode": (["single_image","incremental_image"],), + "mode": (["single_image", "incremental_image"],), "folder_path": ("STRING", {"default": './ComfyUI/input/', "multiline": False}), "image_id": ("INT", {"default": 0, "min": 0, "max": 1000, "step": 1}), "counter_name": ("STRING", {"default": 'counter_batch.txt', "multiline": False}), @@ -618,19 +649,19 @@ class WAS_Load_Image_Batch: CATEGORY = "WAS Suite/IO" - def load_batch_images(self, folder_path, image_id, mode="single_image", counter_name="counter_batch.txt"): + def load_batch_images(self, folder_path, image_id, mode="single_image", counter_name="counter_batch.txt") -> tuple[torch.Tensor] | tuple: - if os.path.exists(folder_path): - fl = self.BatchImageLoader(folder_path, counter_name) - if mode == 'single_image': - image = fl.get_image_by_id(image_id) - else: - image = fl.get_next_image() - self.image = image - + if not os.path.exists(folder_path): + return () + fl = self.BatchImageLoader(folder_path, counter_name) + if mode == 'single_image': + image = fl.get_image_by_id(image_id) + else: + image = fl.get_next_image() + self.image = image + + return (pil2tensor(image), ) - return ( pil2tensor(image), ) - class BatchImageLoader: def __init__(self, directory_path, counter_file="current_image_index.txt"): self.image_paths = [] @@ -647,7 +678,8 @@ class WAS_Load_Image_Batch: f.write(str(self.current_image_index)) def load_images(self, directory_path): - allowed_extensions = ('.jpeg', '.jpg', '.png', '.tiff', '.gif', '.bmp', '.webp') + allowed_extensions = ('.jpeg', '.jpg', '.png', + '.tiff', '.gif', '.bmp', '.webp') for file_name in os.listdir(directory_path): if file_name.lower().endswith(allowed_extensions): image_path = os.path.join(directory_path, file_name) @@ -668,11 +700,10 @@ class WAS_Load_Image_Batch: with open(self.counter_file, "w") as f: f.write(str(self.current_image_index)) return Image.open(image_path) - - @classmethod - def IS_CHANGED(s, **kwargs): - return float("NaN") + @classmethod + def IS_CHANGED(cls, **kwargs): + return float("NaN") # IMAGE PADDING @@ -687,7 +718,7 @@ class WAS_Image_Padding: "required": { "image": ("IMAGE",), "feathering": ("INT", {"default": 120, "min": 0, "max": 2048, "step": 1}), - "feather_second_pass": (["true","false"],), + "feather_second_pass": (["true", "false"],), "left_padding": ("INT", {"default": 512, "min": 8, "max": 48000, "step": 1}), "right_padding": ("INT", {"default": 512, "min": 8, "max": 48000, "step": 1}), "top_padding": ("INT", {"default": 512, "min": 8, "max": 48000, "step": 1}), @@ -695,15 +726,16 @@ class WAS_Image_Padding: }, } - RETURN_TYPES = ("IMAGE","IMAGE") + RETURN_TYPES = ("IMAGE", "IMAGE") FUNCTION = "image_padding" CATEGORY = "WAS Suite/Image" def image_padding(self, image, feathering, left_padding, right_padding, top_padding, bottom_padding, feather_second_pass=True): - padding = self.apply_image_padding(tensor2pil(image), left_padding, right_padding, top_padding, bottom_padding, feathering, second_pass=True) - return ( pil2tensor(padding[0]), pil2tensor(padding[1]) ) - + padding = self.apply_image_padding(tensor2pil( + image), left_padding, right_padding, top_padding, bottom_padding, feathering, second_pass=True) + return (pil2tensor(padding[0]), pil2tensor(padding[1])) + def apply_image_padding(self, image, left_pad=100, right_pad=100, top_pad=100, bottom_pad=100, feather_radius=50, second_pass=True): # Create a mask for the feathered edge mask = Image.new('L', image.size, 255) @@ -711,9 +743,11 @@ class WAS_Image_Padding: # Draw black rectangles at each edge of the image with the specified feather radius draw.rectangle((0, 0, feather_radius*2, image.height), fill=0) - draw.rectangle((image.width-feather_radius*2, 0, image.width, image.height), fill=0) + draw.rectangle((image.width-feather_radius*2, 0, + image.width, image.height), fill=0) draw.rectangle((0, 0, image.width, feather_radius*2), fill=0) - draw.rectangle((0, image.height-feather_radius*2, image.width, image.height), fill=0) + draw.rectangle((0, image.height-feather_radius*2, + image.width, image.height), fill=0) # Blur the mask to create a smooth gradient between the black shapes and the white background mask = mask.filter(ImageFilter.GaussianBlur(radius=feather_radius)) @@ -728,12 +762,15 @@ class WAS_Image_Padding: # Draw black rectangles at each edge of the image with a smaller feather radius feather_radius2 = int(feather_radius / 4) draw2.rectangle((0, 0, feather_radius2*2, image.height), fill=0) - draw2.rectangle((image.width-feather_radius2*2, 0, image.width, image.height), fill=0) + draw2.rectangle((image.width-feather_radius2*2, 0, + image.width, image.height), fill=0) draw2.rectangle((0, 0, image.width, feather_radius2*2), fill=0) - draw2.rectangle((0, image.height-feather_radius2*2, image.width, image.height), fill=0) + draw2.rectangle((0, image.height-feather_radius2*2, + image.width, image.height), fill=0) # Blur the mask to create a smooth gradient between the black shapes and the white background - mask2 = mask2.filter(ImageFilter.GaussianBlur(radius=feather_radius2)) + mask2 = mask2.filter( + ImageFilter.GaussianBlur(radius=feather_radius2)) feathered_im = Image.new('RGBA', image.size, (0, 0, 0, 0)) feathered_im.paste(image, (0, 0), mask) @@ -742,36 +779,37 @@ class WAS_Image_Padding: # Apply the second mask to the feathered image feathered_im.paste(image, (0, 0), mask2) feathered_im.paste(image, (0, 0), mask2) - + else: - + # Apply the fist maskk feathered_im = Image.new('RGBA', image.size, (0, 0, 0, 0)) feathered_im.paste(image, (0, 0), mask) # Calculate the new size of the image with padding added - new_size = (feathered_im.width + left_pad + right_pad, feathered_im.height + top_pad + bottom_pad) + new_size = (feathered_im.width + left_pad + right_pad, + feathered_im.height + top_pad + bottom_pad) # Create a new transparent image with the new size new_im = Image.new('RGBA', new_size, (0, 0, 0, 0)) # Paste the feathered image onto the new image with the padding new_im.paste(feathered_im, (left_pad, top_pad)) - + # Create Padding Mask padding_mask = Image.new('L', new_size, 0) # Create a mask where the transparent pixels have a gradient - gradient = [(int(255 * (1 - p[3] / 255)) if p[3] != 0 else 255) for p in new_im.getdata()] + gradient = [(int(255 * (1 - p[3] / 255)) if p[3] != 0 else 255) + for p in new_im.getdata()] padding_mask.putdata(gradient) - + # Save the new image with alpha channel as a PNG file - return ( new_im, padding_mask.convert('RGB') ) - - - + return (new_im, padding_mask.convert('RGB')) + + # IMAGE THRESHOLD NODE - + class WAS_Image_Threshold: def __init__(self): pass @@ -791,22 +829,22 @@ class WAS_Image_Threshold: CATEGORY = "WAS Suite/Image" def image_threshold(self, image, threshold=0.5): - return ( pil2tensor(self.apply_threshold(tensor2pil(image), threshold)), ) - + return (pil2tensor(self.apply_threshold(tensor2pil(image), threshold)), ) + def apply_threshold(self, input_image, threshold=0.5): # Convert the input image to grayscale grayscale_image = input_image.convert('L') # Apply the threshold to the grayscale image threshold_value = int(threshold * 255) - thresholded_image = grayscale_image.point(lambda x: 255 if x >= threshold_value else 0, mode='L') + thresholded_image = grayscale_image.point( + lambda x: 255 if x >= threshold_value else 0, mode='L') return thresholded_image - - - + + # IMAGE CHROMATIC ABERRATION NODE - + class WAS_Image_Chromatic_Aberration: def __init__(self): @@ -820,7 +858,7 @@ class WAS_Image_Chromatic_Aberration: "red_offset": ("INT", {"default": 2, "min": -255, "max": 255, "step": 1}), "green_offset": ("INT", {"default": -1, "min": -255, "max": 255, "step": 1}), "blue_offset": ("INT", {"default": 1, "min": -255, "max": 255, "step": 1}), - "intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), + "intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), }, } @@ -830,8 +868,7 @@ class WAS_Image_Chromatic_Aberration: CATEGORY = "WAS Suite/Image" def image_chromatic_aberration(self, image, red_offset=4, green_offset=2, blue_offset=0, intensity=1): - return ( pil2tensor(self.apply_chromatic_aberration(tensor2pil(image), red_offset, green_offset, blue_offset, intensity)), ) - + return (pil2tensor(self.apply_chromatic_aberration(tensor2pil(image), red_offset, green_offset, blue_offset, intensity)), ) def apply_chromatic_aberration(self, img, r_offset, g_offset, b_offset, intensity): # split the channels of the image @@ -852,10 +889,9 @@ class WAS_Image_Chromatic_Aberration: return result - - + # IMAGE BLOOM FILTER - + class WAS_Image_Bloom_Filter: def __init__(self): pass @@ -876,33 +912,35 @@ class WAS_Image_Bloom_Filter: CATEGORY = "WAS Suite/Image" def image_bloom(self, image, radius=0.5, intensity=1.0): - return ( pil2tensor(self.apply_bloom_filter(tensor2pil(image), radius, intensity)), ) - + return (pil2tensor(self.apply_bloom_filter(tensor2pil(image), radius, intensity)), ) + def apply_bloom_filter(self, input_image, radius, bloom_factor): # Apply a blur filter to the input image - blurred_image = input_image.filter(ImageFilter.GaussianBlur(radius=radius)) + blurred_image = input_image.filter( + ImageFilter.GaussianBlur(radius=radius)) # Subtract the blurred image from the input image to create a high-pass filter high_pass_filter = ImageChops.subtract(input_image, blurred_image) # Create a blurred version of the bloom filter - bloom_filter = high_pass_filter.filter(ImageFilter.GaussianBlur(radius=radius*2)) + bloom_filter = high_pass_filter.filter( + ImageFilter.GaussianBlur(radius=radius*2)) # Adjust brightness and levels of bloom filter bloom_filter = ImageEnhance.Brightness(bloom_filter).enhance(2.0) # Multiply the bloom image with the bloom factor - bloom_filter = ImageChops.multiply(bloom_filter, Image.new('RGB', input_image.size, (int(255 * bloom_factor), int(255 * bloom_factor), int(255 * bloom_factor)))) + bloom_filter = ImageChops.multiply(bloom_filter, Image.new('RGB', input_image.size, (int( + 255 * bloom_factor), int(255 * bloom_factor), int(255 * bloom_factor)))) # Multiply the bloom filter with the original image using the bloom factor blended_image = ImageChops.screen(input_image, bloom_filter) return blended_image - - - + + # IMAGE REMOVE COLOR - + class WAS_Image_Remove_Color: def __init__(self): pass @@ -928,32 +966,33 @@ class WAS_Image_Remove_Color: CATEGORY = "WAS Suite/Image" def image_remove_color(self, image, clip_threshold=10, target_red=255, target_green=255, target_blue=255, replace_red=255, replace_green=255, replace_blue=255): - return ( pil2tensor(self.apply_remove_color(tensor2pil(image), clip_threshold, (target_red, target_green, target_blue), (replace_red, replace_green, replace_blue))), ) - + return (pil2tensor(self.apply_remove_color(tensor2pil(image), clip_threshold, (target_red, target_green, target_blue), (replace_red, replace_green, replace_blue))), ) + def apply_remove_color(self, image, threshold=10, color=(255, 255, 255), rep_color=(0, 0, 0)): # Create a color image with the same size as the input image color_image = Image.new('RGB', image.size, color) - + # Calculate the difference between the input image and the color image diff_image = ImageChops.difference(image, color_image) - + # Convert the difference image to grayscale gray_image = diff_image.convert('L') - + # Apply a threshold to the grayscale difference image mask_image = gray_image.point(lambda x: 255 if x > threshold else 0) - + # Invert the mask image mask_image = ImageOps.invert(mask_image) - + # Apply the mask to the original image - result_image = Image.composite(Image.new('RGB', image.size, rep_color), image, mask_image) - + result_image = Image.composite( + Image.new('RGB', image.size, rep_color), image, mask_image) + return result_image # IMAGE BLEND MASK NODE - + class WAS_Image_Blend_Mask: def __init__(self): pass @@ -975,23 +1014,24 @@ class WAS_Image_Blend_Mask: CATEGORY = "WAS Suite/Image" def image_blend_mask(self, image_a, image_b, mask, blend_percentage): - + # Convert images to PIL img_a = tensor2pil(image_a) img_b = tensor2pil(image_b) mask = ImageOps.invert(tensor2pil(mask).convert('L')) - + # Mask image masked_img = Image.composite(img_a, img_b, mask.resize(img_a.size)) # Blend image - blend_mask = Image.new(mode = "L", size = img_a.size, color = (round(blend_percentage * 255))) + blend_mask = Image.new(mode="L", size=img_a.size, + color=(round(blend_percentage * 255))) blend_mask = ImageOps.invert(blend_mask) img_result = Image.composite(img_a, masked_img, blend_mask) - + del img_a, img_b, blend_mask, mask - return ( pil2tensor(img_result), ) + return (pil2tensor(img_result), ) # IMAGE BLANK NOE @@ -1002,33 +1042,34 @@ class WAS_Image_Blank: pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 1}), - "height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 1}), - "red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - } - } + "required": { + "width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 1}), + "height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 1}), + "red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + } + } RETURN_TYPES = ("IMAGE",) FUNCTION = "blank_image" CATEGORY = "WAS Suite/Image" - def blank_image(self, width, height, red, green, blue): - + def blank_image(self, width, height, red, green, blue): + # Ensure multiples - width = ( width // 8 ) * 8 - height = ( height // 8 ) * 8 + width = (width // 8) * 8 + height = (height // 8) * 8 # Blend image - blank = Image.new(mode = "RGB", size = (width, height), color = (red, green, blue)) + blank = Image.new(mode="RGB", size=(width, height), + color=(red, green, blue)) + + return (pil2tensor(blank), ) + - return ( pil2tensor(blank), ) - - # IMAGE HIGH PASS class WAS_Image_High_Pass_Filter: @@ -1036,30 +1077,31 @@ class WAS_Image_High_Pass_Filter: pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "image": ("IMAGE",), - "radius": ("INT", {"default": 10, "min": 1, "max": 500, "step": 1}), - "strength": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 255.0, "step": 0.1}) - } - } + "required": { + "image": ("IMAGE",), + "radius": ("INT", {"default": 10, "min": 1, "max": 500, "step": 1}), + "strength": ("FLOAT", {"default": 1.5, "min": 0.0, "max": 255.0, "step": 0.1}) + } + } RETURN_TYPES = ("IMAGE",) FUNCTION = "high_pass" CATEGORY = "WAS Suite/Image" - def high_pass(self, image, radius=10, strength=1.5): + def high_pass(self, image, radius=10, strength=1.5): hpf = tensor2pil(image).convert('L') - return ( pil2tensor(self.apply_hpf(hpf.convert('RGB'), radius, strength)), ) - + return (pil2tensor(self.apply_hpf(hpf.convert('RGB'), radius, strength)), ) + def apply_hpf(self, img, radius=10, strength=1.5): # pil to numpy img_arr = np.array(img).astype('float') # Apply a Gaussian blur with the given radius - blurred_arr = np.array(img.filter(ImageFilter.GaussianBlur(radius=radius))).astype('float') + blurred_arr = np.array(img.filter( + ImageFilter.GaussianBlur(radius=radius))).astype('float') # Apply the High Pass Filter hpf_arr = img_arr - blurred_arr @@ -1067,7 +1109,7 @@ class WAS_Image_High_Pass_Filter: # Convert the numpy array back to a PIL image and return it return Image.fromarray(hpf_arr, mode='RGB') - + # IMAGE LEVELS NODE @@ -1076,34 +1118,34 @@ class WAS_Image_Levels: pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "image": ("IMAGE",), - "black_level": ("FLOAT", {"default": 0.0, "min": 0.0, "max":255.0, "step": 0.1}), - "mid_level": ("FLOAT", {"default": 127.5, "min": 0.0, "max": 255.0, "step": 0.1}), - "white_level": ("FLOAT", {"default": 255, "min": 0.0, "max": 255.0, "step": 0.1}), - } - } + "required": { + "image": ("IMAGE",), + "black_level": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 255.0, "step": 0.1}), + "mid_level": ("FLOAT", {"default": 127.5, "min": 0.0, "max": 255.0, "step": 0.1}), + "white_level": ("FLOAT", {"default": 255, "min": 0.0, "max": 255.0, "step": 0.1}), + } + } RETURN_TYPES = ("IMAGE",) FUNCTION = "apply_image_levels" CATEGORY = "WAS Suite/Image" - def apply_image_levels(self, image, black_level, mid_level, white_level): - - # Convert image to PIL - image = tensor2pil(image) - - #apply image levels - #image = self.adjust_levels(image, black_level, mid_level, white_level) - - levels = self.AdjustLevels(black_level, mid_level, white_level) - image = levels.adjust(image) - - # Return adjust image tensor - return ( pil2tensor(image), ) - + def apply_image_levels(self, image, black_level, mid_level, white_level): + + # Convert image to PIL + image = tensor2pil(image) + + # apply image levels + # image = self.adjust_levels(image, black_level, mid_level, white_level) + + levels = self.AdjustLevels(black_level, mid_level, white_level) + image = levels.adjust(image) + + # Return adjust image tensor + return (pil2tensor(image), ) + def adjust_levels(self, image, black=0.0, mid=1.0, white=255): """ Adjust the black, mid, and white levels of an RGB image. @@ -1137,7 +1179,7 @@ class WAS_Image_Levels: result = Image.merge("RGB", (r, g, b)) return result - + class AdjustLevels: def __init__(self, min_level, mid_level, max_level): self.min_level = min_level @@ -1154,7 +1196,8 @@ class WAS_Image_Levels: im_arr[im_arr < self.min_level] = self.min_level # apply the mid level adjustment - im_arr = (im_arr - self.min_level) * (255 / (self.max_level - self.min_level)) + im_arr = (im_arr - self.min_level) * \ + (255 / (self.max_level - self.min_level)) im_arr[im_arr < 0] = 0 im_arr[im_arr > 255] = 255 im_arr = im_arr.astype(np.uint8) @@ -1173,35 +1216,36 @@ class WAS_Film_Grain: pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "image": ("IMAGE",), - "density": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), - "intensity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), - "highlights": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 255.0, "step": 0.01}), - "supersample_factor": ("INT", {"default": 4, "min": 1, "max": 8, "step": 1}) - } - } + "required": { + "image": ("IMAGE",), + "density": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), + "intensity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}), + "highlights": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 255.0, "step": 0.01}), + "supersample_factor": ("INT", {"default": 4, "min": 1, "max": 8, "step": 1}) + } + } RETURN_TYPES = ("IMAGE",) FUNCTION = "film_grain" CATEGORY = "WAS Suite/Image" - def film_grain(self, image, density, intensity, highlights, supersample_factor): - return ( pil2tensor(self.apply_film_grain(tensor2pil(image), density, intensity, highlights, supersample_factor)), ) + def film_grain(self, image, density, intensity, highlights, supersample_factor): + return (pil2tensor(self.apply_film_grain(tensor2pil(image), density, intensity, highlights, supersample_factor)), ) - def apply_film_grain(self, img, density=0.1, intensity=1.0, highlights=1.0, supersample_factor = 4): + def apply_film_grain(self, img, density=0.1, intensity=1.0, highlights=1.0, supersample_factor=4): """ Apply grayscale noise with specified density, intensity, and highlights to a PIL image. """ # Convert the image to grayscale img_gray = img.convert('L') - + # Super Resolution noise image original_size = img.size - img_gray = img_gray.resize(((img.size[0] * supersample_factor), (img.size[1] * supersample_factor)), Image.Resampling(2)) - + img_gray = img_gray.resize( + ((img.size[0] * supersample_factor), (img.size[1] * supersample_factor)), Image.Resampling(2)) + # Calculate the number of noise pixels to add num_pixels = int(density * img_gray.size[0] * img_gray.size[1]) @@ -1219,13 +1263,13 @@ class WAS_Film_Grain: # Convert the grayscale image back to RGB img_noise = img_gray.convert('RGB') - + # Blur noise image - img_noise = img_noise.filter(ImageFilter.GaussianBlur(radius = 0.125)) - + img_noise = img_noise.filter(ImageFilter.GaussianBlur(radius=0.125)) + # Downsize noise image img_noise = img_noise.resize(original_size, Image.Resampling(1)) - + # Sharpen super resolution result img_noise = img_noise.filter(ImageFilter.EDGE_ENHANCE_MORE) @@ -1238,10 +1282,10 @@ class WAS_Film_Grain: # Return the final image return img_highlights - + # IMAGE FLIP NODE - + class WAS_Image_Flip: def __init__(self): pass @@ -1261,17 +1305,17 @@ class WAS_Image_Flip: CATEGORY = "WAS Suite/Image" def image_flip(self, image, mode): - + # PIL Image image = tensor2pil(image) - + # Rotate Image if mode == 'horizontal': image = image.transpose(0) if mode == 'vertical': image = image.transpose(1) - - return ( pil2tensor(image), ) + + return (pil2tensor(image), ) class WAS_Image_Rotate: @@ -1295,16 +1339,16 @@ class WAS_Image_Rotate: CATEGORY = "WAS Suite/Image" def image_rotate(self, image, mode, rotation, sampler): - + # PIL Image image = tensor2pil(image) - + # Check rotation if rotation > 360: rotation = int(360) if (rotation % 90 != 0): - rotation = int((rotation//90)*90); - + rotation = int((rotation//90)*90) + # Set Sampler match sampler: case 'nearest': @@ -1313,7 +1357,7 @@ class WAS_Image_Rotate: sampler = Image.BICUBIC case 'bilinear': sampler = Image.BILINEAR - + # Rotate Image if mode == 'internal': image = image.rotate(rotation, sampler) @@ -1321,8 +1365,8 @@ class WAS_Image_Rotate: rot = int(rotation / 90) for _ in range(rot): image = image.transpose(2) - - return ( torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0), ) + + return (torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0), ) # IMAGE NOVA SINE FILTER @@ -1347,10 +1391,10 @@ class WAS_Image_Nova_Filter: CATEGORY = "WAS Suite/Image" def nova_sine(self, image, amplitude, frequency): - + # Convert image to numpy img = tensor2pil(image) - + # Convert the image to a numpy array img_array = np.array(img) @@ -1369,7 +1413,7 @@ class WAS_Image_Nova_Filter: # Set the maximum frequency for the sine wave max_freq = img.width / 2 - + # Ensure frequency isn't outside visual representable range if frequency > max_freq: frequency = max_freq @@ -1378,9 +1422,10 @@ class WAS_Image_Nova_Filter: for i in range(img_array.shape[0]): for j in range(img_array.shape[1]): for k in range(img_array.shape[2]): - img_array[i,j,k] = int(sine(img_array[i,j,k]/255, frequency, amplitude) * 255) + img_array[i, j, k] = int( + sine(img_array[i, j, k]/255, frequency, amplitude) * 255) - return ( torch.from_numpy(img_array.astype(np.float32) / 255.0).unsqueeze(0), ) + return (torch.from_numpy(img_array.astype(np.float32) / 255.0).unsqueeze(0), ) # IMAGE CANNY FILTER @@ -1409,116 +1454,124 @@ class WAS_Canny_Filter: def canny_filter(self, image, threshold_low, threshold_high, enable_threshold): self.install_opencv() - + if enable_threshold == 'false': threshold_low = None threshold_high = None - image_canny = Image.fromarray(self.Canny_detector(255. * image.cpu().numpy().squeeze(), threshold_low, threshold_high)).convert('RGB') + image_canny = Image.fromarray(self.Canny_detector( + 255. * image.cpu().numpy().squeeze(), threshold_low, threshold_high)).convert('RGB') + + return (pil2tensor(image_canny), ) - return ( pil2tensor(image_canny), ) - # Defining the Canny Detector function # From: https://www.geeksforgeeks.org/implement-canny-edge-detector-in-python-using-opencv/ - + # here weak_th and strong_th are thresholds for # double thresholding step - def Canny_detector(self, img, weak_th = None, strong_th = None): - + def Canny_detector(self, img, weak_th=None, strong_th=None): + import cv2 - + # conversion of image to grayscale img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - + # Noise reduction step img = cv2.GaussianBlur(img, (5, 5), 1.4) - + # Calculating the gradients - gx = cv2.Sobel(np.float32(img), cv2.CV_64F, 1, 0, 3) - gy = cv2.Sobel(np.float32(img), cv2.CV_64F, 0, 1, 3) - - # Conversion of Cartesian coordinates to polar - mag, ang = cv2.cartToPolar(gx, gy, angleInDegrees = True) - - # setting the minimum and maximum thresholds + gx = cv2.Sobel(np.float32(img), cv2.CV_64F, 1, 0, 3) # type: ignore + gy = cv2.Sobel(np.float32(img), cv2.CV_64F, 0, 1, 3) # type: ignore + + # Conversion of Cartesian coordinates to polar + mag, ang = cv2.cartToPolar(gx, gy, angleInDegrees=True) + + # setting the minimum and maximum thresholds # for double thresholding mag_max = np.max(mag) - if not weak_th:weak_th = mag_max * 0.1 - if not strong_th:strong_th = mag_max * 0.5 - - # getting the dimensions of the input image + if not weak_th: + weak_th = mag_max * 0.1 + if not strong_th: + strong_th = mag_max * 0.5 + + # getting the dimensions of the input image height, width = img.shape - - # Looping through every pixel of the grayscale + + # Looping through every pixel of the grayscale # image for i_x in range(width): for i_y in range(height): - + grad_ang = ang[i_y, i_x] - grad_ang = abs(grad_ang-180) if abs(grad_ang)>180 else abs(grad_ang) - + grad_ang = abs( + grad_ang-180) if abs(grad_ang) > 180 else abs(grad_ang) + + neighb_1_x, neighb_1_y = -1, -1 + neighb_2_x, neighb_2_y = -1, -1 + # selecting the neighbours of the target pixel # according to the gradient direction # In the x axis direction - if grad_ang<= 22.5: + if grad_ang <= 22.5: neighb_1_x, neighb_1_y = i_x-1, i_y neighb_2_x, neighb_2_y = i_x + 1, i_y - + # top right (diagonal-1) direction - elif grad_ang>22.5 and grad_ang<=(22.5 + 45): + elif grad_ang > 22.5 and grad_ang <= (22.5 + 45): neighb_1_x, neighb_1_y = i_x-1, i_y-1 neighb_2_x, neighb_2_y = i_x + 1, i_y + 1 - + # In y-axis direction - elif grad_ang>(22.5 + 45) and grad_ang<=(22.5 + 90): + elif grad_ang > (22.5 + 45) and grad_ang <= (22.5 + 90): neighb_1_x, neighb_1_y = i_x, i_y-1 neighb_2_x, neighb_2_y = i_x, i_y + 1 - + # top left (diagonal-2) direction - elif grad_ang>(22.5 + 90) and grad_ang<=(22.5 + 135): + elif grad_ang > (22.5 + 90) and grad_ang <= (22.5 + 135): neighb_1_x, neighb_1_y = i_x-1, i_y + 1 neighb_2_x, neighb_2_y = i_x + 1, i_y-1 - + # Now it restarts the cycle - elif grad_ang>(22.5 + 135) and grad_ang<=(22.5 + 180): + elif grad_ang > (22.5 + 135) and grad_ang <= (22.5 + 180): neighb_1_x, neighb_1_y = i_x-1, i_y neighb_2_x, neighb_2_y = i_x + 1, i_y - + # Non-maximum suppression step - if width>neighb_1_x>= 0 and height>neighb_1_y>= 0: - if mag[i_y, i_x] neighb_1_x >= 0 and height > neighb_1_y >= 0: + if mag[i_y, i_x] < mag[neighb_1_y, neighb_1_x]: + mag[i_y, i_x] = 0 continue - - if width>neighb_2_x>= 0 and height>neighb_2_y>= 0: - if mag[i_y, i_x] neighb_2_x >= 0 and height > neighb_2_y >= 0: + if mag[i_y, i_x] < mag[neighb_2_y, neighb_2_x]: + mag[i_y, i_x] = 0 + weak_ids = np.zeros_like(img) - strong_ids = np.zeros_like(img) + strong_ids = np.zeros_like(img) ids = np.zeros_like(img) - + # double thresholding step for i_x in range(width): for i_y in range(height): - + grad_mag = mag[i_y, i_x] - - if grad_maggrad_mag>= weak_th: - ids[i_y, i_x]= 1 + + if grad_mag < weak_th: + mag[i_y, i_x] = 0 + elif strong_th > grad_mag >= weak_th: + ids[i_y, i_x] = 1 else: - ids[i_y, i_x]= 2 - + ids[i_y, i_x] = 2 + # finally returning the magnitude of # gradients of edges return mag - + def install_opencv(self): if 'opencv-python' not in packages(): print("\033[34mWAS NS:\033[0m Installing CV2...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) # IMAGE EDGE DETECTION @@ -1542,7 +1595,7 @@ class WAS_Image_Edge: CATEGORY = "WAS Suite/Image" def image_edges(self, image, mode): - + # Convert image to PIL image = tensor2pil(image) @@ -1552,12 +1605,12 @@ class WAS_Image_Edge: image = image.filter(ImageFilter.FIND_EDGES) case "laplacian": image = image.filter(ImageFilter.Kernel((3, 3), (-1, -1, -1, -1, 8, - -1, -1, -1, -1), 1, 0)) + -1, -1, -1, -1), 1, 0)) case _: image = image - return ( torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0), ) - + return (torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0), ) + # IMAGE FDOF NODE @@ -1571,7 +1624,7 @@ class WAS_Image_fDOF: "required": { "image": ("IMAGE",), "depth": ("IMAGE",), - "mode": (["mock","gaussian","box"],), + "mode": (["mock", "gaussian", "box"],), "radius": ("INT", {"default": 8, "min": 1, "max": 128, "step": 1}), "samples": ("INT", {"default": 1, "min": 1, "max": 3, "step": 1}), }, @@ -1582,48 +1635,53 @@ class WAS_Image_fDOF: CATEGORY = "WAS Suite/Image" - def fdof_composite(self, image, depth, radius, samples, mode): - + def fdof_composite(self, image: torch.Tensor, depth: torch.Tensor, radius: int, samples: int, mode: str) -> tuple[torch.Tensor]: + if 'opencv-python' not in packages(): print("\033[34mWAS NS:\033[0m Installing CV2...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) - + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) + import cv2 as cv - #Convert tensor to a PIL Image + # Convert tensor to a PIL Image i = 255. * image.cpu().numpy().squeeze() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + img: Image.Image = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) d = 255. * depth.cpu().numpy().squeeze() - depth_img = Image.fromarray(np.clip(d, 0, 255).astype(np.uint8)) + depth_img: Image.Image = Image.fromarray( + np.clip(d, 0, 255).astype(np.uint8)) - #Apply Fake Depth of Field + # Apply Fake Depth of Field fdof_image = self.portraitBlur(img, depth_img, radius, samples, mode) - return ( torch.from_numpy(np.array(fdof_image).astype(np.float32) / 255.0).unsqueeze(0), ) + return (torch.from_numpy(np.array(fdof_image).astype(np.float32) / 255.0).unsqueeze(0), ) - def portraitBlur(self, img, mask, radius=5, samples=1, mode = 'mock'): + def portraitBlur(self, img: Image.Image, mask: Image.Image, radius: int = 5, samples: int = 1, mode='mock') -> Optional[Image.Image]: mask = mask.resize(img.size).convert('L') + bimg: Optional[Image.Image] = None if mode == 'mock': bimg = medianFilter(img, radius, (radius * 1500), 75) elif mode == 'gaussian': - bimg = img.filter(ImageFilter.GaussianBlur(radius = radius)) + bimg = img.filter(ImageFilter.GaussianBlur(radius=radius)) elif mode == 'box': bimg = img.filter(ImageFilter.BoxBlur(radius)) + else: + return bimg.convert(img.mode) - rimg = None + rimg: Optional[Image.Image] = None if samples > 1: for i in range(samples): - if i == 0: + if not rimg: rimg = Image.composite(img, bimg, mask) else: rimg = Image.composite(rimg, bimg, mask) else: rimg = Image.composite(img, bimg, mask).convert('RGB') - + return rimg - + # TODO: Implement lens_blur mode attempt - def lens_blur(img, radius, amount, mask=None): + def lens_blur(self, img, radius, amount, mask=None): """Applies a lens shape blur effect on an image. Args: @@ -1644,17 +1702,23 @@ class WAS_Image_fDOF: # Create a circular mask for the kernel. mask_shape = (int(radius * 2), int(radius * 2)) - mask = np.ones(mask_shape) if mask is None else cv2.resize(mask, mask_shape, interpolation=cv2.INTER_LINEAR) - mask = cv2.GaussianBlur(mask, (int(radius * 2) + 1, int(radius * 2) + 1), radius / 2) + mask = np.ones(mask_shape) if mask is None else cv2.resize( + mask, mask_shape, interpolation=cv2.INTER_LINEAR) + mask = cv2.GaussianBlur( + mask, (int(radius * 2) + 1, int(radius * 2) + 1), radius / 2) mask /= np.max(mask) # Adjust kernel and mask size to match input image. ksize_x = img.shape[1] // (kernel.shape[1] + 1) ksize_y = img.shape[0] // (kernel.shape[0] + 1) - kernel = cv2.resize(kernel, (ksize_x, ksize_y), interpolation=cv2.INTER_LINEAR) - kernel = cv2.copyMakeBorder(kernel, 0, img.shape[0] - kernel.shape[0], 0, img.shape[1] - kernel.shape[1], cv2.BORDER_CONSTANT, value=0) - mask = cv2.resize(mask, (ksize_x, ksize_y), interpolation=cv2.INTER_LINEAR) - mask = cv2.copyMakeBorder(mask, 0, img.shape[0] - mask.shape[0], 0, img.shape[1] - mask.shape[1], cv2.BORDER_CONSTANT, value=0) + kernel = cv2.resize(kernel, (ksize_x, ksize_y), + interpolation=cv2.INTER_LINEAR) + kernel = cv2.copyMakeBorder( + kernel, 0, img.shape[0] - kernel.shape[0], 0, img.shape[1] - kernel.shape[1], cv2.BORDER_CONSTANT, value=0) + mask = cv2.resize(mask, (ksize_x, ksize_y), + interpolation=cv2.INTER_LINEAR) + mask = cv2.copyMakeBorder( + mask, 0, img.shape[0] - mask.shape[0], 0, img.shape[1] - mask.shape[1], cv2.BORDER_CONSTANT, value=0) # Apply the lens shape blur effect on the image. blurred = cv2.filter2D(img, -1, kernel) @@ -1665,13 +1729,13 @@ class WAS_Image_fDOF: mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR) img_masked = img * mask # Combine the masked image with the blurred image. - blurred = img_masked * (1 - mask) + blurred + blurred = img_masked * (1 - mask) + blurred # type: ignore return blurred - - + + # IMAGE MEDIAN FILTER NODE - + class WAS_Image_Median_Filter: def __init__(self): pass @@ -1693,17 +1757,18 @@ class WAS_Image_Median_Filter: CATEGORY = "WAS Suite/Image" def apply_median_filter(self, image, diameter, sigma_color, sigma_space): - + # Numpy Image image = tensor2pil(image) - + # Apply Median Filter effect image = medianFilter(image, diameter, sigma_color, sigma_space) - - return ( pil2tensor(image), ) + + return (pil2tensor(image), ) # IMAGE SELECT COLOR - + + class WAS_Image_Select_Color: def __init__(self): pass @@ -1726,22 +1791,22 @@ class WAS_Image_Select_Color: CATEGORY = "WAS Suite/Image" def select_color(self, image, red=255, green=255, blue=255, variance=10): - + if 'opencv-python' not in packages(): print("\033[34mWAS NS:\033[0m Installing CV2...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) - + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) + image = self.color_pick(tensor2pil(image), red, green, blue, variance) - - return ( pil2tensor(image), ) - + + return (pil2tensor(image), ) def color_pick(self, image, red=255, green=255, blue=255, variance=10): # Convert image to RGB mode image = image.convert('RGB') # Create a new black image of the same size as the input image - selected_color = Image.new('RGB', image.size, (0,0,0)) + selected_color = Image.new('RGB', image.size, (0, 0, 0)) # Get the width and height of the image width, height = image.size @@ -1750,21 +1815,22 @@ class WAS_Image_Select_Color: for x in range(width): for y in range(height): # Get the color of the pixel - pixel = image.getpixel((x,y)) - r,g,b = pixel + pixel = image.getpixel((x, y)) + r, g, b = pixel # Check if the pixel is within the specified color range if ((r >= red-variance) and (r <= red+variance) and (g >= green-variance) and (g <= green+variance) and - (b >= blue-variance) and (b <= blue+variance)): + (b >= blue-variance) and (b <= blue+variance)): # Set the pixel in the selected_color image to the RGB value of the pixel - selected_color.putpixel((x,y),(r,g,b)) + selected_color.putpixel((x, y), (r, g, b)) # Return the selected color image return selected_color - + # IMAGE CONVERT TO CHANNEL - + + class WAS_Image_Select_Channel: def __init__(self): pass @@ -1774,7 +1840,7 @@ class WAS_Image_Select_Channel: return { "required": { "image": ("IMAGE",), - "channel": (['red','green','blue'],), + "channel": (['red', 'green', 'blue'],), }, } @@ -1784,14 +1850,13 @@ class WAS_Image_Select_Channel: CATEGORY = "WAS Suite/Image" def select_channel(self, image, channel='red'): - + image = self.convert_to_single_channel(tensor2pil(image), channel) - - return ( pil2tensor(image), ) - + + return (pil2tensor(image), ) def convert_to_single_channel(self, image, channel='red'): - + # Convert to RGB mode to access individual channels image = image.convert('RGB') @@ -1803,17 +1868,18 @@ class WAS_Image_Select_Channel: elif channel == 'blue': channel_img = image.split()[2].convert('L') else: - raise ValueError("Invalid channel option. Please choose 'red', 'green', or 'blue'.") + raise ValueError( + "Invalid channel option. Please choose 'red', 'green', or 'blue'.") # Convert the greyscale channel back to RGB mode - channel_img = Image.merge('RGB', (channel_img, channel_img, channel_img)) + channel_img = Image.merge( + 'RGB', (channel_img, channel_img, channel_img)) return channel_img - - - + + # IMAGE CONVERT TO CHANNEL - + class WAS_Image_RGB_Merge: def __init__(self): pass @@ -1834,44 +1900,45 @@ class WAS_Image_RGB_Merge: CATEGORY = "WAS Suite/Image" def merge_channels(self, red_channel, green_channel, blue_channel): - + # Apply mix rgb channels - image = self.mix_rgb_channels(tensor2pil(red_channel).convert('L'), tensor2pil(green_channel).convert('L'), tensor2pil(blue_channel).convert('L')) - - return ( pil2tensor(image), ) - + image = self.mix_rgb_channels(tensor2pil(red_channel).convert('L'), tensor2pil( + green_channel).convert('L'), tensor2pil(blue_channel).convert('L')) + + return (pil2tensor(image), ) def mix_rgb_channels(self, red, green, blue): # Create an empty image with the same size as the channels - width, height = red.size; merged_img = Image.new('RGB', (width, height)) + width, height = red.size + merged_img = Image.new('RGB', (width, height)) # Merge the channels into the new image merged_img = Image.merge('RGB', (red, green, blue)) return merged_img - + # Image Save (NSP Compatible) # Originally From ComfyUI/nodes.py - + class WAS_Image_Save: def __init__(self): self.output_dir = os.path.join(os.getcwd()+'/ComfyUI', "output") @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "images": ("IMAGE", ), - "output_path": ("STRING", {"default": './ComfyUI/output', "multiline": False}), - "filename_prefix": ("STRING", {"default": "ComfyUI"}), - "extension": (['png', 'jpeg', 'tiff', 'gif'], ), - "quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}), - }, - "hidden": { - "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" - }, - } + "required": { + "images": ("IMAGE", ), + "output_path": ("STRING", {"default": './ComfyUI/output', "multiline": False}), + "filename_prefix": ("STRING", {"default": "ComfyUI"}), + "extension": (['png', 'jpeg', 'tiff', 'gif'], ), + "quality": ("INT", {"default": 100, "min": 1, "max": 100, "step": 1}), + }, + "hidden": { + "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" + }, + } RETURN_TYPES = () FUNCTION = "save_images" @@ -1889,17 +1956,19 @@ class WAS_Image_Save: except: digits = 0 return (digits, prefix) - + # Setup custom path or default if output_path.strip() != '': if not os.path.exists(output_path.strip()): - print(f'\033[34mWAS NS\033[0m Error: The path `{output_path.strip()}` specified doesn\'t exist! Defaulting to `{self.output_dir}` directory.') + print( + f'\033[34mWAS NS\033[0m Error: The path `{output_path.strip()}` specified doesn\'t exist! Defaulting to `{self.output_dir}` directory.') else: self.output_dir = os.path.normpath(output_path.strip()) - + # Define counter for files found try: - counter = max(filter(lambda a: a[1][:-1] == filename_prefix and a[1][-1] == "_", map(map_filename, os.listdir(self.output_dir))))[0] + 1 + counter = max(filter(lambda a: a[1][:-1] == filename_prefix and a[1] + [-1] == "_", map(map_filename, os.listdir(self.output_dir))))[0] + 1 except ValueError: counter = 1 except FileNotFoundError: @@ -1918,39 +1987,43 @@ class WAS_Image_Save: metadata.add_text(x, json.dumps(extra_pnginfo[x])) file = f"{filename_prefix}_{counter:05}_.{extension}" if extension == 'png': - img.save(os.path.join(self.output_dir, file), pnginfo=metadata, optimize=True) + img.save(os.path.join(self.output_dir, file), + pnginfo=metadata, optimize=True) elif extension == 'webp': img.save(os.path.join(self.output_dir, file), quality=quality) elif extension == 'jpeg': - img.save(os.path.join(self.output_dir, file), quality=quality, optimize=True) + img.save(os.path.join(self.output_dir, file), + quality=quality, optimize=True) elif extension == 'tiff': - img.save(os.path.join(self.output_dir, file), quality=quality, optimize=True) + img.save(os.path.join(self.output_dir, file), + quality=quality, optimize=True) else: img.save(os.path.join(self.output_dir, file)) paths.append(file) counter += 1 - return { "ui": { "images": paths } } - - + return {"ui": {"images": paths}} + # LOAD IMAGE NODE class WAS_Load_Image: def __init__(self): self.input_dir = os.path.join(os.getcwd()+'/ComfyUI', "input") - + @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return {"required": - {"image_path": ("STRING", {"default": './ComfyUI/input/example.png', "multiline": False}),} + {"image_path": ( + "STRING", {"default": './ComfyUI/input/example.png', "multiline": False}), } } CATEGORY = "WAS Suite/IO" RETURN_TYPES = ("IMAGE", "MASK") FUNCTION = "load_image" - def load_image(self, image_path): - + + def load_image(self, image_path) -> Optional[tuple[torch.Tensor, torch.Tensor]]: + if image_path.startswith('http'): from io import BytesIO i = self.download_image(image_path) @@ -1958,19 +2031,22 @@ class WAS_Load_Image: try: i = Image.open(image_path) except OSError: - print(f'\033[34mWAS NS\033[0m Error: The image `{image_path.strip()}` specified doesn\'t exist!') - i = Image.new(mode='RGB', size=(512,512), color=(0,0,0)) - + print( + f'\033[34mWAS NS\033[0m Error: The image `{image_path.strip()}` specified doesn\'t exist!') + i = Image.new(mode='RGB', size=(512, 512), color=(0, 0, 0)) + if not i: + return + image = i image = np.array(image).astype(np.float32) / 255.0 image = torch.from_numpy(image)[None,] - + if 'A' in i.getbands(): mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 mask = 1. - torch.from_numpy(mask) else: - mask = torch.zeros((64,64), dtype=torch.float32, device="cpu") - return ( image, mask ) + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + return (image, mask) def download_image(self, url): try: @@ -1979,23 +2055,26 @@ class WAS_Load_Image: img = Image.open(BytesIO(response.content)) return img except requests.exceptions.HTTPError as errh: - print(f"\033[34mWAS NS\033[0m Error: HTTP Error: ({url}): {errh}") + print(f"\033[34mWAS NS\033[0m Error: HTTP Error: ({url}): {errh}") except requests.exceptions.ConnectionError as errc: - print(f"\033[34mWAS NS\033[0m Error: Connection Error: ({url}): {errc}") + print( + f"\033[34mWAS NS\033[0m Error: Connection Error: ({url}): {errc}") except requests.exceptions.Timeout as errt: - print(f"\033[34mWAS NS\033[0m Error: Timeout Error: ({url}): {errt}") + print( + f"\033[34mWAS NS\033[0m Error: Timeout Error: ({url}): {errt}") except requests.exceptions.RequestException as err: - print(f"\033[34mWAS NS\033[0m Error: Request Exception: ({url}): {err}") - + print( + f"\033[34mWAS NS\033[0m Error: Request Exception: ({url}): {err}") + @classmethod - def IS_CHANGED(s, image_path): + def IS_CHANGED(cls, image_path): if image_path.startswith('http'): return True m = hashlib.sha256() with open(image_path, 'rb') as f: m.update(f.read()) return m.digest().hex() - + # TENSOR TO IMAGE NODE @@ -2017,16 +2096,17 @@ class WAS_Tensor_Batch_to_Image: CATEGORY = "WAS Suite/Latent" - def tensor_batch_to_image(self, images_batch=None, batch_image_number=0): - + def tensor_batch_to_image(self, images_batch=[], batch_image_number=0): + count = 0 for _ in images_batch: if batch_image_number == count: - return ( images_batch[batch_image_number].unsqueeze(0), ) + return (images_batch[batch_image_number].unsqueeze(0), ) count = count+1 - - print(f"\033[34mWAS NS\033[0m Error: Batch number `{batch_image_number}` is not defined, returning last image") - return( images_batch[-1].unsqueeze(0), ) + + print( + f"\033[34mWAS NS\033[0m Error: Batch number `{batch_image_number}` is not defined, returning last image") + return (images_batch[-1].unsqueeze(0), ) #! LATENT NODES @@ -2035,26 +2115,26 @@ class WAS_Tensor_Batch_to_Image: class WAS_Image_To_Mask: - def __init__(s): + def __init__(self): pass - + @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return {"required": - {"image": ("IMAGE",), - "channel": (["alpha", "red", "green", "blue"], ),} + {"image": ("IMAGE",), + "channel": (["alpha", "red", "green", "blue"], ), } } CATEGORY = "WAS Suite/Latent" RETURN_TYPES = ("MASK",) - + FUNCTION = "image_to_mask" - + def image_to_mask(self, image, channel): - + img = tensor2pil(image) - + mask = None c = channel[0].upper() if c in img.getbands(): @@ -2063,9 +2143,9 @@ class WAS_Image_To_Mask: if c == 'A': mask = 1. - mask else: - mask = torch.zeros((64,64), dtype=torch.float32, device="cpu") - - return ( mask, ) + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + + return (mask, ) # LATENT UPSCALE NODE @@ -2075,10 +2155,10 @@ class WAS_Latent_Upscale: pass @classmethod - def INPUT_TYPES(s): - return {"required": { "samples": ("LATENT",), "mode": (["bilinear", "bicubic"],), - "factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.1}), - "align": (["true", "false"], )}} + def INPUT_TYPES(cls): + return {"required": {"samples": ("LATENT",), "mode": (["bilinear", "bicubic"],), + "factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.1}), + "align": (["true", "false"], )}} RETURN_TYPES = ("LATENT",) FUNCTION = "latent_upscale" @@ -2086,27 +2166,29 @@ class WAS_Latent_Upscale: def latent_upscale(self, samples, mode, factor, align): s = samples.copy() - s["samples"] = torch.nn.functional.interpolate(s['samples'], scale_factor=factor, mode=mode, align_corners=( True if align == 'true' else False )) + s["samples"] = torch.nn.functional.interpolate( + s['samples'], scale_factor=factor, mode=mode, align_corners=(True if align == 'true' else False)) return (s,) - + # LATENT NOISE INJECTION NODE - + + class WAS_Latent_Noise: def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "samples": ("LATENT",), - "noise_std": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), - } - } + "required": { + "samples": ("LATENT",), + "noise_std": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}), + } + } RETURN_TYPES = ("LATENT",) FUNCTION = "inject_noise" - + CATEGORY = "WAS Suite/Latent" def inject_noise(self, samples, noise_std): @@ -2114,8 +2196,8 @@ class WAS_Latent_Noise: noise = torch.randn_like(s["samples"]) * noise_std s["samples"] = s["samples"] + noise return (s,) - - + + # MIDAS DEPTH APPROXIMATION NODE class MiDaS_Depth_Approx: @@ -2124,7 +2206,7 @@ class MiDaS_Depth_Approx: self.midas_checkpoint = os.path.join(self.midas_dir, "checkpoints") @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), @@ -2136,36 +2218,39 @@ class MiDaS_Depth_Approx: RETURN_TYPES = ("IMAGE",) FUNCTION = "midas_approx" - + CATEGORY = "WAS Suite/Image" def midas_approx(self, image, use_cpu, midas_model, invert_depth): - + global MIDAS_INSTALLED - + if not MIDAS_INSTALLED: self.install_midas() - + import cv2 as cv - + # Convert the input image tensor to a PIL Image i = 255. * image.cpu().numpy().squeeze() img = i - + # Deal with 512 support if midas_model == 'DPT_Large_512': - midas_512 = os.path.join(self.midas_checkpoint, 'dpt_beit_large_512.pt') + midas_512 = os.path.join( + self.midas_checkpoint, 'dpt_beit_large_512.pt') if not os.path.exists(midas_512): import urllib.request as req # Midas 512 model - req('https://github.com/isl-org/MiDaS/releases/download/v3_1/dpt_beit_large_512.pt', midas_512) + # TODO: Pretty sure this won't work. + # req('https://github.com/isl-org/MiDaS/releases/download/v3_1/dpt_beit_large_512.pt', midas_512) midas_model = midas_512 print("\033[34mWAS NS:\033[0m Downloading and loading MiDaS Model...") torch.hub.set_dir(self.midas_dir) midas = torch.hub.load("intel-isl/MiDaS", midas_model, trust_repo=True) - device = torch.device("cuda") if torch.cuda.is_available() and use_cpu == 'false' else torch.device("cpu") - + device = torch.device("cuda") if torch.cuda.is_available( + ) and use_cpu == 'false' else torch.device("cpu") + print('\033[34mWAS NS:\033[0m MiDaS is using device:', device) midas.to(device).eval() @@ -2178,7 +2263,7 @@ class MiDaS_Depth_Approx: img = cv.cvtColor(img, cv.COLOR_BGR2RGB) input_batch = transform(img).to(device) - + print('\033[34mWAS NS:\033[0m Approximating depth from image.') with torch.no_grad(): @@ -2192,36 +2277,40 @@ class MiDaS_Depth_Approx: # Invert depth map if invert_depth == 'true': - depth = ( 255 - prediction.cpu().numpy().astype(np.uint8) ) + depth = (255 - prediction.cpu().numpy().astype(np.uint8)) depth = depth.astype(np.float32) else: depth = prediction.cpu().numpy().astype(np.float32) - #depth = depth * 255 / (np.max(depth)) / 255 - depth = (depth - depth.min()) / (depth.max() - depth.min()) # Normalize depth to range [0, 1] - + # depth = depth * 255 / (np.max(depth)) / 255 + # Normalize depth to range [0, 1] + depth = (depth - depth.min()) / (depth.max() - depth.min()) + # depth to RGB depth = cv.cvtColor(depth, cv.COLOR_GRAY2RGB) - - tensor = torch.from_numpy( depth )[None,] - tensors = ( tensor, ) + + tensor = torch.from_numpy(depth)[None,] + tensors = (tensor, ) del midas, device, midas_transforms del transform, img, input_batch, prediction - - return tensors - + + return tensors + def install_midas(self): global MIDAS_INSTALLED if 'timm' not in packages(): print("\033[34mWAS NS:\033[0m Installing timm...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'timm']) + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'timm']) if 'opencv-python' not in packages(): print("\033[34mWAS NS:\033[0m Installing CV2...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) - MIDAS_INSTALLED = True - + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) + MIDAS_INSTALLED = True + # MIDAS REMOVE BACKGROUND/FOREGROUND NODE + class MiDaS_Background_Foreground_Removal: def __init__(self): self.midas_dir = os.path.join(os.getcwd()+'/ComfyUI', "models/midas") @@ -2245,32 +2334,32 @@ class MiDaS_Background_Foreground_Removal: }, } - RETURN_TYPES = ("IMAGE","IMAGE") + RETURN_TYPES = ("IMAGE", "IMAGE") FUNCTION = "midas_remove" - + CATEGORY = "WAS Suite/Image" def midas_remove(self, - image, - midas_model, - use_cpu='false', - remove='background', - threshold='false', - threshold_low=0, - threshold_mid=127, - threshold_high=255, - smoothing=0.25, - background_red=0, - background_green=0, - background_blue=0): - + image, + midas_model, + use_cpu='false', + remove='background', + threshold='false', + threshold_low=0, + threshold_mid=127, + threshold_high=255, + smoothing=0.25, + background_red=0, + background_green=0, + background_blue=0): + global MIDAS_INSTALLED - + if not MIDAS_INSTALLED: self.install_midas() - + import cv2 as cv - + # Convert the input image tensor to a numpy and PIL Image i = 255. * image.cpu().numpy().squeeze() img = i @@ -2280,8 +2369,9 @@ class MiDaS_Background_Foreground_Removal: print("\033[34mWAS NS:\033[0m Downloading and loading MiDaS Model...") torch.hub.set_dir(self.midas_dir) midas = torch.hub.load("intel-isl/MiDaS", midas_model, trust_repo=True) - device = torch.device("cuda") if torch.cuda.is_available() and use_cpu == 'false' else torch.device("cpu") - + device = torch.device("cuda") if torch.cuda.is_available( + ) and use_cpu == 'false' else torch.device("cpu") + print('\033[34mWAS NS:\033[0m MiDaS is using device:', device) midas.to(device).eval() @@ -2294,7 +2384,7 @@ class MiDaS_Background_Foreground_Removal: img = cv.cvtColor(img, cv.COLOR_BGR2RGB) input_batch = transform(img).to(device) - + print('\033[34mWAS NS:\033[0m Approximating depth from image.') with torch.no_grad(): @@ -2308,40 +2398,45 @@ class MiDaS_Background_Foreground_Removal: # Invert depth map if remove == 'foreground': - depth = ( 255 - prediction.cpu().numpy().astype(np.uint8) ) + depth = (255 - prediction.cpu().numpy().astype(np.uint8)) depth = depth.astype(np.float32) else: depth = prediction.cpu().numpy().astype(np.float32) depth = depth * 255 / (np.max(depth)) / 255 depth = Image.fromarray(np.uint8(depth * 255)) - + # Threshold depth mask if threshold == 'true': - levels = self.AdjustLevels(threshold_low, threshold_mid, threshold_high) + levels = self.AdjustLevels( + threshold_low, threshold_mid, threshold_high) depth = levels.adjust(depth.convert('RGB')).convert('L') if smoothing > 0: depth = depth.filter(ImageFilter.GaussianBlur(radius=smoothing)) depth = depth.resize(img_original.size).convert('L') - + # Validate background color arguments - background_red = int(background_red) if isinstance(background_red, (int, float)) else 0 - background_green = int(background_green) if isinstance(background_green, (int, float)) else 0 - background_blue = int(background_blue) if isinstance(background_blue, (int, float)) else 0 + background_red = int(background_red) if isinstance( + background_red, (int, float)) else 0 + background_green = int(background_green) if isinstance( + background_green, (int, float)) else 0 + background_blue = int(background_blue) if isinstance( + background_blue, (int, float)) else 0 # Create background color tuple - background_color = ( background_red, background_green, background_blue ) - + background_color = (background_red, background_green, background_blue) + # Create background image - background = Image.new(mode="RGB", size=img_original.size, color=background_color) - + background = Image.new( + mode="RGB", size=img_original.size, color=background_color) + # Composite final image result_img = Image.composite(img_original, background, depth) del midas, device, midas_transforms del transform, img, img_original, input_batch, prediction - - return ( pil2tensor(result_img), pil2tensor(depth.convert('RGB')) ) - + + return (pil2tensor(result_img), pil2tensor(depth.convert('RGB'))) + class AdjustLevels: def __init__(self, min_level, mid_level, max_level): self.min_level = min_level @@ -2358,7 +2453,8 @@ class MiDaS_Background_Foreground_Removal: im_arr[im_arr < self.min_level] = self.min_level # apply the mid level adjustment - im_arr = (im_arr - self.min_level) * (255 / (self.max_level - self.min_level)) + im_arr = (im_arr - self.min_level) * \ + (255 / (self.max_level - self.min_level)) im_arr[im_arr < 0] = 0 im_arr[im_arr > 255] = 255 im_arr = im_arr.astype(np.uint8) @@ -2368,15 +2464,17 @@ class MiDaS_Background_Foreground_Removal: im = ImageOps.autocontrast(im, cutoff=self.max_level) return im - + def install_midas(self): global MIDAS_INSTALLED if 'timm' not in packages(): print("\033[34mWAS NS:\033[0m Installing timm...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'timm']) + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'timm']) if 'opencv-python' not in packages(): print("\033[34mWAS NS:\033[0m Installing CV2...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) + subprocess.check_call( + [sys.executable, '-m', 'pip', '-q', 'install', 'opencv-python']) MIDAS_INSTALLED = True @@ -2390,42 +2488,43 @@ class WAS_NSP_CLIPTextEncoder: pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "noodle_key": ("STRING", {"default": '__', "multiline": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "text": ("STRING", {"multiline": True}), - "clip": ("CLIP",), - } - } - + "required": { + "noodle_key": ("STRING", {"default": '__', "multiline": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "text": ("STRING", {"multiline": True}), + "clip": ("CLIP",), + } + } + OUTPUT_NODE = True RETURN_TYPES = ("CONDITIONING",) FUNCTION = "nsp_encode" CATEGORY = "WAS Suite/Conditioning" - def nsp_encode(self, clip, text, noodle_key = '__', seed = 0): - + def nsp_encode(self, clip, text, noodle_key='__', seed=0): + # Fetch the NSP Pantry local_pantry = os.getcwd()+'/ComfyUI/custom_nodes/nsp_pantry.json' if not os.path.exists(local_pantry): - response = urlopen('https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json') + response = urlopen( + 'https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json') tmp_pantry = json.loads(response.read()) # Dump JSON locally pantry_serialized = json.dumps(tmp_pantry, indent=4) with open(local_pantry, "w") as f: f.write(pantry_serialized) del response, tmp_pantry - + # Load local pantry with open(local_pantry, 'r') as f: nspterminology = json.load(f) - + if seed > 0 or seed < 1: random.seed(seed) - + # Parse Text new_text = text for term in nspterminology: @@ -2435,13 +2534,14 @@ class WAS_NSP_CLIPTextEncoder: tcount = new_text.count(tkey) # Apply random results for each noodle counted for _ in range(tcount): - new_text = new_text.replace(tkey, random.choice(nspterminology[term]), 1) + new_text = new_text.replace( + tkey, random.choice(nspterminology[term]), 1) seed = seed+1 random.seed(seed) - + print('\033[34mWAS NS\033[0m CLIPTextEncode NSP:', new_text) - - return ([[clip.encode(new_text), {}]],{"ui":{"prompt":new_text}}) + + return ([[clip.encode(new_text), {}]], {"ui": {"prompt": new_text}}) #! SAMPLING NODES @@ -2450,19 +2550,19 @@ class WAS_NSP_CLIPTextEncoder: class WAS_KSampler: @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return {"required": - {"model": ("MODEL",), - "seed": ("SEED",), - "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), - "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), - "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), - "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), - "positive": ("CONDITIONING", ), - "negative": ("CONDITIONING", ), - "latent_image": ("LATENT", ), - "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - } + {"model": ("MODEL",), + "seed": ("SEED",), + "steps": ("INT", {"default": 20, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), + "positive": ("CONDITIONING", ), + "negative": ("CONDITIONING", ), + "latent_image": ("LATENT", ), + "denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + } } RETURN_TYPES = ("LATENT",) @@ -2474,85 +2574,86 @@ class WAS_KSampler: return nodes.common_ksampler(model, seed['seed'], steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise) # SEED NODE - + + class WAS_Seed: @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return {"required": - {"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})} + {"seed": ("INT", {"default": 0, "min": 0, + "max": 0xffffffffffffffff})} } - RETURN_TYPES = ("SEED",) FUNCTION = "seed" CATEGORY = "WAS Suite/Constant" def seed(self, seed): - return ( {"seed": seed,}, ) - + return ({"seed": seed, }, ) + #! TEXT NODES # Text Multiline Node class WAS_Text_Multiline: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("STRING", {"default": '', "multiline": True}), - } - } + "required": { + "text": ("STRING", {"default": '', "multiline": True}), + } + } RETURN_TYPES = ("ASCII",) FUNCTION = "text_multiline" CATEGORY = "WAS Suite/Text" def text_multiline(self, text): - return ( text, ) - - + return (text, ) + + # Text String Node class WAS_Text_String: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("STRING", {"default": '', "multiline": False}), - } - } + "required": { + "text": ("STRING", {"default": '', "multiline": False}), + } + } RETURN_TYPES = ("ASCII",) FUNCTION = "text_string" CATEGORY = "WAS Suite/Text" def text_string(self, text): - return ( text, ) + return (text, ) # Text Random Line class WAS_Text_Random_Line: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("ASCII",), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - + "required": { + "text": ("ASCII",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + RETURN_TYPES = ("ASCII",) FUNCTION = "text_random_line" @@ -2562,136 +2663,137 @@ class WAS_Text_Random_Line: lines = text.split("\n") random.seed(seed) choice = random.choice(lines) - return ( choice, ) + return (choice, ) -# Text Concatenate - +# Text Concatenate + class WAS_Text_Concatenate: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text_a": ("ASCII",), - "text_b": ("ASCII",), - "linebreak_addition": (['true','false'], ), - } - } - + "required": { + "text_a": ("ASCII",), + "text_b": ("ASCII",), + "linebreak_addition": (['true', 'false'], ), + } + } + RETURN_TYPES = ("ASCII",) FUNCTION = "text_concatenate" CATEGORY = "WAS Suite/Text" def text_concatenate(self, text_a, text_b, linebreak_addition): - return ( text_a + ("\n" if linebreak_addition == 'true' else '') + text_b, ) - + return (text_a + ("\n" if linebreak_addition == 'true' else '') + text_b, ) + + +# Text Search and Replace -# Text Search and Replace - class WAS_Search_and_Replace: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("ASCII",), - "find": ("STRING", {"default": '', "multiline": False}), - "replace": ("STRING", {"default": '', "multiline": False}), - } - } - + "required": { + "text": ("ASCII",), + "find": ("STRING", {"default": '', "multiline": False}), + "replace": ("STRING", {"default": '', "multiline": False}), + } + } + RETURN_TYPES = ("ASCII",) FUNCTION = "text_search_and_replace" CATEGORY = "WAS Suite/Text" def text_search_and_replace(self, text, find, replace): - return ( self.replace_substring(text, find, replace), ) - + return (self.replace_substring(text, find, replace), ) + def replace_substring(self, text, find, replace): import re text = re.sub(find, replace, text) return text - -# Text Search and Replace - + +# Text Search and Replace + class WAS_Search_and_Replace_Input: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("ASCII",), - "find": ("ASCII",), - "replace": ("ASCII",), - } - } - + "required": { + "text": ("ASCII",), + "find": ("ASCII",), + "replace": ("ASCII",), + } + } + RETURN_TYPES = ("ASCII",) FUNCTION = "text_search_and_replace" CATEGORY = "WAS Suite/Text" def text_search_and_replace(self, text, find, replace): - return ( self.replace_substring(text, find, replace), ) - + return (self.replace_substring(text, find, replace), ) + def replace_substring(self, text, find, replace): import re text = re.sub(find, replace, text) return text - - + + # Text Parse NSP - + class WAS_Text_Parse_NSP: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "noodle_key": ("STRING", {"default": '__', "multiline": False}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - "text": ("ASCII",), - } - } - + "required": { + "noodle_key": ("STRING", {"default": '__', "multiline": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "text": ("ASCII",), + } + } + OUTPUT_NODE = True RETURN_TYPES = ("ASCII",) FUNCTION = "text_parse_nsp" CATEGORY = "WAS Suite/Text" - def text_parse_nsp(self, text, noodle_key = '__', seed = 0): - + def text_parse_nsp(self, text, noodle_key='__', seed=0): + # Fetch the NSP Pantry local_pantry = os.getcwd()+'/ComfyUI/custom_nodes/nsp_pantry.json' if not os.path.exists(local_pantry): - response = urlopen('https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json') + response = urlopen( + 'https://raw.githubusercontent.com/WASasquatch/noodle-soup-prompts/main/nsp_pantry.json') tmp_pantry = json.loads(response.read()) # Dump JSON locally pantry_serialized = json.dumps(tmp_pantry, indent=4) with open(local_pantry, "w") as f: f.write(pantry_serialized) del response, tmp_pantry - + # Load local pantry with open(local_pantry, 'r') as f: nspterminology = json.load(f) - + if seed > 0 or seed < 1: random.seed(seed) - + # Parse Text new_text = text for term in nspterminology: @@ -2701,31 +2803,32 @@ class WAS_Text_Parse_NSP: tcount = new_text.count(tkey) # Apply random results for each noodle counted for _ in range(tcount): - new_text = new_text.replace(tkey, random.choice(nspterminology[term]), 1) + new_text = new_text.replace( + tkey, random.choice(nspterminology[term]), 1) seed = seed+1 random.seed(seed) - + print('\033[34mWAS NS\033[0m Text Parse NSP:', new_text) - - return ( new_text, ) - - + + return (new_text, ) + + # TEXT SEARCH AND REPLACE - + class WAS_Text_Save: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("ASCII",), - "path": ("STRING", {"default": '', "multiline": False}), - "filename": ("STRING", {"default": f'text_[time]', "multiline": False}), - } - } - + "required": { + "text": ("ASCII",), + "path": ("STRING", {"default": '', "multiline": False}), + "filename": ("STRING", {"default": f'text_[time]', "multiline": False}), + } + } + OUTPUT_NODE = True RETURN_TYPES = () FUNCTION = "save_text_file" @@ -2733,80 +2836,83 @@ class WAS_Text_Save: CATEGORY = "WAS Suite/IO" def save_text_file(self, text, path, filename): - + # Ensure path exists if not os.path.exists(path): - print(f'\033[34mWAS NS\033[0m Error: The path `{path}` doesn\'t exist!') - + print( + f'\033[34mWAS NS\033[0m Error: The path `{path}` doesn\'t exist!') + # Ensure content to save if text.strip == '': - print(f'\033[34mWAS NS\033[0m Error: There is no text specified to save! Text is empty.') - + print( + f'\033[34mWAS NS\033[0m Error: There is no text specified to save! Text is empty.') + # Replace tokens tokens = { - '[time]': f'{round(time.time())}', - } + '[time]': f'{round(time.time())}', + } for k in tokens.keys(): text = self.replace_substring(text, k, tokens[k]) - + # Write text file self.writeTextFile(os.path.join(path, filename + '.txt'), text) - - return( text, ) - + + return (text, ) + # Save Text FileNotFoundError def writeTextFile(self, file, content): try: with open(file, 'w') as f: f.write(content) except OSError: - print(f'\033[34mWAS Node Suite\033[0m Error: Unable to save file `{file}`') - - + print( + f'\033[34mWAS Node Suite\033[0m Error: Unable to save file `{file}`') + # Replace a substring + def replace_substring(self, text, find, replace): import re text = re.sub(find, replace, text) return text - + # TEXT TO CONDITIONIONG class WAS_Text_to_Conditioning: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "clip": ("CLIP",), - "text": ("ASCII",), - } - } - + "required": { + "clip": ("CLIP",), + "text": ("ASCII",), + } + } + RETURN_TYPES = ("CONDITIONING",) FUNCTION = "text_to_conditioning" CATEGORY = "WAS Suite/Text" def text_to_conditioning(self, clip, text): - return ( [[clip.encode(text), {}]], ) - - + return ([[clip.encode(text), {}]], ) + + class WAS_Text_to_Console: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "text": ("ASCII",), - "label": ("STRING", {"default": f'Text Output', "multiline": False}), - } - } - + "required": { + "text": ("ASCII",), + "label": ("STRING", {"default": f'Text Output', "multiline": False}), + } + } + RETURN_TYPES = ("ASCII",) OUTPUT_NODE = True FUNCTION = "text_to_console" @@ -2817,41 +2923,41 @@ class WAS_Text_to_Console: if label.strip() != '': print(f'\033[34mWAS Node Suite \033[33m{label}\033[0m:\n{text}\n') else: - print(f'\033[34mWAS Node Suite \033[33mText to Console\033[0m:\n{text}\n') - return ( text, ) + print( + f'\033[34mWAS Node Suite \033[33mText to Console\033[0m:\n{text}\n') + return (text, ) - -# LOAD TEXT FILE +# LOAD TEXT FILE class WAS_Text_Load_From_File: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "file_path": ("STRING", {"default": '', "multiline": False}), - } - } - + "required": { + "file_path": ("STRING", {"default": '', "multiline": False}), + } + } + RETURN_TYPES = ("ASCII",) FUNCTION = "load_file" CATEGORY = "WAS Suite/IO" def load_file(self, file_path=''): - return ( self.load_text_file(file_path), ) - + return (self.load_text_file(file_path), ) + def load_text_file(self, path): if not os.path.exists(path): - print(f'\033[34mWAS Node Suite\033[0m Error: The path `{file}` specified cannot be found.') + print( + f'\033[34mWAS Node Suite\033[0m Error: The path `{path}` specified cannot be found.') return '' with open(path, 'r') as file: text = file.read() return text - #! NUMBERS @@ -2860,30 +2966,30 @@ class WAS_Text_Load_From_File: # RANDOM NUMBER class WAS_Random_Number: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "number_type": (["integer","float","bool"],), - "minimum": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), - "maximum": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - + "required": { + "number_type": (["integer", "float", "bool"],), + "minimum": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), + "maximum": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + } + } + RETURN_TYPES = ("NUMBER",) FUNCTION = "return_randm_number" CATEGORY = "WAS Suite/Constant" def return_randm_number(self, minimum, maximum, seed, number_type='integer'): - + # Set Generator Seed random.seed(seed) - + # Return random number match number_type: case 'integer': @@ -2892,172 +2998,169 @@ class WAS_Random_Number: number = random.uniform(minimum, maximum) case 'bool': number = random.random() - - # Return number - return ( number, ) + case _: + return + # Return number + return (number, ) # CONSTANT NUMBER class WAS_Constant_Number: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "number_type": (["integer","float","bool"],), - "number": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), - } - } - + "required": { + "number_type": (["integer", "float", "bool"],), + "number": ("FLOAT", {"default": 0, "min": 0xffffffffffffffff, "max": 0xffffffffffffffff}), + } + } + RETURN_TYPES = ("NUMBER",) FUNCTION = "return_constant_number" CATEGORY = "WAS Suite/Constant" def return_constant_number(self, number_type, number): - + # Return number match number_type: case 'integer': - return ( int(number), ) + return (int(number), ) case 'integer': - return ( float(number), ) + return (float(number), ) case 'bool': - return ( ( 1 if int(number) > 0 else 0 ), ) - + return ((1 if int(number) > 0 else 0), ) # NUMBER TO SEED class WAS_Number_To_Seed: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "number": ("NUMBER",), - } - } - + "required": { + "number": ("NUMBER",), + } + } + RETURN_TYPES = ("SEED",) FUNCTION = "number_to_seed" CATEGORY = "WAS Suite/Constant" def return_constant_number(self, number): - return ( {"seed":number,}, ) + return ({"seed": number, }, ) + - - - # NUMBER OPERATIONS - + class WAS_Number_Operation: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "number_a": ("NUMBER",), - "number_b": ("NUMBER",), - "operation": (["addition","subtraction","division","floor division","multiplication","exponentiation","modulus","greater-than","greater-than or equels","less-than","less-than or equals","equals","does not equal"],), - } - } - + "required": { + "number_a": ("NUMBER",), + "number_b": ("NUMBER",), + "operation": (["addition", "subtraction", "division", "floor division", "multiplication", "exponentiation", "modulus", "greater-than", "greater-than or equels", "less-than", "less-than or equals", "equals", "does not equal"],), + } + } + RETURN_TYPES = ("NUMBER",) FUNCTION = "math_operations" CATEGORY = "WAS Suite/Operations" def math_operations(self, number_a, number_b, operation="addition"): - + # Return random number match operation: case 'addition': - return ( (number_a + number_b), ) + return ((number_a + number_b), ) case 'subtraction': - return ( (number_a - number_b), ) + return ((number_a - number_b), ) case 'division': - return ( (number_a / number_b), ) + return ((number_a / number_b), ) case 'floor division': - return ( (number_a // number_b), ) + return ((number_a // number_b), ) case 'multiplication': - return ( (number_a * number_b), ) + return ((number_a * number_b), ) case 'exponentiation': - return ( (number_a ** number_b), ) + return ((number_a ** number_b), ) case 'modulus': - return ( (number_a % number_b), ) + return ((number_a % number_b), ) case 'greater-than': - return ( +(number_a > number_b), ) + return (+(number_a > number_b), ) case 'greater-than or equals': - return ( +(number_a >= number_b), ) + return (+(number_a >= number_b), ) case 'less-than': - return ( +(number_a < number_b), ) + return (+(number_a < number_b), ) case 'less-than or equals': - return ( +(number_a <= number_b), ) + return (+(number_a <= number_b), ) case 'equals': - return ( +(number_a == number_b), ) + return (+(number_a == number_b), ) case 'does not equal': - return ( +(number_a != number_b), ) - + return (+(number_a != number_b), ) + + +#! MISC + -#! MISC - - # INPUT SWITCH - + class WAS_Input_Switch: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "input_a": ("*",), - "input_b": ("*",), - "boolean": ("NUMBER",), - } - } - + "required": { + "input_a": ("*",), + "input_b": ("*",), + "boolean": ("NUMBER",), + } + } + RETURN_TYPES = ("*",) FUNCTION = "input_switch" CATEGORY = "WAS Suite/Operations" def input_switch(self, input_a, input_b, boolean=0): - + if int(boolean) == 1: - return ( input_a, ) + return (input_a, ) else: - return ( input_b, ) - - + return (input_b, ) + # DEBUG INPUT TO CONSOLE - - + + class WAS_Debug_to_Console: - def __init__(s): + def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): return { - "required": { - "debug_input": ("*",), - "label": ("STRING", {"default": f'Debug Input', "multiline": False}), - } - } - + "required": { + "debug_input": ("*",), + "label": ("STRING", {"default": f'Debug Input', "multiline": False}), + } + } + RETURN_TYPES = ("*",) OUTPUT_NODE = True FUNCTION = "debug_to_console" @@ -3066,16 +3169,15 @@ class WAS_Debug_to_Console: def debug_to_console(self, label, debug_input=None): if label.strip() != '': - print(f'\033[34mWAS Node Suite \033[33m{label}\033[0m:\n{debug_input}\n') + print( + f'\033[34mWAS Node Suite \033[33m{label}\033[0m:\n{debug_input}\n') else: - print(f'\033[34mWAS Node Suite \033[33mDebug to Console\033[0m:\n{debug_input}\n') - return ( debug_input, ) - - + print( + f'\033[34mWAS Node Suite \033[33mDebug to Console\033[0m:\n{debug_input}\n') + return (debug_input, ) # NODE MAPPING - NODE_CLASS_MAPPINGS = { "CLIPTextEncode (NSP)": WAS_NSP_CLIPTextEncoder, "Constant Number": WAS_Constant_Number, @@ -3133,4 +3235,4 @@ NODE_CLASS_MAPPINGS = { "Text to Console": WAS_Text_to_Console, } -print('\033[34mWAS Node Suite: \033[92mLoaded\033[0m') \ No newline at end of file +print('\033[34mWAS Node Suite: \033[92mLoaded\033[0m') diff --git a/workflows/several_filters_test_workflow.jpg b/workflows/several_filters_test_workflow.jpg new file mode 100644 index 0000000..3a44506 Binary files /dev/null and b/workflows/several_filters_test_workflow.jpg differ diff --git a/workflows/several_filters_test_workflow.json b/workflows/several_filters_test_workflow.json new file mode 100644 index 0000000..3792386 --- /dev/null +++ b/workflows/several_filters_test_workflow.json @@ -0,0 +1,684 @@ +{ + "last_node_id": 20, + "last_link_id": 27, + "nodes": [ + { + "id": 2, + "type": "PreviewImage", + "pos": [ + -442, + -186 + ], + "size": { + "0": 210, + "1": 250 + }, + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 1 + } + ], + "properties": {} + }, + { + "id": 3, + "type": "Image Canny Filter", + "pos": [ + -555, + 192 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 2 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 3 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "true", + 0.5, + 1 + ] + }, + { + "id": 4, + "type": "PreviewImage", + "pos": [ + -195, + 193 + ], + "size": { + "0": 210, + "1": 250 + }, + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 3 + } + ], + "properties": {} + }, + { + "id": 1, + "type": "Image Load", + "pos": [ + -1164, + 137 + ], + "size": { + "0": 315, + "1": 78 + }, + "flags": {}, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 1, + 2, + 7, + 13, + 15 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null, + "slot_index": 1 + } + ], + "properties": {}, + "widgets_values": [ + "./ComfyUI/input/example.png" + ] + }, + { + "id": 8, + "type": "PreviewImage", + "pos": [ + 34, + 186 + ], + "size": { + "0": 210, + "1": 250 + }, + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 14 + } + ], + "properties": {} + }, + { + "id": 10, + "type": "Image Rotate", + "pos": [ + -560, + 350 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 13, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 14 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "transpose", + 90, + "nearest" + ] + }, + { + "id": 7, + "type": "Image Resize", + "pos": [ + -561, + 492 + ], + "size": { + "0": 315, + "1": 154 + }, + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 7 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 8, + 23 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "resize", + "nearest", + 2, + 1024, + 1536 + ] + }, + { + "id": 6, + "type": "PreviewImage", + "pos": [ + -195, + 499 + ], + "size": [ + 628.8941372958095, + 589.2248913473331 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 8 + } + ], + "properties": {} + }, + { + "id": 15, + "type": "PreviewImage", + "pos": [ + 466, + 501 + ], + "size": { + "0": 628.8941650390625, + "1": 589.2249145507812 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 20 + } + ], + "properties": {} + }, + { + "id": 11, + "type": "Image Bloom Filter", + "pos": [ + -564, + 705 + ], + "size": [ + 315, + 82 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 16 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 20 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + 959, + 0.7000000000000001 + ] + }, + { + "id": 13, + "type": "Reroute", + "pos": [ + -728, + 1163 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 15, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "", + "type": "IMAGE", + "links": [ + 16, + 22, + 24 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false + } + }, + { + "id": 12, + "type": "Image fDOF Filter", + "pos": [ + -568, + 897 + ], + "size": { + "0": 315, + "1": 126 + }, + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 22 + }, + { + "name": "depth", + "type": "IMAGE", + "link": 23, + "slot_index": 1 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 19 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "gaussian", + 14, + 3 + ] + }, + { + "id": 17, + "type": "Image Style Filter", + "pos": [ + -824, + 1228 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 24, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 25, + 26 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "fairy tale" + ] + }, + { + "id": 20, + "type": "PreviewImage", + "pos": [ + 517, + 1204 + ], + "size": { + "0": 628.8941650390625, + "1": 589.2249145507812 + }, + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 27, + "slot_index": 0 + } + ], + "properties": {} + }, + { + "id": 19, + "type": "Image Style Filter", + "pos": [ + -186, + 1186 + ], + "size": { + "0": 315, + "1": 58 + }, + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 26, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 27 + ], + "slot_index": 0 + } + ], + "properties": {}, + "widgets_values": [ + "perpetua" + ] + }, + { + "id": 18, + "type": "PreviewImage", + "pos": [ + -195, + 1299 + ], + "size": { + "0": 628.8941650390625, + "1": 589.2249145507812 + }, + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 25 + } + ], + "properties": {} + }, + { + "id": 16, + "type": "PreviewImage", + "pos": [ + 467, + -142 + ], + "size": { + "0": 628.8941650390625, + "1": 589.2249145507812 + }, + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 19 + } + ], + "properties": {} + } + ], + "links": [ + [ + 1, + 1, + 0, + 2, + 0, + "IMAGE" + ], + [ + 2, + 1, + 0, + 3, + 0, + "IMAGE" + ], + [ + 3, + 3, + 0, + 4, + 0, + "IMAGE" + ], + [ + 7, + 1, + 0, + 7, + 0, + "IMAGE" + ], + [ + 8, + 7, + 0, + 6, + 0, + "IMAGE" + ], + [ + 13, + 1, + 0, + 10, + 0, + "IMAGE" + ], + [ + 14, + 10, + 0, + 8, + 0, + "IMAGE" + ], + [ + 15, + 1, + 0, + 13, + 0, + "*" + ], + [ + 16, + 13, + 0, + 11, + 0, + "IMAGE" + ], + [ + 19, + 12, + 0, + 16, + 0, + "IMAGE" + ], + [ + 20, + 11, + 0, + 15, + 0, + "IMAGE" + ], + [ + 22, + 13, + 0, + 12, + 0, + "IMAGE" + ], + [ + 23, + 7, + 0, + 12, + 1, + "IMAGE" + ], + [ + 24, + 13, + 0, + 17, + 0, + "IMAGE" + ], + [ + 25, + 17, + 0, + 18, + 0, + "IMAGE" + ], + [ + 26, + 17, + 0, + 19, + 0, + "IMAGE" + ], + [ + 27, + 19, + 0, + 20, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": {}, + "version": 0.4 +} \ No newline at end of file