From cfb40e63a70a515d5ad5c8c711060b37d54e8a39 Mon Sep 17 00:00:00 2001 From: Level Pixel Dev Date: Wed, 13 Nov 2024 07:53:27 +0600 Subject: [PATCH] Added conversation nodes and image overlay node --- README.md | 20 ++- __init__.py | 8 +- nodes/convert/convert_LP.py | 305 ++++++++++++++++++++++++++++++++++ nodes/image/image_utils_LP.py | 112 ++++++++++++- nodes/io/text_inputs_LP.py | 53 ++++++ pyproject.toml | 2 +- 6 files changed, 491 insertions(+), 9 deletions(-) create mode 100644 nodes/convert/convert_LP.py create mode 100644 nodes/io/text_inputs_LP.py diff --git a/README.md b/README.md index c361950..6c23875 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,12 @@ ![banner_LevelPixel_with_logo](https://github.com/user-attachments/assets/ef79f2c9-04fb-485f-aba5-6cd00cb14d8c) -In this Level Pixel node pack you will find: +The purpose of this package is to collect the most necessary and atomic nodes for working with any tasks, adapted for use in cycles and conditions. The package of nodes is aimed at those users who need all the basic things to create multitasking complex workflows using multimodal neural models and software solutions. -LLM nodes, LLaVa nodes, Image Remove Background based on RemBG, Tag Category Filter nodes, Model Unloader nodes, Autotagger, File Counter, Image Loader From Path, Load Image, Fast Checker Pattern, Simple Float Slider. +*Our dream is to see object-oriented programming in ComfyUI. We will try to get closer to it.* + +**In this Level Pixel node pack you will find:** +LLM nodes, LLaVa and other VLM nodes, Image Remove Background based on RemBG, Tag Category Filter nodes, Model Unloader nodes, Autotagger, File Counter, Image Loader From Path, Load Image, Fast Checker Pattern, Float Slider, Load LoRA Tag, Image Overlay, Conversion nodes. ## Contacts: @@ -61,7 +64,7 @@ The core functionality is taken from [ComfyUI_VLM_nodes](https://github.com/goka A more improved version of rembg nodes for ComfyUI with an extended list of models. To use on GPU, at least CUDA 12.4 (Pytorch cu124) is required, so I recommend upgrading to newer versions of ComfyUI and Pytorch. -If GPU still doesn't work, run: +If GPU still doesn't work, use for your python: ``` pip uninstall rembg @@ -108,6 +111,12 @@ Loads images from a specific folder or path. It is convenient because you can sp This is a new image loading node that can retrieve the name of the files you load into your workflow. +## Image Overlay + +A node that allows you to overlay one image on another with the ability to specify a mask. In this package, Image Overlay has an extended range of specified sizes for the final image, and also has another standard image size. + +The core functionality is taken from [efficiency-nodes-comfyui](https://github.com/jags111/efficiency-nodes-comfyui) and belongs to its authors. + ## Fast Checker Pattern Quickly creates a background image with a checkerboard pattern according to the specified parameters for subsequent testing of images with a transparent background. You need to combine the resulting background image with your image with a transparent background in other ComfyUI nodes (at the moment there is no universal node, but perhaps we will make one in the future). @@ -126,6 +135,9 @@ There are a few more nodes in this package that have some unusual uses: * Preview Image Bridge - only output an image to the screen if there is a connection to the output node. Useful in loops and conditions where the execution of this node is not required due to current conditions (variables). * Show Text Bridge - only output text to the screen if there is a connection to the output node. Useful in loops and conditions where the execution of this node is not required due to current conditions (variables). * Show Text - output text to the screen with mandatory execution. The node is executed in any case, whether the output is connected or not. +* Text - a simple node for entering multi-line text (similar to Prompt from other node packages). +* String - a simple node for entering single-line text (similar to String from other node packages). +* Conversion nodes - a variety of different nodes that allow you to transform different types of variables into other variables. The big difference from other current node packages is that they cover a larger number of variable types. Conversion nodes: StringToFloat, StringToInt, StringToBool, StringToNumber, StringToCombo, IntToString, FloatToString, BoolToString, FloatToInt, IntToFloat, IntToBool, BoolToInt. # Credits @@ -137,6 +149,8 @@ Tag Filter nodes for ComfyUI/[comfyui_tag_fillter](https://github.com/sugarkwork Load LoRA Tag node for ComfyUI/[comfyui_lora_tag_loader](https://github.com/badjeff/comfyui_lora_tag_loader) - Thanks to the author for this great node for LoRAs! +Efficiency-nodes-comfyui/[efficiency-nodes-comfyui](https://github.com/jags111/efficiency-nodes-comfyui) - Thanks for Image Overlay! + RemBG nodes for ComfyUI/[rembg-comfyui-node](https://github.com/Loewen-Hob/rembg-comfyui-node-better) - RemBG nodes for ComfyUI. RemBG software package/[rembg](https://github.com/danielgatis/rembg) - Best software to remove background for any object in the picture. diff --git a/__init__.py b/__init__.py index ddf57b9..42d136f 100644 --- a/__init__.py +++ b/__init__.py @@ -33,19 +33,21 @@ check_requirements_installed(llama_cpp_agent_path) init() node_list = [ + "convert.convert_LP", "image.image_utils_LP", - "io.numbers_utils_LP", "io.folder_workers_LP", "io.image_loaders_LP", "io.image_outputs_LP", - "io.text_outputs_LP", "io.lora_tag_loader_LP", + "io.numbers_utils_LP", + "io.text_inputs_LP", + "io.text_outputs_LP", "llm.llm_LP", "tags.tags_utils_LP", "text.text_utils_LP", "unloaders.model_unloaders_LP", + "vlm.autotagger_LP", "vlm.llava_LP", - "vlm.autotagger_LP" ] NODE_CLASS_MAPPINGS = {} diff --git a/nodes/convert/convert_LP.py b/nodes/convert/convert_LP.py new file mode 100644 index 0000000..424e963 --- /dev/null +++ b/nodes/convert/convert_LP.py @@ -0,0 +1,305 @@ +import sys + +class AnyType(str): + def __ne__(self, __value: object) -> bool: + return False + +any = AnyType("*") + +class StringToFloat: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "string_to_float" + CATEGORY = "LevelPixel/Conversion" + + def string_to_float(self, string): + return (float(string),) + +class StringToInt: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": ("STRING", {"default": ""}), + } + } + + RETURN_TYPES = ("INT",) + FUNCTION = "string_to_int" + CATEGORY = "LevelPixel/Conversion" + + def string_to_int(self, string): + return (int(string),) + +class StringToBool: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": ("STRING", {"multiline": False, "default": ""}), + }, + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("BOOLEAN",) + FUNCTION = "string_to_bool" + CATEGORY = "LevelPixel/Conversion" + + def string_to_bool(self, string): + + if string == "True" or string == "true" or string == "yes" or string == "1": + boolean_out = True + if string == "False" or string == "false" or string == "no" or string == "0": + boolean_out = False + else: + if string.startswith('-') and string[1:].replace('.','',1).isdigit(): + float_out = -float(string[1:]) + if float_out > 0: + boolean_out = True + if float_out <= 0: + boolean_out = False + else: + if string.replace('.','',1).isdigit(): + float_out = float(string) + if float_out > 0: + boolean_out = True + if float_out <= 0: + boolean_out = False + else: + pass + + return (boolean_out,) + +class StringToNumber: + @classmethod + def INPUT_TYPES(s): + return {"required": {"string": ("STRING", {"multiline": False, "default": ""}), + "round_integer": (["round", "round down","round up"],), + }, + } + + RETURN_TYPES = ("INT", "FLOAT",) + RETURN_NAMES = ("INT", "FLOAT",) + FUNCTION = "string_to_number" + CATEGORY = "LevelPixel/Conversion" + + def string_to_number(self, string, round_integer): + if string.startswith('-') and string[1:].replace('.','',1).isdigit(): + float_out = -float(string[1:]) + else: + if string.replace('.','',1).isdigit(): + float_out = float(string) + else: + print(f"[Error] String To Number. Not a number.") + return {} + + if round_integer == "round up": + if string.startswith('-'): + int_out = int(float_out) + else: + int_out = int(float_out) + 1 + elif round_integer == "round down": + if string.startswith('-'): + int_out = int(float_out) - 1 + else: + int_out = int(float_out) + else: + int_out = round(float_out) + + return (int_out, float_out,) + +class StringToCombo: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "string": ("STRING", {"multiline": False, "default": ""}), + }, + } + + RETURN_TYPES = (any,) + RETURN_NAMES = ("any",) + FUNCTION = "string_to_combo" + CATEGORY = "LevelPixel/Conversion" + + def string_to_combo(self, string): + text_list = list() + if string != "": + values = string.split(',') + text_list = values[0] + print(text_list) + + return (text_list,) + +class IntToString: + @classmethod + def INPUT_TYPES(s): + return {"required": {"int": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, }), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("STRING",) + FUNCTION = 'int_to_string' + CATEGORY = "LevelPixel/Conversion" + + def int_to_string(self, int): + return (f'{int}', ) + +class FloatToString: + @classmethod + def INPUT_TYPES(s): + return {"required": {"float": ("FLOAT", {"default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, }), + } + } + + RETURN_TYPES = ('STRING', ) + RETURN_NAMES = ('STRING', ) + FUNCTION = 'float_to_string' + CATEGORY = "LevelPixel/Conversion" + + def float_to_string(self, float): + return (f'{float}', ) + +class BoolToString: + @classmethod + def INPUT_TYPES(s): + return {"required": {"bool": ("BOOLEAN", {"default": False,}), + } + } + + RETURN_TYPES = ('STRING', ) + RETURN_NAMES = ('STRING', ) + FUNCTION = 'bool_to_string' + CATEGORY = "LevelPixel/Conversion" + + def bool_to_string(self, bool): + return (f'{bool}', ) + +class FloatToInt: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"float": ("FLOAT", {"default": 0.0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, }), + "round_integer": (["round", "round down","round up"],), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("INT",) + FUNCTION = "float_to_int" + CATEGORY = "LevelPixel/Conversion" + + def float_to_int(self, float, round_integer): + if round_integer == "round up": + if float < 0.0: + int_out = int(float) + else: + int_out = int(float) + 1 + elif round_integer == "round down": + if float < 0.0: + int_out = int(float) - 1 + else: + int_out = int(float) + else: + int_out = round(float) + return (int_out,) + +class IntToFloat: + @classmethod + def INPUT_TYPES(cls): + return {"required": {"int": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, }), + } + } + + RETURN_TYPES = ("FLOAT",) + RETURN_NAMES = ("FLOAT",) + FUNCTION = "int_to_float" + CATEGORY = "LevelPixel/Conversion" + + def int_to_float(self, int): + return (float(int),) + +class IntToBool: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "int": ("INT", {"default": 0, "min": -0xffffffffffffffff, "max": 0xffffffffffffffff, }), + }, + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ("BOOLEAN",) + FUNCTION = "int_to_bool" + CATEGORY = "LevelPixel/Conversion" + + def int_to_bool(self, int): + if int > 0: + boolean_out = True + if int < 1: + boolean_out = False + else: + pass + + return (boolean_out,) + +class BoolToInt: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "bool": ("BOOLEAN", {"multiline": False, "default": False}), + }, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("INT",) + FUNCTION = "bool_to_int" + CATEGORY = "LevelPixel/Conversion" + + def bool_to_int(self, bool): + if bool == True: + int_out = 1 + if bool == False: + int_out = 0 + else: + pass + + return (int_out,) + +NODE_CLASS_MAPPINGS = { + "StringToInt|LP": StringToInt, + "StringToFloat|LP": StringToFloat, + "StringToBool|LP": StringToBool, + "StringToNumber|LP": StringToNumber, + "StringToCombo|LP": StringToCombo, + "IntToString|LP": IntToString, + "FloatToString|LP": FloatToString, + "BoolToString|LP": BoolToString, + "FloatToInt|LP": FloatToInt, + "IntToFloat|LP": IntToFloat, + "IntToBool|LP": IntToBool, + "BoolToInt|LP": BoolToInt, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "StringToInt|LP": "Convert String To Int [LP]", + "StringToFloat|LP": "Convert String To Float [LP]", + "StringToBool|LP": "Convert String To Bool [LP]", + "StringToNumber|LP": "Convert String To Number [LP]", + "StringToCombo|LP": "Convert String To Combo [LP]", + "IntToString|LP": "Convert Int To String [LP]", + "FloatToString|LP": "Convert Float To String [LP]", + "BoolToString|LP": "Convert Bool To String [LP]", + "FloatToInt|LP": "Convert Float To Int [LP]", + "IntToFloat|LP": "Convert Int To Float [LP]", + "IntToBool|LP": "Convert Int To Bool [LP]", + "BoolToInt|LP": "Convert Bool To Int [LP]", +} \ No newline at end of file diff --git a/nodes/image/image_utils_LP.py b/nodes/image/image_utils_LP.py index 457be65..e84b1a9 100644 --- a/nodes/image/image_utils_LP.py +++ b/nodes/image/image_utils_LP.py @@ -1,9 +1,11 @@ import numpy as np import io import torch -from PIL import Image +from PIL import Image, ImageOps import matplotlib.pyplot as plt from rembg import new_session, remove +import comfy.sd +import comfy.utils color_mapping = { "white": (255, 255, 255), @@ -166,12 +168,118 @@ class ImageRemoveBackground: image = pil2tensor(remove(tensor2pil(image), session = session)) return (image,) + +class ImageOverlay: + + @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.1}), + "width": ("INT", {"default": 1024, "min": 0, "max": 32768, "step": 64}), + "height": ("INT", {"default": 1024, "min": 0, "max": 32768, "step": 64}), + "x_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}), + "y_offset": ("INT", {"default": 0, "min": -48000, "max": 48000, "step": 10}), + "rotation": ("INT", {"default": 0, "min": -180, "max": 180, "step": 5}), + "opacity": ("FLOAT", {"default": 0, "min": 0, "max": 100, "step": 5}), + }, + "optional": {"optional_mask": ("MASK",),} + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "apply_overlay_image" + CATEGORY = "LevelPixel/Image" + + def apply_overlay_image(self, base_image, overlay_image, overlay_resize, resize_method, rescale_factor, + width, height, x_offset, y_offset, rotation, opacity, optional_mask=None): + + # Pack tuples and assign variables + size = width, height + location = x_offset, y_offset + mask = optional_mask + + # Check for different sizing options + if overlay_resize != "None": + #Extract overlay_image size and store in Tuple "overlay_image_size" (WxH) + overlay_image_size = overlay_image.size() + overlay_image_size = (overlay_image_size[2], overlay_image_size[1]) + if overlay_resize == "Fit": + h_ratio = base_image.size()[1] / overlay_image_size[1] + w_ratio = base_image.size()[2] / overlay_image_size[0] + ratio = min(h_ratio, w_ratio) + overlay_image_size = tuple(round(dimension * ratio) for dimension in overlay_image_size) + elif overlay_resize == "Resize by rescale_factor": + overlay_image_size = tuple(int(dimension * rescale_factor) for dimension in overlay_image_size) + elif overlay_resize == "Resize to width & heigth": + overlay_image_size = (size[0], size[1]) + + samples = overlay_image.movedim(-1, 1) + overlay_image = comfy.utils.common_upscale(samples, overlay_image_size[0], overlay_image_size[1], resize_method, False) + overlay_image = overlay_image.movedim(1, -1) + + overlay_image = tensor2pil(overlay_image) + + # Add Alpha channel to overlay + overlay_image = overlay_image.convert('RGBA') + overlay_image.putalpha(Image.new("L", overlay_image.size, 255)) + + # If mask connected, check if the overlay_image image has an alpha channel + if mask is not None: + # Convert mask to pil and resize + mask = tensor2pil(mask) + mask = mask.resize(overlay_image.size) + # Apply mask as overlay's alpha + overlay_image.putalpha(ImageOps.invert(mask)) + + # Rotate the overlay image + overlay_image = overlay_image.rotate(rotation, expand=True) + + # Apply opacity on overlay image + r, g, b, a = overlay_image.split() + a = a.point(lambda x: max(0, int(x * (1 - opacity / 100)))) + overlay_image.putalpha(a) + + # Split the base_image tensor along the first dimension to get a list of tensors + base_image_list = torch.unbind(base_image, dim=0) + + # Convert each tensor to a PIL image, apply the overlay, and then convert it back to a tensor + processed_base_image_list = [] + for tensor in base_image_list: + # Convert tensor to PIL Image + image = tensor2pil(tensor) + + # Paste the overlay image onto the base image + if mask is None: + image.paste(overlay_image, location) + else: + image.paste(overlay_image, location, overlay_image) + + # Convert PIL Image back to tensor + processed_tensor = pil2tensor(image) + + # Append to list + processed_base_image_list.append(processed_tensor) + + # Combine the processed images back into a single tensor + base_image = torch.stack([tensor.squeeze() for tensor in processed_base_image_list]) + + # Return the edited base image + return (base_image,) + + + NODE_CLASS_MAPPINGS = { + "ImageOverlay|LP": ImageOverlay, "FastCheckerPattern|LP": FastCheckerPattern, - "ImageRemoveBackground|LP": ImageRemoveBackground + "ImageRemoveBackground|LP": ImageRemoveBackground, } NODE_DISPLAY_NAME_MAPPINGS = { + "ImageOverlay|LP": "Image Overlay [LP]", "FastCheckerPattern|LP": "Fast Checker Pattern [LP]", "ImageRemoveBackground|LP": "Image Remove Background [LP]", } diff --git a/nodes/io/text_inputs_LP.py b/nodes/io/text_inputs_LP.py new file mode 100644 index 0000000..de6a4f8 --- /dev/null +++ b/nodes/io/text_inputs_LP.py @@ -0,0 +1,53 @@ + + +class Text: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "TEXT": ("STRING", {"default": "", "multiline": True, "placeholder": "Text"}),} + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("TEXT",) + FUNCTION = "text" + + CATEGORY = "LevelPixel/IO" + + @staticmethod + def text(TEXT): + return TEXT, + +class String: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return {"required": { + "STRING": ("STRING", {"default": "", "multiline": False, "placeholder": "String"}),} + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("STRING",) + FUNCTION = "string" + + CATEGORY = "LevelPixel/IO" + + @staticmethod + def string(STRING): + return STRING, + +NODE_CLASS_MAPPINGS = { + "Text|LP": Text, + "String|LP": String, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Text|LP": "Text [LP]", + "String|LP": "String [LP]", +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 054aaa9..a39515e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_level_pixel" description = "Various nodes of the Level Pixel company. Includes convenient advanced nodes for working with images from folders; counting files in a folder; cleaning memory; tag filters. Model Unloader, LLM Unloader (GGUF unloaders), Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Image Remove Background based on RemBG, Autotagger." -version = "1.0.8" +version = "1.0.9" license = { file = "LICENSE" } dependencies = ["torch>=2.0.1", "torchvision>=0.15.2", "numpy", "matplotlib", "scikit-build-core>=0.10.7", "rembg>=2.0.59", "onnxruntime-gpu>=1.18.0", "onnxruntime>=1.20.0"]