fix: 🔥 various preparing for the first tag
This commit is contained in:
+44
-69
@@ -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()))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
@@ -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
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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
@@ -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,
|
||||
]
|
||||
@@ -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
@@ -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,
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
Vendored
+362
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user