diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50ed59b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 WAS + +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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d8edad8 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .efficiency_nodes import NODE_CLASS_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS'] diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..ede1dc9 Binary files /dev/null and b/__pycache__/__init__.cpython-310.pyc differ diff --git a/__pycache__/efficiency_nodes.cpython-310.pyc b/__pycache__/efficiency_nodes.cpython-310.pyc new file mode 100644 index 0000000..0dc42e0 Binary files /dev/null and b/__pycache__/efficiency_nodes.cpython-310.pyc differ diff --git a/efficiency_nodes.py b/efficiency_nodes.py new file mode 100644 index 0000000..19475b3 --- /dev/null +++ b/efficiency_nodes.py @@ -0,0 +1,353 @@ +# Efficiency Nodes - A collection of some of my ComfyUI custom nodes +# by Luciano Cirino (Discord: TSC#9184) - April 2023 + +from PIL import Image, ImageFilter, ImageEnhance, ImageOps, ImageDraw, ImageChops, ImageFont +from PIL.PngImagePlugin import PngInfo +import numpy as np +import torch + +import os +import sys +import json +import hashlib +import copy +import traceback +import copy +import folder_paths + +import model_management +import importlib +import matplotlib.pyplot as plt +import random + +global my_dir, comfy_dir + +# Get the absolute path of the parent directory of the current script +my_dir = os.path.dirname(os.path.abspath(__file__)) + +# Construct the absolute path to the ComfyUI directory +comfy_dir = os.path.join(my_dir, '..', '..') + +# Add the ComfyUI directory path to the sys.path list +sys.path.append(comfy_dir) + +# Import functions from nodes.py in the ComfyUI directory +from nodes import common_ksampler, before_node_execution, interrupt_processing +import comfy.samplers +import comfy.sd +import comfy.utils + +# Tensor to PIL (grabbed from WAS Suite) +def tensor2pil(image: torch.Tensor) -> Image.Image: + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +# Convert PIL to Tensor (grabbed from WAS Suite) +def pil2tensor(image: Image.Image) -> torch.Tensor: + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + + +# TSC Efficient Loader +class TSC_EfficientLoader: + @classmethod + def INPUT_TYPES(s): + return {"required": { "ckpt_name": (folder_paths.get_filename_list("checkpoints"), ), + "vae_name": (folder_paths.get_filename_list("vae"),), #.insert(0, "default.pt") + "clip_skip": ("INT", {"default": -1, "min": -24, "max": -1, "step": 1}), + "positive": ("STRING", {"default": "Positive prompt goes here.","multiline": True}), + "negative": ("STRING", {"default": "Negative prompt goes here.", "multiline": True}), + }} + + RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "CLIP", "VAE", ) + RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "CLIP", "VAE", ) + FUNCTION = "efficientloader" + + CATEGORY = "Efficiency Nodes/Loaders" + + def efficientloader(self, ckpt_name, vae_name, clip_skip, positive, negative, output_vae=True, output_clip=True): + ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name) + out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_vae=True, output_clip=True, embedding_directory=folder_paths.get_folder_paths("embeddings")) + + vae_path = folder_paths.get_full_path("vae", vae_name) + vae = comfy.sd.VAE(ckpt_path=vae_path) + + clip = out[1] #second entry + clip = clip.clone() + clip.clip_layer(clip_skip) + + return (out[0], [[clip.encode(positive), {}]], [[clip.encode(negative), {}]], out[1], vae, ) + + +# TSC KSampler (Efficient) +last_helds = { + "results": [[] for _ in range(15)], + "latent": [[] for _ in range(15)], + "images": [[] for _ in range(15)] +} +class TSC_KSampler: + + def __init__(self): + self.output_dir = os.path.join(comfy_dir, 'temp') + self.type = "temp" + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"sampler_state": (["Sample", "Hold"], ), + "my_unique_id": ("INT", {"default": 0, "min": 0, "max": 15}), + "model": ("MODEL",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "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}), + }, + "optional": { "optional_vae": ("VAE",), }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", "IMAGE", ) + RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", "IMAGE", ) + FUNCTION = "sample" + OUTPUT_NODE = True + CATEGORY = "Efficiency Nodes/Sampling" + + def sample(self, sampler_state, my_unique_id, model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=1.0, prompt=None, extra_pnginfo=None, optional_vae=(None,)): + + global last_helds + last_results = last_helds["results"][my_unique_id] + last_latent = last_helds["latent"][my_unique_id] + last_images = last_helds["images"][my_unique_id] + + vae = optional_vae + empty_image = pil2tensor(Image.new('RGBA', (1, 1), (0, 0, 0, 0))) + + # Adjust for KSampler states + if sampler_state == "Hold": + if last_results is not None or last_latent is not None: + return {"ui": {"images": last_results}, "result": (model, positive, negative, {"samples": last_latent}, vae, last_images, )} + else: + return (model, positive, negative, latent_image, vae, empty_image, ) + + samples = common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent_image, denoise=denoise) + latent = samples[0]["samples"] + last_helds["latent"][my_unique_id] = latent + + if vae == (None,): + last_results = None + print('\033[38;2;62;116;77mTSC_Nodes:\033[0m TSC_KSampler({}): no vae input detected, preview disabled'.format(my_unique_id)) + return {"ui": {"images": list()}, "result": (model, positive, negative, {"samples": latent}, vae, empty_image, )} + + images = vae.decode(latent) + last_helds["images"][my_unique_id] = images + + filename_prefix = "TSC_KS_{:02d}".format(my_unique_id) + + def map_filename(filename): + prefix_len = len(os.path.basename(filename_prefix)) + prefix = filename[:prefix_len + 1] + try: + digits = int(filename[prefix_len + 1:].split('_')[0]) + except: + digits = 0 + return (digits, prefix) + + def compute_vars(input): + input = input.replace("%width%", str(images[0].shape[1])) + input = input.replace("%height%", str(images[0].shape[0])) + return input + + filename_prefix = compute_vars(filename_prefix) + + subfolder = os.path.dirname(os.path.normpath(filename_prefix)) + filename = os.path.basename(os.path.normpath(filename_prefix)) + + full_output_folder = os.path.join(self.output_dir, subfolder) + + try: + counter = max(filter(lambda a: a[1][:-1] == filename and a[1][-1] == "_", + map(map_filename, os.listdir(full_output_folder))))[0] + 1 + except ValueError: + counter = 1 + except FileNotFoundError: + os.makedirs(full_output_folder, exist_ok=True) + counter = 1 + + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + results = 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])) + file = f"{filename}_{counter:05}_.png" + img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }); + counter += 1 + last_helds["results"][my_unique_id] = results + + # Output results to ui and node outputs + return {"ui": {"images": results}, "result": (model, positive, negative, {"samples":latent}, vae, images, )} + + +# TSC Image Overlay +class TSC_ImageOverlay: + #upscale_methods = ["nearest-exact", "bilinear", "area"] + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "base_image": ("IMAGE",), + "overlay_image": ("IMAGE",), + "overlay_resize": (["None", "Fit", "Resize by rescale_factor", "Resize to width & heigth"],), + "resize_method": (["nearest-exact", "bilinear", "area"],), + "rescale_factor": ("FLOAT", {"default": 1, "min": 0.01, "max": 16.0, "step": 0.01}), + "width": ("INT", {"default": 512, "min": 0, "max": 7680, "step": 64}), + "height": ("INT", {"default": 512, "min": 0, "max": 4320, "step": 64}), + "x_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), + "y_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), + "rotation": ("INT", {"default": 0, "min": -180, "max": 180, "step": 1}), + "opacity": ("FLOAT", {"default": 0, "min": 0, "max": 100, "step": .5}), + }, + "optional": {"optional_mask": ("MASK",),} + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "overlayimage" + CATEGORY = "Efficiency Nodes/Image" + + def overlayimage(self, base_image, overlay_image, overlay_resize, resize_method, rescale_factor, width, height, x_offset, y_offset, rotation, opacity, optional_mask=None): + result = self.apply_overlay(tensor2pil(base_image), overlay_image, overlay_resize, resize_method, rescale_factor, (int(width), int(height)), + (int(x_offset), int(y_offset)), int(rotation), opacity, optional_mask) + return (pil2tensor(result),) + + def apply_overlay(self, base, overlay, size_option, resize_method, rescale_factor, size, location, rotation, opacity, mask): + + # Check for different sizing options + if size_option != "None": + #Extract overlay size and store in Tuple "overlay_size" (WxH) + overlay_size = overlay.size() + overlay_size = (overlay_size[2], overlay_size[1]) + #print(overlay_size) + if size_option == "Fit": + overlay_size = (base.size[0],base.size[1]) + elif size_option == "Resize by rescale_factor": + overlay_size = tuple(int(dimension * rescale_factor) for dimension in overlay_size) + #overlay_size = (int(overlay.size()[0] * rescale_factor), int(overlay.size()[1] * rescale_factor)) + elif size_option == "Resize to width & heigth": + overlay_size = (size[0], size[1]) + + samples = overlay.movedim(-1, 1) + overlay = comfy.utils.common_upscale(samples, overlay_size[0], overlay_size[1], resize_method, False) + overlay = overlay.movedim(1, -1) + + overlay = tensor2pil(overlay) + + # Add Alpha channel to overlay + overlay.putalpha(Image.new("L", overlay.size, 255)) + + # If mask connected, check if the overlay image has an alpha channel + if mask is not None: + # Convert mask to pil and resize + mask = tensor2pil(mask) + mask = mask.resize(overlay.size) + + #Test + #samples = mask.movedim(-1, 1) + #mask = comfy.utils.common_upscale(samples, overlay_size[0], overlay_size[1], resize_method, False) + #mask = mask.movedim(1, -1) + #mask = tensor2pil(mask) + + # Apply mask as overlay's alpha + overlay.putalpha(ImageOps.invert(mask)) + + # Rotate the overlay image + overlay = overlay.rotate(rotation, expand=False) + + # Apply opacity on overlay image + overlay = overlay.point(lambda x: int(x * (1 - opacity / 100))) + + # Paste the overlay image onto the base image + if mask is None: + base.paste(overlay, location) + else: + base.paste(overlay, location, overlay) + + # Return the edited base image + return base + + +# TSC Evaluate Integers +class TSC_EvaluateInts: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "python_expression": ("STRING", {"default": "((a + b) - c) / 2", "multiline": False}), + "print_to_console": (["False", "True"],),}, + "optional": { + "a": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), + "b": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}), + "c": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 1}),} + } + RETURN_TYPES = ("INT", "FLOAT",) + OUTPUT_NODE = True + FUNCTION = "evaluate" + CATEGORY = "Efficiency Nodes/Math" + + def evaluate(self, python_expression, print_to_console, a=0, b=0, c=0): + int_result = int(eval(python_expression)) + float_result = float(eval(python_expression)) + if print_to_console=="True": + print("\n\033[31mEvaluate Integers Debug:\033[0m") + print(f"\033[90m{{a = {a} , b = {b} , c = {c}}} \033[0m") + print(f"{python_expression} = \033[92m INT: " + str(int_result) + " , FLOAT: " + str(float_result) + "\033[0m") + return (int_result, float_result,) + +# TSC Evaluate Strings +class TSC_EvaluateStrs: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "python_expression": ("STRING", {"default": "a + b + c", "multiline": False}), + "print_to_console": (["False", "True"],)}, + "optional": { + "a": ("STRING", {"default": "Hello", "multiline": False}), + "b": ("STRING", {"default": " World", "multiline": False}), + "c": ("STRING", {"default": "!", "multiline": False}),} + } + RETURN_TYPES = ("STRING",) + OUTPUT_NODE = True + FUNCTION = "evaluate" + CATEGORY = "Efficiency Nodes/Math" + + def evaluate(self, python_expression, print_to_console, a="", b="", c=""): + result = str(eval(python_expression)) + if print_to_console=="True": + print("\n\033[31mEvaluate Strings Debug:\033[0m") + print(f"\033[90ma = {a} \nb = {b} \nc = {c}\033[0m") + print(f"{python_expression} = \033[92m" + result + "\033[0m") + return (result,) + + +# NODE MAPPING +NODE_CLASS_MAPPINGS = { + "KSampler (Efficient)": TSC_KSampler, + "Efficient Loader": TSC_EfficientLoader, + "Image Overlay": TSC_ImageOverlay, + "Evaluate Integers": TSC_EvaluateInts, + "Evaluate Strings": TSC_EvaluateStrs, +} \ No newline at end of file diff --git a/workflows/HiResFix.png b/workflows/HiResFix.png new file mode 100644 index 0000000..e12acc5 Binary files /dev/null and b/workflows/HiResFix.png differ diff --git a/workflows/ImgOverlay.png b/workflows/ImgOverlay.png new file mode 100644 index 0000000..7a5c702 Binary files /dev/null and b/workflows/ImgOverlay.png differ diff --git a/workflows/python_expresssion_examples.txt b/workflows/python_expresssion_examples.txt new file mode 100644 index 0000000..7644c08 --- /dev/null +++ b/workflows/python_expresssion_examples.txt @@ -0,0 +1,44 @@ +Here are a few python expression examples one can use in "Evaluate Integers/Strings" nodes. +note: All variables are optional in the expression. + +______________________________________________ +"EVALUATE INTEGERS" NODE EXPRESSION EXAMPLES: +Add: a + b + c +Subtract: a - b - c +Multiply: a * b * c +Divide: a / b / c +Modulus: a % b % c +Exponentiation: a ** b ** c +Bitwise NOT: ~a, ~b, ~c +Left Shift: a << b << c +Right Shift: a >> b >> c +Greater Than: a > b > c +Less Than: a < b < c +Greater Than or Equal To: a >= b >= c +Less Than or Equal To: a <= b <= c +Equal To: a == b == c +Not Equal To: a != b != c +Absolute Value: abs(a) + abs(b) + abs(c) +Maximum: max(a, b, c) + +______________________________________________ + +"EVALUATE STRINGS" NODE EXPRESSION EXAMPLES: +Concatenate: a + b + c +Repeat: a * len(b) * len(c) +Slice: a[slice(int(b), int(c))] +Find: a.find(b, int(c)) +Count: a.count(b, int(c)) +Replace: a.replace(b, c) +Uppercase: a.upper() + b.upper() + c.upper() +Lowercase: a.lower() + b.lower() + c.lower() +Capitalize: a.capitalize() + b.capitalize() + c.capitalize() +Title case: a.title() + b.title() + c.title() +Swap case: a.swapcase() + b.swapcase() + c.swapcase() +Strip leading/trailing characters: a.strip(b) + c.strip(b) +Split: a.split(b) + c.split(b) +Join: b.join([a, c]) + +______________________________________________ + +If you need help writing python expressions, I suggest using ChatGPT!