6181 lines
213 KiB
Python
6181 lines
213 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)
|
|
|
|
#! 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}`.')
|
|
else:
|
|
print(f'\033[34mWAS Node Suite\033[0m Error: Unable to create default conf file at `{WAS_CONFIG_FILE}`.')
|
|
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)
|
|
|
|
# SET TEXT TYPE
|
|
TEXT_TYPE = "TEXT"
|
|
if 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.')
|
|
|
|
|
|
# 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.')
|
|
|
|
|
|
|
|
#! 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/Adjustment"
|
|
|
|
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), )
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
# 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
|
|
tokens = TextTokens()
|
|
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("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("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, )
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
#! 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]
|
|
print(tensor)
|
|
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
|
|
|
|
print(result)
|
|
|
|
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 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 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,
|
|
}
|
|
|
|
print('\033[34mWAS Node Suite: \033[92mLoaded\033[0m')
|