Files
ltdrdata-was-node-suite-com…/WAS_Node_Suite.py
T
Jordan Thompson af02f866f9 Add Crop Face and Paste Face Crop Nodes
Remove Workflows (Now located in Wiki)
Add Image Crop Face node
Add Image Paste Face Crop node
2023-04-22 00:15:29 -07:00

6500 lines
226 KiB
Python

# 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 folder_paths as comfy_paths
import glob
import hashlib
import json
import nodes
import numpy as np
import os
import random
import re
import requests
import socket
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) )
MODELS_DIR = os.path.join(( os.getcwd()+os.sep+'ComfyUI' if not os.getcwd().startswith('/content') else os.getcwd() ), 'models')
WAS_SUITE_ROOT = os.path.dirname(NODE_FILE)
WAS_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_suite_settings.json')
WAS_HISTORY_DATABASE = os.path.join(WAS_SUITE_ROOT, 'was_history.json')
WAS_CONFIG_FILE = os.path.join(WAS_SUITE_ROOT, 'was_suite_config.json')
STYLES_PATH = os.path.join(WAS_SUITE_ROOT, 'styles.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)
# Check Write Access
if not os.access(WAS_SUITE_ROOT, os.W_OK) or not os.access(MODELS_DIR, os.W_OK):
print(f'\033[34mWAS Node Suite\033[0m Error: There is no write access to `{WAS_SUITE_ROOT}` or `{MODELS_DIR}`. Write access is required!')
exit
#! 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.')
#! WAS SUITE CONFIG
was_conf_template = {
"webui_styles": "None",
"webui_styles_persistent_update": True,
"blip_model_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_capfilt_large.pth",
"blip_model_vqa_url": "https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_vqa_capfilt_large.pth",
"sam_model_vith_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
"sam_model_vitl_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
"sam_model_vitb_url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
"history_display_limit": 32,
"use_legacy_ascii_text": True, # ASCII Legacy is True For Now
}
# Create, Load, or Update Config
def getSuiteConfig():
try:
with open(WAS_CONFIG_FILE, "r") as f:
was_config = json.load(f)
except OSError as e:
print(e)
return False
except Exception as e:
print(e)
return False
return was_config
def updateSuiteConfig(conf):
try:
with open(WAS_CONFIG_FILE, "w", encoding='utf-8') as f:
json.dump(conf, f, indent=4)
except OSError as e:
print(e)
return False
except Exception as e:
print(e)
return False
return True
if not os.path.exists(WAS_CONFIG_FILE):
if updateSuiteConfig(was_conf_template):
print(f'\033[34mWAS Node Suite:\033[0m Created default conf file at `{WAS_CONFIG_FILE}`.')
was_config = getSuiteConfig()
else:
print(f'\033[34mWAS Node Suite\033[0m Error: Unable to create default conf file at `{WAS_CONFIG_FILE}`. Using internal config template.')
was_config = was_conf_tempalte
else:
was_config = getSuiteConfig()
update_config = False
for sett_ in was_conf_template.keys():
if not was_config.__contains__(sett_):
was_config.update({sett_: was_conf_template[sett_]})
update_config = True
if update_config:
updateSuiteConfig(was_config)
# Convert WebUI Styles
if was_config.__contains__('webui_styles'):
webui_styles_file = was_config['webui_styles'].strip()
if was_config.__contains__('webui_styles_persistent_update'):
styles_persist = was_config['webui_styles_persistent_update']
else:
styles_persist = True
if webui_styles_file != "None" and os.path.exists(webui_styles_file):
print(f'\033[34mWAS Node Suite:\033[0m Importing styles from `{webui_styles_file}`.')
import csv
styles = {}
with open(webui_styles_file, 'r') as data:
for line in csv.DictReader(data):
if "\ufeffname" in line:
name = "\ufeffname"
elif "name" in line:
name = "name"
else:
name = "name"
styles[line[name]] = {"prompt": line['prompt'], "negative_prompt": line['negative_prompt']}
if styles:
if not os.path.exists(STYLES_PATH) or styles_persist:
with open(STYLES_PATH, "w", encoding='utf-8') as f:
json.dump(styles, f, indent=4)
del styles
print(f'\033[34mWAS Node Suite:\033[0m Styles import complete.')
# SET TEXT TYPE
TEXT_TYPE = "TEXT"
if was_config and was_config.__contains__('use_legacy_ascii_text'):
if was_config['use_legacy_ascii_text']:
TEXT_TYPE = "ASCII"
print(f'\033[34mWAS Node Suite\033[0m Warning: use_legacy_ascii_text is `True` in `was_suite_config.json`. `ASCII` type is deprecated and the default will be `TEXT` in the future.')
#! SUITE SPECIFIC CLASSES & FUNCTIONS
# Freeze PIP modules
def packages(versions=False):
import sys
import subprocess
return [( r.decode().split('==')[0] if not versions else r.decode() ) for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).split()]
# Tensor to PIL
def tensor2pil(image):
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
# Convert PIL to Tensor
def pil2tensor(image):
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
# PIL Hex
def pil2hex(image):
return hashlib.sha256(np.array(tensor2pil(image)).astype(np.uint16).tobytes()).hexdigest()
# Tensor to SAM-compatible NumPy
def tensor2sam(image):
# Convert tensor to numpy array in HWC uint8 format with pixel values in [0, 255]
sam_image = np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
# Transpose the image to HWC format if it's in CHW format
if sam_image.shape[0] == 3:
sam_image = np.transpose(sam_image, (1, 2, 0))
return sam_image
# SAM-compatible NumPy to tensor
def sam2tensor(image):
# Convert the image to float32 and normalize the pixel values to [0, 1]
float_image = image.astype(np.float32) / 255.0
# Transpose the image from HWC format to CHW format
chw_image = np.transpose(float_image, (2, 0, 1))
# Convert the numpy array to a tensor
tensor_image = torch.from_numpy(chw_image)
return tensor_image
# 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')
def resizeImage(image, max_size):
width, height = image.size
if width > height:
if width > max_size:
new_width = max_size
new_height = int(height * (max_size / width))
else:
if height > max_size:
new_height = max_size
new_width = int(width * (max_size / height))
resized_image = image.resize((new_width, new_height))
return resized_image
# 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 catExists(self, category):
return self.data.__contains__(category)
def keyExists(self, category, key):
return self.data[category].__contains__(key)
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 updateCat(self, category, dictionary):
if self.data.__contains__(category):
Exception(f"\033[34mWAS Node Suite\033[0m Error: The database category `{category}` already exists!")
self.data[category].update(dictionary)
self._save()
def get(self, category, key):
return self.data.get(category, {}).get(key, None)
def getDB(self):
return self.data
def insertCat(self, category):
if self.data.__contains__(category):
Exception(f"\033[34mWAS Node Suite\033[0m Error: The database category `{category}` already exists!")
self.data[category] = {}
self._save()
def getDict(self, category):
if not self.data.__contains__(category):
ValueError(f"\033[34mWAS Node Suite\033[0m Error: The database category `{category}` does not exist!")
return self.data[category]
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, indent=4)
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)
# WAS Token Class
class TextTokens:
def __init__(self):
self.WDB = WDB
if not self.WDB.getDB().__contains__('custom_tokens'):
self.WDB.insertCat('custom_tokens')
self.custom_tokens = self.WDB.getDict('custom_tokens')
self.tokens = {
'[time]': str(time.time()).replace('.','_'),
'[hostname]': socket.gethostname(),
}
if '.' in self.tokens['[time]']:
self.tokens['[time]'] = self.tokens['[time]'].split('.')[0]
try:
self.tokens['[user]'] = ( os.getlogin() if os.getlogin() else 'null' )
except Exception:
self.tokens['[user]'] = 'null'
def addToken(self, name, value):
self.custom_tokens.update({name: value})
self._update()
def removeToken (self, name):
self.custom_tokens.pop(name)
self._update()
def format_time(self, format_code):
return time.strftime(format_code, time.localtime(time.time()))
def parseTokens(self, text):
tokens = self.tokens.copy()
if self.custom_tokens:
tokens.update(self.custom_tokens)
# Update time
tokens['[time]'] = str(time.time())
if '.' in tokens['[time]']:
tokens['[time]'] = tokens['[time]'].split('.')[0]
for token, value in tokens.items():
if token.startswith('[time('):
continue
text = text.replace(token, value)
def replace_custom_time(match):
format_code = match.group(1)
return self.format_time(format_code)
text = re.sub(r'\[time\((.*?)\)\]', replace_custom_time, text)
return text
def _update(self):
self.WDB.updateCat('custom_tokens', self.custom_tokens)
# Update image history
def update_history_images(new_paths):
HDB = WASDatabase(WAS_HISTORY_DATABASE)
if HDB.catExists("History") and HDB.keyExists("History", "Images"):
saved_paths = HDB.get("History", "Images")
for path_ in saved_paths:
if not os.path.exists(path_):
saved_paths.remove(path_)
if isinstance(new_paths, str):
if new_paths in saved_paths:
saved_paths.remove(new_paths)
saved_paths.append(new_paths)
elif isinstance(new_paths, list):
for path_ in new_paths:
if path_ in saved_paths:
saved_paths.remove(path_)
saved_paths.append(path_)
HDB.update("History", "Images", saved_paths)
else:
if not HDB.catExists("History"):
HDB.insertCat("History")
if isinstance(new_paths, str):
HDB.insert("History", "Images", [new_paths])
elif isinstance(new_paths, list):
HDB.insert("History", "Images", new_paths)
# Update text file history
def update_history_text_files(new_paths):
HDB = WASDatabase(WAS_HISTORY_DATABASE)
if HDB.catExists("History") and HDB.keyExists("History", "TextFiles"):
saved_paths = HDB.get("History", "TextFiles")
for path_ in saved_paths:
if not os.path.exists(path_):
saved_paths.remove(path_)
if isinstance(new_paths, str):
if new_paths in saved_paths:
saved_paths.remove(new_paths)
saved_paths.append(new_paths)
elif isinstance(new_paths, list):
for path_ in new_paths:
if path_ in saved_paths:
saved_paths.remove(path_)
saved_paths.append(path_)
HDB.update("History", "TextFiles", saved_paths)
else:
if not HDB.catExists("History"):
HDB.insertCat("History")
if isinstance(new_paths, str):
HDB.insert("History", "TextFiles", [new_paths])
elif isinstance(new_paths, list):
HDB.insert("History", "TextFiles", new_paths)
# WAS Filter Class
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
def stitch_image(self, image_a, image_b, mode='right', fuzzy_zone=50):
def linear_gradient(start_color, end_color, size, start, end, mode='horizontal'):
width, height = size
gradient = Image.new('RGB', (width, height), end_color)
draw = ImageDraw.Draw(gradient)
for i in range(0, start):
if mode == "horizontal":
draw.line((i, 0, i, height-1), start_color)
elif mode == "vertical":
draw.line((0, i, width-1, i), start_color)
for i in range(start, end):
if mode == "horizontal":
curr_color = (
int(start_color[0] + (float(i - start) / (end - start)) * (end_color[0] - start_color[0])),
int(start_color[1] + (float(i - start) / (end - start)) * (end_color[1] - start_color[1])),
int(start_color[2] + (float(i - start) / (end - start)) * (end_color[2] - start_color[2]))
)
draw.line((i, 0, i, height-1), curr_color)
elif mode == "vertical":
curr_color = (
int(start_color[0] + (float(i - start) / (end - start)) * (end_color[0] - start_color[0])),
int(start_color[1] + (float(i - start) / (end - start)) * (end_color[1] - start_color[1])),
int(start_color[2] + (float(i - start) / (end - start)) * (end_color[2] - start_color[2]))
)
draw.line((0, i, width-1, i), curr_color)
for i in range(end, width if mode == 'horizontal' else height):
if mode == "horizontal":
draw.line((i, 0, i, height-1), end_color)
elif mode == "vertical":
draw.line((0, i, width-1, i), end_color)
return gradient
image_a = image_a.convert('RGB')
image_b = image_b.convert('RGB')
offset = int(fuzzy_zone / 2)
canvas_width = int(image_a.size[0] + image_b.size[0] - fuzzy_zone) if mode == 'right' or mode == 'left' else image_a.size[0]
canvas_height = int(image_a.size[1] + image_b.size[1] - fuzzy_zone) if mode == 'top' or mode == 'bottom' else image_a.size[1]
canvas = Image.new('RGB', (canvas_width, canvas_height), (0,0,0))
im_ax = 0
im_ay = 0
im_bx = 0
im_by = 0
image_a_mask = None
image_b_mask = None
if mode == 'top':
image_a_mask = linear_gradient((0,0,0), (255,255,255), image_a.size, 0, fuzzy_zone, 'vertical')
image_b_mask = linear_gradient((255,255,255), (0,0,0), image_b.size, int(image_b.size[1] - fuzzy_zone), image_b.size[1], 'vertical')
im_ay = image_b.size[1] - fuzzy_zone
elif mode == 'bottom':
image_a_mask = linear_gradient((255,255,255), (0,0,0), image_a.size, int(image_a.size[1] - fuzzy_zone), image_a.size[1], 'vertical')
image_b_mask = linear_gradient((0,0,0), (255,255,255), image_b.size, 0, fuzzy_zone, 'vertical').convert('L')
im_by = image_a.size[1] - fuzzy_zone
elif mode == 'left':
image_a_mask = linear_gradient((0,0,0), (255,255,255), image_a.size, 0, fuzzy_zone, 'horizontal')
image_b_mask = linear_gradient((255,255,255), (0,0,0), image_b.size, int(image_b.size[0] - fuzzy_zone), image_b.size[0], 'horizontal')
im_ax = image_b.size[0] - fuzzy_zone
elif mode == 'right':
image_a_mask = linear_gradient((255,255,255), (0,0,0), image_a.size, int(image_a.size[0] - fuzzy_zone), image_a.size[0], 'horizontal')
image_b_mask = linear_gradient((0,0,0), (255,255,255), image_b.size, 0, fuzzy_zone, 'horizontal')
im_bx = image_b.size[0] - fuzzy_zone
Image.Image.paste(canvas, image_a, (im_ax, im_ay), image_a_mask.convert('L'))
Image.Image.paste(canvas, image_b, (im_bx, im_by), image_b_mask.convert('L'))
return canvas
# FILTERS
# SHADOWS AND HIGHLIGHTS ADJUSTMENTS
def shadows_and_highlights(self, image, shadow_thresh=30, highlight_thresh=220, shadow_factor=0.5, highlight_factor=1.5, shadow_smooth=None, highlight_smooth=None, simplify_masks=None):
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
alpha = None
if image.mode.endswith('A'):
alpha = image.getchannel('A')
image = image.convert('RGB')
# Convert the image to grayscale
grays = image.convert('L')
if shadow_smooth is not None or highlight_smooth is not None and simplify_masks is not None:
simplify = float(simplify_masks)
grays = grays.filter(ImageFilter.GaussianBlur(radius=simplify))
# Create shadow and highlight masks
shadow_mask = Image.eval(grays, lambda x: 255 if x < shadow_thresh else 0)
highlight_mask = Image.eval(grays, lambda x: 255 if x > highlight_thresh else 0)
image_shadow = image.copy()
image_highlight = image.copy()
if shadow_smooth is not None:
shadow_mask = shadow_mask.filter(ImageFilter.GaussianBlur(radius=shadow_smooth))
if highlight_smooth is not None:
highlight_mask = highlight_mask.filter(ImageFilter.GaussianBlur(radius=highlight_smooth))
image_shadow = Image.eval(image_shadow, lambda x: x * shadow_factor)
image_highlight = Image.eval(image_highlight, lambda x: x * highlight_factor)
if shadow_smooth is not None:
shadow_mask = shadow_mask.filter(ImageFilter.GaussianBlur(radius=shadow_smooth))
if highlight_smooth is not None:
highlight_mask = highlight_mask.filter(ImageFilter.GaussianBlur(radius=highlight_smooth))
result = image.copy()
result.paste(image_shadow, shadow_mask)
result.paste(image_highlight, highlight_mask)
result = pilgram.css.blending.color(result, image)
if alpha:
result.putalpha(alpha)
return (result, shadow_mask, highlight_mask)
# DRAGAN PHOTOGRAPHY FILTER
def dragan_filter(self, image, saturation=1, contrast=1, sharpness=1, brightness=1, highpass_radius=3, highpass_samples=1, highpass_strength=1, colorize=True):
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
alpha = None
if image.mode == 'RGBA':
alpha = image.getchannel('A')
grayscale_image = image if image.mode == 'L' else image.convert('L')
contrast_enhancer = ImageEnhance.Contrast(grayscale_image)
contrast_image = contrast_enhancer.enhance(contrast)
saturation_enhancer = ImageEnhance.Color(contrast_image) if image.mode != 'L' else None
saturation_image = contrast_image if saturation_enhancer is None else saturation_enhancer.enhance(saturation)
sharpness_enhancer = ImageEnhance.Sharpness(saturation_image)
sharpness_image = sharpness_enhancer.enhance(sharpness)
brightness_enhancer = ImageEnhance.Brightness(sharpness_image)
brightness_image = brightness_enhancer.enhance(brightness)
blurred_image = brightness_image.filter(ImageFilter.GaussianBlur(radius=-highpass_radius))
highpass_filter = ImageChops.subtract(image, blurred_image.convert('RGB'))
blank_image = Image.new('RGB', image.size, (127, 127, 127))
highpass_image = ImageChops.screen(blank_image, highpass_filter.resize(image.size))
if not colorize:
highpass_image = highpass_image.convert('L').convert('RGB')
highpassed_image = pilgram.css.blending.overlay(brightness_image.convert('RGB'), highpass_image)
for _ in range((highpass_samples if highpass_samples > 0 else 1)):
highpassed_image = pilgram.css.blending.overlay(highpassed_image, highpass_image)
final_image = ImageChops.blend(brightness_image.convert('RGB'), highpassed_image, highpass_strength)
if colorize:
final_image = pilgram.css.blending.color(final_image, image)
if alpha:
final_image.putalpha(alpha)
return final_image
# Sparkle - Fairy Tale Filter
def sparkle(self, image):
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
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
# Perlin Noise (relies on perlin_noise package: https://github.com/salaxieb/perlin_noise/blob/master/perlin_noise/perlin_noise.py)
def perlin_noise(self, width, height, shape, density, octaves, seed):
if 'pythonperlin' not in packages():
print("\033[34mWAS NS:\033[0m Installing pythonperlin...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'pythonperlin'])
from pythonperlin import perlin
if seed > 4294967294:
seed = random.randint(0,4294967294)
print(f'\033[34mWAS NS:\033[0m Seed too large for perlin; rescaled to: {seed}')
# Density range
min_density = 1
max_density = 100
# Map the density to a range of 0 to 1
density = int(10 ** (np.log10(min_density) + (1.0 - density) * (np.log10(max_density) - np.log10(min_density))))
# Set grid shape for randomly seeded gradients
shape = (shape,shape)
# Calcualte shape and density
shape = (width // density, height // density)
density = min(width // shape[0], height // shape[1])
# Generate Noise
x = perlin(shape, dens=density, octaves=octaves, seed=seed)
min_val, max_val = np.min(x), np.max(x)
data_scaled = (x - min_val) / (max_val - min_val) * 255
data_scaled = data_scaled.astype(np.uint8)
return Image.fromarray(data_scaled).convert('RGB')
# Worley Noise Generator
class worley_noise:
def __init__(self, height=512, width=512, density=50, option=0, use_broadcast_ops=True):
self.height = height
self.width = width
self.density = density
self.use_broadcast_ops = use_broadcast_ops
self.image = self.generateImage(option)
def generate_points(self):
self.points = np.random.randint(0, (self.width, self.height), (self.density, 2))
def calculate_noise(self, option):
self.data = np.zeros((self.height, self.width))
for h in range(self.height):
for w in range(self.width):
distances = np.sqrt(np.sum((self.points - np.array([w, h])) ** 2, axis=1))
self.data[h, w] = np.sort(distances)[option]
def broadcast_calculate_noise(self, option):
xs = np.arange(self.width)
ys = np.arange(self.height)
x_dist = np.power(self.points[:, 0, np.newaxis] - xs, 2)
y_dist = np.power(self.points[:, 1, np.newaxis] - ys, 2)
d = np.sqrt(x_dist[:, :, np.newaxis] + y_dist[:, np.newaxis, :])
distances = np.sort(d, axis=0)
self.data = distances[option]
def generateImage(self, option):
self.generate_points()
if self.use_broadcast_ops:
self.broadcast_calculate_noise(option)
else:
self.calculate_noise(option)
min_val, max_val = np.min(self.data), np.max(self.data)
data_scaled = (self.data - min_val) / (max_val - min_val) * 255
data_scaled = data_scaled.astype(np.uint8)
return Image.fromarray(data_scaled).convert('RGB')
def make_seamless(self, image, blending=0.5, tiled=False, tiles=2):
if 'img2texture' not in packages():
print("\033[34mWAS NS:\033[0m Installing img2texture...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'git+https://github.com/WASasquatch/img2texture.git'])
from img2texture import img2tex
from img2texture._tiling import tile
texture = img2tex(src=image, dst=None, pct=blending, return_result=True)
if tiled:
texture = tile(source=texture, target=None, horizontal=tiles, vertical=tiles, return_result=True)
return texture
# 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
#! IMAGE FILTER NODES
# IMAGE ADJUSTMENTS NODES
# IMAGE SHADOW AND HIGHLIGHT ADJUSTMENTS
class WAS_Shadow_And_Highlight_Adjustment:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"shadow_threshold": ("FLOAT", {"default": 75, "min": 0.0, "max": 255.0, "step": 0.1}),
"shadow_factor": ("FLOAT", {"default": 1.5, "min": -12.0, "max": 12.0, "step": 0.1}),
"shadow_smoothing": ("FLOAT", {"default": 0.25, "min": -255.0, "max": 255.0, "step": 0.1}),
"highlight_threshold": ("FLOAT", {"default": 175, "min": 0.0, "max": 255.0, "step": 0.1}),
"highlight_factor": ("FLOAT", {"default": 0.5, "min": -12.0, "max": 12.0, "step": 0.1}),
"highlight_smoothing": ("FLOAT", {"default": 0.25, "min": -255.0, "max": 255.0, "step": 0.1}),
"simplify_isolation": ("FLOAT", {"default": 0, "min": -255.0, "max": 255.0, "step": 0.1}),
}
}
RETURN_TYPES = ("IMAGE","IMAGE","IMAGE")
RETURN_NAMES = ("image","shadow_map","highlight_map")
FUNCTION = "apply_shadow_and_highlight"
CATEGORY = "WAS Suite/Image/Adjustment"
def apply_shadow_and_highlight(self, image, shadow_threshold=30, highlight_threshold=220, shadow_factor=1.5, highlight_factor=0.5, shadow_smoothing=0, highlight_smoothing=0, simplify_isolation=0):
WFilter = WAS_Filter_Class()
result, shadows, highlights = WFilter.shadows_and_highlights(tensor2pil(image), shadow_threshold, highlight_threshold, shadow_factor, highlight_factor, shadow_smoothing, highlight_smoothing, simplify_isolation)
result, shadows, highlights = WFilter.shadows_and_highlights(tensor2pil(image), shadow_threshold, highlight_threshold, shadow_factor, highlight_factor, shadow_smoothing, highlight_smoothing, simplify_isolation)
return (pil2tensor(result), pil2tensor(shadows), pil2tensor(highlights) )
# SIMPLE IMAGE ADJUST
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/Filter"
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/Filter"
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'])
# Import Pilgram module
import pilgram
# Convert image to PIL
image = tensor2pil(image)
# WAS Filters
WFilter = WAS_Filter_Class()
# Apply blending
if style:
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), )
# IMAGE CROP FACE
class WAS_Image_Crop_Face:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"crop_padding_factor": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 2.0, "step": 0.01}),
"cascade_xml": ([
"haarcascade_frontalface_default.xml",
"haarcascade_frontalface_alt.xml",
"haarcascade_frontalface_alt2.xml",
"haarcascade_frontalface_alt_tree.xml",
"haarcascade_upperbody.xml"
],),
"use_face_recognition_gpu": (["false","true"],),
}
}
RETURN_TYPES = ("IMAGE", "CROP_DATA")
FUNCTION = "image_crop_face"
CATEGORY = "WAS Suite/Image/Process"
def image_crop_face(self, image, cascade_xml=None, crop_padding_factor=0.25, use_face_recognition_gpu="false"):
use_fr = False if use_face_recognition_gpu.strip().lower() == 'false' else True
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'])
if use_fr:
if 'face_recognition' not in packages():
print("\033[34mWAS NS:\033[0m Installing face_recognition...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'face_recognition'])
return self.crop_face(tensor2pil(image), cascade_xml, crop_padding_factor, use_fr)
def crop_face(self, image, cascade_name=None, padding=0.25, use_fr=False):
import cv2
if use_fr:
import face_recognition
img = np.array(image.convert('RGB'))
if use_fr:
face_location = face_recognition.face_locations(img)
else:
face_location = None
cascades = [os.path.join(os.path.join(WAS_SUITE_ROOT, 'res'), 'haarcascade_frontalface_default.xml'),
os.path.join(os.path.join(WAS_SUITE_ROOT, 'res'), 'haarcascade_frontalface_alt.xml'),
os.path.join(os.path.join(WAS_SUITE_ROOT, 'res'), 'haarcascade_frontalface_alt2.xml'),
os.path.join(os.path.join(WAS_SUITE_ROOT, 'res'), 'haarcascade_frontalface_alt_tree.xml'),
os.path.join(os.path.join(WAS_SUITE_ROOT, 'res'), 'haarcascade_upperbody.xml')]
if cascade_name:
for cascade in cascades:
if os.path.basename(cascade) == cascade_name:
cascades.remove(cascade)
cascades.insert(0, cascade)
break
faces = None
if not face_location:
if use_fr:
print(f"\033[34mWAS NS\033[0m Warning: Unable to find any faces with face_recognition, switching to cascade recognition...")
for cascade in cascades:
if not os.path.exists(cascade):
print(f"\033[34mWAS NS\033[0m Error: Unable to find cascade XML file at `{cascade}`.",
"Did you pull the latest files from https://github.com/WASasquatch/was-node-suite-comfyui repo?")
return (pil2tensor(Image.new("RGB", (512,512), (0,0,0))), ((0,0),(0,0,0,0)))
face_cascade = cv2.CascadeClassifier(cascade)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5)
if len(faces) != 0:
print("\033[34mWAS NS\033[0m: Face found with:", os.path.basename(cascade))
break
if len(faces) == 0:
print("\033[34mWAS NS\033[0m Warning: No faces found in the image!")
return (pil2tensor(Image.new("RGB", (512,512), (0,0,0))), ((0,0),(0,0,0,0)))
else:
print("\033[34mWAS NS\033[0m: Face found with: face_recognition model")
faces = face_location
# Assume there is only one face in the image
x, y, w, h = faces[0]
# Check if the face region aligns with the edges of the original image
left_adjust = max(0, -x)
right_adjust = max(0, x + w - img.shape[1])
top_adjust = max(0, -y)
bottom_adjust = max(0, y + h - img.shape[0])
# Check if the face region is near any edges, and if so, pad in the opposite direction
if left_adjust < w:
x += right_adjust
elif right_adjust < w:
x -= left_adjust
if top_adjust < h:
y += bottom_adjust
elif bottom_adjust < h:
y -= top_adjust
w -= left_adjust + right_adjust
h -= top_adjust + bottom_adjust
# Calculate padding around face
face_size = min(h, w)
y_pad = int(face_size * padding)
x_pad = int(face_size * padding)
# Calculate square coordinates around face
center_x = x + w // 2
center_y = y + h // 2
half_size = (face_size + max(x_pad, y_pad)) // 2
top = max(0, center_y - half_size)
bottom = min(img.shape[0], center_y + half_size)
left = max(0, center_x - half_size)
right = min(img.shape[1], center_x + half_size)
# Ensure square crop of the original image
crop_size = min(right - left, bottom - top)
left = center_x - crop_size // 2
right = center_x + crop_size // 2
top = center_y - crop_size // 2
bottom = center_y + crop_size // 2
# Crop face from original image
face_img = img[top:bottom, left:right, :]
# Resize image
size = max(face_img.copy().shape[:2])
pad_h = (size - face_img.shape[0]) // 2
pad_w = (size - face_img.shape[1]) // 2
face_img = cv2.copyMakeBorder(face_img, pad_h, pad_h, pad_w, pad_w, cv2.BORDER_CONSTANT, value=[0,0,0])
min_size = 64 # Set minimum size for padded image
if size < min_size:
size = min_size
face_img = cv2.resize(face_img, (size, size))
# Convert numpy array back to PIL image
face_img = Image.fromarray(face_img)
# Resize image to a multiple of 8
original_size = face_img.size
face_img.resize((((face_img.size[0] // 8) * 8 + 8), ((face_img.size[1] // 8) * 8 + 8)))
# Return face image and coordinates
return (pil2tensor(face_img), (original_size, (left, top, right, bottom)))
# IMAGE PASTE FACE CROP
class WAS_Image_Paste_Face_Crop:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"crop_image": ("IMAGE",),
"crop_data": ("CROP_DATA",),
"crop_blending": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
"crop_sharpening": ("INT", {"default": 0, "min": 0, "max": 3, "step": 1}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE")
RETURN_NAMES = ("IMAGE", "MASK_IMAGE")
FUNCTION = "image_paste_face"
CATEGORY = "WAS Suite/Image/Process"
def image_paste_face(self, image, crop_image, crop_data=None, crop_blending=0.25, crop_sharpening=0):
result_image, result_mask = self.paste_face(tensor2pil(image), tensor2pil(crop_image), crop_data[0], crop_data[1], crop_blending, crop_sharpening)
return(result_image, result_mask)
def paste_face(self, image, face_img, original_size, face_coords, blend_amount=0.25, sharpen_amount=1):
face_img = face_img.convert("RGB").resize(original_size)
if sharpen_amount > 0:
for _ in range(sharpen_amount):
face_img = face_img.filter(ImageFilter.SHARPEN)
if blend_amount > 1.0:
blend_amount = 1.0
elif blend_amount < 0.0:
blend_amount = 0.0
blend_ratio = (max(face_img.size[0], face_img.size[1]) / 2) * float(blend_amount)
blend = image.convert("RGBA")
mask = Image.new("L", image.size, 0)
offset_x = int(original_size[0] * (blend_amount + blend_amount / 2.5))
offset_y = int(original_size[1] * (blend_amount + blend_amount / 2.5))
mask_block_size = (original_size[0]-offset_x, original_size[1]-offset_y)
mask_block = Image.new("L", mask_block_size, 255)
Image.Image.paste(mask, mask_block, (int(face_coords[0]+offset_x/2), int(face_coords[1]+offset_y/2)))
Image.Image.paste(blend, face_img, (face_coords[0], face_coords[1]))
mask = mask.filter(ImageFilter.BoxBlur(radius=blend_ratio/2))
mask = mask.filter(ImageFilter.GaussianBlur(radius=blend_ratio/2))
blend.putalpha(mask)
image = Image.alpha_composite(image.convert("RGBA"), blend)
return (pil2tensor(image), pil2tensor(mask))
# 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:
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/Filter"
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:
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)
else:
image = image
return (pil2tensor(image), )
# IMAGE PERLIN NOISE FILTER
class WAS_Image_Perlin_Noise_Filter:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 512, "max": 2048, "min": 64, "step": 1}),
"height": ("INT", {"default": 512, "max": 2048, "min": 64, "step": 1}),
"shape": ("INT", {"default": 4, "max": 8, "min": 2, "step": 2}),
"density": ("FLOAT", {"default": 0.25, "max": 1.0, "min": 0.0, "step": 0.01}),
"octaves": ("INT", {"default": 4, "max": 8, "min": 0, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "perlin_noise_filter"
CATEGORY = "WAS Suite/Image/Generate/Noise"
def perlin_noise_filter(self, width, height, shape, density, octaves, seed):
if width > 1024 or height > 1024 and octaves > 6:
octaves = 6
WFilter = WAS_Filter_Class()
image = WFilter.perlin_noise(width, height, shape, density, octaves, seed)
return (pil2tensor(image), )
# IMAGE VORONOI NOISE FILTER
class WAS_Image_Voronoi_Noise_Filter:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 512, "max": 4096, "min": 64, "step": 1}),
"height": ("INT", {"default": 512, "max": 4096, "min": 64, "step": 1}),
"density": ("INT", {"default": 50, "max": 256, "min": 10, "step": 2}),
"modulator": ("INT", {"default": 0, "max": 8, "min": 0, "step": 1}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "voronoi_noise_filter"
CATEGORY = "WAS Suite/Image/Generate/Noise"
def voronoi_noise_filter(self, width, height, density, modulator, seed):
WFilter = WAS_Filter_Class()
image = WFilter.worley_noise(height=width, width=height, density=density, option=modulator, use_broadcast_ops=True).image
return (pil2tensor(image), )
# IMAGE MAKE SEAMLESS
class WAS_Image_Make_Seamless:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"blending": ("FLOAT", {"default": 0.4, "max": 1.0, "min": 0.0, "step": 0.01}),
"tiled": (["true", "false"],),
"tiles": ("INT", {"default": 2, "max": 6, "min": 2, "step": 2}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "make_seamless"
CATEGORY = "WAS Suite/Image/Process"
def make_seamless(self, image, blending, tiled, tiles):
WFilter = WAS_Filter_Class()
image = WFilter.make_seamless(tensor2pil(image), blending, tiled, tiles)
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/Analyze"
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/Analyze"
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:
if mode == 'Black White Levels':
image = WFilter.black_white_levels(image)
elif mode == 'RGB Levels':
image = WFilter.channel_frequency(image)
else:
image = 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/Generate"
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/Filter"
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/Transform"
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/Transform"
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):
self.HDB = WASDatabase(WAS_HISTORY_DATABASE)
@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",TEXT_TYPE)
RETURN_NAMES = ("image","filename_text")
FUNCTION = "load_batch_images"
CATEGORY = "WAS Suite/IO"
def load_batch_images(self, path, pattern='*', index=0, mode="single_image", label='Batch 001'):
if not os.path.exists(path):
return (None, )
fl = self.BatchImageLoader(path, label, pattern)
new_paths = fl.image_paths
if mode == 'single_image':
image, filename = fl.get_image_by_id(index)
else:
image, filename = fl.get_next_image()
# Update history
update_history_images(new_paths)
return (pil2tensor(image), filename)
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]), os.path.basename(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), os.path.basename(image_path))
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# IMAGE HISTORY NODE
class WAS_Image_History:
def __init__(self):
self.HDB = WASDatabase(WAS_HISTORY_DATABASE)
self.conf = getSuiteConfig()
@classmethod
def INPUT_TYPES(cls):
HDB = WASDatabase(WAS_HISTORY_DATABASE)
conf = getSuiteConfig()
paths = ['No History']
if HDB.catExists("History") and HDB.keyExists("History", "Images"):
history_paths = HDB.get("History", "Images")
if conf.__contains__('history_display_limit'):
history_paths = history_paths[-conf['history_display_limit']:]
paths = []
for path_ in history_paths:
paths.append(os.path.join('...'+os.sep+os.path.basename(os.path.dirname(path_)), os.path.basename(path_)))
return {
"required": {
"image": (paths,),
},
}
RETURN_TYPES = ("IMAGE",TEXT_TYPE)
RETURN_NAMES = ("image","filename_text")
FUNCTION = "image_history"
CATEGORY = "WAS Suite/History"
def image_history(self, image):
self.HDB = WASDatabase(WAS_HISTORY_DATABASE)
paths = {}
if self.HDB.catExists("History") and self.HDB.keyExists("History", "Images"):
history_paths = self.HDB.get("History", "Images")
for path_ in history_paths:
paths.update({os.path.join('...'+os.sep+os.path.basename(os.path.dirname(path_)), os.path.basename(path_)): path_})
if os.path.exists(paths[image]) and paths.__contains__(image):
return (pil2tensor(Image.open(paths[image]).convert('RGB')), os.path.basename(paths[image]))
else:
raise ValueError(f"\033[34mWAS NS\033[0m Error: The image `{image}` does not exist!")
return (pil2tensor(Image.new('RGB', (512,512), (0, 0, 0, 0))), 'null')
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# IMAGE PADDING
class WAS_Image_Stitch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_a": ("IMAGE",),
"image_b": ("IMAGE",),
"stitch": (["top", "left", "bottom", "right"],),
"feathering": ("INT", {"default": 50, "min": 0, "max": 2048, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "image_stitching"
CATEGORY = "WAS Suite/Image/Transform"
def image_stitching(self, image_a, image_b, stitch="right", feathering=50):
valid_stitches = ["top", "left", "bottom", "right"]
if stitch not in valid_stitches:
raise ValueError(f"\033[34mWAS NS\033[0m Error: The stitch mode `{stitch}` is not valid. Valid sitch modes are {', '.join(valid_stitches)}")
if feathering > 2048:
raise ValueError(f"\033[34mWAS NS\033[0m Error: The stitch feathering of `{feathering}` is too high. Please choose a value between `0` and `2048`")
WFilter = WAS_Filter_Class();
stitched_image = WFilter.stitch_image(tensor2pil(image_a), tensor2pil(image_b), stitch, feathering)
return (pil2tensor(stitched_image), )
# 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/Transform"
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/Process"
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/Filter"
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/Filter"
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/Process"
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/Process"
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/Filter"
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/Adjustment"
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/Filter"
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/Transform"
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/Transform"
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:
if sampler == 'nearest':
sampler = Image.NEAREST
elif sampler == 'bicubic':
sampler = Image.BICUBIC
elif sampler == 'bilinear':
sampler = Image.BILINEAR
else:
sampler == Image.BILINEAR
# 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/Filter"
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/Filter"
def canny_filter(self, image, threshold_low, threshold_high, enable_threshold):
self.install_opencv()
if enable_threshold == 'false':
threshold_low = None
threshold_high = None
image_canny = Image.fromarray(self.Canny_detector(
255. * image.cpu().numpy().squeeze(), threshold_low, threshold_high)).convert('RGB')
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/Filter"
def image_edges(self, image, mode):
# Convert image to PIL
image = tensor2pil(image)
# Detect edges
if mode:
if mode == "normal":
image = image.filter(ImageFilter.FIND_EDGES)
elif mode == "laplacian":
image = image.filter(ImageFilter.Kernel((3, 3), (-1, -1, -1, -1, 8,
-1, -1, -1, -1), 1, 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/Filter"
def fdof_composite(self, image, depth, radius, samples, mode):
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, mask, radius, samples, mode='mock'):
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
# IMAGE DRAGAN PHOTOGRAPHY FILTER
class WAS_Dragon_Filter:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"saturation": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 16.0, "step": 0.01}),
"contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 16.0, "step": 0.01}),
"brightness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 16.0, "step": 0.01}),
"sharpness": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 6.0, "step": 0.01}),
"highpass_radius": ("FLOAT", {"default": 6.0, "min": 0.0, "max": 255.0, "step": 0.01}),
"highpass_samples": ("INT", {"default": 1, "min": 0, "max": 6.0, "step": 1}),
"highpass_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"colorize": (["true","false"],),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "apply_dragan_filter"
CATEGORY = "WAS Suite/Image/Filter"
def apply_dragan_filter(self, image, saturation, contrast, sharpness, brightness, highpass_radius, highpass_samples, highpass_strength, colorize):
WFilter = WAS_Filter_Class()
image = WFilter.dragan_filter(tensor2pil(image), saturation, contrast, sharpness, brightness, highpass_radius, highpass_samples, highpass_strength, colorize)
return (pil2tensor(image), )
# 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/Filter"
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/Process"
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/Process"
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/Process"
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)
# Define token system
tokens = TextTokens()
output_path = tokens.parseTokens(output_path)
# 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]))
# Parse prefix tokens
filename_prefix = tokens.parseTokens(filename_prefix)
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")
self.HDB = WASDatabase(WAS_HISTORY_DATABASE)
@classmethod
def INPUT_TYPES(cls):
return {"required":
{"image_path": (
"STRING", {"default": './ComfyUI/input/example.png', "multiline": False}), }
}
RETURN_TYPES = ("IMAGE", "MASK", TEXT_TYPE)
RETURN_NAMES = ("image", "mask", "filename_text")
FUNCTION = "load_image"
CATEGORY = "WAS Suite/IO"
def load_image(self, image_path):
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
# Update history
update_history_images(image_path)
image = i.convert('RGB')
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, os.path.basename(image_path))
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 float("NaN")
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/Transform"
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/Image/Transform"
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": (["area", "bicubic", "bilinear", "nearest"],),
"factor": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.01}),
"align": (["true", "false"], )}}
RETURN_TYPES = ("LATENT",)
FUNCTION = "latent_upscale"
CATEGORY = "WAS Suite/Latent/Transform"
def latent_upscale(self, samples, mode, factor, align):
valid_modes = ["area", "bicubic", "bilinear", "nearest"]
if mode not in valid_modes:
raise ValueError(f"\033[34mWAS NS\033[0m Error: Invalid interpolation mode `{mode}` selected. Valid modes are: {', '.join(valid_modes)}")
align = True if align == 'true' else False
if not isinstance(factor, float) or factor <= 0:
raise ValueError(f"\033[34mWAS NS\033[0m Error: The input `factor` is `{factor}`, but should be a positive or negative float.")
s = samples.copy()
shape = s['samples'].shape
size = tuple(int(round(dim * factor)) for dim in shape[-2:])
if mode in ['linear', 'bilinear', 'bicubic', 'trilinear']:
s["samples"] = torch.nn.functional.interpolate(
s['samples'], size=size, mode=mode, align_corners=align)
else:
s["samples"] = torch.nn.functional.interpolate(s['samples'], size=size, mode=mode)
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/Generate"
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(MODELS_DIR, '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/AI"
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(MODELS_DIR, '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/AI"
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.path.join(WAS_SUITE_ROOT, '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 < 0:
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/Number"
def seed(self, seed):
return ({"seed": seed, }, )
#! TEXT NODES
class WAS_Prompt_Styles_Selector:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
style_list = []
if os.path.exists(STYLES_PATH):
with open(STYLES_PATH, "r") as f:
if len(f.readlines()) != 0:
f.seek(0)
data = f.read()
styles = json.loads(data)
for style in styles.keys():
style_list.append(style)
if not style_list:
style_list.append("None")
return {
"required": {
"style": (style_list,),
}
}
RETURN_TYPES = (TEXT_TYPE,TEXT_TYPE)
FUNCTION = "load_style"
CATEGORY = "WAS Suite/Text"
def load_style(self, style):
styles = {}
# Load styles from file
if os.path.exists(STYLES_PATH):
with open(STYLES_PATH, 'r') as data:
styles = json.load(data)
else:
print(f'\033[34mWAS NS\033[0m Error: The styles file does not exist at `{STYLES_PATH}`. Unable to load styles! Have you imported your AUTOMATIC1111 WebUI styles?')
if styles and style != None or style != 'None':
prompt = styles[style]['prompt']
negative_prompt = styles[style]['negative_prompt']
else:
prompt = ''
negative_prompt = ''
return (prompt, negative_prompt)
# 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 = (TEXT_TYPE,)
FUNCTION = "text_multiline"
CATEGORY = "WAS Suite/Text"
def text_multiline(self, text):
import io
new_text = []
for line in io.StringIO(text):
if not line.strip().startswith('#'):
if not line.strip().startswith("\n"):
line = line.replace("\n", '')
new_text.append(line)
new_text = "\n".join(new_text)
return (new_text, )
# Text Parse Embeddings
class WAS_Text_Parse_Embeddings_By_Name:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE, ),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_parse_embeddings"
CATEGORY = "WAS Suite/Text/Parse"
def text_parse_embeddings(self, text):
return (self.convert_a1111_embeddings(text), )
def convert_a1111_embeddings(self, text):
import re
for filename in os.listdir(os.path.join(MODELS_DIR, 'embeddings')):
basename, ext = os.path.splitext(filename)
pattern = re.compile(r'\b{}\b'.format(re.escape(basename)))
replacement = 'embedding:{}'.format(basename)
text = re.sub(pattern, replacement, text)
return text
# Text Dictionary Concatenate
class WAS_Dictionary_Update:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"dictionary_a": ("DICT", ),
"dictionary_b": ("DICT", ),
},
"optional": {
"dictionary_c": ("DICT", ),
"dictionary_d": ("DICT", ),
}
}
RETURN_TYPES = ("DICT",)
FUNCTION = "dictionary_update"
CATEGORY = "WAS Suite/Text"
def dictionary_update(self, dictionary_a, dictionary_b, dictionary_c=None, dictionary_d=None):
return_dictionary = {**dictionary_a, **dictionary_b}
if dictionary_c is not None:
return_dictionary = {**return_dictionary, **dictionary_c}
if dictionary_d is not None:
return_dictionary = {**return_dictionary, **dictionary_d}
return (return_dictionary, )
# Text String Node
class WAS_Text_String:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": '', "multiline": False}),
},
"optional": {
"text_b": ("STRING", {"default": '', "multiline": False}),
"text_c": ("STRING", {"default": '', "multiline": False}),
"text_d": ("STRING", {"default": '', "multiline": False}),
}
}
RETURN_TYPES = (TEXT_TYPE,TEXT_TYPE,TEXT_TYPE,TEXT_TYPE)
FUNCTION = "text_string"
CATEGORY = "WAS Suite/Text"
def text_string(self, text='', text_b='', text_c='', text_d=''):
return (text, text_b, text_c, text_d)
# Text Random Line
class WAS_Text_Random_Line:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = (TEXT_TYPE,)
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, )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# Text Concatenate
class WAS_Text_Concatenate:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text_a": (TEXT_TYPE,),
"text_b": (TEXT_TYPE,),
"linebreak_addition": (['false','true'], ),
},
"optional": {
"text_c": (TEXT_TYPE,),
"text_d": (TEXT_TYPE,),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_concatenate"
CATEGORY = "WAS Suite/Text"
def text_concatenate(self, text_a, text_b, text_c=None, text_d=None, linebreak_addition='false'):
return_text = text_a + ("\n" if linebreak_addition == 'true' else '') + text_b
if text_c:
return_text = return_text + ("\n" if linebreak_addition == 'true' else '') + text_c
if text_d:
return_text = return_text + ("\n" if linebreak_addition == 'true' else '') + text_d
return (return_text, )
# Text Search and Replace
class WAS_Search_and_Replace:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
"find": ("STRING", {"default": '', "multiline": False}),
"replace": ("STRING", {"default": '', "multiline": False}),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_search_and_replace"
CATEGORY = "WAS Suite/Text/Search"
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": (TEXT_TYPE,),
"find": (TEXT_TYPE,),
"replace": (TEXT_TYPE,), }
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_search_and_replace"
CATEGORY = "WAS Suite/Text/Search"
def text_search_and_replace(self, text, find, replace):
# Parse Text
new_text = text
tcount = new_text.count(find)
for _ in range(tcount):
new_text = new_text.replace(find, replace, 1)
return (new_text, )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# Text Search and Replace By Dictionary
class WAS_Search_and_Replace_Dictionary:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
"dictionary": ("DICT",),
"replacement_key": ("STRING", {"default": "__", "multiline": False}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_search_and_replace_dict"
CATEGORY = "WAS Suite/Text/Search"
def text_search_and_replace_dict(self, text, dictionary, replacement_key, seed):
random.seed(seed)
# Parse Text
new_text = text
for term in dictionary.keys():
tkey = f'{replacement_key}{term}{replacement_key}'
tcount = new_text.count(tkey)
for _ in range(tcount):
new_text = new_text.replace(tkey, random.choice(dictionary[term]), 1)
if seed > 0 or seed < 0:
seed = seed + 1
random.seed(seed)
return (new_text, )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# 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": (TEXT_TYPE,),
}
}
OUTPUT_NODE = True
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_parse_nsp"
CATEGORY = "WAS Suite/Text/Parse"
def text_parse_nsp(self, text, noodle_key='__', seed=0):
# Fetch the NSP Pantry
local_pantry = os.path.join(WAS_SUITE_ROOT, '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 < 0:
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": (TEXT_TYPE,),
"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 Warning: The path `{path}` doesn\'t exist! Creating it...')
try:
os.mkdir(path)
except OSError as e:
print(
f'\033[34mWAS NS\033[0m Warning: The path `{path}` could not be created! Is there write access?\n{e}')
# 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.')
# Parse filename tokens
tokens = TextTokens()
filename = tokens.parseTokens(filename)
# Write text file
file_path = os.path.join(path, filename + '.txt')
self.writeTextFile(file_path, text)
# Write file to file history
update_history_text_files(file_path)
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}`')
# TEXT FILE HISTORY NODE
class WAS_Text_File_History:
def __init__(self):
self.HDB = WASDatabase(WAS_HISTORY_DATABASE)
self.conf = getSuiteConfig()
@classmethod
def INPUT_TYPES(cls):
HDB = WASDatabase(WAS_HISTORY_DATABASE)
conf = getSuiteConfig()
paths = ['No History',]
if HDB.catExists("History") and HDB.keyExists("History", "TextFiles"):
history_paths = HDB.get("History", "TextFiles")
if conf.__contains__('history_display_limit'):
history_paths = history_paths[-conf['history_display_limit']:]
paths = []
for path_ in history_paths:
paths.append(os.path.join('...'+os.sep+os.path.basename(os.path.dirname(path_)), os.path.basename(path_)))
return {
"required": {
"file": (paths,),
"dictionary_name": ("STRING", {"default": '[filename]', "multiline": True}),
},
}
RETURN_TYPES = (TEXT_TYPE,"DICT")
FUNCTION = "text_file_history"
CATEGORY = "WAS Suite/History"
def text_file_history(self, file=None, dictionary_name='[filename]]'):
file_path = file.strip()
filename = ( os.path.basename(file_path).split('.', 1)[0]
if '.' in os.path.basename(file_path) else os.path.basename(file_path) )
if dictionary_name != '[filename]' or dictionary_name not in [' ', '']:
filename = dictionary_name
if not os.path.exists(file_path):
print('\033[34mWAS Node Suite\033[0m Error: The path `{file_path}` specified cannot be found.')
return ('', {filename: []})
with open(file_path, 'r', encoding="utf-8", newline='\n') as file:
text = file.read()
# Write to file history
update_history_text_files(file_path)
import io
lines = []
for line in io.StringIO(text):
if not line.strip().startswith('#'):
if not line.strip().startswith("\n"):
line = line.replace("\n", '')
lines.append(line.replace("\n",''))
dictionary = {filename: lines}
return ("\n".join(lines), dictionary)
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# TEXT TO CONDITIONIONG
class WAS_Text_to_Conditioning:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"clip": ("CLIP",),
"text": (TEXT_TYPE,),
}
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "text_to_conditioning"
CATEGORY = "WAS Suite/Text/Operations"
def text_to_conditioning(self, clip, text):
return ([[clip.encode(text), {}]], )
# TEXT PARSE TOKENS
class WAS_Text_Parse_Tokens:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_parse_tokens"
CATEGORY = "WAS Suite/Text/Tokens"
def text_parse_tokens(self, text):
# Token Parser
tokens = TextTokens()
return (tokens.parseTokens(text), )
# TEXT ADD TOKENS
class WAS_Text_Add_Tokens:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"tokens": ("STRING", {"default": "[hello]: world", "multiline": True}),
}
}
RETURN_TYPES = ()
FUNCTION = "text_add_tokens"
OUTPUT_NODE = True
CATEGORY = "WAS Suite/Text/Tokens"
def text_add_tokens(self, tokens):
import io
# Token Parser
tk = TextTokens()
# Parse out Tokens
for line in io.StringIO(tokens):
parts = line.split(':')
token = parts[0].strip()
token_value = parts[1].strip()
tk.addToken(token, token_value)
# Current Tokens
print(f'\033[34mWAS Node Suite\033[0m Current Custom Tokens:')
print(json.dumps(tk.custom_tokens, indent=4))
return tokens
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# TEXT ADD TOKEN BY INPUT
class WAS_Text_Add_Token_Input:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"token_name": (TEXT_TYPE, ),
"token_value": (TEXT_TYPE, ),
}
}
RETURN_TYPES = ()
FUNCTION = "text_add_token"
OUTPUT_NODE = True
CATEGORY = "WAS Suite/Text/Tokens"
def text_add_token(self, token_name, token_value):
if token_name.strip() == '':
print(f'\033[34mWAS Node Suite\033[0m Error: a `token_name` is required for a token; token name provided is empty.')
pass
# Token Parser
tk = TextTokens()
# Add Tokens
tk.addToken(token_name, token_value)
# Current Tokens
print(f'\033[34mWAS Node Suite\033[0m Current Custom Tokens:')
print(json.dumps(tk.custom_tokens, indent=4))
return (token_name, token_value)
# TEXT TO CONSOLE
class WAS_Text_to_Console:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
"label": ("STRING", {"default": f'Text Output', "multiline": False}),
}
}
RETURN_TYPES = (TEXT_TYPE,)
OUTPUT_NODE = True
FUNCTION = "text_to_console"
CATEGORY = "WAS Suite/Debug"
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, )
# DICT TO CONSOLE
class WAS_Dictionary_To_Console:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"dictionary": ("DICT",),
"label": ("STRING", {"default": f'Dictionary Output', "multiline": False}),
}
}
RETURN_TYPES = ("DICT",)
OUTPUT_NODE = True
FUNCTION = "text_to_console"
CATEGORY = "WAS Suite/Debug"
def text_to_console(self, dictionary, label):
if label.strip() != '':
print(f'\033[34mWAS Node Suite \033[33m{label}\033[0m:\n')
from pprint import pprint
pprint(dictionary, indent=4)
print('')
else:
print(
f'\033[34mWAS Node Suite \033[33mText to Console\033[0m:\n')
pprint(dictionary, indent=4)
print('')
return (dictionary, )
# 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}),
"dictionary_name": ("STRING", {"default": '[filename]', "multiline": False}),
}
}
RETURN_TYPES = (TEXT_TYPE,"DICT")
FUNCTION = "load_file"
CATEGORY = "WAS Suite/IO"
def load_file(self, file_path='', dictionary_name='[filename]]'):
filename = ( os.path.basename(file_path).split('.', 1)[0]
if '.' in os.path.basename(file_path) else os.path.basename(file_path) )
if dictionary_name != '[filename]':
filename = dictionary_name
if not os.path.exists(file_path):
print(
f'\033[34mWAS Node Suite\033[0m Error: The path `{file_path}` specified cannot be found.')
return ('', {filename: []})
with open(file_path, 'r', encoding="utf-8", newline='\n') as file:
text = file.read()
# Write to file history
update_history_text_files(file_path)
import io
lines = []
for line in io.StringIO(text):
if not line.strip().startswith('#'):
if ( not line.strip().startswith("\n")
or not line.strip().startswith("\r")
or not line.strip().startswith("\r\n") ):
line = line.replace("\n", '').replace("\r",'').replace("\r\n",'')
lines.append(line.replace("\n",'').replace("\r",'').replace("\r\n",''))
dictionary = {filename: lines}
return ("\n".join(lines), dictionary)
# LOAD TEXT TO STRING
class WAS_Text_To_String:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": (TEXT_TYPE,),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "text_to_string"
CATEGORY = "WAS Suite/Text/Operations"
def text_to_string(self, text):
return (text, )
# BLIP CAPTION IMAGE
class WAS_BLIP_Analyze_Image:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mode": (["caption", "interrogate"], ),
"question": ("STRING", {"default": "What does the background consist of?", "multiline": True}),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "blip_caption_image"
CATEGORY = "WAS Suite/Text/AI"
def blip_caption_image(self, image, mode, question):
if ( 'timm' not in packages()
or 'transformers' not in packages()
or 'GitPython' not in packages()
or 'fairscale' not in packages() ):
print("\033[34mWAS NS:\033[0m Installing BLIP dependencies...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'transformers==4.26.1', 'timm>=0.4.12', 'gitpython', 'fairscale>=0.4.4'])
if 'transformers==4.26.1' not in packages(True):
print("\033[34mWAS NS:\033[0m Installing BLIP compatible `transformers` (transformers==4.26.1)...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', '--upgrade', '--force-reinstall', 'transformers==4.26.1'])
if not os.path.exists(os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'BLIP')):
from git.repo.base import Repo
print("\033[34mWAS NS:\033[0m Installing BLIP...")
Repo.clone_from('https://github.com/WASasquatch/BLIP-Python', os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'BLIP'))
# Not sure this is needed
def create_fake_fairscale(self):
class FakeFairscale:
def checkpoint_wrapper(self):
pass
sys.modules["fairscale.nn.checkpoint.checkpoint_activations"] = FakeFairscale
def transformImage_legacy(input_image, image_size, device):
raw_image = input_image.convert('RGB')
raw_image = raw_image.resize((image_size, image_size))
transform = transforms.Compose([
transforms.Resize(raw_image.size, interpolation=InterpolationMode.BICUBIC),
transforms.ToTensor(),
transforms.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711))
])
image = transform(raw_image).unsqueeze(0).to(device)
return image
def transformImage(input_image, image_size, device):
raw_image = input_image.convert('RGB')
raw_image = raw_image.resize((image_size, image_size))
transform = transforms.Compose([
transforms.Resize(raw_image.size, interpolation=InterpolationMode.BICUBIC),
transforms.ToTensor(),
transforms.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711))
])
image = transform(raw_image).unsqueeze(0).to(device)
return image.view(1, -1, image_size, image_size) # Change the shape of the output tensor
sys.path.append(os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'BLIP'))
from torchvision import transforms
from torchvision.transforms.functional import InterpolationMode
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
conf = getSuiteConfig()
image = tensor2pil(image)
size = 384
if 'transformers==4.26.1' in packages(True):
print("Using Legacy `transformImaage()`")
tensor = transformImage_legacy(image, size, device)
else:
tensor = transformImage(image, size, device)
if mode == 'caption':
from models.blip import blip_decoder
blip_dir = os.path.join(MODELS_DIR, 'blip')
if not os.path.exists(blip_dir):
os.mkdir(blip_dir)
torch.hub.set_dir(blip_dir)
if conf.__contains__('blip_model_url'):
model_url = conf['blip_model_url']
else:
model_url = 'https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_capfilt_large.pth'
model = blip_decoder(pretrained=model_url, image_size=size, vit='base')
model.eval()
model = model.to(device)
with torch.no_grad():
caption = model.generate(tensor, sample=False, num_beams=6, max_length=74, min_length=20)
# nucleus sampling
#caption = model.generate(tensor, sample=True, top_p=0.9, max_length=75, min_length=10)
print(f"\033[34mWAS NS\033[33m BLIP Caption:\033[0m", caption[0])
return (caption[0], )
elif mode == 'interrogate':
from models.blip_vqa import blip_vqa
blip_dir = os.path.join(MODELS_DIR, 'blip')
if not os.path.exists(blip_dir):
os.mkdir(blip_dir)
torch.hub.set_dir(blip_dir)
if conf.__contains__('blip_model_vqa_url'):
model_url = conf['blip_model_vqa_url']
else:
model_url = 'https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_vqa_capfilt_large.pth'
model = blip_vqa(pretrained=model_url, image_size=size, vit='base')
model.eval()
model = model.to(device)
with torch.no_grad():
answer = model(tensor, question, train=False, inference='generate')
print(f"\033[34mWAS NS\033[33m BLIP Answer:\033[0m", answer[0])
return (answer[0], )
else:
print(f"\033[34mWAS NS\033[0m Error: The selected mode `{mode}` is not a valid selection!")
return ('Invalid BLIP mode!', )
# SAM MODEL LOADER
class WAS_SAM_Model_Loader:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"model_size": (["ViT-H (91M)", "ViT-L (308M)", "ViT-B (636M)"], ),
}
}
RETURN_TYPES = ("SAM_MODEL",)
FUNCTION = "sam_load_model"
CATEGORY = "WAS Suite/Image/AI/SAM"
def sam_load_model(self, model_size):
conf = getSuiteConfig()
model_filename_mapping = {
"ViT-H (91M)": "sam_vit_h_4b8939.pth",
"ViT-L (308M)": "sam_vit_l_0b3195.pth",
"ViT-B (636M)": "sam_vit_b_01ec64.pth",
}
model_url_mapping = {
"ViT-H (91M)": conf['sam_model_vith_url'] if conf.__contains__('sam_model_vith_url') else r"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth",
"ViT-L (308M)": conf['sam_model_vitl_url'] if conf.__contains__('sam_model_vitl_url') else r"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth",
"ViT-B (636M)": conf['sam_model_vitb_url'] if conf.__contains__('sam_model_vitb_url') else r"https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth",
}
model_url = model_url_mapping[model_size]
model_filename = model_filename_mapping[model_size]
if ( 'GitPython' not in packages() ):
print("\033[34mWAS NS:\033[0m Installing SAM dependencies...")
subprocess.check_call([sys.executable, '-m', 'pip', '-q', 'install', 'gitpython'])
if not os.path.exists(os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'SAM')):
from git.repo.base import Repo
print("\033[34mWAS NS:\033[0m Installing SAM...")
Repo.clone_from('https://github.com/facebookresearch/segment-anything', os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'SAM'))
sys.path.append(os.path.join(WAS_SUITE_ROOT, 'repos'+os.sep+'SAM'))
sam_dir = os.path.join(( os.getcwd()+os.sep+'ComfyUI' if not os.getcwd().startswith('/content') else os.getcwd() ), 'models'+os.sep+'sam')
if not os.path.exists(sam_dir):
os.mkdir(sam_dir)
sam_file = os.path.join(sam_dir, model_filename)
if not os.path.exists(sam_file):
print("\033[34mWAS NS:\033[0m Selected SAM model not found. Downloading...")
r = requests.get(model_url, allow_redirects=True)
open(sam_file, 'wb').write(r.content)
from segment_anything import build_sam
sam_model = build_sam(checkpoint=sam_file)
return (sam_model, )
# SAM PARAMETERS
class WAS_SAM_Parameters:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"points": ("STRING", {"default": "[128, 128]; [0, 0]", "multiline": False}),
"labels": ("STRING", {"default": "[1, 0]", "multiline": False}),
}
}
RETURN_TYPES = ("SAM_PARAMETERS",)
FUNCTION = "sam_parameters"
CATEGORY = "WAS Suite/Image/AI/SAM"
def sam_parameters(self, points, labels):
parameters = {
"points": np.asarray(np.matrix(points)),
"labels": np.array(np.matrix(labels))[0]
}
return (parameters,)
# SAM COMBINE PARAMETERS
class WAS_SAM_Combine_Parameters:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"sam_parameters_a": ("SAM_PARAMETERS",),
"sam_parameters_b": ("SAM_PARAMETERS",),
}
}
RETURN_TYPES = ("SAM_PARAMETERS",)
FUNCTION = "sam_combine_parameters"
CATEGORY = "WAS Suite/Image/AI/SAM"
def sam_combine_parameters(self, sam_parameters_a, sam_parameters_b):
parameters = {
"points": np.concatenate(
(sam_parameters_a["points"],
sam_parameters_b["points"]),
axis=0
),
"labels": np.concatenate(
(sam_parameters_a["labels"],
sam_parameters_b["labels"])
)
}
return (parameters,)
# SAM IMAGE MASK
class WAS_SAM_Image_Mask:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"sam_model": ("SAM_MODEL",),
"sam_parameters": ("SAM_PARAMETERS",),
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "MASK",)
FUNCTION = "sam_image_mask"
CATEGORY = "WAS Suite/Image/AI/SAM"
def sam_image_mask(self, sam_model, sam_parameters, image):
image = tensor2sam(image)
points = sam_parameters["points"]
labels = sam_parameters["labels"]
from segment_anything import SamPredictor
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
sam_model.to(device=device)
predictor = SamPredictor(sam_model)
predictor.set_image(image)
masks, scores, logits = predictor.predict(
point_coords=points,
point_labels=labels,
multimask_output=False
)
sam_model.to(device='cpu')
mask = np.expand_dims(masks, axis=-1)
image = np.repeat(mask, 3, axis=-1)
image = torch.from_numpy(image)
mask = torch.from_numpy(mask)
mask = mask.squeeze(2)
mask = mask.squeeze().to(torch.float32)
return (image, mask, )
# IMAGE BOUNDS
class WAS_Image_Bounds:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE_BOUNDS",)
FUNCTION = "image_bounds"
CATEGORY = "WAS Suite/Image/Bound"
def image_bounds(self, image):
_, height, width, _ = image.shape
image_bounds = [0, height - 1, 0, width - 1]
return (image_bounds,)
# INSET IMAGE BOUNDS
class WAS_Inset_Image_Bounds:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image_bounds": ("IMAGE_BOUNDS",),
"inset_left": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"inset_right": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"inset_top": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"inset_bottom": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE_BOUNDS",)
FUNCTION = "inset_image_bounds"
CATEGORY = "WAS Suite/Image/Bound"
def inset_image_bounds(self, image_bounds, inset_left, inset_right, inset_top, inset_bottom):
# Unpack the image bounds
rmin, rmax, cmin, cmax = image_bounds
# Apply insets
rmin = rmin + inset_top
rmax = rmax - inset_bottom
cmin = cmin + inset_left
cmax = cmax - inset_right
# Check if the resulting bounds are valid
if rmin > rmax or cmin > cmax:
raise ValueError("\033[34mWAS NS\033[33m Error:\033[0m Invalid insets provided. Please make sure the insets do not exceed the image bounds.")
image_bounds = [rmin, rmax, cmin, cmax]
return (image_bounds,)
# WAS BOUNDED IMAGE BLEND
class WAS_Bounded_Image_Blend:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"target": ("IMAGE",),
"target_bounds": ("IMAGE_BOUNDS",),
"source": ("IMAGE",),
"blend_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
"feathering": ("INT", {"default": 16, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "bounded_image_blend"
CATEGORY = "WAS Suite/Image/Bound"
def bounded_image_blend(self, target, target_bounds, source, blend_factor, feathering):
# Convert PyTorch tensors to PIL images
target_pil = Image.fromarray((target.squeeze(0).cpu().numpy() * 255).clip(0, 255).astype(np.uint8))
source_pil = Image.fromarray((source.squeeze(0).cpu().numpy() * 255).astype(np.uint8))
# Extract the target bounds
rmin, rmax, cmin, cmax = target_bounds
# Calculate the dimensions of the target bounds
width = cmax - cmin + 1
height = rmax - rmin + 1
# Resize the source image to match the dimensions of the target bounds
source_resized = source_pil.resize((width, height), Image.ANTIALIAS)
# Create the blend mask with the same size as the target image
blend_mask = Image.new('L', target_pil.size, 0)
# Create the feathered mask portion the size of the target bounds
if feathering > 0:
inner_mask = Image.new('L', (width - (2 * feathering), height - (2 * feathering)), 255)
inner_mask = ImageOps.expand(inner_mask, border=feathering, fill=0)
inner_mask = inner_mask.filter(ImageFilter.GaussianBlur(radius=feathering))
else:
inner_mask = Image.new('L', (width, height), 255)
# Paste the feathered mask portion into the blend mask at the target bounds position
blend_mask.paste(inner_mask, (cmin, rmin))
# Create a blank image with the same size and mode as the target
source_positioned = Image.new(target_pil.mode, target_pil.size)
# Paste the source image onto the blank image using the target bounds
source_positioned.paste(source_resized, (cmin, rmin))
# Create a blend mask using the blend_mask and blend factor
blend_mask = blend_mask.point(lambda p: p * blend_factor).convert('L')
# Blend the source and target images using the blend mask
result = Image.composite(source_positioned, target_pil, blend_mask)
# Convert the result back to a PyTorch tensor
result = torch.from_numpy(np.array(result).astype(np.float32) / 255).unsqueeze(0)
return (result,)
# BOUNDED IMAGE CROP
class WAS_Bounded_Image_Crop:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"image_bounds": ("IMAGE_BOUNDS",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "bounded_image_crop"
CATEGORY = "WAS Suite/Image/Bound"
def bounded_image_crop(self, image, image_bounds):
# Unpack the image bounds
rmin, rmax, cmin, cmax = image_bounds
# Check if the provided bounds are valid
if rmin > rmax or cmin > cmax:
raise ValueError("\033[34mWAS NS\033[33m Error:\033[0m Invalid bounds provided. Please make sure the bounds are within the image dimensions.")
# Crop the image using the provided bounds and return it
return (image[:, rmin:rmax+1, cmin:cmax+1, :],)
# WAS BOUNDED IMAGE BLEND WITH MASK
class WAS_Bounded_Image_Blend_With_Mask:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"target": ("IMAGE",),
"target_mask": ("MASK",),
"target_bounds": ("IMAGE_BOUNDS",),
"source": ("IMAGE",),
"blend_factor": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
"feathering": ("INT", {"default": 16, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "bounded_image_blend_with_mask"
CATEGORY = "WAS Suite/Image/Bound"
def bounded_image_blend_with_mask(self, target, target_mask, target_bounds, source, blend_factor, feathering):
# Convert PyTorch tensors to PIL images
target_pil = Image.fromarray((target.squeeze(0).cpu().numpy() * 255).clip(0, 255).astype(np.uint8))
target_mask_pil = Image.fromarray((target_mask.cpu().numpy() * 255).astype(np.uint8), mode='L')
source_pil = Image.fromarray((source.squeeze(0).cpu().numpy() * 255).astype(np.uint8))
# Extract the target bounds
rmin, rmax, cmin, cmax = target_bounds
# Create a blank image with the same size and mode as the target
source_positioned = Image.new(target_pil.mode, target_pil.size)
# Paste the source image onto the blank image using the target bounds
source_positioned.paste(source_pil, (cmin, rmin))
# Create a blend mask using the target mask and blend factor
blend_mask = target_mask_pil.point(lambda p: p * blend_factor).convert('L')
# Apply feathering (Gaussian blur) to the blend mask if feather_amount is greater than 0
if feathering > 0:
blend_mask = blend_mask.filter(ImageFilter.GaussianBlur(radius=feathering))
# Blend the source and target images using the blend mask
result = Image.composite(source_positioned, target_pil, blend_mask)
# Convert the result back to a PyTorch tensor
result_tensor = torch.from_numpy(np.array(result).astype(np.float32) / 255).unsqueeze(0)
return (result_tensor,)
# WAS BOUNDED IMAGE CROP WITH MASK
class WAS_Bounded_Image_Crop_With_Mask:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"padding_left": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"padding_right": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"padding_top": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
"padding_bottom": ("INT", {"default": 64, "min": 0, "max": 0xffffffffffffffff}),
}
}
RETURN_TYPES = ("IMAGE", "IMAGE_BOUNDS",)
FUNCTION = "bounded_image_crop_with_mask"
CATEGORY = "WAS Suite/Image/Bound"
def bounded_image_crop_with_mask(self, image, mask, padding_left, padding_right, padding_top, padding_bottom):
# Get the bounding box coordinates of the mask
rows = torch.any(mask, axis=1)
cols = torch.any(mask, axis=0)
rmin, rmax = torch.where(rows)[0][[0, -1]]
cmin, cmax = torch.where(cols)[0][[0, -1]]
# Apply padding
rmin = max(rmin - padding_top, 0)
rmax = min(rmax + padding_bottom, mask.shape[0] - 1)
cmin = max(cmin - padding_left, 0)
cmax = min(cmax + padding_right, mask.shape[1] - 1)
bounds = [rmin, rmax, cmin, cmax]
# Crop the image using the computed coordinates and return it
return (image[:, rmin:rmax+1, cmin:cmax+1, :], bounds,)
#! 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/Number"
def return_randm_number(self, minimum, maximum, seed, number_type='integer'):
# Set Generator Seed
random.seed(seed)
# Return random number
if number_type:
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, )
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# TRUE RANDOM NUMBER
class WAS_True_Random_Number:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_key": ("STRING",{"default":"00000000-0000-0000-0000-000000000000", "multiline": False}),
"minimum": ("FLOAT", {"default": 0, "min": -18446744073709551615, "max": 18446744073709551615}),
"maximum": ("FLOAT", {"default": 10000000, "min": -18446744073709551615, "max": 18446744073709551615}),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "return_true_randm_number"
CATEGORY = "WAS Suite/Number"
def return_true_randm_number(self, api_key=None, minimum=0, maximum=10):
# Get Random Number
number = self.get_random_numbers(api_key=api_key, minimum=minimum, maximum=maximum)[0]
# Return number
return (number, )
def get_random_numbers(self, api_key=None, amount=1, minimum=0, maximum=10):
'''Get random number(s) from random.org'''
if api_key in [None, '00000000-0000-0000-0000-000000000000', '']:
print("\033[34mWAS NS\033[33m Error:\033[0m No API key provided! A valid RANDOM.ORG API key is required to use `True Random.org Number Generator`")
return [0]
url = "https://api.random.org/json-rpc/2/invoke"
headers = {"Content-Type": "application/json"}
payload = {
"jsonrpc": "2.0",
"method": "generateIntegers",
"params": {
"apiKey": api_key,
"n": amount,
"min": minimum,
"max": maximum,
"replacement": True,
"base": 10
},
"id": 1
}
response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code == 200:
data = response.json()
if "result" in data:
return data["result"]["random"]["data"]
return [0]
@classmethod
def IS_CHANGED(cls, **kwargs):
return float("NaN")
# 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/Number"
def return_constant_number(self, number_type, number):
# Return number
if number_type:
if number_type == 'integer':
return (int(number), )
elif number_type == 'integer':
return (float(number), )
elif number_type == 'bool':
return ((1 if int(number) > 0 else 0), )
else:
return (number, )
# 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/Number/Operations"
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/Number/Operations"
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/Number/Operations"
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/Number/Operations"
def int_to_number(self, int_input):
return (int(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/Number/Operations"
def float_to_number(self, float_input):
return ( float(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/Number/Operations"
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 = (TEXT_TYPE,)
FUNCTION = "number_to_text"
CATEGORY = "WAS Suite/Number/Operations"
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/Number"
def number_pi(self):
return (math.pi, )
# Boolean
class WAS_Boolean:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"boolean_number": ("INT", {"default":1, "min":0, "max":1, "step":1}),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "return_boolean"
CATEGORY = "WAS Suite/Logic"
def return_boolean(self, boolean_number=1):
return (int(boolean_number), )
# 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/Number/Operations"
def math_operations(self, number_a, number_b, operation="addition"):
# Return random number
if operation:
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), )
else:
return number_a
# NUMBER MULTIPLE OF
class WAS_Number_Multiple_Of:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number": ("NUMBER",),
"multiple": ("INT", {"default": 8, "min": -18446744073709551615, "max": 18446744073709551615}),
}
}
RETURN_TYPES =("NUMBER",)
FUNCTION = "number_multiple_of"
CATEGORY = "WAS Suite/Number/Functions"
def number_multiple_of(self, number, multiple=8):
if number % multiple != 0:
return ((number // multiple) * multiple + multiple, )
return (number, )
#! MISC
# Image Width and Height to Number
class WAS_Image_Size_To_Number:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("NUMBER", "NUMBER",)
RETURN_NAMES = ("width_num", "height_num",)
FUNCTION = "image_width_height"
CATEGORY = "WAS Suite/Number/Operations"
def image_width_height(self, image):
image = tensor2pil(image)
if image.size:
return( image.size[0], image.size[1] )
return ( 0, 0 )
# Latent Width and Height to Number
class WAS_Latent_Size_To_Number:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"samples": ("LATENT",),
}
}
RETURN_TYPES = ("NUMBER","NUMBER")
RETURN_NAMES = ("tensor_w_num","tensor_h_num")
FUNCTION = "latent_width_height"
CATEGORY = "WAS Suite/Number/Operations"
def latent_width_height(self, samples):
size_dict = {}
i = 0
for tensor in samples['samples'][0]:
if not isinstance(tensor, torch.Tensor):
raise ValueError(f'\033[34mWAS NS\033[33m Error: Input should be a torch.Tensor')
shape = tensor.shape
tensor_height = shape[-2]
tensor_width = shape[-1]
size_dict.update({i:[tensor_width, tensor_height]})
return (size_dict[0][0], size_dict[0][1])
# LATENT INPUT SWITCH
class WAS_Latent_Input_Switch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"latent_a": ("LATENT",),
"latent_b": ("LATENT",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "latent_input_switch"
CATEGORY = "WAS Suite/Logic"
def latent_input_switch(self, latent_a, latent_b, boolean_number=1):
if int(boolean_number) == 1:
return (latent_a, )
else:
return (latent_b, )
# NUMBER INPUT CONDITION
class WAS_Number_Input_Condition:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number_a": ("NUMBER",),
"number_b": ("NUMBER",),
"comparison": (["greater-than", "greater-than or equels", "less-than", "less-than or equals", "equals", "does not equal", "divisible by", "if A odd", "if A even", "if A prime", "factor of"],),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "number_input_condition"
CATEGORY = "WAS Suite/Logic"
def number_input_condition(self, number_a, number_b, comparison="greater-than"):
if comparison:
if comparison == 'greater-than':
result = number_a if number_a > number_b else number_b
elif comparison == 'greater-than or equals':
result = number_a if number_a >= number_b else number_b
elif comparison == 'less-than':
result = number_a if number_a < number_b else number_b
elif comparison == 'less-than or equals':
result = number_a if number_a <= number_b else number_b
elif comparison == 'equals':
result = number_a if number_a == number_b else number_b
elif comparison == 'does not equal':
result = number_a if number_a != number_b else number_b
elif comparison == 'divisible by':
result = number_a if number_b % number_a == 0 else number_b
elif comparison == 'if A odd':
result = number_a if number_a % 2 != 0 else number_b
elif comparison == 'if A even':
result = number_a if number_a % 2 == 0 else number_b
elif comparison == 'if A prime':
result = number_a if self.is_prime(number_a) else number_b
elif comparison == 'factor of':
result = number_a if number_b % number_a == 0 else number_b
else:
result = number_a
return (result,)
def is_prime(self, n):
if n <= 1:
return False
elif n <= 3:
return True
elif n % 2 == 0 or n % 3 == 0:
return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
# NUMBER INPUT SWITCH
class WAS_Number_Input_Switch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"number_a": ("NUMBER",),
"number_b": ("NUMBER",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "number_input_switch"
CATEGORY = "WAS Suite/Logic"
def number_input_switch(self, number_a, number_b, boolean_number=1):
if int(boolean_number) == 1:
return (number_a, )
else:
return (number_b, )
# IMAGE INPUT SWITCH
class WAS_Image_Input_Switch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image_a": ("IMAGE",),
"image_b": ("IMAGE",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "image_input_switch"
CATEGORY = "WAS Suite/Logic"
def image_input_switch(self, image_a, image_b, boolean_number=1):
if int(boolean_number) == 1:
return (image_a, )
else:
return (image_b, )
# CONDITIONING INPUT SWITCH
class WAS_Conditioning_Input_Switch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"conditioning_a": ("CONDITIONING",),
"conditioning_b": ("CONDITIONING",),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = ("CONDITIONING",)
FUNCTION = "conditioning_input_switch"
CATEGORY = "WAS Suite/Logic"
def conditioning_input_switch(self, conditioning_a, conditioning_b, boolean_number=1):
if int(boolean_number) == 1:
return (conditioning_a, )
else:
return (conditioning_b, )
# TEXT INPUT SWITCH
class WAS_Text_Input_Switch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text_a": (TEXT_TYPE,),
"text_b": (TEXT_TYPE,),
"boolean_number": ("NUMBER",),
}
}
RETURN_TYPES = (TEXT_TYPE,)
FUNCTION = "text_input_switch"
CATEGORY = "WAS Suite/Logic"
def text_input_switch(self, text_a, text_b, boolean_number=1):
if int(boolean_number) == 1:
return (text_a, )
else:
return (text_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,
"Conditioning Input Switch": WAS_Conditioning_Input_Switch,
"Constant Number": WAS_Constant_Number,
"Debug Number to Console": WAS_Debug_Number_to_Console,
"Dictionary to Console": WAS_Dictionary_To_Console,
"Latent Input Switch": WAS_Latent_Input_Switch,
"Logic Boolean": WAS_Boolean,
"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 Crop Face": WAS_Image_Crop_Face,
"Image Paste Face": WAS_Image_Paste_Face_Crop,
"Image Dragan Photography Filter": WAS_Dragon_Filter,
"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 History Loader": WAS_Image_History,
"Image Input Switch": WAS_Image_Input_Switch,
"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 Perlin Noise Filter": WAS_Image_Perlin_Noise_Filter,
"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 Seamless Texture": WAS_Image_Make_Seamless,
"Image Select Channel": WAS_Image_Select_Channel,
"Image Select Color": WAS_Image_Select_Color,
"Image Shadows and Highlights": WAS_Shadow_And_Highlight_Adjustment,
"Image Size to Number": WAS_Image_Size_To_Number,
"Image Stitch": WAS_Image_Stitch,
"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,
"Image Voronoi Noise Filter": WAS_Image_Voronoi_Noise_Filter,
"KSampler (WAS)": WAS_KSampler,
"Latent Noise Injection": WAS_Latent_Noise,
"Latent Size to Number": WAS_Latent_Size_To_Number,
"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 Input Switch": WAS_Number_Input_Switch,
"Number Input Condition": WAS_Number_Input_Condition,
"Number Multiple Of": WAS_Number_Multiple_Of,
"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,
"Prompt Styles Selector": WAS_Prompt_Styles_Selector,
"Random Number": WAS_Random_Number,
"Save Text File": WAS_Text_Save,
"Seed": WAS_Seed,
"Tensor Batch to Image": WAS_Tensor_Batch_to_Image,
"BLIP Analyze Image": WAS_BLIP_Analyze_Image,
"SAM Model Loader": WAS_SAM_Model_Loader,
"SAM Parameters": WAS_SAM_Parameters,
"SAM Parameters Combine": WAS_SAM_Combine_Parameters,
"SAM Image Mask": WAS_SAM_Image_Mask,
"Image Bounds": WAS_Image_Bounds,
"Inset Image Bounds": WAS_Inset_Image_Bounds,
"Bounded Image Blend": WAS_Bounded_Image_Blend,
"Bounded Image Blend with Mask": WAS_Bounded_Image_Blend_With_Mask,
"Bounded Image Crop": WAS_Bounded_Image_Crop,
"Bounded Image Crop with Mask": WAS_Bounded_Image_Crop_With_Mask,
"Text Dictionary Update": WAS_Dictionary_Update,
"Text Add Tokens": WAS_Text_Add_Tokens,
"Text Add Token by Input": WAS_Text_Add_Token_Input,
"Text Concatenate": WAS_Text_Concatenate,
"Text File History Loader": WAS_Text_File_History,
"Text Find and Replace by Dictionary": WAS_Search_and_Replace_Dictionary,
"Text Find and Replace Input": WAS_Search_and_Replace_Input,
"Text Find and Replace": WAS_Search_and_Replace,
"Text Input Switch": WAS_Text_Input_Switch,
"Text Multiline": WAS_Text_Multiline,
"Text Parse A1111 Embeddings": WAS_Text_Parse_Embeddings_By_Name,
"Text Parse Noodle Soup Prompts": WAS_Text_Parse_NSP,
"Text Parse Tokens": WAS_Text_Parse_Tokens,
"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,
"True Random.org Number Generator": WAS_True_Random_Number,
}
print('\033[34mWAS Node Suite: \033[92mLoaded\033[0m')