Added conversation nodes and image overlay node

This commit is contained in:
Level Pixel Dev
2024-11-13 07:53:27 +06:00
parent c6edbb21b6
commit cfb40e63a7
6 changed files with 491 additions and 9 deletions
+17 -3
View File
@@ -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.
+5 -3
View File
@@ -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 = {}
+305
View File
@@ -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]",
}
+110 -2
View File
@@ -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]",
}
+53
View File
@@ -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]",
}
+1 -1
View File
@@ -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"]