From 109601d14d24709ddcda0464b0f3e3fc9954acb5 Mon Sep 17 00:00:00 2001 From: Silversith Date: Sat, 1 Apr 2023 23:28:48 +0200 Subject: [PATCH] remove file, as creator edited fixed it for Python 3.8 --- custom_nodes/WAS_Node_Suite.py | 4057 -------------------------------- 1 file changed, 4057 deletions(-) delete mode 100644 custom_nodes/WAS_Node_Suite.py diff --git a/custom_nodes/WAS_Node_Suite.py b/custom_nodes/WAS_Node_Suite.py deleted file mode 100644 index d61684bc6..000000000 --- a/custom_nodes/WAS_Node_Suite.py +++ /dev/null @@ -1,4057 +0,0 @@ -# By WASasquatch (Discord: WAS#0263) -# -# 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, -# 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. -from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops, ImageFont -from PIL.PngImagePlugin import PngInfo -from io import BytesIO -from typing import Optional -from urllib.request import urlopen -import comfy.samplers -import comfy.sd -import comfy.utils -import glob -import hashlib -import json -import nodes -import numpy as np -import os -import random -import requests -import subprocess -import sys -import time -import torch - -sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy")) -sys.path.append('..'+os.sep+'ComfyUI') - - -# GLOBALS -NODE_FILE = os.path.abspath(__file__) -MIDAS_INSTALLED = False -CUSTOM_NODES_DIR = ( os.path.dirname(os.path.dirname(NODE_FILE)) - if os.path.dirname(os.path.dirname(NODE_FILE)) == 'was-node-suite-comfyui' - or os.path.dirname(os.path.dirname(NODE_FILE)) == 'was-node-suite-comfyui-main' - else os.path.dirname(NODE_FILE) ) -WAS_SUITE_ROOT = os.path.dirname(NODE_FILE) -WAS_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_suite_settings.json') - -# WAS Suite Locations Debug -print('\033[34mWAS Node Suite:\033[0m Running At:', NODE_FILE) -print('\033[34mWAS Node Suite:\033[0m Running From:', WAS_SUITE_ROOT) - -#! SUITE SPECIFIC CLASSES & FUNCTIONS - -# Freeze PIP modules -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: 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: Image.Image) -> torch.Tensor: - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - -# PIL 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) - 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') - -# WAS SETTINGS MANAGER - -class WASDatabase: - """ - The WAS Suite Database Class provides a simple key-value database that stores - data in a flatfile using the JSON format. Each key-value pair is associated with - a category. - - Attributes: - filepath (str): The path to the JSON file where the data is stored. - data (dict): The dictionary that holds the data read from the JSON file. - - Methods: - insert(category, key, value): Inserts a key-value pair into the database - under the specified category. - get(category, key): Retrieves the value associated with the specified - key and category from the database. - update(category, key): Update a value associated with the specified - key and category from the database. - delete(category, key): Deletes the key-value pair associated with the - specified key and category from the database. - _save(): Saves the current state of the database to the JSON file. - """ - def __init__(self, filepath): - self.filepath = filepath - try: - with open(filepath, 'r') as f: - self.data = json.load(f) - except FileNotFoundError: - self.data = {} - - def insert(self, category, key, value): - if category not in self.data: - self.data[category] = {} - self.data[category][key] = value - self._save() - - def update(self, category, key, value): - if category in self.data and key in self.data[category]: - self.data[category][key] = value - self._save() - - def get(self, category, key): - return self.data.get(category, {}).get(key, None) - - def delete(self, category, key): - if category in self.data and key in self.data[category]: - del self.data[category][key] - self._save() - - def _save(self): - try: - with open(self.filepath, 'w') as f: - json.dump(self.data, f) - except FileNotFoundError: - print(f"\033[34mWAS Node Suite\033[0m Warning: Cannot save database to file '{self.filepath}'." - " Storing the data in the object instead. Does the folder and node file have write permissions?") - -# Initialize the settings database -WDB = WASDatabase(WAS_DATABASE) - -class WAS_Filter_Class(): - - # TOOLS - - def fig2img(self, plot): - import io - buf = io.BytesIO() - plot.savefig(buf) - buf.seek(0) - img = Image.open(buf) - return img - - # FILTERS - - # 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) - saturation_enhancer = ImageEnhance.Color(image) - image = saturation_enhancer.enhance(1.5) - - bloom = image.filter(ImageFilter.GaussianBlur(radius=20)) - bloom = ImageEnhance.Brightness(bloom).enhance(1.2) - bloom.putalpha(128) - bloom = bloom.convert(image.mode) - image = Image.alpha_composite(image, bloom) - - width, height = image.size - # Particls A - particles = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(particles) - for i in range(5000): - x = random.randint(0, width) - y = random.randint(0, height) - r = random.randint(0, 255) - g = random.randint(0, 255) - b = random.randint(0, 255) - draw.point((x, y), fill=(r, g, b, 255)) - particles = particles.filter(ImageFilter.GaussianBlur(radius=1)) - particles.putalpha(128) - - particles2 = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(particles2) - for i in range(5000): - x = random.randint(0, width) - y = random.randint(0, height) - r = random.randint(0, 255) - g = random.randint(0, 255) - b = random.randint(0, 255) - 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) - - return image - - def digital_distortion(self, image, amplitude=5, line_width=2): - # Convert the PIL image to a numpy array - im = np.array(image) - - # Create a sine wave with the given amplitude - x, y, z = im.shape - sine_wave = amplitude * np.sin(np.linspace(-np.pi, np.pi, y)) - sine_wave = sine_wave.astype(int) - - # Create the left and right distortion matrices - left_distortion = np.zeros((x, y, z), dtype=np.uint8) - right_distortion = np.zeros((x, y, z), dtype=np.uint8) - for i in range(y): - left_distortion[:, i, :] = np.roll(im[:, i, :], -sine_wave[i], axis=0) - right_distortion[:, i, :] = np.roll(im[:, i, :], sine_wave[i], axis=0) - - # Combine the distorted images and add scan lines as a mask - distorted_image = np.maximum(left_distortion, right_distortion) - scan_lines = np.zeros((x, y), dtype=np.float32) - scan_lines[::line_width, :] = 1 - scan_lines = np.minimum(scan_lines * amplitude*50.0, 1) # Scale scan line values - scan_lines = np.tile(scan_lines[:, :, np.newaxis], (1, 1, z)) # Add channel dimension - distorted_image = np.where(scan_lines > 0, np.random.permutation(im), distorted_image) - distorted_image = np.roll(distorted_image, np.random.randint(0, y), axis=1) - - # Convert the numpy array back to a PIL image - distorted_image = Image.fromarray(distorted_image) - - return distorted_image - - def signal_distortion(self, image, amplitude): - # Convert the image to a numpy array for easy manipulation - img_array = np.array(image) - - # Generate random shift values for each row of the image - row_shifts = np.random.randint(-amplitude, amplitude + 1, size=img_array.shape[0]) - - # Create an empty array to hold the distorted image - distorted_array = np.zeros_like(img_array) - - # Loop through each row of the image - for y in range(img_array.shape[0]): - # Determine the X-axis shift value for this row - x_shift = row_shifts[y] - - # Use modular function to determine where to shift - x_shift = x_shift + y % (amplitude * 2) - amplitude - - # Shift the pixels in this row by the X-axis shift value - distorted_array[y,:] = np.roll(img_array[y,:], x_shift, axis=0) - - # Convert the distorted array back to a PIL image - distorted_image = Image.fromarray(distorted_array) - - return distorted_image - - def tv_vhs_distortion(self, image, amplitude=10): - # Convert the PIL image to a NumPy array. - np_image = np.array(image) - - # Generate random shift values for each row of the image - offset_variance = int(image.height / amplitude) - row_shifts = np.random.randint(-offset_variance, offset_variance + 1, size=image.height) - - # Create an empty array to hold the distorted image - distorted_array = np.zeros_like(np_image) - - # Loop through each row of the image - for y in range(np_image.shape[0]): - # Determine the X-axis shift value for this row - x_shift = row_shifts[y] - - # Use modular function to determine where to shift - x_shift = x_shift + y % (offset_variance * 2) - offset_variance - - # Shift the pixels in this row by the X-axis shift value - distorted_array[y,:] = np.roll(np_image[y,:], x_shift, axis=0) - - # Apply distortion and noise to the image using NumPy functions. - h, w, c = distorted_array.shape - x_scale = np.linspace(0, 1, w) - y_scale = np.linspace(0, 1, h) - x_idx = np.broadcast_to(x_scale, (h, w)) - y_idx = np.broadcast_to(y_scale.reshape(h, 1), (h, w)) - noise = np.random.rand(h, w, c) * 0.1 - distortion = np.sin(x_idx * 50) * 0.5 + np.sin(y_idx * 50) * 0.5 - distorted_array = distorted_array + distortion[:, :, np.newaxis] + noise - - # Convert the distorted array back to a PIL image - distorted_image = Image.fromarray(np.uint8(distorted_array)) - distorted_image = distorted_image.resize((image.width, image.height)) - - # Apply color enhancement to the original image. - image_enhance = ImageEnhance.Color(image) - image = image_enhance.enhance(0.5) - - # Overlay the distorted image over the original image. - effect_image = ImageChops.overlay(image, distorted_image) - result_image = ImageChops.overlay(image, effect_image) - result_image = ImageChops.blend(image, result_image, 0.25) - - return result_image - - def gradient(self, size, mode='horizontal', colors=None, tolerance=0): - # Parse colors as JSON if it is a string - if isinstance(colors, str): - colors = json.loads(colors) - - colors = {int(k): [int(c) for c in v] for k, v in colors.items()} - - # Set default colors if not provided - if colors is None: - colors = {0:[255,0,0],50:[0,255,0],100:[0,0,255]} - - # Create a new image with a black background - img = Image.new('RGB', size, color=(0, 0, 0)) - - # Determine the color spectrum between the color stops - color_stop_positions = sorted(colors.keys()) - color_stop_count = len(color_stop_positions) - color_stop_index = 0 - spectrum = [] - for i in range(256): - if color_stop_index < color_stop_count - 1 and i > int(color_stop_positions[color_stop_index + 1]): - color_stop_index += 1 - start_pos = color_stop_positions[color_stop_index] - end_pos = color_stop_positions[color_stop_index + 1] if color_stop_index < color_stop_count - 1 else start_pos - start = colors[start_pos] - end = colors[end_pos] - if end_pos - start_pos == 0: - r, g, b = start - else: - r = round(start[0] + (i - start_pos) * (end[0] - start[0]) / (end_pos - start_pos)) - g = round(start[1] + (i - start_pos) * (end[1] - start[1]) / (end_pos - start_pos)) - b = round(start[2] + (i - start_pos) * (end[2] - start[2]) / (end_pos - start_pos)) - spectrum.append((r, g, b)) - - # Draw the gradient - draw = ImageDraw.Draw(img) - if mode == 'horizontal': - for x in range(size[0]): - pos = int(x * 100 / (size[0] - 1)) - color = spectrum[pos] - if tolerance > 0: - color = tuple([round(c / tolerance) * tolerance for c in color]) - draw.line((x, 0, x, size[1]), fill=color) - elif mode == 'vertical': - for y in range(size[1]): - pos = int(y * 100 / (size[1] - 1)) - color = spectrum[pos] - if tolerance > 0: - color = tuple([round(c / tolerance) * tolerance for c in color]) - draw.line((0, y, size[0], y), fill=color) - - return img - - - # Version 2 optimized based on Mark Setchell's ideas - def gradient_map(self, image, gradient_map, reverse=False): - - # Reverse the image - if reverse: - gradient_map = gradient_map.transpose(Image.FLIP_LEFT_RIGHT) - - # Convert image to Numpy array and average RGB channels - na = np.array(image) - grey = np.mean(na, axis=2).astype(np.uint8) - - # Convert gradient map to Numpy array - cmap = np.array(gradient_map.convert('RGB')) - - # Make output image, same height and width as grey image, but 3-channel RGB - result = np.zeros((*grey.shape, 3), dtype=np.uint8) - - # Reshape grey to match the shape of result - grey_reshaped = grey.reshape(-1) - - # Take entries from RGB gradient map according to grayscale values in image - np.take(cmap.reshape(-1, 3), grey_reshaped, axis=0, out=result.reshape(-1, 3)) - - # Convert result to PIL image - result_image = Image.fromarray(result) - - return result_image - - - # Analyze Filters - - def black_white_levels(self, image): - - if 'matplotlib' not in packages(): - print("\033[34mWAS NS:\033[0m Installing matplotlib...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'matplotlib']) - - import matplotlib.pyplot as plt - - # convert to grayscale - image = image.convert('L') - - # Calculate the histogram of grayscale intensities - hist = image.histogram() - - # Find the minimum and maximum grayscale intensity values - min_val = 0 - max_val = 255 - for i in range(256): - if hist[i] > 0: - min_val = i - break - for i in range(255, -1, -1): - if hist[i] > 0: - max_val = i - break - - # Create a graph of the grayscale histogram - plt.figure(figsize=(16, 8)) - plt.hist(image.getdata(), bins=256, range=(0, 256), color='black', alpha=0.7) - plt.xlim([0, 256]) - plt.ylim([0, max(hist)]) - plt.axvline(min_val, color='red', linestyle='dashed') - plt.axvline(max_val, color='red', linestyle='dashed') - plt.title('Black and White Levels') - plt.xlabel('Intensity') - plt.ylabel('Frequency') - - return self.fig2img(plt) - - def channel_frequency(self, image): - - if 'matplotlib' not in packages(): - print("\033[34mWAS NS:\033[0m Installing matplotlib...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'matplotlib']) - - import matplotlib.pyplot as plt - - # Split the image into its RGB channels - r, g, b = image.split() - - # Calculate the frequency of each color in each channel - r_freq = r.histogram() - g_freq = g.histogram() - b_freq = b.histogram() - - # Create a graph to hold the frequency maps - fig, axs = plt.subplots(1, 3, figsize=(16, 4)) - axs[0].set_title('Red Channel') - axs[1].set_title('Green Channel') - axs[2].set_title('Blue Channel') - - # Plot the frequency of each color in each channel - axs[0].plot(range(256), r_freq, color='red') - axs[1].plot(range(256), g_freq, color='green') - axs[2].plot(range(256), b_freq, color='blue') - - # Set the axis limits and labels - for ax in axs: - ax.set_xlim([0, 255]) - ax.set_xlabel('Color Intensity') - ax.set_ylabel('Frequency') - - return self.fig2img(plt) - - - def generate_palette(self, img, n_colors=16, cell_size=128, padding=10, font_path=None, font_size=15): - - if 'scikit-learn' not in packages(): - print("\033[34mWAS NS:\033[0m Installing scikit-learn...") - subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'scikit-learn']) - - from sklearn.cluster import KMeans - - # Resize the image to speed up processing - img = img.resize((img.width // 2, img.height // 2), resample=Image.BILINEAR) - # Convert the image to a numpy array - pixels = np.array(img) - # Flatten the pixel array to get a 2D array of RGB values - pixels = pixels.reshape((-1, 3)) - # Initialize the KMeans model with the specified number of colors - kmeans = KMeans(n_clusters=n_colors, random_state=0, n_init='auto').fit(pixels) - # Get the cluster centers and convert them to integer values - cluster_centers = np.uint8(kmeans.cluster_centers_) - # Calculate the size of the palette image based on the number of colors - palette_size = (cell_size * (int(np.sqrt(n_colors))+1)//2*2, cell_size * (int(np.sqrt(n_colors))+1)//2*2) - # Create a square image with the cluster centers as the color palette - palette = Image.new('RGB', palette_size, color='white') - draw = ImageDraw.Draw(palette) - if font_path: - font = ImageFont.truetype(font_path, font_size) - else: - font = ImageFont.load_default() - stroke_width = 1 - for i in range(n_colors): - color = tuple(cluster_centers[i]) - x = i % int(np.sqrt(n_colors)) - y = i // int(np.sqrt(n_colors)) - # Calculate the position of the cell and text - cell_x = x * cell_size + padding - cell_y = y * cell_size + padding - text_x = cell_x + ( padding / 2 ) - text_y = int(cell_y + cell_size / 1.2) - font.getsize('A')[1] - padding - # Draw the cell and text with padding - draw.rectangle((cell_x, cell_y, cell_x + cell_size - padding * 2, cell_y + cell_size - padding * 2), fill=color, outline='black', width=1) - draw.text((text_x+1, text_y+1), f"R: {color[0]} G: {color[1]} B: {color[2]}", font=font, fill='black') - draw.text((text_x, text_y), f"R: {color[0]} G: {color[1]} B: {color[2]}", font=font, fill='white') - # Resize the image back to the original size - palette = palette.resize((palette.width * 2, palette.height * 2), resample=Image.NEAREST) - return palette - -# 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_found = [] - -if os.path.basename(CUSTOM_NODES_DIR) == 'was-node-suite-comfyui': - legacy_was_nodes.append('WAS_Node_Suite.py') - -f_disp = False -node_path_dir = os.getcwd()+os.sep+'ComfyUI'+os.sep+'custom_nodes'+os.sep -for f in legacy_was_nodes: - file = f'{node_path_dir}{f}' - if os.path.exists(file): - if not f_disp: - 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") - for f in legacy_was_nodes_found: - archive.write(f, basename(f)) - try: - os.remove(f) - except OSError: - pass - archive.close() -if f_disp: - print('\033[34mWAS Node Suite:\033[0m Legacy cleanup complete.') - -#! IMAGE FILTER NODES - -# IMAGE FILTER ADJUSTMENTS - - -class WAS_Image_Filters: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "brightness": ("FLOAT", {"default": 0.0, "min": -1.0, "max": 1.0, "step": 0.01}), - "contrast": ("FLOAT", {"default": 1.0, "min": -1.0, "max": 2.0, "step": 0.01}), - "saturation": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 5.0, "step": 0.01}), - "sharpness": ("FLOAT", {"default": 1.0, "min": -5.0, "max": 5.0, "step": 0.01}), - "blur": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}), - "gaussian_blur": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1024.0, "step": 0.1}), - "edge_enhance": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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 = 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)) - - 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))) - # Composite Original and Enhanced Version - 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, ) - - -# IMAGE STYLE FILTER - -class WAS_Image_Style_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "style": ([ - "1977", - "aden", - "brannan", - "brooklyn", - "clarendon", - "earlybird", - "fairy tale", - "gingham", - "hudson", - "inkwell", - "kelvin", - "lark", - "lofi", - "maven", - "mayfair", - "moon", - "nashville", - "perpetua", - "reyes", - "rise", - "sci-fi", - "slumber", - "stinson", - "toaster", - "valencia", - "walden", - "willow", - "xpro2" - ],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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']) - - # Convert image to PIL - image = tensor2pil(image) - - # WAS Filters - WFilter = WAS_Filter_Class() - - # Apply blending - if style == "1977": - out_image = pilgram._1977(image) - elif style == "aden": - out_image = pilgram.aden(image) - elif style == "brannan": - out_image = pilgram.brannan(image) - elif style == "brooklyn": - out_image = pilgram.brooklyn(image) - elif style == "clarendon": - out_image = pilgram.clarendon(image) - elif style == "earlybird": - out_image = pilgram.earlybird(image) - elif style == "fairy tale": - out_image = WFilter.sparkle(image) - elif style == "gingham": - out_image = pilgram.gingham(image) - elif style == "hudson": - out_image = pilgram.hudson(image) - elif style == "inkwell": - out_image = pilgram.inkwell(image) - elif style == "kelvin": - out_image = pilgram.kelvin(image) - elif style == "lark": - out_image = pilgram.lark(image) - elif style == "lofi": - out_image = pilgram.lofi(image) - elif style == "maven": - out_image = pilgram.maven(image) - elif style == "mayfair": - out_image = pilgram.mayfair(image) - elif style == "moon": - out_image = pilgram.moon(image) - elif style == "nashville": - out_image = pilgram.nashville(image) - elif style == "perpetua": - out_image = pilgram.perpetua(image) - elif style == "reyes": - out_image = pilgram.reyes(image) - elif style == "rise": - out_image = pilgram.rise(image) - elif style == "slumber": - out_image = pilgram.slumber(image) - elif style == "stinson": - out_image = pilgram.stinson(image) - elif style == "toaster": - out_image = pilgram.toaster(image) - elif style == "valencia": - out_image = pilgram.valencia(image) - elif style == "walden": - out_image = pilgram.walden(image) - elif style == "willow": - out_image = pilgram.willow(image) - elif style == "xpro2": - out_image = pilgram.xpro2(image) - else: - out_image = image - - out_image = out_image.convert("RGB") - - return (torch.from_numpy(np.array(out_image).astype(np.float32) / 255.0).unsqueeze(0),) - - -# COMBINE NODE - -class WAS_Image_Blending_Mode: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image_a": ("IMAGE",), - "image_b": ("IMAGE",), - "mode": ([ - "add", - "color", - "color_burn", - "color_dodge", - "darken", - "difference", - "exclusion", - "hard_light", - "hue", - "lighten", - "multiply", - "overlay", - "screen", - "soft_light" - ],), - "blend_percentage": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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']) - - # Import Pilgram module - import pilgram - - # Convert images to PIL - img_a = tensor2pil(image_a) - img_b = tensor2pil(image_b) - - # Apply blending - if mode == "color": - out_image = pilgram.css.blending.color(img_a, img_b) - elif mode == "color_burn": - out_image = pilgram.css.blending.color_burn(img_a, img_b) - elif mode == "color_dodge": - out_image = pilgram.css.blending.color_dodge(img_a, img_b) - elif mode == "darken": - out_image = pilgram.css.blending.darken(img_a, img_b) - elif mode == "difference": - out_image = pilgram.css.blending.difference(img_a, img_b) - elif mode == "exclusion": - out_image = pilgram.css.blending.exclusion(img_a, img_b) - elif mode == "hard_light": - out_image = pilgram.css.blending.hard_light(img_a, img_b) - elif mode == "hue": - out_image = pilgram.css.blending.hue(img_a, img_b) - elif mode == "lighten": - out_image = pilgram.css.blending.lighten(img_a, img_b) - elif mode == "multiply": - out_image = pilgram.css.blending.multiply(img_a, img_b) - elif mode == "add": - out_image = pilgram.css.blending.normal(img_a, img_b) - elif mode == "overlay": - out_image = pilgram.css.blending.overlay(img_a, img_b) - elif mode == "screen": - out_image = pilgram.css.blending.screen(img_a, img_b) - elif mode == "soft_light": - out_image = pilgram.css.blending.soft_light(img_a, img_b) - else: - 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 = ImageOps.invert(blend_mask) - out_image = Image.composite(img_a, out_image, blend_mask) - - return (pil2tensor(out_image), ) - - -# IMAGE BLEND NODE - -class WAS_Image_Blend: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image_a": ("IMAGE",), - "image_b": ("IMAGE",), - "blend_percentage": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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 = 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), ) - - - -# IMAGE MONITOR DISTORTION FILTER - -class WAS_Image_Monitor_Distortion_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["Digital Distortion", "Signal Distortion", "TV Distortion"],), - "amplitude": ("INT", {"default": 5, "min": 1, "max": 255, "step": 1}), - "offset": ("INT", {"default": 10, "min": 1, "max": 255, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_monitor_filters" - - CATEGORY = "WAS Suite/Image" - - def image_monitor_filters(self, image, mode="Digital Distortion", amplitude=5, offset=5): - - # Convert images to PIL - image = tensor2pil(image) - - # WAS Filters - WFilter = WAS_Filter_Class() - - # Apply image effect - if mode == 'Digital Distortion': - image = WFilter.digital_distortion(image, amplitude, offset) - elif mode == 'Signal Distortion': - image = WFilter.signal_distortion(image, amplitude) - elif mode == 'TV Distortion': - image = WFilter.tv_vhs_distortion(image, amplitude) - - return (pil2tensor(image), ) - - -# IMAGE GENERATE COLOR PALETTE - -class WAS_Image_Color_Palette: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "colors": ("INT", {"default": 16, "min": 8, "max": 256, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_generate_palette" - - CATEGORY = "WAS Suite/Image" - - def image_generate_palette(self, image, colors=16): - - # Convert images to PIL - image = tensor2pil(image) - - # WAS Filters - WFilter = WAS_Filter_Class() - - res_dir = os.path.join(WAS_SUITE_ROOT, 'res') - font = os.path.join(res_dir, 'font.ttf') - - if not os.path.exists(font): - font = None - else: - print(f'\033[34mWAS NS:\033[0m Found font at `{font}`') - - # Generate Color Palette - image = WFilter.generate_palette(image, colors, 128, 10, font, 15) - - return (pil2tensor(image), ) - - - -# IMAGE ANALYZE - -class WAS_Image_Analyze: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["Black White Levels", "RGB Levels"],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_analyze" - - CATEGORY = "WAS Suite/Image" - - def image_analyze(self, image, mode='Black White Levels'): - - # Convert images to PIL - image = tensor2pil(image) - - # WAS Filters - WFilter = WAS_Filter_Class() - - # Analye Image - if mode == 'Black White Levels': - image = WFilter.black_white_levels(image) - elif mode == 'RGB Levels': - image = WFilter.channel_frequency(image) - - return (pil2tensor(image), ) - - -# IMAGE GENERATE GRADIENT - -class WAS_Image_Generate_Gradient: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - gradient_stops = '''0:255,0,0 -25:255,255,255 -50:0,255,0 -75:0,0,255''' - return { - "required": { - "width": ("INT", {"default":512, "max": 4096, "min": 64, "step":1}), - "height": ("INT", {"default":512, "max": 4096, "min": 64, "step":1}), - "direction": (["horizontal", "vertical"],), - "tolerance": ("INT", {"default":0, "max": 255, "min": 0, "step":1}), - "gradient_stops": ("STRING", {"default": gradient_stops, "multiline": True}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_gradient" - - CATEGORY = "WAS Suite/Image" - - def image_gradient(self, gradient_stops, width=512, height=512, direction='horizontal', tolerance=0): - - import io - - # WAS Filters - WFilter = WAS_Filter_Class() - - colors_dict = {} - stops = io.StringIO(gradient_stops.strip().replace(' ','')) - for stop in stops: - parts = stop.split(':') - colors = parts[1].replace('\n','').split(',') - colors_dict[parts[0].replace('\n','')] = colors - - image = WFilter.gradient((width, height), direction, colors_dict, tolerance) - - return (pil2tensor(image), ) - -# IMAGE GRADIENT MAP - -class WAS_Image_Gradient_Map: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "gradient_image": ("IMAGE",), - "flip_left_right": (["false", "true"],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_gradient_map" - - CATEGORY = "WAS Suite/Image" - - def image_gradient_map(self, image, gradient_image, flip_left_right='false'): - - # Convert images to PIL - image = tensor2pil(image) - gradient_image = tensor2pil(gradient_image) - - # WAS Filters - WFilter = WAS_Filter_Class() - - image = WFilter.gradient_map(image, gradient_image, (True if flip_left_right == 'true' else False)) - - return (pil2tensor(image), ) - - -# IMAGE TRANSPOSE - -class WAS_Image_Transpose: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "image_overlay": ("IMAGE",), - "width": ("INT", {"default": 512, "min": -48000, "max": 48000, "step": 1}), - "height": ("INT", {"default": 512, "min": -48000, "max": 48000, "step": 1}), - "X": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), - "Y": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), - "rotation": ("INT", {"default": 0, "min": -360, "max": 360, "step": 1}), - "feathering": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_transpose" - - CATEGORY = "WAS Suite/Image" - - def image_transpose(self, image: torch.Tensor, image_overlay: torch.Tensor, width: int, height: int, X: int, Y: int, rotation: int, feathering: int = 0): - return (pil2tensor(self.apply_transpose_image(tensor2pil(image), tensor2pil(image_overlay), (width, height), (X, Y), rotation, feathering)), ) - - def apply_transpose_image(self, image_bg, image_element, size, loc, rotate=0, feathering=0): - - # Apply transformations to the element image - image_element = image_element.rotate(rotate, expand=True) - image_element = image_element.resize(size) - - # Create a mask for the image with the faded border - if feathering > 0: - mask = Image.new('L', image_element.size, 255) # Initialize with 255 instead of 0 - draw = ImageDraw.Draw(mask) - for i in range(feathering): - alpha_value = int(255 * (i + 1) / feathering) # Invert the calculation for alpha value - draw.rectangle((i, i, image_element.size[0] - i, image_element.size[1] - i), fill=alpha_value) - alpha_mask = Image.merge('RGBA', (mask, mask, mask, mask)) - image_element = Image.composite(image_element, Image.new('RGBA', image_element.size, (0, 0, 0, 0)), alpha_mask) - - # Create a new image of the same size as the base image with an alpha channel - new_image = Image.new('RGBA', image_bg.size, (0, 0, 0, 0)) - new_image.paste(image_element, loc) - - # Paste the new image onto the base image - image_bg = image_bg.convert('RGBA') - image_bg.paste(new_image, (0, 0), new_image) - - return image_bg - - - -# IMAGE RESCALE - -class WAS_Image_Rescale: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["rescale", "resize"],), - "supersample": (["true", "false"],), - "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}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_rescale" - - CATEGORY = "WAS Suite/Image" - - def image_rescale(self, image: torch.Tensor, mode="rescale", supersample='true', resampling="lanczos", rescale_factor=2, resize_width=1024, resize_height=1024): - return (pil2tensor(self.apply_resize_image(tensor2pil(image), mode, supersample, rescale_factor, resize_width, resize_height, resampling)), ) - - def apply_resize_image(self, image: Image.Image, mode='scale', supersample='true', 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) - 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) - - # Define a dictionary of resampling filters - resample_filters = { - 'nearest': 0, - 'bilinear': 2, - 'bicubic': 3, - 'lanczos': 1 - } - - # Apply supersample - if supersample == 'true': - image = image.resize((new_width * 8, new_height * 8), resample=Image.Resampling(resample_filters[resample])) - - # Resize the image using the given resampling filter - resized_image = image.resize((new_width, new_height), resample=Image.Resampling(resample_filters[resample])) - - return resized_image - - -# LOAD IMAGE BATCH - -class WAS_Load_Image_Batch: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mode": (["single_image", "incremental_image"],), - "index": ("INT", {"default": 0, "min": 0, "max": 150000, "step": 1}), - "label": ("STRING", {"default": 'Batch 001', "multiline": False}), - "path": ("STRING", {"default": './ComfyUI/input/', "multiline": False}), - "pattern": ("STRING", {"default": '*', "multiline": False}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "load_batch_images" - - CATEGORY = "WAS Suite/IO" - - def load_batch_images(self, path, pattern='*', index=0, mode="single_image", label='Batch 001') -> tuple[torch.Tensor] | tuple: - - if not os.path.exists(path): - return (None, ) - fl = self.BatchImageLoader(path, label, pattern) - if mode == 'single_image': - image = fl.get_image_by_id(index) - else: - image = fl.get_next_image() - self.image = image - - return (pil2tensor(image), ) - - class BatchImageLoader: - def __init__(self, directory_path, label, pattern): - self.WDB = WDB - self.image_paths = [] - self.load_images(directory_path, pattern) - self.image_paths.sort() # sort the image paths by name - stored_directory_path = self.WDB.get('Batch Paths', label) - stored_pattern = self.WDB.get('Batch Patterns', label) - if stored_directory_path != directory_path or stored_pattern != pattern: - self.index = 0 - self.WDB.insert('Batch Counters', label, 0) - self.WDB.insert('Batch Paths', label, directory_path) - self.WDB.insert('Batch Patterns', label, pattern) - else: - self.index = self.WDB.get('Batch Counters', label) - self.label = label - - def load_images(self, directory_path, pattern): - allowed_extensions = ('.jpeg', '.jpg', '.png', - '.tiff', '.gif', '.bmp', '.webp') - for file_name in glob.glob(os.path.join(directory_path, pattern), recursive=True): - if file_name.lower().endswith(allowed_extensions): - image_path = os.path.join(directory_path, file_name) - self.image_paths.append(image_path) - - def get_image_by_id(self, image_id): - if image_id < 0 or image_id >= len(self.image_paths): - raise ValueError(f"\033[34mWAS NS\033[0m Error: Invalid image index `{image_id}`") - return Image.open(self.image_paths[image_id]) - - def get_next_image(self): - if self.index >= len(self.image_paths): - self.index = 0 - image_path = self.image_paths[self.index] - self.index += 1 - if self.index == len(self.image_paths): - self.index = 0 - print(f'\033[34mWAS NS \033[33m{self.label}\033[0m Index:', self.index) - self.WDB.insert('Batch Counters', self.label, self.index) - return Image.open(image_path) - - @classmethod - def IS_CHANGED(cls, **kwargs): - return float("NaN") - - -# IMAGE PADDING - -class WAS_Image_Padding: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "feathering": ("INT", {"default": 120, "min": 0, "max": 2048, "step": 1}), - "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}), - "bottom_padding": ("INT", {"default": 512, "min": 8, "max": 48000, "step": 1}), - }, - } - - 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])) - - 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) - draw = ImageDraw.Draw(mask) - - # 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((0, 0, image.width, feather_radius*2), 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)) - - # Apply mask if second_pass is False, apply both masks if second_pass is True - if second_pass: - - # Create a second mask for the additional feathering pass - mask2 = Image.new('L', image.size, 255) - draw2 = ImageDraw.Draw(mask2) - - # 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((0, 0, image.width, feather_radius2*2), 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)) - - feathered_im = Image.new('RGBA', image.size, (0, 0, 0, 0)) - feathered_im.paste(image, (0, 0), mask) - feathered_im.paste(image, (0, 0), mask) - - # 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) - - # 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()] - padding_mask.putdata(gradient) - - # Save the new image with alpha channel as a PNG file - return (new_im, padding_mask.convert('RGB')) - - -# IMAGE THRESHOLD NODE - -class WAS_Image_Threshold: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "threshold": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_threshold" - - CATEGORY = "WAS Suite/Image" - - def image_threshold(self, image, threshold=0.5): - 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') - - return thresholded_image - - -# IMAGE CHROMATIC ABERRATION NODE - -class WAS_Image_Chromatic_Aberration: - - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "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}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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)), ) - - def apply_chromatic_aberration(self, img, r_offset, g_offset, b_offset, intensity): - # split the channels of the image - r, g, b = img.split() - - # apply the offset to each channel - r_offset_img = ImageChops.offset(r, r_offset, 0) - g_offset_img = ImageChops.offset(g, 0, g_offset) - b_offset_img = ImageChops.offset(b, 0, b_offset) - - # blend the original image with the offset channels - blended_r = ImageChops.blend(r, r_offset_img, intensity) - blended_g = ImageChops.blend(g, g_offset_img, intensity) - blended_b = ImageChops.blend(b, b_offset_img, intensity) - - # merge the channels back into an RGB image - result = Image.merge("RGB", (blended_r, blended_g, blended_b)) - - return result - - -# IMAGE BLOOM FILTER - -class WAS_Image_Bloom_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "radius": ("FLOAT", {"default": 10, "min": 0.0, "max": 1024, "step": 0.1}), - "intensity": ("FLOAT", {"default": 1, "min": 0.0, "max": 1.0, "step": 0.1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_bloom" - - 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)), ) - - 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)) - - # 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)) - - # 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)))) - - # 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 - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "target_red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "target_green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "target_blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "replace_red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "replace_green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "replace_blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), - "clip_threshold": ("INT", {"default": 10, "min": 0, "max": 255, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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))), ) - - 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) - - return result_image - - -# IMAGE REMOVE BACKGROUND - -class WAS_Remove_Background: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["background", "foreground"],), - "threshold": ("INT", {"default": 127, "min": 0, "max": 255, "step": 1}), - "threshold_tolerance": ("INT", {"default": 2, "min": 1, "max": 24, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_remove_background" - - CATEGORY = "WAS Suite/Image" - - def image_remove_background(self, image, mode='background', threshold=127, threshold_tolerance=2): - return (pil2tensor(self.remove_background(tensor2pil(image), mode, threshold, threshold_tolerance)), ) - - def remove_background(self, image, mode, threshold, threshold_tolerance): - grayscale_image = image.convert('L') - if mode == 'background': - grayscale_image = ImageOps.invert(grayscale_image) - threshold = 255 - threshold # adjust the threshold for "background" mode - blurred_image = grayscale_image.filter( - ImageFilter.GaussianBlur(radius=threshold_tolerance)) - binary_image = blurred_image.point( - lambda x: 0 if x < threshold else 255, '1') - mask = binary_image.convert('L') - inverted_mask = ImageOps.invert(mask) - transparent_image = image.copy() - transparent_image.putalpha(inverted_mask) - - return transparent_image - - -# IMAGE BLEND MASK NODE - -class WAS_Image_Blend_Mask: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image_a": ("IMAGE",), - "image_b": ("IMAGE",), - "mask": ("IMAGE",), - "blend_percentage": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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 = 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), ) - - -# IMAGE BLANK NOE - - -class WAS_Image_Blank: - def __init__(self): - pass - - @classmethod - 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}), - } - } - RETURN_TYPES = ("IMAGE",) - FUNCTION = "blank_image" - - CATEGORY = "WAS Suite/Image" - - def blank_image(self, width, height, red, green, blue): - - # Ensure multiples - width = (width // 8) * 8 - height = (height // 8) * 8 - - # Blend image - blank = Image.new(mode="RGB", size=(width, height), - color=(red, green, blue)) - - return (pil2tensor(blank), ) - - -# IMAGE HIGH PASS - -class WAS_Image_High_Pass_Filter: - def __init__(self): - pass - - @classmethod - 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}) - } - } - RETURN_TYPES = ("IMAGE",) - FUNCTION = "high_pass" - - CATEGORY = "WAS Suite/Image" - - 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)), ) - - 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') - - # Apply the High Pass Filter - hpf_arr = img_arr - blurred_arr - hpf_arr = np.clip(hpf_arr * strength, 0, 255).astype('uint8') - - # Convert the numpy array back to a PIL image and return it - return Image.fromarray(hpf_arr, mode='RGB') - - -# IMAGE LEVELS NODE - -class WAS_Image_Levels: - def __init__(self): - pass - - @classmethod - 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}), - } - } - 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 adjust_levels(self, image, black=0.0, mid=1.0, white=255): - """ - Adjust the black, mid, and white levels of an RGB image. - """ - # Create a new empty image with the same size and mode as the original image - result = Image.new(image.mode, image.size) - - # Check that the mid value is within the valid range - if mid < 0 or mid > 1: - raise ValueError("mid value must be between 0 and 1") - - # Create a lookup table to map the pixel values to new values - lut = [] - for i in range(256): - if i < black: - lut.append(0) - elif i > white: - lut.append(255) - else: - lut.append(int(((i - black) / (white - black)) ** mid * 255.0)) - - # Split the image into its red, green, and blue channels - r, g, b = image.split() - - # Apply the lookup table to each channel - r = r.point(lut) - g = g.point(lut) - b = b.point(lut) - - # Merge the channels back into an RGB image - 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 - self.mid_level = mid_level - self.max_level = max_level - - def adjust(self, im): - # load the image - - # convert the image to a numpy array - im_arr = np.array(im) - - # apply the min level adjustment - 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 < 0] = 0 - im_arr[im_arr > 255] = 255 - im_arr = im_arr.astype(np.uint8) - - # apply the max level adjustment - im = Image.fromarray(im_arr) - im = ImageOps.autocontrast(im, cutoff=self.max_level) - - return im - - -# FILM GRAIN NODE - -class WAS_Film_Grain: - def __init__(self): - pass - - @classmethod - 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}) - } - } - 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 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)) - - # Calculate the number of noise pixels to add - num_pixels = int(density * img_gray.size[0] * img_gray.size[1]) - - # Create a list of noise pixel positions - noise_pixels = [] - for i in range(num_pixels): - x = random.randint(0, img_gray.size[0]-1) - y = random.randint(0, img_gray.size[1]-1) - noise_pixels.append((x, y)) - - # Apply the noise to the grayscale image - for x, y in noise_pixels: - value = random.randint(0, 255) - img_gray.putpixel((x, y), value) - - # 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)) - - # 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) - - # Blend the noisy color image with the original color image - img_final = Image.blend(img, img_noise, intensity) - - # Adjust the highlights - enhancer = ImageEnhance.Brightness(img_final) - img_highlights = enhancer.enhance(highlights) - - # Return the final image - return img_highlights - - -# IMAGE FLIP NODE - -class WAS_Image_Flip: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["horizontal", "vertical",],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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), ) - - -class WAS_Image_Rotate: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["transpose", "internal",],), - "rotation": ("INT", {"default": 0, "min": 0, "max": 360, "step": 90}), - "sampler": (["nearest", "bilinear", "bicubic"],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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) - - # Set Sampler - if sampler == 'nearest': - sampler = Image.NEAREST - elif sampler == 'bicubic': - sampler = Image.BICUBIC - elif sampler == 'bilinear': - sampler = Image.BILINEAR - else: - sampler = Image.NEAREST # default to nearest if none of the above - - # Rotate Image - if mode == 'internal': - image = image.rotate(rotation, sampler) - else: - 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), ) - - -# IMAGE NOVA SINE FILTER - -class WAS_Image_Nova_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "amplitude": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.001}), - "frequency": ("FLOAT", {"default": 3.14, "min": 0.0, "max": 100.0, "step": 0.001}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "nova_sine" - - 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) - - # Define a sine wave function - def sine(x, freq, amp): - return amp * np.sin(2 * np.pi * freq * x) - - # Calculate the sampling frequency of the image - resolution = img.info.get('dpi') # PPI - physical_size = img.size # pixels - - if resolution is not None: - # Convert PPI to pixels per millimeter (PPM) - ppm = 25.4 / resolution - physical_size = tuple(int(pix * ppm) for pix in physical_size) - - # 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 - - # Apply levels to the image using the sine function - 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) - - return (torch.from_numpy(img_array.astype(np.float32) / 255.0).unsqueeze(0), ) - - -# IMAGE CANNY FILTER - - -class WAS_Canny_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "enable_threshold": (['false', 'true'],), - "threshold_low": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), - "threshold_high": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "canny_filter" - - CATEGORY = "WAS Suite/Image" - - 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') - - 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): - - 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) # 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 - height, width = img.shape - - # 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) - - 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: - 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): - 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): - 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): - 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): - 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] < 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] < 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) - 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_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 - - # 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']) - - -# IMAGE EDGE DETECTION - -class WAS_Image_Edge: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "mode": (["normal", "laplacian"],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "image_edges" - - CATEGORY = "WAS Suite/Image" - - def image_edges(self, image, mode): - - # Convert image to PIL - image = tensor2pil(image) - - # Detect edges - if mode == "normal": - image = image.filter(ImageFilter.FIND_EDGES) - elif mode == "laplacian": - kernel = (-1, -1, -1, -1, 8, -1, -1, -1, -1) - image = image.filter(ImageFilter.Kernel((3, 3), kernel, scale=1, offset=0)) - else: - image = image - - return (torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0), ) - - -# IMAGE FDOF NODE - -class WAS_Image_fDOF: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "depth": ("IMAGE",), - "mode": (["mock", "gaussian", "box"],), - "radius": ("INT", {"default": 8, "min": 1, "max": 128, "step": 1}), - "samples": ("INT", {"default": 1, "min": 1, "max": 3, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "fdof_composite" - - CATEGORY = "WAS Suite/Image" - - 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']) - - import cv2 as cv - - # Convert tensor to a PIL Image - i = 255. * image.cpu().numpy().squeeze() - img: Image.Image = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - d = 255. * depth.cpu().numpy().squeeze() - depth_img: Image.Image = Image.fromarray( - np.clip(d, 0, 255).astype(np.uint8)) - - # 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), ) - - 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)) - elif mode == 'box': - bimg = img.filter(ImageFilter.BoxBlur(radius)) - else: - return - bimg.convert(img.mode) - rimg: Optional[Image.Image] = None - if samples > 1: - for i in range(samples): - 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(self, img, radius, amount, mask=None): - """Applies a lens shape blur effect on an image. - - Args: - img (numpy.ndarray): The input image as a numpy array. - radius (float): The radius of the lens shape. - amount (float): The amount of blur to be applied. - mask (numpy.ndarray): An optional mask image specifying where to apply the blur. - - Returns: - numpy.ndarray: The blurred image as a numpy array. - """ - # Create a lens shape kernel. - kernel = cv2.getGaussianKernel(ksize=int(radius * 10), sigma=0) - kernel = np.dot(kernel, kernel.T) - - # Normalize the kernel. - kernel /= np.max(kernel) - - # 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.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) - - # Apply the lens shape blur effect on the image. - blurred = cv2.filter2D(img, -1, kernel) - blurred = cv2.filter2D(blurred, -1, mask * amount) - - if mask is not None: - # Apply the mask to the original image. - 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 # type: ignore - - return blurred - - -# IMAGE MEDIAN FILTER NODE - -class WAS_Image_Median_Filter: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "diameter": ("INT", {"default": 2.0, "min": 0.1, "max": 255, "step": 1}), - "sigma_color": ("FLOAT", {"default": 10.0, "min": -255.0, "max": 255.0, "step": 0.1}), - "sigma_space": ("FLOAT", {"default": 10.0, "min": -255.0, "max": 255.0, "step": 0.1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "apply_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), ) - -# IMAGE SELECT COLOR - - -class WAS_Image_Select_Color: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "red": ("INT", {"default": 255.0, "min": 0.0, "max": 255.0, "step": 0.1}), - "green": ("INT", {"default": 255.0, "min": 0.0, "max": 255.0, "step": 0.1}), - "blue": ("INT", {"default": 255.0, "min": 0.0, "max": 255.0, "step": 0.1}), - "variance": ("INT", {"default": 10, "min": 0, "max": 255, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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']) - - image = self.color_pick(tensor2pil(image), red, green, blue, variance) - - 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)) - - # Get the width and height of the image - width, height = image.size - - # Loop through every pixel in the image - 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 - - # 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)): - # Set the pixel in the selected_color image to the RGB value of the pixel - 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 - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "channel": (['red', 'green', 'blue'],), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "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), ) - - def convert_to_single_channel(self, image, channel='red'): - - # Convert to RGB mode to access individual channels - image = image.convert('RGB') - - # Extract the desired channel and convert to greyscale - if channel == 'red': - channel_img = image.split()[0].convert('L') - elif channel == 'green': - channel_img = image.split()[1].convert('L') - elif channel == 'blue': - channel_img = image.split()[2].convert('L') - else: - 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)) - - return channel_img - - -# IMAGE CONVERT TO CHANNEL - -class WAS_Image_RGB_Merge: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "red_channel": ("IMAGE",), - "green_channel": ("IMAGE",), - "blue_channel": ("IMAGE",), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "merge_channels" - - 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), ) - - 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)) - - # 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()+os.sep+'ComfyUI', "output") - - @classmethod - 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}), - "overwrite_mode": (["false", "prefix_as_filename"],), - }, - "hidden": { - "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" - }, - } - - RETURN_TYPES = () - FUNCTION = "save_images" - - OUTPUT_NODE = True - - CATEGORY = "WAS Suite/IO" - - def save_images(self, images, output_path='', filename_prefix="ComfyUI", extension='png', quality=100, prompt=None, extra_pnginfo=None, overwrite_mode='false'): - def map_filename(filename): - prefix_len = len(filename_prefix) - prefix = filename[:prefix_len + 1] - try: - digits = int(filename[prefix_len + 1:].split('_')[0]) - 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 Warning: The path `{output_path.strip()}` specified doesn\'t exist! Creating directory.') - os.mkdir(output_path.strip()) - self.output_dir = os.path.normpath(output_path.strip()) - - # Setup counter - 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 - except ValueError: - counter = 1 - except FileNotFoundError: - os.mkdir(self.output_dir) - counter = 1 - - paths = list() - for image in images: - i = 255. * image.cpu().numpy() - img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - metadata = PngInfo() - if prompt is not None: - metadata.add_text("prompt", json.dumps(prompt)) - if extra_pnginfo is not None: - for x in extra_pnginfo: - metadata.add_text(x, json.dumps(extra_pnginfo[x])) - - - if overwrite_mode == 'prefix_as_filename': - file = f"{filename_prefix}.{extension}" - else: - file = f"{filename_prefix}_{counter:05}_.{extension}" - if os.path.exists(os.path.join(self.output_dir, file)): - counter += 1 - file = f"{filename_prefix}_{counter:05}_.{extension}" - - if extension == 'png': - 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) - elif extension == 'tiff': - 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) - if overwrite_mode == 'false': - counter += 1 - - return {"ui": {"images": paths}} - - -# LOAD IMAGE NODE -class WAS_Load_Image: - - def __init__(self): - self.input_dir = os.path.join(os.getcwd()+os.sep+'ComfyUI', "input") - - @classmethod - def INPUT_TYPES(cls): - return {"required": - {"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) -> Optional[tuple[torch.Tensor, torch.Tensor]]: - - if image_path.startswith('http'): - from io import BytesIO - i = self.download_image(image_path) - else: - 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)) - 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) - - def download_image(self, url): - try: - response = requests.get(url) - response.raise_for_status() - 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}") - except requests.exceptions.ConnectionError as 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}") - except requests.exceptions.RequestException as err: - print( - f"\033[34mWAS NS\033[0m Error: Request Exception: ({url}): {err}") - - @classmethod - 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 - -class WAS_Tensor_Batch_to_Image: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "images_batch": ("IMAGE",), - "batch_image_number": ("INT", {"default": 0, "min": 0, "max": 64, "step": 1}), - }, - } - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "tensor_batch_to_image" - - CATEGORY = "WAS Suite/Latent" - - 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), ) - 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), ) - - -#! LATENT NODES - -# IMAGE TO MASK - -class WAS_Image_To_Mask: - - def __init__(self): - self.channels = {'alpha': 'A', 'red': 0, 'green': 1, 'blue': 2} - - @classmethod - def INPUT_TYPES(cls): - return {"required": - {"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): - i = tensor2pil(image) - mask = np.array(i.getchannel(self.channels[channel])).astype(np.float32) / 255.0 - mask = 1. - torch.from_numpy(mask) - return (mask, ) - - -# LATENT UPSCALE NODE - -class WAS_Latent_Upscale: - def __init__(self): - pass - - @classmethod - 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" - - CATEGORY = "WAS Suite/Latent" - - 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)) - return (s,) - -# LATENT NOISE INJECTION NODE - - -class WAS_Latent_Noise: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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): - s = samples.copy() - noise = torch.randn_like(s["samples"]) * noise_std - s["samples"] = s["samples"] + noise - return (s,) - - -# MIDAS DEPTH APPROXIMATION NODE - -class MiDaS_Depth_Approx: - def __init__(self): - self.midas_dir = os.path.join(os.getcwd()+os.sep+'ComfyUI', 'models'+os.sep+'midas') - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "use_cpu": (["false", "true"],), - "midas_model": (["DPT_Large", "DPT_Hybrid", "DPT_Small"],), - "invert_depth": (["false", "true"],), - }, - } - - 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 - - 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") - - print('\033[34mWAS NS:\033[0m MiDaS is using device:', device) - - midas.to(device).eval() - midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms") - - if midas_model == "DPT_Large" or midas_model == "DPT_Hybrid": - transform = midas_transforms.dpt_transform - else: - transform = midas_transforms.small_transform - - 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(): - prediction = midas(input_batch) - prediction = torch.nn.functional.interpolate( - prediction.unsqueeze(1), - size=img.shape[:2], - mode="bicubic", - align_corners=False, - ).squeeze() - - # Invert depth map - if invert_depth == 'true': - 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 - # 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, ) - - del midas, device, midas_transforms - del transform, img, input_batch, prediction - - 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']) - 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 - -# MIDAS REMOVE BACKGROUND/FOREGROUND NODE - - -class MiDaS_Background_Foreground_Removal: - def __init__(self): - self.midas_dir = os.path.join(os.getcwd()+os.sep+'ComfyUI', 'models'+os.sep+'midas') - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE",), - "use_cpu": (["false", "true"],), - "midas_model": (["DPT_Large", "DPT_Hybrid", "DPT_Small"],), - "remove": (["background", "foregroud"],), - "threshold": (["false", "true"],), - "threshold_low": ("FLOAT", {"default": 10, "min": 0, "max": 255, "step": 1}), - "threshold_mid": ("FLOAT", {"default": 200, "min": 0, "max": 255, "step": 1}), - "threshold_high": ("FLOAT", {"default": 210, "min": 0, "max": 255, "step": 1}), - "smoothing": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 16.0, "step": 0.01}), - "background_red": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - "background_green": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - "background_blue": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), - }, - } - - 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): - - 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 - # Original image - img_original = tensor2pil(image).convert('RGB') - - 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") - - print('\033[34mWAS NS:\033[0m MiDaS is using device:', device) - - midas.to(device).eval() - midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms") - - if midas_model == "DPT_Large" or midas_model == "DPT_Hybrid": - transform = midas_transforms.dpt_transform - else: - transform = midas_transforms.small_transform - - 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(): - prediction = midas(input_batch) - prediction = torch.nn.functional.interpolate( - prediction.unsqueeze(1), - size=img.shape[:2], - mode="bicubic", - align_corners=False, - ).squeeze() - - # Invert depth map - if remove == 'foreground': - 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) - 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 - - # Create background color tuple - background_color = (background_red, background_green, background_blue) - - # Create background image - 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'))) - - class AdjustLevels: - def __init__(self, min_level, mid_level, max_level): - self.min_level = min_level - self.mid_level = mid_level - self.max_level = max_level - - def adjust(self, im): - # load the image - - # convert the image to a numpy array - im_arr = np.array(im) - - # apply the min level adjustment - 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 < 0] = 0 - im_arr[im_arr > 255] = 255 - im_arr = im_arr.astype(np.uint8) - - # apply the max level adjustment - im = Image.fromarray(im_arr) - 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']) - 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 - - -#! CONDITIONING NODES - - -# NSP CLIPTextEncode NODE - -class WAS_NSP_CLIPTextEncoder: - def __init__(self): - pass - - @classmethod - 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",), - } - } - - OUTPUT_NODE = True - RETURN_TYPES = ("CONDITIONING",) - FUNCTION = "nsp_encode" - - CATEGORY = "WAS Suite/Conditioning" - - def nsp_encode(self, clip, text, noodle_key='__', seed=0): - - # Fetch the NSP Pantry - local_pantry = os.getcwd()+os.sep+'ComfyUI'+os.sep+'custom_nodes'+os.sep+'nsp_pantry.json' - if not os.path.exists(local_pantry): - 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: - # Target Noodle - tkey = f'{noodle_key}{term}{noodle_key}' - # How many occurances? - 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) - 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}}) - - -#! SAMPLING NODES - -# KSAMPLER - -class WAS_KSampler: - @classmethod - 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}), - }} - - RETURN_TYPES = ("LATENT",) - FUNCTION = "sample" - - CATEGORY = "WAS Suite/Sampling" - - def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0): - 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(cls): - return {"required": - {"seed": ("INT", {"default": 0, "min": 0, - "max": 0xffffffffffffffff})} - } - - RETURN_TYPES = ("SEED",) - FUNCTION = "seed" - - CATEGORY = "WAS Suite/Constant" - - def seed(self, seed): - return ({"seed": seed, }, ) - - -#! TEXT NODES - -# Text Multiline Node - -class WAS_Text_Multiline: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": '', "multiline": True}), - } - } - RETURN_TYPES = ("ASCII",) - FUNCTION = "text_multiline" - - CATEGORY = "WAS Suite/Text" - - def text_multiline(self, text): - return (text, ) - - -# Text String Node - -class WAS_Text_String: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("STRING", {"default": '', "multiline": False}), - } - } - RETURN_TYPES = ("ASCII",) - FUNCTION = "text_string" - - CATEGORY = "WAS Suite/Text" - - def text_string(self, text): - return (text, ) - - -# Text Random Line - -class WAS_Text_Random_Line: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("ASCII",), - "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), - } - } - - RETURN_TYPES = ("ASCII",) - FUNCTION = "text_random_line" - - CATEGORY = "WAS Suite/Text" - - def text_random_line(self, text, seed): - lines = text.split("\n") - random.seed(seed) - choice = random.choice(lines) - return (choice, ) - - -# Text Concatenate - -class WAS_Text_Concatenate: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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, ) - - -# Text Search and Replace - -class WAS_Search_and_Replace: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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), ) - - def replace_substring(self, text, find, replace): - import re - text = re.sub(find, replace, text) - return text - - -# Text Search and Replace - -class WAS_Search_and_Replace_Input: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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), ) - - 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__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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): - - # Fetch the NSP Pantry - local_pantry = os.getcwd()+os.sep+'ComfyUI'+os.sep+'custom_nodes'+os.sep+'nsp_pantry.json' - if not os.path.exists(local_pantry): - 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: - # Target Noodle - tkey = f'{noodle_key}{term}{noodle_key}' - # How many occurances? - 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) - seed = seed+1 - random.seed(seed) - - print('\033[34mWAS NS\033[0m Text Parse NSP:', new_text) - - return (new_text, ) - - -# TEXT SEARCH AND REPLACE - -class WAS_Text_Save: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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" - - 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!') - - # 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.') - - # Replace tokens - tokens = { - '[time]': f'{round(time.time())}', - } - for k in tokens.keys(): - filename = self.replace_substring(filename, k, tokens[k]) - - # Write text file - self.writeTextFile(os.path.join(path, filename + '.txt'), text) - - return (text, ) - - # Save Text FileNotFoundError - def writeTextFile(self, file, content): - try: - with open(file, 'w', encoding='utf-8', newline='\n') as f: - f.write(content) - except OSError: - print( - f'\033[34mWAS Node Suite\033[0m Error: Unable to save file `{file}`') - - # Replace a substring - def replace_substring(self, string, substring, replacement): - import re - pattern = re.compile(re.escape(substring)) - string = pattern.sub(replacement, string) - return string - - - -# TEXT TO CONDITIONIONG - -class WAS_Text_to_Conditioning: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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), {}]], ) - - -class WAS_Text_to_Console: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("ASCII",), - "label": ("STRING", {"default": f'Text Output', "multiline": False}), - } - } - - RETURN_TYPES = ("ASCII",) - OUTPUT_NODE = True - FUNCTION = "text_to_console" - - CATEGORY = "WAS Suite/Text" - - def text_to_console(self, text, label): - 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, ) - - -# LOAD TEXT FILE - -class WAS_Text_Load_From_File: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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), ) - - def load_text_file(self, path): - if not os.path.exists(path): - print( - f'\033[34mWAS Node Suite\033[0m Error: The path `{path}` specified cannot be found.') - return '' - with open(path, 'r', encoding="utf-8", newline='\n') as file: - text = file.read() - return text - -# LOAD TEXT TO STRING - -class WAS_Text_To_String: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "text": ("ASCII",), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "text_to_string" - - CATEGORY = "WAS Suite/Text" - - def text_to_string(self, text): - return (text, ) - - -#! NUMBERS - - -# RANDOM NUMBER - -class WAS_Random_Number: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number_type": (["integer", "float", "bool"],), - "minimum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}), - "maximum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}), - "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 - if number_type == 'integer': - number = random.randint(minimum, maximum) - elif number_type == 'float': - number = random.uniform(minimum, maximum) - elif number_type == 'bool': - number = random.random() - else: - return - - # Return number - return (number, ) - - -# CONSTANT NUMBER - -class WAS_Constant_Number: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number_type": (["integer", "float", "bool"],), - "number": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}), - } - } - - RETURN_TYPES = ("NUMBER",) - FUNCTION = "return_constant_number" - - CATEGORY = "WAS Suite/Constant" - - def return_constant_number(self, number_type, number): - - # Return number - if number_type == 'integer': - return (int(number), ) - elif number_type == 'float': - return (float(number), ) - elif number_type == 'bool': - return ((1 if int(number) > 0 else 0), ) - else: - return - - -# NUMBER TO SEED - -class WAS_Number_To_Seed: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - } - } - - RETURN_TYPES = ("SEED",) - FUNCTION = "number_to_seed" - - CATEGORY = "WAS Suite/Constant" - - def number_to_seed(self, number): - return ({"seed": number, }, ) - - -# NUMBER TO INT - -class WAS_Number_To_Int: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - } - } - - RETURN_TYPES = ("INT",) - FUNCTION = "number_to_int" - - CATEGORY = "WAS Suite/Constant" - - def number_to_int(self, number): - return (int(number), ) - - - -# NUMBER TO FLOAT - -class WAS_Number_To_Float: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - } - } - - RETURN_TYPES = ("FLOAT",) - FUNCTION = "number_to_float" - - CATEGORY = "WAS Suite/Constant" - - def number_to_float(self, number): - return (float(number), ) - - - -# INT TO NUMBER - -class WAS_Int_To_Number: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "int_input": ("INT",), - } - } - - RETURN_TYPES = ("NUMBER",) - FUNCTION = "int_to_number" - - CATEGORY = "WAS Suite/Constant" - - def int_to_number(self, int_input): - return (int_input, ) - - - -# NUMBER TO FLOAT - -class WAS_Float_To_Number: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "float_input": ("FLOAT",), - } - } - - RETURN_TYPES = ("NUMBER",) - FUNCTION = "float_to_number" - - CATEGORY = "WAS Suite/Constant" - - def float_to_number(self, float_input): - return ( float_input, ) - - -# NUMBER TO STRING - -class WAS_Number_To_String: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - } - } - - RETURN_TYPES = ("STRING",) - FUNCTION = "number_to_string" - - CATEGORY = "WAS Suite/Constant" - - def number_to_string(self, number): - return ( str(number), ) - -# NUMBER TO STRING - -class WAS_Number_To_Text: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - } - } - - RETURN_TYPES = ("ASCII",) - FUNCTION = "number_to_text" - - CATEGORY = "WAS Suite/Constant" - - def number_to_text(self, number): - return ( str(number), ) - - -# NUMBER PI - -class WAS_Number_PI: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": {} - } - - RETURN_TYPES = ("NUMBER",) - FUNCTION = "number_pi" - - CATEGORY = "WAS Suite/Constant" - - def number_pi(self): - return (math.pi, ) - -# NUMBER OPERATIONS - - -class WAS_Number_Operation: - def __init__(self): - pass - - @classmethod - 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"],), - } - } - - RETURN_TYPES = ("NUMBER",) - FUNCTION = "math_operations" - - CATEGORY = "WAS Suite/Operations" - - def math_operations(self, number_a, number_b, operation="addition"): - - if operation == 'addition': - return ((number_a + number_b),) - elif operation == 'subtraction': - return ((number_a - number_b),) - elif operation == 'division': - return ((number_a / number_b),) - elif operation == 'floor division': - return ((number_a // number_b),) - elif operation == 'multiplication': - return ((number_a * number_b),) - elif operation == 'exponentiation': - return ((number_a ** number_b),) - elif operation == 'modulus': - return ((number_a % number_b),) - elif operation == 'greater-than': - return (+(number_a > number_b),) - elif operation == 'greater-than or equals': - return (+(number_a >= number_b),) - elif operation == 'less-than': - return (+(number_a < number_b),) - elif operation == 'less-than or equals': - return (+(number_a <= number_b),) - elif operation == 'equals': - return (+(number_a == number_b),) - elif operation == 'does not equal': - return (+(number_a != number_b),) - - -#! MISC - - -# INPUT SWITCH - -class WAS_Input_Switch: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "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, ) - else: - return (input_b, ) - - -# DEBUG INPUT TO CONSOLE - - -class WAS_Debug_Number_to_Console: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "number": ("NUMBER",), - "label": ("STRING", {"default": 'Debug to Console', "multiline": False}), - } - } - - RETURN_TYPES = ("NUMBER",) - OUTPUT_NODE = True - FUNCTION = "debug_to_console" - - CATEGORY = "WAS Suite/Debug" - - def debug_to_console(self, number, label): - if label.strip() != '': - print(f'\033[34mWAS Node Suite \033[33m{label}\033[0m:\n{number}\n') - else: - print(f'\033[34mWAS Node Suite \033[33mDebug to Console\033[0m:\n{number}\n') - return (number, ) - - @classmethod - def IS_CHANGED(cls, **kwargs): - return float("NaN") - - -# NODE MAPPING -NODE_CLASS_MAPPINGS = { - "CLIPTextEncode (NSP)": WAS_NSP_CLIPTextEncoder, - "Constant Number": WAS_Constant_Number, - "Debug Number to Console": WAS_Debug_Number_to_Console, - "Float to Number": WAS_Float_To_Number, - "Image Analyze": WAS_Image_Analyze, - "Image Blank": WAS_Image_Blank, - "Image Blend by Mask": WAS_Image_Blend_Mask, - "Image Blend": WAS_Image_Blend, - "Image Blending Mode": WAS_Image_Blending_Mode, - "Image Bloom Filter": WAS_Image_Bloom_Filter, - "Image Canny Filter": WAS_Canny_Filter, - "Image Chromatic Aberration": WAS_Image_Chromatic_Aberration, - "Image Color Palette": WAS_Image_Color_Palette, - "Image Edge Detection Filter": WAS_Image_Edge, - "Image Film Grain": WAS_Film_Grain, - "Image Filter Adjustments": WAS_Image_Filters, - "Image Flip": WAS_Image_Flip, - "Image Gradient Map": WAS_Image_Gradient_Map, - "Image Generate Gradient": WAS_Image_Generate_Gradient, - "Image High Pass Filter": WAS_Image_High_Pass_Filter, - "Image Levels Adjustment": WAS_Image_Levels, - "Image Load": WAS_Load_Image, - "Image Median Filter": WAS_Image_Median_Filter, - "Image Mix RGB Channels": WAS_Image_RGB_Merge, - "Image Monitor Effects Filter": WAS_Image_Monitor_Distortion_Filter, - "Image Nova Filter": WAS_Image_Nova_Filter, - "Image Padding": WAS_Image_Padding, - "Image Remove Background (Alpha)": WAS_Remove_Background, - "Image Remove Color": WAS_Image_Remove_Color, - "Image Resize": WAS_Image_Rescale, - "Image Rotate": WAS_Image_Rotate, - "Image Save": WAS_Image_Save, - "Image Select Channel": WAS_Image_Select_Channel, - "Image Select Color": WAS_Image_Select_Color, - "Image Style Filter": WAS_Image_Style_Filter, - "Image Threshold": WAS_Image_Threshold, - "Image Transpose": WAS_Image_Transpose, - "Image fDOF Filter": WAS_Image_fDOF, - "Image to Latent Mask": WAS_Image_To_Mask, - "Int to Number": WAS_Int_To_Number, - "KSampler (WAS)": WAS_KSampler, - "Latent Noise Injection": WAS_Latent_Noise, - "Latent Upscale by Factor (WAS)": WAS_Latent_Upscale, - "Load Image Batch": WAS_Load_Image_Batch, - "Load Text File": WAS_Text_Load_From_File, - "MiDaS Depth Approximation": MiDaS_Depth_Approx, - "MiDaS Mask Image": MiDaS_Background_Foreground_Removal, - "Number Operation": WAS_Number_Operation, - "Number to Float": WAS_Number_To_Float, - "Number PI": WAS_Number_PI, - "Number to Int": WAS_Number_To_Int, - "Number to Seed": WAS_Number_To_Seed, - "Number to String": WAS_Number_To_String, - "Number to Text": WAS_Number_To_Text, - "Random Number": WAS_Random_Number, - "Save Text File": WAS_Text_Save, - "Seed": WAS_Seed, - "Tensor Batch to Image": WAS_Tensor_Batch_to_Image, - "Text Concatenate": WAS_Text_Concatenate, - "Text Find and Replace Input": WAS_Search_and_Replace_Input, - "Text Find and Replace": WAS_Search_and_Replace, - "Text Multiline": WAS_Text_Multiline, - "Text Parse Noodle Soup Prompts": WAS_Text_Parse_NSP, - "Text Random Line": WAS_Text_Random_Line, - "Text String": WAS_Text_String, - "Text to Conditioning": WAS_Text_to_Conditioning, - "Text to Console": WAS_Text_to_Console, - "Text to String": WAS_Text_To_String, -} - -print('\033[34mWAS Node Suite: \033[92mLoaded\033[0m')