Files
cubiq-ComfyUI_essentials/essentials.py
T
2023-09-26 14:42:34 +02:00

468 lines
13 KiB
Python

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("*")
EPSILON = 1e-7
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 }),
}
}
RETURN_TYPES = ("IMAGE", "INT", "INT",)
RETURN_NAMES = ("IMAGE", "width", "height",)
FUNCTION = "execute"
CATEGORY = "essentials"
def execute(self, image, width, height, keep_proportion, interpolation="nearest"):
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": (["top-left", "top-center", "top-right", "right-center", "bottom-right", "bottom-center", "bottom-left", "left-center", "center"],),
"x_offset": ("INT", { "default": 0, "min": -99999, "step": 1, "display": "number" }),
"y_offset": ("INT", { "default": 0, "min": -99999, "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_offset, y_offset):
_, oh, ow, _ = image.shape
width = min(ow, width)
height = min(oh, height)
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
x += x_offset
y += y_offset
x2 = x+width
y2 = y+height
if x2 > ow:
x2 = ow
if x < 0:
x = 0
if y2 > oh:
y2 = oh
if y < 0:
y = 0
image = image[:, y:y2, x:x2, :]
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 )
def min_(tensor_list):
# return the element-wise min of the tensor list.
x = torch.stack(tensor_list)
mn = x.min(axis=0)[0]
return mn
def max_(tensor_list):
# return the element-wise max of the tensor list.
x = torch.stack(tensor_list)
mx = x.max(axis=0)[0]
return mx
# From https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/
class ImageCAS:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"amount": ("FLOAT", {"default": 0.8, "min": 0, "max": 1, "step": 0.05}),
},
}
RETURN_TYPES = ("IMAGE",)
CATEGORY = "essentials"
FUNCTION = "execute"
def execute(self, image, amount):
img = F.pad(p(image), pad=(1, 1, 1, 1)).cpu()
a = img[..., :-2, :-2]
b = img[..., :-2, 1:-1]
c = img[..., :-2, 2:]
d = img[..., 1:-1, :-2]
e = img[..., 1:-1, 1:-1]
f = img[..., 1:-1, 2:]
g = img[..., 2:, :-2]
h = img[..., 2:, 1:-1]
i = img[..., 2:, 2:]
# Computing contrast
cross = (b, d, e, f, h)
mn = min_(cross)
mx = max_(cross)
diag = (a, c, g, i)
mn2 = min_(diag)
mx2 = max_(diag)
mx = mx + mx2
mn = mn + mn2
# Computing local weight
inv_mx = torch.reciprocal(mx + EPSILON)
amp = inv_mx * torch.minimum(mn, (2 - mx))
# scaling
amp = torch.sqrt(amp)
w = - amp * (amount * (1/5 - 1/8) + 1/8)
div = torch.reciprocal(1 + 4*w)
output = ((b + d + f + h)*w + e) * div
output = output.clamp(0, 1)
#output = torch.nan_to_num(output) # what am I doing?!
output = pb(output)
return (output,)
class SimpleMath:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"optional": {
"a": ("INT,FLOAT", { "default": 0.0, "step": 0.1 }),
"b": ("INT,FLOAT", { "default": 0.0, "step": 0.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,
"ImageCASharpening+": ImageCAS,
"MaskBlur+": MaskBlur,
"MaskFlip+": MaskFlip,
"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",
"ImageCASharpening+": "🔧 Image Contrast Adaptive Sharpening",
"MaskBlur+": "🔧 Mask Blur",
"MaskFlip+": "🔧 Mask Flip",
"MaskPreview+": "🔧 Mask Preview",
"SimpleMath+": "🔧 Simple Math",
"ConsoleDebug+": "🔧 Console Debug",
}