fix: 🔥 various preparing for the first tag

This commit is contained in:
melMass
2023-06-27 20:49:37 +02:00
parent afd0843145
commit 793784a5fd
17 changed files with 994 additions and 228 deletions
+44 -69
View File
@@ -1,74 +1,49 @@
from .utils import log
import traceback
from .log import log
from .utils import here
from pathlib import Path
import importlib
NODE_CLASS_MAPPINGS = {}
try:
from .nodes.deep_bump import DeepBump
NODE_CLASS_MAPPINGS["Deep Bump (mtb)"] = DeepBump
except Exception:
log.error("DeepBump nodes failed to load.")
from .nodes.latent_processing import LatentLerp
from .nodes.roop import Roop
# from .nodes.geometries import LoadGeometry, GeometryInfo
try:
from .nodes.fun import QRNode
NODE_CLASS_MAPPINGS["QR Code (mtb)"] = QRNode
except Exception:
log.error("QRNode failed to load.")
NODE_CLASS_MAPPINGS_DEBUG = {}
from .nodes.image_processing import (
ImageCompare,
Denoise,
Blur,
HSVtoRGB,
RGBtoHSV,
ColorCorrect,
MaskToImage,
ColoredImage,
ImagePremultiply
)
try:
from .nodes.image_processing import DeglazeImage
except Exception:
log.error("DeglazeImage failed to load. This is probably an opencv mismatch. This node requires opencv-python-contrib.")
from .nodes.crop import Crop, Uncrop, BoundingBox, BBoxFromMask
from .nodes.conditions import (
SmartStep,
StylesLoader,
TextToImage
)
from .nodes.video import LoadImageSequence, SaveImageSequence
from .nodes.mask import ImageRemoveBackgroundRembg
# from .nodes.videopose import MMPoseEstimation
# NODE MAPPING
NODE_CLASS_MAPPINGS = {
"Latent Lerp (mtb) [DEPRECATED]": LatentLerp,
"Deep Bump (mtb)": DeepBump,
def load_nodes():
errors = []
nodes = []
for filename in (here / "nodes").iterdir():
if filename.suffix == '.py':
module_name = filename.stem
module_path = filename.resolve().as_posix()
try:
module = importlib.import_module(f".nodes.{module_name}",package=__package__)
_nodes = getattr(module, '__nodes__')
nodes.extend(_nodes)
# Use the `nodes` variable here as needed
log.debug(f"Imported __nodes__ from {module_name}")
except Exception:
error_message = traceback.format_exc().splitlines()[-1]
errors.append(f"Failed to import {module_name}. {error_message}")
# log.error(f"Failed to import {module_name}. {error_message}")
if errors:
log.error(f"Some nodes failed to load:\n\t" + "\n\t".join(errors) + "\n\n" + "Check that you properly installed the dependencies.\n" + "If you think this is a bug, please report it on the github page (https://github.com/melMass/comfy_mtb/issues)")
return nodes
nodes = load_nodes()
for node_class in nodes:
class_name = node_class.__name__
class_name = node_class.__name__
"Int to Number (mtb)": IntToNumber,
"Bounding Box (mtb)": BoundingBox,
"Bounding Box From Mask (mtb)": BBoxFromMask,
"Crop (mtb)": Crop,
"Uncrop (mtb)": Uncrop,
"ImageBlur (mtb)": Blur,
"Denoise (mtb)": Denoise,
"ImageCompare (mtb)": ImageCompare,
"RGB to HSV (mtb)": RGBtoHSV,
"HSV to RGB (mtb)": HSVtoRGB,
"Color Correct (mtb)": ColorCorrect,
"Modulo (mtb)": Modulo,
"Deglaze Image (mtb)": DeglazeImage,
"Smart Step (mtb)": SmartStep,
"Styles Loader (mtb)": StylesLoader,
"Text to Image (mtb)": TextToImage,
"Load Image Sequence (mtb)": LoadImageSequence,
"Save Image Sequence (mtb)": SaveImageSequence,
"Mask to Image (mtb)": MaskToImage,
"Image Remove Background RemBG (mtb)": ImageRemoveBackgroundRembg,
"Colored Image (mtb)": ColoredImage,
"Image Premultiply (mtb)": ImagePremultiply,
"Face Swap [roop] (mtb)": Roop,
# "MMPose Estimation (mtb)": MMPoseEstimation,
# "Load Geometry (mtb)": LoadGeometry,
# "Geometry Info (mtb)": GeometryInfo,
}
NODE_CLASS_MAPPINGS[class_name] = node_class
NODE_CLASS_MAPPINGS_DEBUG[class_name] = node_class.__doc__
def get_summary(docstring):
return docstring.strip().split('\n\n', 1)[0]
log.debug(f"Loaded the following nodes:\n\t" + "\n\t".join(f"{k}: {get_summary(doc) if doc else '-'}" for k,doc in NODE_CLASS_MAPPINGS_DEBUG.items()))
+48
View File
@@ -0,0 +1,48 @@
from logging import getLogger
import logging
class Formatter(logging.Formatter):
grey = "\x1b[38;20m"
yellow = "\x1b[33;20m"
red = "\x1b[31;20m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"
# format = "%(asctime)s - [%(name)s] - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)"
format = "[%(name)s] | %(levelname)s -> %(message)s"
FORMATS = {
logging.DEBUG: grey + format + reset,
logging.INFO: grey + format + reset,
logging.WARNING: yellow + format + reset,
logging.ERROR: red + format + reset,
logging.CRITICAL: bold_red + format + reset
}
def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt)
return formatter.format(record)
def mklog(name, level=logging.DEBUG):
logger = logging.getLogger(name)
logger.setLevel(level)
# create console handler with a higher log level
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(Formatter())
logger.addHandler(ch)
return logger
#- The main app logger
log = mklog(__package__)
def log_user(arg):
print("\033[34mComfy MTB Utils:\033[0m {arg}")
+104 -55
View File
@@ -1,6 +1,15 @@
from ..utils import log, pil2tensor
from ..utils import pil2tensor
from ..utils import here
from ..log import log
import folder_paths
from pathlib import Path
import shutil
import csv
class SmartStep:
"""Utils to control the steps start/stop of the KAdvancedSampler in percentage"""
def __init__(self):
pass
@@ -8,54 +17,54 @@ class SmartStep:
def INPUT_TYPES(cls):
return {
"required": {
"step": ("INT",{"default": 20, "min": 1, "max": 10000, "step": 1},),
"step": (
"INT",
{"default": 20, "min": 1, "max": 10000, "step": 1},
),
"start_percent": (
"INT",
{"default": 0, "min": 0, "max": 100, "step": 1},
),
"end_percent": (
"end_percent": (
"INT",
{"default": 0, "min": 0, "max": 100, "step": 1},
),
}
}
RETURN_TYPES = ("INT","INT","INT")
RETURN_NAMES = ("step","start","end")
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("step", "start", "end")
FUNCTION = "do_step"
CATEGORY = "conditioning"
def do_step(self, step,start_percent,end_percent):
def do_step(self, step, start_percent, end_percent):
start = int(step * start_percent / 100)
end = int(step * end_percent / 100)
return (step,start, end)
import folder_paths
from pathlib import Path
import shutil
from ..utils import here
return (step, start, end)
def install_default_styles():
styles_dir = Path(folder_paths.base_path) / "styles"
styles_dir.mkdir(parents=True, exist_ok=True)
default_style = here / "styles.csv"
dest_style = styles_dir / "default.csv"
print("\n\n\n\tINSTALLING DEFAULT STYLE\n\n\n")
log.debug("\n\n\n\tINSTALLING DEFAULT STYLE\n\n\n")
shutil.copy2(default_style.as_posix(), dest_style.as_posix())
print("\n\n\n\tDEFAULT STYLE INSTALLED\n\n\n")
log.debug("\n\n\n\tDEFAULT STYLE INSTALLED\n\n\n")
return dest_style
import csv
# class that load csv files and populate a dropdown from the rows
class StylesLoader:
"""Load csv files and populate a dropdown from the rows (à la A111)"""
options = {}
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
input_dir = Path(folder_paths.base_path) / "styles"
@@ -63,9 +72,13 @@ class StylesLoader:
install_default_styles()
if not (files := [f for f in input_dir.iterdir() if f.suffix == ".csv"]):
log.error("No styles found in the styles folder, place at least one csv file in the styles folder")
return {"required":
{"style_name": (["error"],), }
log.error(
"No styles found in the styles folder, place at least one csv file in the styles folder"
)
return {
"required": {
"style_name": (["error"],),
}
}
for file in files:
with open(file, "r", encoding="utf8") as f:
@@ -73,49 +86,82 @@ class StylesLoader:
for row in parsed:
log.debug(f"Adding style {row[0]}")
cls.options[row[0]] = (row[1], row[2])
return {"required":
{"style_name": (list(cls.options.keys()),), }
return {
"required": {
"style_name": (list(cls.options.keys()),),
}
}
CATEGORY = "conditioning"
RETURN_TYPES = ("STRING","STRING")
RETURN_NAMES = ("positive","negative")
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("positive", "negative")
FUNCTION = "load_style"
def load_style(self, style_name):
return (self.options[style_name][0],self.options[style_name][1])
return (self.options[style_name][0], self.options[style_name][1])
class TextToImage:
fonts= {}
"""Utils to convert text to image using a font
The tool looks for any .ttf file in the Comfy folder hierarchy.
"""
fonts = {}
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
fonts = list(Path(folder_paths.base_path).glob("**/*.ttf"))
if not fonts:
log.error("No fonts found in the fonts folder, place at least one ttf file in the fonts folder")
return {"required":
{"font": (["error"],), }
log.error(
"No fonts found in the fonts folder, place at least one ttf file in the fonts folder"
)
return {
"required": {
"font": (["error"],),
}
}
for font in fonts:
log.debug(f"Adding font {font}")
cls.fonts[font.stem] = font.as_posix()
return {
"required": {
"text": ("STRING", {"default": "Hello world!"},),
"text": (
"STRING",
{"default": "Hello world!"},
),
"font": ((sorted(cls.fonts.keys())),),
"wrap": ("INT", {"default": 120, "min": 0, "max": 8096, "step": 1},),
"font_size": ("INT", {"default": 12, "min": 1, "max": 100, "step": 1},),
"width": ("INT", {"default": 512, "min": 1, "max": 1000, "step": 1},),
"height": ("INT", {"default": 512, "min": 1, "max": 8096, "step": 1},),
"wrap": (
"INT",
{"default": 120, "min": 0, "max": 8096, "step": 1},
),
"font_size": (
"INT",
{"default": 12, "min": 1, "max": 100, "step": 1},
),
"width": (
"INT",
{"default": 512, "min": 1, "max": 1000, "step": 1},
),
"height": (
"INT",
{"default": 512, "min": 1, "max": 8096, "step": 1},
),
# "position": (["INT"], {"default": 0, "min": 0, "max": 100, "step": 1}),
"color": ("STRING",{"default": "black"},), # TODO: add color picker
"background": ("STRING",{"default": "white"},), # TODO: add color picker
"color": (
"COLOR",
{"default": "black"},
),
"background": (
"COLOR",
{"default": "white"},
),
}
}
@@ -124,29 +170,32 @@ class TextToImage:
FUNCTION = "text_to_image"
CATEGORY = "utils"
def text_to_image(self, text, font, wrap,font_size,width,height, color, background):
def text_to_image(
self, text, font, wrap, font_size, width, height, color, background
):
from PIL import Image, ImageDraw, ImageFont
import textwrap
# import uuid
# import os
font = self.fonts[font]
font = ImageFont.truetype(font, font_size)
if wrap == 0:
wrap = width / font_size
lines = textwrap.wrap(text, width= wrap )
lines = textwrap.wrap(text, width=wrap)
log.debug(f"Lines: {lines}")
line_height = font.getsize('hg')[1]
img_height = height # line_height * len(lines)
img_width = width # max(font.getsize(line)[0] for line in lines)
line_height = font.getsize("hg")[1]
img_height = height # line_height * len(lines)
img_width = width # max(font.getsize(line)[0] for line in lines)
img = Image.new('RGBA', (img_width, img_height), background)
img = Image.new("RGBA", (img_width, img_height), background)
draw = ImageDraw.Draw(img)
y_text = 0
for line in lines:
width, height = font.getsize(line)
draw.text((0, y_text), line, color, font=font)
y_text += height
# img.save(os.path.join(folder_paths.base_path, f'{str(uuid.uuid4())}.png'))
return (pil2tensor(img),)
return (pil2tensor(img),)
__nodes__ = [SmartStep, TextToImage, StylesLoader]
+25 -6
View File
@@ -5,6 +5,7 @@ import numpy as np
class BoundingBox:
"""The bounding box (BBOX) custom type used by other nodes"""
def __init__(self):
pass
@@ -34,6 +35,7 @@ class BoundingBox:
class BBoxFromMask:
"""From a mask extract the bounding box"""
def __init__(self):
pass
@@ -85,6 +87,11 @@ class BBoxFromMask:
class Crop:
"""Crops an image and an optional mask to a given bounding box
The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type
The BBOX input takes precedence over the tuple input
"""
def __init__(self):
pass
@@ -93,9 +100,9 @@ class Crop:
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
},
"optional": {
"mask": ("MASK",),
"x": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"y": ("INT", {"default": 0, "max": 10000000, "min": 0, "step": 1}),
"width": (
@@ -116,27 +123,32 @@ class Crop:
CATEGORY = "image/crop"
def do_crop(
self, image: torch.Tensor, mask, x=0, y=0, width=256, height=256, bbox=None
self, image: torch.Tensor, mask=None, x=0, y=0, width=256, height=256, bbox=None
):
image = image.numpy()
mask = mask.numpy()
if mask:
mask = mask.numpy()
if bbox != None:
x, y, width, height = bbox
cropped_image = image[:, y : y + height, x : x + width, :]
cropped_mask = mask[y : y + height, x : x + width]
cropped_mask = mask[y : y + height, x : x + width] if mask != None else None
crop_data = (x, y, width, height)
return (
torch.from_numpy(cropped_image),
torch.from_numpy(cropped_mask),
torch.from_numpy(cropped_mask) if mask != None else None,
crop_data,
)
class Uncrop:
"""Uncrops an image to a given bounding box
The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type
The BBOX input takes precedence over the tuple input"""
def __init__(self):
pass
@@ -190,7 +202,6 @@ class Uncrop:
mask_block = Image.new("L", (bb_width, bb_height), 255)
mask_block = inset_border(mask_block, int(blend_ratio / 2), (0))
print(bbox)
mask.paste(mask_block, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height))
blend.paste(crop_img, (bb_x, bb_y, bb_x + bb_width, bb_y + bb_height))
@@ -201,3 +212,11 @@ class Uncrop:
image = Image.alpha_composite(image.convert("RGBA"), blend)
return (pil2tensor(image.convert("RGB")),)
__nodes__ = [
BBoxFromMask,
BoundingBox,
Crop,
Uncrop
]
+10 -11
View File
@@ -2,8 +2,9 @@ import onnxruntime as ort
import numpy as np
import pathlib
import onnxruntime as ort
import torch
import numpy as np
from .. import utils as utils_inference
from ..log import log
# Disable MS telemetry
ort.disable_telemetry_events()
@@ -17,7 +18,7 @@ def color_to_normals(color_img, overlap, progress_callback):
img = np.mean(color_img[:3], axis=0, keepdimss=True)
# Split image in tiles
print("DeepBump Color → Normals : tilling")
log.debug("DeepBump Color → Normals : tilling")
tile_size = 256
overlaps = {
"SMALL": tile_size // 6,
@@ -30,18 +31,18 @@ def color_to_normals(color_img, overlap, progress_callback):
)
# Load model
print("DeepBump Color → Normals : loading model")
log.debug("DeepBump Color → Normals : loading model")
addon_path = str(pathlib.Path(__file__).parent.absolute())
ort_session = ort.InferenceSession(f"{addon_path}/models/deepbump256.onnx")
# Predict normal map for each tile
print("DeepBump Color → Normals : generating")
log.debug("DeepBump Color → Normals : generating")
pred_tiles = utils_inference.tiles_infer(
tiles, ort_session, progress_callback=progress_callback
)
# Merge tiles
print("DeepBump Color → Normals : merging")
log.debug("DeepBump Color → Normals : merging")
pred_img = utils_inference.tiles_merge(
pred_tiles,
(stride_size, stride_size),
@@ -235,11 +236,6 @@ def normals_to_height(normals_img, seamless, progress_callback):
# - ADDON
import numpy as np
# import imageio.v3 as iio
class DeepBump:
def __init__(self):
pass
@@ -287,7 +283,7 @@ class DeepBump:
in_img = np.transpose(image, (2, 0, 1)) / 255
print(f"Input image shape: {in_img.shape}")
log.debug(f"Input image shape: {in_img.shape}")
# Apply processing
if mode == "Color to Normals":
@@ -304,3 +300,6 @@ class DeepBump:
out_img = (np.transpose(out_img, (1, 2, 0)) * 255).astype(np.uint8)
return (utils_inference.pil2tensor(out_img),)
__nodes__ = [DeepBump]
+4 -1
View File
@@ -57,4 +57,7 @@ class QRNode:
# that we now resize without filtering
code = code.resize((width, height), Image.NEAREST)
return (pil2tensor(code),)
return (pil2tensor(code),)
__nodes__ = [QRNode]
+69
View File
@@ -0,0 +1,69 @@
import torch
import folder_paths
import os
class SaveTensors:
"""Debug node that will probably be removed in the future"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"filename_prefix": ("STRING", {"default": "ComfyPickle"}),
},
"optional": {
"image": ("IMAGE",),
"mask": ("MASK",),
"latent": ("LATENT",),
},
}
FUNCTION = "save"
OUTPUT_NODE = True
RETURN_TYPES = ()
CATEGORY = "utils"
def save(
self,
filename_prefix,
image: torch.Tensor = None,
mask: torch.Tensor = None,
latent: torch.Tensor = None,
):
(
full_output_folder,
filename,
counter,
subfolder,
filename_prefix,
) = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
if image is not None:
image_file = f"{filename}_image_{counter:05}.pt"
torch.save(image, os.path.join(full_output_folder, image_file))
# np.save(os.path.join(full_output_folder, image_file), image.cpu().numpy())
if mask is not None:
mask_file = f"{filename}_mask_{counter:05}.pt"
torch.save(mask, os.path.join(full_output_folder, mask_file))
# np.save(os.path.join(full_output_folder, mask_file), mask.cpu().numpy())
if latent is not None:
# for latent we must use pickle
latent_file = f"{filename}_latent_{counter:05}.pt"
torch.save(latent, os.path.join(full_output_folder, latent_file))
# pickle.dump(latent, open(os.path.join(full_output_folder, latent_file), "wb"))
# np.save(os.path.join(full_output_folder, latent_file), latent[""].cpu().numpy())
return f"{filename_prefix}_{counter:05}"
__nodes__ = [
SaveTensors,
]
+67 -66
View File
@@ -9,11 +9,16 @@ from PIL import Image, ImageChops
from ..utils import tensor2pil, pil2tensor, img_np_to_tensor, img_tensor_to_np
import cv2
import torch
from ..log import log
import folder_paths
from PIL.PngImagePlugin import PngInfo
import json
import os
try:
from cv2.ximgproc import guidedFilter
except ImportError:
print("guidedFilter not found")
log.error("guidedFilter not found, use opencv-contrib-python")
class ColorCorrect:
@@ -160,7 +165,6 @@ class ColorCorrect:
saturation: float = 1.0,
value: float = 1.0,
):
# Apply color correction operations
image = self.gamma_correction_tensor(image, gamma)
image = self.contrast_adjustment_tensor(image, contrast)
@@ -221,12 +225,10 @@ class RGBtoHSV:
def convert(self, image):
image = image.numpy()
# image = image.transpose(1,2,3,0)
image = np.squeeze(image)
image = rgb2hsv(image)
image = np.expand_dims(image, axis=0)
# image = image.transpose(3,0,1,2)
return (torch.from_numpy(image),)
@@ -286,11 +288,9 @@ class Denoise:
def denoise(self, image: torch.Tensor, weight):
image = image.numpy()
# image = image.transpose(1,2,3,0)
image = image.squeeze()
image = denoise_tv_chambolle(image, weight=weight)
# image = image.transpose(3,0,1,2)
image = np.expand_dims(image, axis=0)
return (torch.from_numpy(image),)
@@ -322,17 +322,12 @@ class Blur:
def blur(self, image: torch.Tensor, sigmaX, sigmaY):
image = image.numpy()
image = image.transpose(1, 2, 3, 0)
# image = ndimage.gaussian_filter(image, sigma)
image = gaussian(image, sigma=(sigmaX, sigmaY, 0, 0))
# (image, sigma=sigma, multichannel=True)
image = image.transpose(3, 0, 1, 2)
return (torch.from_numpy(image),)
#https://github.com/lllyasviel/AdverseCleaner/blob/main/clean.py
# https://github.com/lllyasviel/AdverseCleaner/blob/main/clean.py
def deglaze_np_img(np_img):
y = np_img.copy()
for _ in range(64):
@@ -341,33 +336,34 @@ def deglaze_np_img(np_img):
y = guidedFilter(np_img, y, 4, 16)
return y
class DeglazeImage:
@classmethod
def INPUT_TYPES(cls):
return {"required": {"image": ("IMAGE", ) } }
return {"required": {"image": ("IMAGE",)}}
CATEGORY = "image"
RETURN_TYPES = ("IMAGE", )
RETURN_TYPES = ("IMAGE",)
FUNCTION = "deglaze_image"
def deglaze_image(self, image):
return (img_np_to_tensor(deglaze_np_img(img_tensor_to_np(image))),)
class MaskToImage:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"color": ("COLOR",),
"background": ("COLOR", {"default": "#000000"})
}
}
"required": {
"mask": ("MASK",),
"color": ("COLOR",),
"background": ("COLOR", {"default": "#000000"}),
}
}
CATEGORY = "image/mask"
@@ -375,37 +371,38 @@ class MaskToImage:
FUNCTION = "render_mask"
def render_mask(self, mask,color, background):
def render_mask(self, mask, color, background):
mask = img_tensor_to_np(mask)
mask = Image.fromarray(mask).convert("L")
image = Image.new("RGBA", mask.size, color=color)
# apply the mask
image = Image.composite(image, Image.new("RGBA", mask.size, color=background), mask)
image = Image.composite(
image, Image.new("RGBA", mask.size, color=background), mask
)
# image = ImageChops.multiply(image, mask)
# apply over background
# image = Image.alpha_composite(Image.new("RGBA", image.size, color=background), image)
image = pil2tensor(image.convert("RGB"))
print(image.shape)
return (image,)
class ColoredImage:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"color": ("COLOR",),
"width": ("INT",{"default": 512, "min": 16, "max": 8160}),
"height": ("INT",{"default": 512, "min": 16, "max": 8160}),
}
}
"required": {
"color": ("COLOR",),
"width": ("INT", {"default": 512, "min": 16, "max": 8160}),
"height": ("INT", {"default": 512, "min": 16, "max": 8160}),
}
}
CATEGORY = "image"
@@ -413,55 +410,59 @@ class ColoredImage:
FUNCTION = "render_img"
def render_img(self, color,width,height):
image = Image.new("RGB", (width,height), color=color)
def render_img(self, color, width, height):
image = Image.new("RGB", (width, height), color=color)
image = pil2tensor(image)
# print(image.shape)
return (image,)
class ImagePremultiply:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"invert": (["True","False"], {"default": "False"})
}
}
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"invert": (["True", "False"], {"default": "False"}),
}
}
CATEGORY = "image"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "premultiply"
def premultiply(self, image, mask, invert):
invert = invert == "True"
image = tensor2pil(image)
def premultiply(self, image, mask, invert):
invert = invert == "True"
image = tensor2pil(image)
mask = tensor2pil(mask).convert("L")
# apply the mask as premultiplied alpha over a transparent image
if invert:
mask = ImageChops.invert(mask)
image.putalpha(mask)
# if invert:
# image = Image.composite(image,Image.new("RGBA", image.size, color=(0,0,0,0)), mask)
# else:
# image = Image.composite(Image.new("RGBA", image.size, color=(0,0,0,0)), image, mask)
return (pil2tensor(image),)
__nodes__ = [
ColorCorrect,
HSVtoRGB,
RGBtoHSV,
ImageCompare,
Denoise,
Blur,
DeglazeImage,
MaskToImage,
ColoredImage,
ImagePremultiply,
]
+5
View File
@@ -1,6 +1,7 @@
import torch
class LatentLerp:
"""Linear interpolation (blend) between two latent vectors"""
def __init__(self):
pass
@@ -26,3 +27,7 @@ class LatentLerp:
torch.lerp(a["samples"], b["samples"], t, out=a["samples"])
return (a,)
__nodes__ = [
LatentLerp,
]
+6 -2
View File
@@ -7,7 +7,7 @@ class ImageRemoveBackgroundRembg:
pass
@classmethod
def INPUT_TYPES(s):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
@@ -27,7 +27,6 @@ class ImageRemoveBackgroundRembg:
CATEGORY = "image"
# bgcolor: Optional[Tuple[int, int, int, int]]
def remove_background(self, image, alpha_matting, alpha_matting_foreground_threshold, alpha_matting_background_threshold, alpha_matting_erode_size, post_process_mask, bgcolor):
print(f"Background Color: {bgcolor}")
image = remove(
data=tensor2pil(image),
alpha_matting=alpha_matting == "True",
@@ -51,3 +50,8 @@ class ImageRemoveBackgroundRembg:
return (pil2tensor(image), pil2tensor(mask), pil2tensor(image_on_bg))
__nodes__ = [
ImageRemoveBackgroundRembg,
]
+26
View File
@@ -0,0 +1,26 @@
class IntToNumber:
"""Node addon for the WAS Suite. Converts a "comfy" INT to a NUMBER."""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"int": ("INT", {"default": 0, "min": 0, "max": 1e9, "step": 1}),
}
}
RETURN_TYPES = ("NUMBER",)
FUNCTION = "int_to_number"
CATEGORY = "number"
def int_to_number(self, int):
return (int,)
__nodes__ = [
IntToNumber,
]
+13 -1
View File
@@ -9,8 +9,12 @@ import folder_paths
from pathlib import Path
import json
from ..utils import log
from ..log import log
class LoadImageSequence:
"""Load an image sequence from a folder. The current frame is used to determine which image to load.
Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.
"""
@classmethod
def INPUT_TYPES(cls):
return {
@@ -69,6 +73,10 @@ def resolve_path(path, frame):
return re.sub("#+", padded_number, path)
class SaveImageSequence:
"""Save an image sequence to a folder. The current frame is used to determine which image to save.
This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.
"""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
@@ -135,3 +143,7 @@ class SaveImageSequence:
__nodes__ = [
LoadImageSequence,
SaveImageSequence,
]
+148
View File
@@ -0,0 +1,148 @@
import os
import requests
from rich.console import Console
from tqdm import tqdm
try:
import folder_paths
except ModuleNotFoundError:
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "../../.."))
import folder_paths
models_to_download = {
"DeepBump": {
"size": 25.5,
"download_url": "https://github.com/HugoTini/DeepBump/raw/master/deepbump256.onnx",
"destination": "deepbump",
},
"Roop": {
"size": 660,
"download_url": [
"https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_mobilenet0.25_Final.pth",
"https://github.com/xinntao/facexlib/releases/download/v0.1.0/detection_Resnet50_Final.pth",
"https://huggingface.co/deepinsight/inswapper/resolve/main/inswapper_128.onnx",
],
"destination": "roop",
},
}
console = Console()
from urllib.parse import urlparse
from pathlib import Path
def download_model(download_url, destination):
if isinstance(download_url, list):
for url in download_url:
download_model(url, destination)
return
filename = os.path.basename(urlparse(download_url).path)
response = requests.get(download_url, stream=True)
total_size = int(response.headers.get("content-length", 0))
destination_path = os.path.join(destination, filename)
with open(destination_path, "wb") as file:
with tqdm(
total=total_size, unit="B", unit_scale=True, desc=destination_path, ncols=80
) as progress_bar:
for data in response.iter_content(chunk_size=4096):
file.write(data)
progress_bar.update(len(data))
console.print(
f"Downloaded model from {download_url} to {destination_path}",
style="bold green",
)
def ask_user_for_downloads(models_to_download):
console.print("Choose models to download:")
choices = {}
for i, model_name in enumerate(models_to_download.keys(), start=1):
choices[str(i)] = model_name
console.print(f"{i}. {model_name}")
console.print(
"Enter the numbers of the models you want to download (comma-separated):"
)
user_input = console.input(">> ")
selected_models = user_input.split(",")
models_to_download_selected = {}
for choice in selected_models:
choice = choice.strip()
if choice in choices:
model_name = choices[choice]
models_to_download_selected[model_name] = models_to_download[model_name]
elif choice == "":
# download all
models_to_download_selected = models_to_download
else:
console.print(f"Invalid choice: {choice}. Skipping.")
return models_to_download_selected
def handle_interrupt():
console.print("Interrupted by user.", style="bold red")
def main(models_to_download):
try:
models_to_download_selected = {}
def check_destination(urls, destination):
if isinstance(urls, list):
for url in urls:
check_destination(url, destination)
return
filename = os.path.basename(urlparse(urls).path)
destination = os.path.join(folder_paths.models_dir, destination)
if not os.path.exists(destination):
os.makedirs(destination)
destination_path = os.path.join(destination, filename)
if os.path.exists(destination_path):
url_name = os.path.basename(urlparse(urls).path)
console.print(
f"Checkpoint '{url_name}' for {model_name} already exists in '{destination}'"
)
else:
model_details["destination"] = destination
models_to_download_selected[model_name] = model_details
for model_name, model_details in models_to_download.items():
destination = model_details["destination"]
download_url = model_details["download_url"]
check_destination(download_url, destination)
if not models_to_download_selected:
console.print("No new models to download.")
return
models_to_download_selected = ask_user_for_downloads(
models_to_download_selected
)
for model_name, model_details in models_to_download_selected.items():
download_url = model_details["download_url"]
destination = model_details["destination"]
download_model(download_url, destination)
except KeyboardInterrupt:
handle_interrupt()
if __name__ == "__main__":
main(models_to_download)
+18 -7
View File
@@ -8,8 +8,9 @@ from rich.progress import Progress
console = Console(stderr=True)
def get_imported_modules(filename):
with open(filename, 'r') as file:
with open(filename, "r") as file:
tree = ast.parse(file.read())
imported_modules = []
@@ -21,7 +22,9 @@ def get_imported_modules(filename):
)
elif isinstance(node, ast.ImportFrom):
if node.module:
imported_modules.append((node.module, node.module in sys.builtin_module_names))
imported_modules.append(
(node.module, node.module in sys.builtin_module_names)
)
return imported_modules
@@ -36,7 +39,7 @@ def list_imported_modules(folder):
for root, _, files in os.walk(folder):
for file in files:
if file.endswith('.py'):
if file.endswith(".py"):
file_path = os.path.join(root, file)
imported_modules = get_imported_modules(file_path)
modules.extend(imported_modules)
@@ -47,9 +50,11 @@ def list_imported_modules(folder):
return modules
if __name__ == '__main__':
if __name__ == "__main__":
if len(sys.argv) < 2:
console.print("[bold red]Please provide the folder path as a command-line argument.[/bold red]")
console.print(
"[bold red]Please provide the folder path as a command-line argument.[/bold red]"
)
sys.exit(1)
# folder_path = input("Enter the folder path: ")
@@ -78,5 +83,11 @@ if __name__ == '__main__':
console.print(table)
json_data = json.dumps([{"module": module, "type": "Built-in" if is_builtin else "External"} for module, is_builtin in imported_modules], indent=4)
print(json_data)
json_data = json.dumps(
[
{"module": module, "type": "Built-in" if is_builtin else "External"}
for module, is_builtin in imported_modules
],
indent=4,
)
print(json_data)
+22 -9
View File
@@ -3,12 +3,24 @@ import numpy as np
import torch
from pathlib import Path
import sys
from logging import getLogger
import logging
log = getLogger(__package__)
log.setLevel(logging.DEBUG)
def add_path(path, prepend=False):
if isinstance(path, list):
for p in path:
add_path(p, prepend)
return
if isinstance(path, Path):
path = path.resolve().as_posix()
if path not in sys.path:
if prepend:
sys.path.insert(0, path)
else:
sys.path.append(path)
# Get the absolute path of the parent directory of the current script
here = Path(__file__).parent.resolve()
@@ -19,11 +31,12 @@ comfy_dir = here.parent.parent
font_path = here / "font.ttf"
# Add extern folder to path
extern = (here / "extern", here / "extern" / "SadTalker")
sys.path.extend([ x.as_posix() for x in extern])
add_path(here / "extern")
add_path(here / "extern" / "SadTalker")
# Add the ComfyUI directory path to the sys.path list
sys.path.append(comfy_dir.resolve().as_posix())
# Add the ComfyUI directory and custom nodes path to the sys.path list
add_path(comfy_dir)
add_path((comfy_dir / "custom_nodes"))
# Tensor to PIL (grabbed from WAS Suite)
def tensor2pil(image: torch.Tensor) -> Image.Image:
+23 -1
View File
@@ -65,6 +65,17 @@ const custom = (key,val) => {
ctx.fillText(this.name, widgetWidth * 0.5, widgetY + 14);
// ctx.strokeStyle = "#fff";
// ctx.strokeRect(border, widgetY + border, widgetWidth - border * 2, height - border * 2);
// ctx.fillStyle = "#000";
// ctx.fillRect(widgetWidth/2 - border / 2 , widgetY + border / 2 , widgetWidth/2 + border / 2, height + border / 2);
// ctx.fillStyle = this.value;
// ctx.fillRect(widgetWidth/2, widgetY, widgetWidth/2, height);
}
widget.mouse = function (e, pos, node) {
if (e.type === "pointerdown") {
console.log({e,pos,node})
@@ -80,7 +91,18 @@ const custom = (key,val) => {
const picker = document.createElement("input");
picker.type = "color";
picker.value = this.value;
// picker.style.position = "absolute";
// picker.style.left = ( pos[0]) + "px";
// picker.style.top = ( pos[1]) + "px";
// place at screen center
// picker.style.position = "absolute";
// picker.style.left = (window.innerWidth / 2) + "px";
// picker.style.top = (window.innerHeight / 2) + "px";
// picker.style.transform = "translate(-50%, -50%)";
// picker.style.zIndex = 1000;
document.body.appendChild(picker);
+362
View File
@@ -0,0 +1,362 @@
// #region patterns
const float = '-?\\d*(?:\\.\\d+)';
export const number = `(${float}?)`;
export const percentage = `(${float}?%)`;
export const numberOrPercentage = `(${float}?%?)`;
const clamp = (num, min, max) => Math.min(Math.max(min, num), max);
const hexCharacters = 'a-f\\d';
const match3or4Hex = `#?[${hexCharacters}]{3}[${hexCharacters}]?`;
const match6or8Hex = `#?[${hexCharacters}]{6}([${hexCharacters}]{2})?`;
const nonHexChars = new RegExp(`[^#${hexCharacters}]`, 'gi');
const validHexSize = new RegExp(`^${match3or4Hex}$|^${match6or8Hex}$`, 'i');
export const hex_pattern = new RegExp(/^#([a-f0-9]{3,4}|[a-f0-9]{4}(?:[a-f0-9]{2}){1,2})\b$/, "i");
export const hsl3_pattern = new RegExp(`^
hsla?\\(
\\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*,
\\s*${percentage}\\s*,
\\s*${percentage}\\s*
(?:,\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const hsl4_pattern = new RegExp(`^
hsla?\\(
\\s*(-?\\d*(?:\\.\\d+)?(?:deg|rad|turn)?)\\s*
\\s+${percentage}
\\s+${percentage}
\\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const rgb3_pattern = new RegExp(`^
rgba?\\(
\\s*${number}\\s*,
\\s*${number}\\s*,
\\s*${number}\\s*
(?:,\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''))
export const rgb4_pattern = new RegExp(`^
rgba?\\(
\\s*${number}
\\s+${number}
\\s+${number}
\\s*(?:\\s*\\/\\s*${numberOrPercentage}\\s*)?
\\)
$
`.replace(/\n|\s/g, ''));
export const transparent_pattern = new RegExp(/^transparent$/, 'i');
// #endregion
// #region utils
/* 500 => 255, -10 => 0, 128 => 128 */
const parseRGB = (num) => {
let n = num;
if (typeof n !== 'number') {
n = n.endsWith('%') ? (parseFloat(n) * 255) / 100 : parseFloat(n);
}
return clamp(Math.round(n), 0, 255);
};
/* 200 => 100, -100 => 0, 50 => 50 */
const parsePercentage = (percentage) => clamp(parseFloat(percentage), 0, 100);
/* '50%' => 5.0, 200 => 1, -10 => 0 */
function parseAlpha(alpha) {
let a = alpha;
if (typeof a !== 'number') {
a = a.endsWith('%') ? parseFloat(a) / 100 : parseFloat(a);
}
return clamp(a, 0, 1);
}
export function getHEX(hex) {
const [r, g, b, a] = hex2Rgb(hex, { format: 'array' });
return getRGB([null, ...[r, g, b, a]]);
}
export function getHSL([, h, s, l, a = 1]) {
let hh = h;
if (hh.endsWith('turn')) {
hh = (parseFloat(hh) * 360) / 1;
} else if (hh.endsWith('rad')) {
hh = Math.round((parseFloat(hh) * 180) / Math.PI);
} else {
hh = parseFloat(hh);
}
return {
type: 'hsl',
values: [hh, parsePercentage(s), parsePercentage(l)],
alpha: parseAlpha(a === null ? 1 : a)
};
}
export function getRGB([, r, g, b, a = 1]) {
return {
type: 'rgb',
values: [r, g, b].map(parseRGB),
alpha: parseAlpha(a === null ? 1 : a)
};
}
export function hex2Rgb(hex, options = {}) {
if (typeof hex !== 'string' || nonHexChars.test(hex) || !validHexSize.test(hex)) {
throw new TypeError('Expected a valid hex string');
}
hex = hex.replace(/^#/, '');
let alphaFromHex = 1;
if (hex.length === 8) {
alphaFromHex = Number.parseInt(hex.slice(6, 8), 16) / 255;
hex = hex.slice(0, 6);
}
if (hex.length === 4) {
alphaFromHex = Number.parseInt(hex.slice(3, 4).repeat(2), 16) / 255;
hex = hex.slice(0, 3);
}
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const number = Number.parseInt(hex, 16);
const red = number >> 16;
const green = (number >> 8) & 255;
const blue = number & 255;
const alpha = typeof options.alpha === 'number' ? options.alpha : alphaFromHex;
if (options.format === 'array') {
return [red, green, blue, alpha];
}
if (options.format === 'css') {
const alphaString = alpha === 1 ? '' : ` / ${Number((alpha * 100).toFixed(2))}%`;
return `rgb(${red} ${green} ${blue}${alphaString})`;
}
return {red, green, blue, alpha};
}
// #endregion
// #region colorNames
export const colorName = {
aliceblue: [240, 248, 255],
antiquewhite: [250, 235, 215],
aqua: [0, 255, 255],
aquamarine: [127, 255, 212],
azure: [240, 255, 255],
beige: [245, 245, 220],
bisque: [255, 228, 196],
black: [0, 0, 0],
blanchedalmond: [255, 235, 205],
blue: [0, 0, 255],
blueviolet: [138, 43, 226],
brown: [165, 42, 42],
burlywood: [222, 184, 135],
cadetblue: [95, 158, 160],
chartreuse: [127, 255, 0],
chocolate: [210, 105, 30],
coral: [255, 127, 80],
cornflowerblue: [100, 149, 237],
cornsilk: [255, 248, 220],
crimson: [220, 20, 60],
cyan: [0, 255, 255],
darkblue: [0, 0, 139],
darkcyan: [0, 139, 139],
darkgoldenrod: [184, 134, 11],
darkgray: [169, 169, 169],
darkgreen: [0, 100, 0],
darkgrey: [169, 169, 169],
darkkhaki: [189, 183, 107],
darkmagenta: [139, 0, 139],
darkolivegreen: [85, 107, 47],
darkorange: [255, 140, 0],
darkorchid: [153, 50, 204],
darkred: [139, 0, 0],
darksalmon: [233, 150, 122],
darkseagreen: [143, 188, 143],
darkslateblue: [72, 61, 139],
darkslategray: [47, 79, 79],
darkslategrey: [47, 79, 79],
darkturquoise: [0, 206, 209],
darkviolet: [148, 0, 211],
deeppink: [255, 20, 147],
deepskyblue: [0, 191, 255],
dimgray: [105, 105, 105],
dimgrey: [105, 105, 105],
dodgerblue: [30, 144, 255],
firebrick: [178, 34, 34],
floralwhite: [255, 250, 240],
forestgreen: [34, 139, 34],
fuchsia: [255, 0, 255],
gainsboro: [220, 220, 220],
ghostwhite: [248, 248, 255],
gold: [255, 215, 0],
goldenrod: [218, 165, 32],
gray: [128, 128, 128],
green: [0, 128, 0],
greenyellow: [173, 255, 47],
grey: [128, 128, 128],
honeydew: [240, 255, 240],
hotpink: [255, 105, 180],
indianred: [205, 92, 92],
indigo: [75, 0, 130],
ivory: [255, 255, 240],
khaki: [240, 230, 140],
lavender: [230, 230, 250],
lavenderblush: [255, 240, 245],
lawngreen: [124, 252, 0],
lemonchiffon: [255, 250, 205],
lightblue: [173, 216, 230],
lightcoral: [240, 128, 128],
lightcyan: [224, 255, 255],
lightgoldenrodyellow: [250, 250, 210],
lightgray: [211, 211, 211],
lightgreen: [144, 238, 144],
lightgrey: [211, 211, 211],
lightpink: [255, 182, 193],
lightsalmon: [255, 160, 122],
lightseagreen: [32, 178, 170],
lightskyblue: [135, 206, 250],
lightslategray: [119, 136, 153],
lightslategrey: [119, 136, 153],
lightsteelblue: [176, 196, 222],
lightyellow: [255, 255, 224],
lime: [0, 255, 0],
limegreen: [50, 205, 50],
linen: [250, 240, 230],
magenta: [255, 0, 255],
maroon: [128, 0, 0],
mediumaquamarine: [102, 205, 170],
mediumblue: [0, 0, 205],
mediumorchid: [186, 85, 211],
mediumpurple: [147, 112, 219],
mediumseagreen: [60, 179, 113],
mediumslateblue: [123, 104, 238],
mediumspringgreen: [0, 250, 154],
mediumturquoise: [72, 209, 204],
mediumvioletred: [199, 21, 133],
midnightblue: [25, 25, 112],
mintcream: [245, 255, 250],
mistyrose: [255, 228, 225],
moccasin: [255, 228, 181],
navajowhite: [255, 222, 173],
navy: [0, 0, 128],
oldlace: [253, 245, 230],
olive: [128, 128, 0],
olivedrab: [107, 142, 35],
orange: [255, 165, 0],
orangered: [255, 69, 0],
orchid: [218, 112, 214],
palegoldenrod: [238, 232, 170],
palegreen: [152, 251, 152],
paleturquoise: [175, 238, 238],
palevioletred: [219, 112, 147],
papayawhip: [255, 239, 213],
peachpuff: [255, 218, 185],
peru: [205, 133, 63],
pink: [255, 192, 203],
plum: [221, 160, 221],
powderblue: [176, 224, 230],
purple: [128, 0, 128],
rebeccapurple: [102, 51, 153],
red: [255, 0, 0],
rosybrown: [188, 143, 143],
royalblue: [65, 105, 225],
saddlebrown: [139, 69, 19],
salmon: [250, 128, 114],
sandybrown: [244, 164, 96],
seagreen: [46, 139, 87],
seashell: [255, 245, 238],
sienna: [160, 82, 45],
silver: [192, 192, 192],
skyblue: [135, 206, 235],
slateblue: [106, 90, 205],
slategray: [112, 128, 144],
slategrey: [112, 128, 144],
snow: [255, 250, 250],
springgreen: [0, 255, 127],
steelblue: [70, 130, 180],
tan: [210, 180, 140],
teal: [0, 128, 128],
thistle: [216, 191, 216],
tomato: [255, 99, 71],
turquoise: [64, 224, 208],
violet: [238, 130, 238],
wheat: [245, 222, 179],
white: [255, 255, 255],
whitesmoke: [245, 245, 245],
yellow: [255, 255, 0],
yellowgreen: [154, 205, 50]
}
// #endregion
export const parseCSSColor = (str, debug=false) => {
if (typeof str !== 'string') {
console.error(`parseCSSColor: expected a string found ${typeof str}`,str);
return null;
}
const hex = hex_pattern.exec(str);
if (hex) {
if (debug){
console.debug('parseCSSColor: hex', hex);
}
return getHEX(hex[0]);
}
const hsl = hsl4_pattern.exec(str) || hsl3_pattern.exec(str);
if (hsl) {
if (debug){
console.debug('parseCSSColor: hsl', hsl);
}
return getHSL(hsl);
}
const rgb =
rgb4_pattern.exec(str) ||
rgb3_pattern.exec(str)
if (rgb) {
if (debug){
console.debug('parseCSSColor: rgb', rgb);
}
return getRGB(rgb);
}
if (transparent_pattern.exec(str)) {
if (debug){
console.debug('parseCSSColor: transparent');
}
return getRGB([null, 0, 0, 0, 0]);
}
const cn = colorName[str.toLowerCase()];
if (cn) {
if (debug){
console.debug('parseCSSColor: colorName', cn);
}
return getRGB([null, cn[0], cn[1], cn[2], 1]);
}
console.error('parseCSSColor: unknown color', str);
return null;
};
export default parseCSSColor;