From e5da1a895ff57cdb29c5fbd42e063512ad33effd Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Sun, 26 Mar 2023 15:31:29 -0700 Subject: [PATCH] Adding types Update WAS_Node_Suite.py Create several_filters_test_workflow.jpg Update several_filters_test_workflow.json Create several_filters_test_workflow.json Turned on type annotation checking Still a lot of Any / Unknown, but I think this caught a few more missed renames --- WAS_Node_Suite.py | 1742 +++++++++--------- workflows/several_filters_test_workflow.jpg | Bin 0 -> 98488 bytes workflows/several_filters_test_workflow.json | 684 +++++++ 3 files changed, 1606 insertions(+), 820 deletions(-) create mode 100644 workflows/several_filters_test_workflow.jpg create mode 100644 workflows/several_filters_test_workflow.json 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 0000000000000000000000000000000000000000..3a44506ef68ca30d3ab93a5a465b594f42e5d802 GIT binary patch literal 98488 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 literal 0 HcmV?d00001 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