start!
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/__pycache__/
|
||||
@@ -1 +1,23 @@
|
||||
# ComfyUI_essentials
|
||||
# :wrench: ComfyUI Essentials
|
||||
|
||||
Essential nodes that are weirdly missing from ComfyUI core. With few exceptions they are new features and not commodities. I hope this will be just a temporary repository until the nodes get included into ComfyUI.
|
||||
|
||||
## Node list
|
||||
|
||||
- Get Image Size
|
||||
- Image Resize (adds "keep proportions" to scale image)
|
||||
- Image Crop (includes auto crop to all sides and center)
|
||||
- Image Flip
|
||||
- Image Desaturate
|
||||
- Image Posterize
|
||||
- Mask Blur
|
||||
- Mask Flip
|
||||
- Mask Grow / Shrink (same as Mask grow but adds shrink)
|
||||
- Mask Preview
|
||||
- Simple Math
|
||||
- Console Debug (outputs any input to console)
|
||||
|
||||
- TODO: Mask Save
|
||||
- TODO: documentation
|
||||
|
||||
Let me know if anything's missing
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .essentials import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', module="torchvision")
|
||||
import ast
|
||||
import math
|
||||
import random
|
||||
import operator as op
|
||||
import numpy as np
|
||||
from scipy.ndimage import grey_dilation, grey_erosion
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
import torchvision.transforms.v2 as T
|
||||
|
||||
from nodes import MAX_RESOLUTION, SaveImage
|
||||
import folder_paths
|
||||
import comfy.utils
|
||||
|
||||
def p(image):
|
||||
return image.permute([0,3,1,2])
|
||||
def pb(image):
|
||||
return image.permute([0,2,3,1])
|
||||
|
||||
operators = {
|
||||
ast.Add: op.add,
|
||||
ast.Sub: op.sub,
|
||||
ast.Mult: op.mul,
|
||||
ast.Div: op.truediv,
|
||||
ast.FloorDiv: op.floordiv,
|
||||
ast.Pow: op.pow,
|
||||
ast.BitXor: op.xor,
|
||||
ast.USub: op.neg,
|
||||
ast.Mod: op.mod,
|
||||
}
|
||||
|
||||
# from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
|
||||
class AnyType(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
any = AnyType("*")
|
||||
|
||||
class GetImageSize:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "INT")
|
||||
RETURN_NAMES = ("width", "height")
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image):
|
||||
return (image.shape[2], image.shape[1],)
|
||||
|
||||
class ImageResize:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
||||
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
||||
"interpolation": (["nearest", "bilinear", "bicubic", "area", "nearest-exact", "lanczos"],),
|
||||
"keep_proportion": ("BOOLEAN", { "default": False }),
|
||||
"antialias": ("BOOLEAN", { "default": True }),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "INT",)
|
||||
RETURN_NAMES = ("IMAGE", "width", "height",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image, width, height, keep_proportion, interpolation="bicubic", antialias=True):
|
||||
if keep_proportion is True:
|
||||
_, oh, ow, _ = image.shape
|
||||
width = ow if width == 0 else width
|
||||
height = oh if height == 0 else height
|
||||
ratio = min(width / ow, height / oh)
|
||||
width = round(ow*ratio)
|
||||
height = round(oh*ratio)
|
||||
|
||||
outputs = p(image)
|
||||
if interpolation == "lanczos":
|
||||
outputs = comfy.utils.lanczos(outputs, width, height)
|
||||
else:
|
||||
outputs = F.interpolate(outputs, size=(height, width), mode=interpolation)
|
||||
outputs = pb(outputs)
|
||||
|
||||
return(outputs, outputs.shape[2], outputs.shape[1],)
|
||||
|
||||
class ImageFlip:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"axis": (["x", "y", "xy"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image, axis):
|
||||
dim = ()
|
||||
if "y" in axis:
|
||||
dim += (1,)
|
||||
if "x" in axis:
|
||||
dim += (2,)
|
||||
image = torch.flip(image, dim)
|
||||
|
||||
return(image,)
|
||||
|
||||
class ImageCrop:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"width": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
||||
"height": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
|
||||
"position": (["free", "center", "top-left", "top-center", "top-right", "right-center", "bottom-right", "bottom-center", "bottom-left", "left-center"],),
|
||||
"x": ("INT", { "default": 0, "min": 0, "step": 1, "display": "number" }),
|
||||
"y": ("INT", { "default": 0, "min": 0, "step": 1, "display": "number" }),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE","INT","INT",)
|
||||
RETURN_NAMES = ("IMAGE","x","y",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image, width, height, position, x, y):
|
||||
_, oh, ow, _ = image.shape
|
||||
|
||||
width = min(ow, width)
|
||||
height = min(oh, height)
|
||||
|
||||
if x+width > ow:
|
||||
width = ow-x
|
||||
if y+height > oh:
|
||||
height = oh-y
|
||||
|
||||
if "center" in position:
|
||||
x = round((ow-width) / 2)
|
||||
y = round((oh-height) / 2)
|
||||
if "top" in position:
|
||||
y = 0
|
||||
if "bottom" in position:
|
||||
y = oh-height
|
||||
if "left" in position:
|
||||
x = 0
|
||||
if "right" in position:
|
||||
x = ow-width
|
||||
|
||||
image = image[:, y:y+height, x:x+width, :]
|
||||
|
||||
return(image, x, y, )
|
||||
|
||||
class ImageDesaturate:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image):
|
||||
#image = p(image)
|
||||
#image = T.Grayscale(3)(image)
|
||||
#image = pb(image)
|
||||
#image = image.mean(dim=3, keepdim=True)
|
||||
#image = image.repeat(1, 1, 1, 3)
|
||||
image = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2]
|
||||
image = image.unsqueeze(-1).repeat(1, 1, 1, 3)
|
||||
return(image,)
|
||||
|
||||
class ImagePosterize:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",),
|
||||
"threshold": ("FLOAT", { "default": 0.50, "min": 0.00, "max": 1.00, "step": 0.05, "display": "number" }),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, image, threshold):
|
||||
image = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2]
|
||||
#image = image.mean(dim=3, keepdim=True)
|
||||
image = (image > threshold).float()
|
||||
image = image.unsqueeze(-1).repeat(1, 1, 1, 3)
|
||||
|
||||
return(image,)
|
||||
|
||||
class MaskFlip:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"axis": (["x", "y", "xy"],),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, mask, axis):
|
||||
dim = ()
|
||||
if "y" in axis:
|
||||
dim += (0,)
|
||||
if "x" in axis:
|
||||
dim += (1,)
|
||||
mask = torch.flip(mask, dim)
|
||||
|
||||
return(mask,)
|
||||
|
||||
class MaskBlur:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"size": ("INT", { "default": 5, "min": 1, "step": 1, "display": "number" }),
|
||||
"sigma": ("FLOAT", { "default": 1.0, "min": 0, "step": 0.5, "display": "number" }),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, mask, size, sigma):
|
||||
if size % 2 == 0:
|
||||
size+=1
|
||||
|
||||
blurred = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||||
blurred = p(blurred)
|
||||
blurred = T.GaussianBlur(size, sigma)(blurred)
|
||||
blurred = pb(blurred)
|
||||
blurred = blurred[0, :, :, 0]
|
||||
|
||||
return(blurred,)
|
||||
|
||||
class MaskPreview(SaveImage):
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
self.type = "temp"
|
||||
self.prefix_append = "_temp_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for x in range(5))
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"mask": ("MASK",), },
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, mask, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
|
||||
preview = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||||
results = self.save_images(preview, filename_prefix, prompt, extra_pnginfo)
|
||||
|
||||
return( results )
|
||||
|
||||
class GrowShrinkMask:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",),
|
||||
"amount": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 1}),
|
||||
"tapered_corners": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
CATEGORY = "essentials"
|
||||
FUNCTION = "execute"
|
||||
|
||||
def execute(self, mask, amount, tapered_corners):
|
||||
c = 0 if tapered_corners else 1
|
||||
kernel = np.array([[c, 1, c],
|
||||
[1, 1, 1],
|
||||
[c, 1, c]])
|
||||
output = mask.numpy().copy()
|
||||
if amount < 0:
|
||||
amount = -amount
|
||||
grey_action = grey_erosion
|
||||
else:
|
||||
grey_action = grey_dilation
|
||||
|
||||
while amount > 0:
|
||||
output = grey_action(output, footprint=kernel)
|
||||
amount -= 1
|
||||
output = torch.from_numpy(output)
|
||||
|
||||
return (output,)
|
||||
|
||||
class SimpleMath:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"optional": {
|
||||
"a": ("FLOAT", { "default": 0.0, "step": 1 }),
|
||||
"b": ("FLOAT", { "default": 0.0, "step": 1 }),
|
||||
},
|
||||
"required": {
|
||||
"value": ("STRING", { "multiline": False, "default": "" }),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "FLOAT", )
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
|
||||
def execute(self, value, a = 0.0, b = 0.0):
|
||||
def eval_(node):
|
||||
if isinstance(node, ast.Num): # number
|
||||
return node.n
|
||||
elif isinstance(node, ast.Name): # variable
|
||||
if node.id == "a":
|
||||
return a
|
||||
if node.id == "b":
|
||||
return b
|
||||
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
|
||||
return operators[type(node.op)](eval_(node.left), eval_(node.right))
|
||||
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
|
||||
return operators[type(node.op)](eval_(node.operand))
|
||||
else:
|
||||
return 0
|
||||
|
||||
result = eval_(ast.parse(value, mode='eval').body)
|
||||
|
||||
if math.isnan(result):
|
||||
result = 0.0
|
||||
|
||||
return (round(result), result, )
|
||||
|
||||
class ConsoleDebug:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"value": (any, {}),
|
||||
},
|
||||
"optional": {
|
||||
"prefix": ("STRING", { "multiline": False, "default": "Value:" })
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "essentials"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def execute(self, value, prefix):
|
||||
print(f"\033[96m{prefix} {value}\033[0m")
|
||||
|
||||
return (None,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"GetImageSize+": GetImageSize,
|
||||
|
||||
"ImageResize+": ImageResize,
|
||||
"ImageCrop+": ImageCrop,
|
||||
"ImageFlip+": ImageFlip,
|
||||
|
||||
"ImageDesaturate+": ImageDesaturate,
|
||||
"ImagePosterize+": ImagePosterize,
|
||||
|
||||
"MaskBlur+": MaskBlur,
|
||||
"MaskFlip+": MaskFlip,
|
||||
"GrowShrinkMask+": GrowShrinkMask,
|
||||
"MaskPreview+": MaskPreview,
|
||||
|
||||
"SimpleMath+": SimpleMath,
|
||||
"ConsoleDebug+": ConsoleDebug,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"GetImageSize+": "🔧 Get Image Size",
|
||||
|
||||
"ImageResize+": "🔧 Image Resize",
|
||||
"ImageCrop+": "🔧 Image Crop",
|
||||
"ImageFlip+": "🔧 Image Flip",
|
||||
|
||||
"ImageDesaturate+": "🔧 Image Desaturate",
|
||||
"ImagePosterize+": "🔧 Image Posterize",
|
||||
|
||||
"MaskBlur+": "🔧 Mask Blur",
|
||||
"MaskFlip+": "🔧 Mask Flip",
|
||||
"GrowShrinkMask+": "🔧 Mask Grow/Shrink",
|
||||
"MaskPreview+": "🔧 Mask Preview",
|
||||
|
||||
"SimpleMath+": "🔧 Simple Math",
|
||||
"ConsoleDebug+": "🔧 Console Debug",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user