Spaces:
Running
Running
import warnings | |
warnings.filterwarnings('ignore', module="torchvision") | |
import ast | |
import math | |
import random | |
import os | |
import operator as op | |
import numpy as np | |
from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter | |
import io | |
import torch | |
import torch.nn.functional as F | |
import torchvision.transforms.v2 as T | |
from nodes import MAX_RESOLUTION, SaveImage, common_ksampler | |
import folder_paths | |
import comfy.utils | |
import comfy.samplers | |
STOCHASTIC_SAMPLERS = ["euler_ancestral", "dpm_2_ancestral", "dpmpp_2s_ancestral", "dpmpp_sde", "dpmpp_sde_gpu", "dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde", "dpmpp_3m_sde_gpu", "ddpm"] | |
def p(image): | |
return image.permute([0,3,1,2]) | |
def pb(image): | |
return image.permute([0,2,3,1]) | |
# from https://github.com/pythongosssss/ComfyUI-Custom-Scripts | |
class AnyType(str): | |
def __ne__(self, __value: object) -> bool: | |
return False | |
any = AnyType("*") | |
EPSILON = 1e-5 | |
class GetImageSize: | |
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: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), | |
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), | |
"interpolation": (["nearest", "bilinear", "bicubic", "area", "nearest-exact", "lanczos"],), | |
"keep_proportion": ("BOOLEAN", { "default": False }), | |
"condition": (["always", "only if bigger", "only if smaller"],), | |
} | |
} | |
RETURN_TYPES = ("IMAGE", "INT", "INT",) | |
RETURN_NAMES = ("IMAGE", "width", "height",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, width, height, keep_proportion, interpolation="nearest", condition="always"): | |
if keep_proportion is True: | |
_, oh, ow, _ = image.shape | |
if width == 0 and oh < height: | |
width = MAX_RESOLUTION | |
elif width == 0 and oh >= height: | |
width = ow | |
if height == 0 and ow < width: | |
height = MAX_RESOLUTION | |
elif height == 0 and ow >= width: | |
height = ow | |
#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 "always" in condition or ("bigger" in condition and (oh > height or ow > width)) or ("smaller" in condition and (oh < height or ow < width)): | |
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: | |
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: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"width": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), | |
"height": ("INT", { "default": 256, "min": 0, "max": MAX_RESOLUTION, "step": 8, }), | |
"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, }), | |
"y_offset": ("INT", { "default": 0, "min": -99999, "step": 1, }), | |
} | |
} | |
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: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"factor": ("FLOAT", { "default": 1.00, "min": 0.00, "max": 1.00, "step": 0.05, }), | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, factor): | |
grayscale = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2] | |
grayscale = (1.0 - factor) * image + factor * grayscale.unsqueeze(-1).repeat(1, 1, 1, 3) | |
return(grayscale,) | |
class ImagePosterize: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"threshold": ("FLOAT", { "default": 0.50, "min": 0.00, "max": 1.00, "step": 0.05, }), | |
} | |
} | |
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 ImageEnhanceDifference: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image1": ("IMAGE",), | |
"image2": ("IMAGE",), | |
"exponent": ("FLOAT", { "default": 0.75, "min": 0.00, "max": 1.00, "step": 0.05, }), | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image1, image2, exponent): | |
if image1.shape != image2.shape: | |
image2 = p(image2) | |
image2 = comfy.utils.common_upscale(image2, image1.shape[2], image1.shape[1], upscale_method='bicubic', crop='center') | |
image2 = pb(image2) | |
diff_image = image1 - image2 | |
diff_image = torch.pow(diff_image, exponent) | |
diff_image = torch.clamp(diff_image, 0, 1) | |
return(diff_image,) | |
class ImageExpandBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"size": ("INT", { "default": 16, "min": 1, "step": 1, }), | |
"method": (["expand", "repeat all", "repeat first", "repeat last"],) | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, size, method): | |
orig_size = image.shape[0] | |
if orig_size == size: | |
return (image,) | |
if size <= 1: | |
return (image[:size],) | |
if 'expand' in method: | |
out = torch.empty([size] + list(image.shape)[1:], dtype=image.dtype, device=image.device) | |
if size < orig_size: | |
scale = (orig_size - 1) / (size - 1) | |
for i in range(size): | |
out[i] = image[min(round(i * scale), orig_size - 1)] | |
else: | |
scale = orig_size / size | |
for i in range(size): | |
out[i] = image[min(math.floor((i + 0.5) * scale), orig_size - 1)] | |
elif 'all' in method: | |
out = image.repeat([math.ceil(size / image.shape[0])] + [1] * (len(image.shape) - 1))[:size] | |
elif 'first' in method: | |
if size < image.shape[0]: | |
out = image[:size] | |
else: | |
out = torch.cat([image[:1].repeat(size-image.shape[0], 1, 1, 1), image], dim=0) | |
elif 'last' in method: | |
if size < image.shape[0]: | |
out = image[:size] | |
else: | |
out = torch.cat((image, image[-1:].repeat((size-image.shape[0], 1, 1, 1))), dim=0) | |
return (out,) | |
class ExtractKeyframes: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"threshold": ("FLOAT", { "default": 0.85, "min": 0.00, "max": 1.00, "step": 0.01, }), | |
} | |
} | |
RETURN_TYPES = ("IMAGE", "STRING") | |
RETURN_NAMES = ("KEYFRAMES", "indexes") | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, threshold): | |
window_size = 2 | |
variations = torch.sum(torch.abs(image[1:] - image[:-1]), dim=[1, 2, 3]) | |
#variations = torch.sum((image[1:] - image[:-1]) ** 2, dim=[1, 2, 3]) | |
threshold = torch.quantile(variations.float(), threshold).item() | |
keyframes = [] | |
for i in range(image.shape[0] - window_size + 1): | |
window = image[i:i + window_size] | |
variation = torch.sum(torch.abs(window[-1] - window[0])).item() | |
if variation > threshold: | |
keyframes.append(i + window_size - 1) | |
return (image[keyframes], ','.join(map(str, keyframes)),) | |
class MaskFlip: | |
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 += (1,) | |
if "x" in axis: | |
dim += (2,) | |
mask = torch.flip(mask, dims=dim) | |
return(mask,) | |
class MaskBlur: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"mask": ("MASK",), | |
"amount": ("FLOAT", { "default": 6.0, "min": 0, "step": 0.5, }), | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, mask, amount): | |
size = int(6 * amount +1) | |
if size % 2 == 0: | |
size+= 1 | |
blurred = mask.unsqueeze(1) | |
blurred = T.GaussianBlur(size, amount)(blurred) | |
blurred = blurred.squeeze(1) | |
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)) | |
self.compress_level = 4 | |
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) | |
return self.save_images(preview, filename_prefix, prompt, extra_pnginfo) | |
class MaskBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"mask1": ("MASK",), | |
"mask2": ("MASK",), | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, mask1, mask2): | |
if mask1.shape[1:] != mask2.shape[1:]: | |
mask2 = F.interpolate(mask2.unsqueeze(1), size=(mask1.shape[1], mask1.shape[2]), mode="bicubic").squeeze(1) | |
out = torch.cat((mask1, mask2), dim=0) | |
return (out,) | |
class MaskExpandBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"mask": ("MASK",), | |
"size": ("INT", { "default": 16, "min": 1, "step": 1, }), | |
"method": (["expand", "repeat all", "repeat first", "repeat last"],) | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, mask, size, method): | |
orig_size = mask.shape[0] | |
if orig_size == size: | |
return (mask,) | |
if size <= 1: | |
return (mask[:size],) | |
if 'expand' in method: | |
out = torch.empty([size] + list(mask.shape)[1:], dtype=mask.dtype, device=mask.device) | |
if size < orig_size: | |
scale = (orig_size - 1) / (size - 1) | |
for i in range(size): | |
out[i] = mask[min(round(i * scale), orig_size - 1)] | |
else: | |
scale = orig_size / size | |
for i in range(size): | |
out[i] = mask[min(math.floor((i + 0.5) * scale), orig_size - 1)] | |
elif 'all' in method: | |
out = mask.repeat([math.ceil(size / mask.shape[0])] + [1] * (len(mask.shape) - 1))[:size] | |
elif 'first' in method: | |
if size < mask.shape[0]: | |
out = mask[:size] | |
else: | |
out = torch.cat([mask[:1].repeat(size-mask.shape[0], 1, 1), mask], dim=0) | |
elif 'last' in method: | |
if size < mask.shape[0]: | |
out = mask[:size] | |
else: | |
out = torch.cat((mask, mask[-1:].repeat((size-mask.shape[0], 1, 1))), dim=0) | |
return (out,) | |
def cubic_bezier(t, p): | |
p0, p1, p2, p3 = p | |
return (1 - t)**3 * p0 + 3 * (1 - t)**2 * t * p1 + 3 * (1 - t) * t**2 * p2 + t**3 * p3 | |
class MaskFromColor: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE", ), | |
"red": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }), | |
"green": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }), | |
"blue": ("INT", { "default": 255, "min": 0, "max": 255, "step": 1, }), | |
"threshold": ("INT", { "default": 0, "min": 0, "max": 127, "step": 1, }), | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, red, green, blue, threshold): | |
temp = (torch.clamp(image, 0, 1.0) * 255.0).round().to(torch.int) | |
color = torch.tensor([red, green, blue]) | |
lower_bound = (color - threshold).clamp(min=0) | |
upper_bound = (color + threshold).clamp(max=255) | |
lower_bound = lower_bound.view(1, 1, 1, 3) | |
upper_bound = upper_bound.view(1, 1, 1, 3) | |
mask = (temp >= lower_bound) & (temp <= upper_bound) | |
mask = mask.all(dim=-1) | |
mask = mask.float() | |
return (mask, ) | |
class MaskFromBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"mask": ("MASK", ), | |
"start": ("INT", { "default": 0, "min": 0, "step": 1, }), | |
"length": ("INT", { "default": -1, "min": -1, "step": 1, }), | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, mask, start, length): | |
if length<0: | |
length = mask.shape[0] | |
start = min(start, mask.shape[0]-1) | |
length = min(mask.shape[0]-start, length) | |
return (mask[start:start + length], ) | |
class ImageFromBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE", ), | |
"start": ("INT", { "default": 0, "min": 0, "step": 1, }), | |
"length": ("INT", { "default": -1, "min": -1, "step": 1, }), | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, start, length): | |
if length<0: | |
length = image.shape[0] | |
start = min(start, image.shape[0]-1) | |
length = min(image.shape[0]-start, length) | |
return (image[start:start + length], ) | |
class ImageCompositeFromMaskBatch: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image_from": ("IMAGE", ), | |
"image_to": ("IMAGE", ), | |
"mask": ("MASK", ) | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image_from, image_to, mask): | |
frames = mask.shape[0] | |
if image_from.shape[1] != image_to.shape[1] or image_from.shape[2] != image_to.shape[2]: | |
image_to = p(image_to) | |
image_to = comfy.utils.common_upscale(image_to, image_from.shape[2], image_from.shape[1], upscale_method='bicubic', crop='center') | |
image_to = pb(image_to) | |
if frames < image_from.shape[0]: | |
image_from = image_from[:frames] | |
elif frames > image_from.shape[0]: | |
image_from = torch.cat((image_from, image_from[-1].unsqueeze(0).repeat(frames-image_from.shape[0], 1, 1, 1)), dim=0) | |
mask = mask.unsqueeze(3).repeat(1, 1, 1, 3) | |
if image_from.shape[1] != mask.shape[1] or image_from.shape[2] != mask.shape[2]: | |
mask = p(mask) | |
mask = comfy.utils.common_upscale(mask, image_from.shape[2], image_from.shape[1], upscale_method='bicubic', crop='center') | |
mask = pb(mask) | |
out = mask * image_to + (1 - mask) * image_from | |
return (out, ) | |
class TransitionMask: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"width": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }), | |
"height": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }), | |
"frames": ("INT", { "default": 16, "min": 1, "max": 9999, "step": 1, }), | |
"start_frame": ("INT", { "default": 0, "min": 0, "step": 1, }), | |
"end_frame": ("INT", { "default": 9999, "min": 0, "step": 1, }), | |
"transition_type": (["horizontal slide", "vertical slide", "horizontal bar", "vertical bar", "center box", "horizontal door", "vertical door", "circle", "fade"],), | |
"timing_function": (["linear", "in", "out", "in-out"],) | |
} | |
} | |
RETURN_TYPES = ("MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, width, height, frames, start_frame, end_frame, transition_type, timing_function): | |
if timing_function == 'in': | |
tf = [0.0, 0.0, 0.5, 1.0] | |
elif timing_function == 'out': | |
tf = [0.0, 0.5, 1.0, 1.0] | |
elif timing_function == 'in-out': | |
tf = [0, 1, 0, 1] | |
#elif timing_function == 'back': | |
# tf = [0, 1.334, 1.334, 0] | |
else: | |
tf = [0, 0, 1, 1] | |
out = [] | |
end_frame = min(frames, end_frame) | |
transition = end_frame - start_frame | |
if start_frame > 0: | |
out = out + [torch.full((height, width), 0.0, dtype=torch.float32, device="cpu")] * start_frame | |
for i in range(transition): | |
frame = torch.full((height, width), 0.0, dtype=torch.float32, device="cpu") | |
progress = i/(transition-1) | |
if timing_function != 'linear': | |
progress = cubic_bezier(progress, tf) | |
if "horizontal slide" in transition_type: | |
pos = round(width*progress) | |
frame[:, :pos] = 1.0 | |
elif "vertical slide" in transition_type: | |
pos = round(height*progress) | |
frame[:pos, :] = 1.0 | |
elif "box" in transition_type: | |
box_w = round(width*progress) | |
box_h = round(height*progress) | |
x1 = (width - box_w) // 2 | |
y1 = (height - box_h) // 2 | |
x2 = x1 + box_w | |
y2 = y1 + box_h | |
frame[y1:y2, x1:x2] = 1.0 | |
elif "circle" in transition_type: | |
radius = math.ceil(math.sqrt(pow(width,2)+pow(height,2))*progress/2) | |
c_x = width // 2 | |
c_y = height // 2 | |
# is this real life? Am I hallucinating? | |
x = torch.arange(0, width, dtype=torch.float32, device="cpu") | |
y = torch.arange(0, height, dtype=torch.float32, device="cpu") | |
y, x = torch.meshgrid((y, x), indexing="ij") | |
circle = ((x - c_x) ** 2 + (y - c_y) ** 2) <= (radius ** 2) | |
frame[circle] = 1.0 | |
elif "horizontal bar" in transition_type: | |
bar = round(height*progress) | |
y1 = (height - bar) // 2 | |
y2 = y1 + bar | |
frame[y1:y2, :] = 1.0 | |
elif "vertical bar" in transition_type: | |
bar = round(width*progress) | |
x1 = (width - bar) // 2 | |
x2 = x1 + bar | |
frame[:, x1:x2] = 1.0 | |
elif "horizontal door" in transition_type: | |
bar = math.ceil(height*progress/2) | |
if bar > 0: | |
frame[:bar, :] = 1.0 | |
frame[-bar:, :] = 1.0 | |
elif "vertical door" in transition_type: | |
bar = math.ceil(width*progress/2) | |
if bar > 0: | |
frame[:, :bar] = 1.0 | |
frame[:, -bar:] = 1.0 | |
elif "fade" in transition_type: | |
frame[:,:] = progress | |
out.append(frame) | |
if end_frame < frames: | |
out = out + [torch.full((height, width), 1.0, dtype=torch.float32, device="cpu")] * (frames - end_frame) | |
out = torch.stack(out, dim=0) | |
return (out, ) | |
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 torch.clamp(mn, min=0) | |
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 torch.clamp(mx, max=1) | |
# From https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/ | |
class ImageCAS: | |
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) # this seems the only way to ensure there are no NaNs | |
output = pb(output) | |
return (output,) | |
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, | |
} | |
op_functions = { | |
'min': min, | |
'max': max | |
} | |
class SimpleMath: | |
def __init__(self): | |
pass | |
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)) | |
elif isinstance(node, ast.Call): # custom function | |
if node.func.id in op_functions: | |
args =[eval_(arg) for arg in node.args] | |
return op_functions[node.func.id](*args) | |
else: | |
return 0 | |
result = eval_(ast.parse(value, mode='eval').body) | |
if math.isnan(result): | |
result = 0.0 | |
return (round(result), result, ) | |
class ModelCompile(): | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"model": ("MODEL",), | |
"fullgraph": ("BOOLEAN", { "default": False }), | |
"dynamic": ("BOOLEAN", { "default": False }), | |
"mode": (["default", "reduce-overhead", "max-autotune", "max-autotune-no-cudagraphs"],), | |
}, | |
} | |
RETURN_TYPES = ("MODEL", ) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, model, fullgraph, dynamic, mode): | |
work_model = model.clone() | |
torch._dynamo.config.suppress_errors = True | |
work_model.model.diffusion_model = torch.compile(work_model.model.diffusion_model, dynamic=dynamic, fullgraph=fullgraph, mode=mode) | |
return( work_model, ) | |
class ConsoleDebug: | |
def __init__(self): | |
pass | |
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,) | |
class DebugTensorShape: | |
def __init__(self): | |
pass | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"tensor": (any, {}), | |
}, | |
} | |
RETURN_TYPES = () | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
OUTPUT_NODE = True | |
def execute(self, tensor): | |
shapes = [] | |
def tensorShape(tensor): | |
if isinstance(tensor, dict): | |
for k in tensor: | |
tensorShape(tensor[k]) | |
elif isinstance(tensor, list): | |
for i in range(len(tensor)): | |
tensorShape(tensor[i]) | |
elif hasattr(tensor, 'shape'): | |
shapes.append(list(tensor.shape)) | |
tensorShape(tensor) | |
print(f"\033[96mShapes found: {shapes}\033[0m") | |
return (None,) | |
class BatchCount: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"batch": (any, {}), | |
}, | |
} | |
RETURN_TYPES = ("INT",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, batch): | |
count = 0 | |
if hasattr(batch, 'shape'): | |
count = batch.shape[0] | |
elif isinstance(batch, dict) and 'samples' in batch: | |
count = batch['samples'].shape[0] | |
elif isinstance(batch, list) or isinstance(batch, dict): | |
count = len(batch) | |
return (count, ) | |
class ImageSeamCarving: | |
def INPUT_TYPES(cls): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"width": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }), | |
"height": ("INT", { "default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1, }), | |
"energy": (["backward", "forward"],), | |
"order": (["width-first", "height-first"],), | |
}, | |
"optional": { | |
"keep_mask": ("MASK",), | |
"drop_mask": ("MASK",), | |
} | |
} | |
RETURN_TYPES = ("IMAGE",) | |
CATEGORY = "essentials" | |
FUNCTION = "execute" | |
def execute(self, image, width, height, energy, order, keep_mask=None, drop_mask=None): | |
try: | |
from .carve import seam_carving | |
except ImportError as e: | |
raise Exception(e) | |
img = p(image) | |
if keep_mask is not None: | |
#keep_mask = keep_mask.reshape((-1, 1, keep_mask.shape[-2], keep_mask.shape[-1])).movedim(1, -1) | |
keep_mask = p(keep_mask.unsqueeze(-1)) | |
if keep_mask.shape[2] != img.shape[2] or keep_mask.shape[3] != img.shape[3]: | |
keep_mask = F.interpolate(keep_mask, size=(img.shape[2], img.shape[3]), mode="bilinear") | |
if drop_mask is not None: | |
drop_mask = p(drop_mask.unsqueeze(-1)) | |
if drop_mask.shape[2] != img.shape[2] or drop_mask.shape[3] != img.shape[3]: | |
drop_mask = F.interpolate(drop_mask, size=(img.shape[2], img.shape[3]), mode="bilinear") | |
out = [] | |
for i in range(img.shape[0]): | |
resized = seam_carving( | |
T.ToPILImage()(img[i]), | |
size=(width, height), | |
energy_mode=energy, | |
order=order, | |
keep_mask=T.ToPILImage()(keep_mask[i]) if keep_mask is not None else None, | |
drop_mask=T.ToPILImage()(drop_mask[i]) if drop_mask is not None else None, | |
) | |
out.append(T.ToTensor()(resized)) | |
out = torch.stack(out) | |
out = pb(out) | |
return(out, ) | |
class CLIPTextEncodeSDXLSimplified: | |
def INPUT_TYPES(s): | |
return {"required": { | |
"width": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}), | |
"height": ("INT", {"default": 1024.0, "min": 0, "max": MAX_RESOLUTION}), | |
"text": ("STRING", {"multiline": True, "default": ""}), | |
"clip": ("CLIP", ), | |
}} | |
RETURN_TYPES = ("CONDITIONING",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, clip, width, height, text): | |
crop_w = 0 | |
crop_h = 0 | |
width = width*4 | |
height = height*4 | |
target_width = width | |
target_height = height | |
text_g = text_l = text | |
tokens = clip.tokenize(text_g) | |
tokens["l"] = clip.tokenize(text_l)["l"] | |
if len(tokens["l"]) != len(tokens["g"]): | |
empty = clip.tokenize("") | |
while len(tokens["l"]) < len(tokens["g"]): | |
tokens["l"] += empty["l"] | |
while len(tokens["l"]) > len(tokens["g"]): | |
tokens["g"] += empty["g"] | |
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True) | |
return ([[cond, {"pooled_output": pooled, "width": width, "height": height, "crop_w": crop_w, "crop_h": crop_h, "target_width": target_width, "target_height": target_height}]], ) | |
class KSamplerVariationsStochastic: | |
def INPUT_TYPES(s): | |
return {"required":{ | |
"model": ("MODEL",), | |
"latent_image": ("LATENT", ), | |
"noise_seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), | |
"steps": ("INT", {"default": 25, "min": 1, "max": 10000}), | |
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), | |
"sampler": (comfy.samplers.KSampler.SAMPLERS, ), | |
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), | |
"positive": ("CONDITIONING", ), | |
"negative": ("CONDITIONING", ), | |
"variation_seed": ("INT:seed", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), | |
"variation_strength": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step":0.05, "round": 0.01}), | |
#"variation_sampler": (comfy.samplers.KSampler.SAMPLERS, ), | |
"cfg_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step":0.05, "round": 0.01}), | |
}} | |
RETURN_TYPES = ("LATENT", ) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, model, latent_image, noise_seed, steps, cfg, sampler, scheduler, positive, negative, variation_seed, variation_strength, cfg_scale, variation_sampler="dpmpp_2m_sde"): | |
# Stage 1: composition sampler | |
force_full_denoise = False # return with leftover noise = "enable" | |
disable_noise = False # add noise = "enable" | |
end_at_step = max(int(steps * (1-variation_strength)), 1) | |
start_at_step = 0 | |
work_latent = latent_image.copy() | |
batch_size = work_latent["samples"].shape[0] | |
work_latent["samples"] = work_latent["samples"][0].unsqueeze(0) | |
stage1 = common_ksampler(model, noise_seed, steps, cfg, sampler, scheduler, positive, negative, work_latent, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise)[0] | |
print(stage1) | |
if batch_size > 1: | |
stage1["samples"] = stage1["samples"].clone().repeat(batch_size, 1, 1, 1) | |
# Stage 2: variation sampler | |
force_full_denoise = True | |
disable_noise = True | |
cfg = max(cfg * cfg_scale, 1.0) | |
start_at_step = end_at_step | |
end_at_step = steps | |
return common_ksampler(model, variation_seed, steps, cfg, variation_sampler, scheduler, positive, negative, stage1, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise) | |
# From https://github.com/BlenderNeko/ComfyUI_Noise/ | |
def slerp(val, low, high): | |
dims = low.shape | |
low = low.reshape(dims[0], -1) | |
high = high.reshape(dims[0], -1) | |
low_norm = low/torch.norm(low, dim=1, keepdim=True) | |
high_norm = high/torch.norm(high, dim=1, keepdim=True) | |
low_norm[low_norm != low_norm] = 0.0 | |
high_norm[high_norm != high_norm] = 0.0 | |
omega = torch.acos((low_norm*high_norm).sum(1)) | |
so = torch.sin(omega) | |
res = (torch.sin((1.0-val)*omega)/so).unsqueeze(1)*low + (torch.sin(val*omega)/so).unsqueeze(1) * high | |
return res.reshape(dims) | |
class KSamplerVariationsWithNoise: | |
def INPUT_TYPES(s): | |
return {"required": { | |
"model": ("MODEL", ), | |
"latent_image": ("LATENT", ), | |
"main_seed": ("INT:seed", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), | |
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}), | |
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}), | |
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ), | |
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ), | |
"positive": ("CONDITIONING", ), | |
"negative": ("CONDITIONING", ), | |
"variation_strength": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step":0.01, "round": 0.01}), | |
#"start_at_step": ("INT", {"default": 0, "min": 0, "max": 10000}), | |
#"end_at_step": ("INT", {"default": 10000, "min": 0, "max": 10000}), | |
#"return_with_leftover_noise": (["disable", "enable"], ), | |
"variation_seed": ("INT:seed", {"default": random.randint(0, 0xffffffffffffffff), "min": 0, "max": 0xffffffffffffffff}), | |
}} | |
RETURN_TYPES = ("LATENT",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, model, latent_image, main_seed, steps, cfg, sampler_name, scheduler, positive, negative, variation_strength, variation_seed): | |
generator = torch.manual_seed(main_seed) | |
batch_size, _, height, width = latent_image["samples"].shape | |
base_noise = torch.randn((1, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).repeat(batch_size, 1, 1, 1).cpu() | |
generator = torch.manual_seed(variation_seed) | |
variation_noise = torch.randn((batch_size, 4, height, width), dtype=torch.float32, device="cpu", generator=generator).cpu() | |
slerp_noise = slerp(variation_strength, base_noise, variation_noise) | |
device = comfy.model_management.get_torch_device() | |
end_at_step = steps #min(steps, end_at_step) | |
start_at_step = 0 #min(start_at_step, end_at_step) | |
real_model = None | |
comfy.model_management.load_model_gpu(model) | |
real_model = model.model | |
sampler = comfy.samplers.KSampler(real_model, steps=steps, device=device, sampler=sampler_name, scheduler=scheduler, denoise=1.0, model_options=model.model_options) | |
sigmas = sampler.sigmas | |
sigma = sigmas[start_at_step] - sigmas[end_at_step] | |
sigma /= model.model.latent_format.scale_factor | |
sigma = sigma.cpu().numpy() | |
work_latent = latent_image.copy() | |
work_latent["samples"] = latent_image["samples"].clone() + slerp_noise * sigma | |
force_full_denoise = True | |
#if return_with_leftover_noise == "enable": | |
# force_full_denoise = False | |
disable_noise = True | |
return common_ksampler(model, main_seed, steps, cfg, sampler_name, scheduler, positive, negative, work_latent, denoise=1.0, disable_noise=disable_noise, start_step=start_at_step, last_step=end_at_step, force_full_denoise=force_full_denoise) | |
class SDXLEmptyLatentSizePicker: | |
def __init__(self): | |
self.device = comfy.model_management.intermediate_device() | |
def INPUT_TYPES(s): | |
return {"required": { | |
"resolution": (["704x1408 (0.5)","704x1344 (0.52)","768x1344 (0.57)","768x1280 (0.6)","832x1216 (0.68)","832x1152 (0.72)","896x1152 (0.78)","896x1088 (0.82)","960x1088 (0.88)","960x1024 (0.94)","1024x1024 (1.0)","1024x960 (1.07)","1088x960 (1.13)","1088x896 (1.21)","1152x896 (1.29)","1152x832 (1.38)","1216x832 (1.46)","1280x768 (1.67)","1344x768 (1.75)","1344x704 (1.91)","1408x704 (2.0)","1472x704 (2.09)","1536x640 (2.4)","1600x640 (2.5)","1664x576 (2.89)","1728x576 (3.0)",], {"default": "1024x1024 (1.0)"}), | |
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}), | |
}} | |
RETURN_TYPES = ("LATENT","INT","INT",) | |
RETURN_NAMES = ("LATENT","width", "height",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, resolution, batch_size): | |
width, height = resolution.split(" ")[0].split("x") | |
width = int(width) | |
height = int(height) | |
latent = torch.zeros([batch_size, 4, height // 8, width // 8], device=self.device) | |
return ({"samples":latent}, width, height,) | |
LUTS_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "luts") | |
# From https://github.com/yoonsikp/pycubelut/blob/master/pycubelut.py (MIT license) | |
class ImageApplyLUT: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"lut_file": ([f for f in os.listdir(LUTS_DIR) if f.endswith('.cube')], ), | |
"log_colorspace": ("BOOLEAN", { "default": False }), | |
"clip_values": ("BOOLEAN", { "default": False }), | |
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.1 }), | |
}} | |
RETURN_TYPES = ("IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
# TODO: check if we can do without numpy | |
def execute(self, image, lut_file, log_colorspace, clip_values, strength): | |
from colour.io.luts.iridas_cube import read_LUT_IridasCube | |
lut = read_LUT_IridasCube(os.path.join(LUTS_DIR, lut_file)) | |
lut.name = lut_file | |
if clip_values: | |
if lut.domain[0].max() == lut.domain[0].min() and lut.domain[1].max() == lut.domain[1].min(): | |
lut.table = np.clip(lut.table, lut.domain[0, 0], lut.domain[1, 0]) | |
else: | |
if len(lut.table.shape) == 2: # 3x1D | |
for dim in range(3): | |
lut.table[:, dim] = np.clip(lut.table[:, dim], lut.domain[0, dim], lut.domain[1, dim]) | |
else: # 3D | |
for dim in range(3): | |
lut.table[:, :, :, dim] = np.clip(lut.table[:, :, :, dim], lut.domain[0, dim], lut.domain[1, dim]) | |
out = [] | |
for img in image: # TODO: is this more resource efficient? should we use a batch instead? | |
lut_img = img.numpy().copy() | |
is_non_default_domain = not np.array_equal(lut.domain, np.array([[0., 0., 0.], [1., 1., 1.]])) | |
dom_scale = None | |
if is_non_default_domain: | |
dom_scale = lut.domain[1] - lut.domain[0] | |
lut_img = lut_img * dom_scale + lut.domain[0] | |
if log_colorspace: | |
lut_img = lut_img ** (1/2.2) | |
lut_img = lut.apply(lut_img) | |
if log_colorspace: | |
lut_img = lut_img ** (2.2) | |
if is_non_default_domain: | |
lut_img = (lut_img - lut.domain[0]) / dom_scale | |
lut_img = torch.from_numpy(lut_img) | |
if strength < 1.0: | |
lut_img = strength * lut_img + (1 - strength) * img | |
out.append(lut_img) | |
out = torch.stack(out) | |
return (out, ) | |
FONTS_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts") | |
class DrawText: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"text": ("STRING", { "multiline": True, "default": "Hello, World!" }), | |
"font": ([f for f in os.listdir(FONTS_DIR) if f.endswith('.ttf') or f.endswith('.otf')], ), | |
"size": ("INT", { "default": 56, "min": 1, "max": 9999, "step": 1 }), | |
"color": ("STRING", { "multiline": False, "default": "#FFFFFF" }), | |
"background_color": ("STRING", { "multiline": False, "default": "#00000000" }), | |
"shadow_distance": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }), | |
"shadow_blur": ("INT", { "default": 0, "min": 0, "max": 100, "step": 1 }), | |
"shadow_color": ("STRING", { "multiline": False, "default": "#000000" }), | |
"alignment": (["left", "center", "right"],), | |
"width": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1 }), | |
"height": ("INT", { "default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1 }), | |
}, | |
} | |
RETURN_TYPES = ("IMAGE", "MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, text, font, size, color, background_color, shadow_distance, shadow_blur, shadow_color, alignment, width, height): | |
font = ImageFont.truetype(os.path.join(FONTS_DIR, font), size) | |
lines = text.split("\n") | |
# Calculate the width and height of the text | |
text_width = max(font.getbbox(line)[2] for line in lines) | |
line_height = font.getmask(text).getbbox()[3] + font.getmetrics()[1] # add descent to height | |
text_height = line_height * len(lines) | |
width = width if width > 0 else text_width | |
height = height if height > 0 else text_height | |
background_color = ImageColor.getrgb(background_color) | |
image = Image.new('RGBA', (width + shadow_distance, height + shadow_distance), color=background_color) | |
image_shadow = None | |
if shadow_distance > 0: | |
image_shadow = Image.new('RGBA', (width + shadow_distance, height + shadow_distance), color=background_color) | |
for i, line in enumerate(lines): | |
line_width = font.getbbox(line)[2] | |
#text_height =font.getbbox(line)[3] | |
if alignment == "left": | |
x = 0 | |
elif alignment == "center": | |
x = (width - line_width) / 2 | |
elif alignment == "right": | |
x = width - line_width | |
y = i * line_height | |
draw = ImageDraw.Draw(image) | |
draw.text((x, y), line, font=font, fill=color) | |
if image_shadow is not None: | |
draw = ImageDraw.Draw(image_shadow) | |
draw.text((x + shadow_distance, y + shadow_distance), line, font=font, fill=shadow_color) | |
if image_shadow is not None: | |
image_shadow = image_shadow.filter(ImageFilter.GaussianBlur(shadow_blur)) | |
image = Image.alpha_composite(image_shadow, image) | |
image = pb(T.ToTensor()(image).unsqueeze(0)) | |
mask = image[:, :, :, 3] if image.shape[3] == 4 else torch.ones_like(image[:, :, :, 0]) | |
return (image[:, :, :, :3], mask,) | |
class RemBGSession: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"model": (["u2net: general purpose", "u2netp: lightweight general purpose", "u2net_human_seg: human segmentation", "u2net_cloth_seg: cloths Parsing", "silueta: very small u2net", "isnet-general-use: general purpose", "isnet-anime: anime illustrations", "sam: general purpose"],), | |
"providers": (['CPU', 'CUDA', 'ROCM', 'DirectML', 'OpenVINO', 'CoreML', 'Tensorrt', 'Azure'],), | |
}, | |
} | |
RETURN_TYPES = ("REMBG_SESSION",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, model, providers): | |
from rembg import new_session as rembg_new_session | |
model = model.split(":")[0] | |
return (rembg_new_session(model, providers=[providers+"ExecutionProvider"]),) | |
class ImageRemoveBackground: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"rembg_session": ("REMBG_SESSION",), | |
"image": ("IMAGE",), | |
}, | |
} | |
RETURN_TYPES = ("IMAGE", "MASK",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, rembg_session, image): | |
from rembg import remove as rembg | |
image = p(image) | |
output = [] | |
for img in image: | |
img = T.ToPILImage()(img) | |
img = rembg(img, session=rembg_session) | |
output.append(T.ToTensor()(img)) | |
output = torch.stack(output, dim=0) | |
output = pb(output) | |
mask = output[:, :, :, 3] if output.shape[3] == 4 else torch.ones_like(output[:, :, :, 0]) | |
return(output[:, :, :, :3], mask,) | |
class NoiseFromImage: | |
def INPUT_TYPES(s): | |
return { | |
"required": { | |
"image": ("IMAGE",), | |
"noise_size": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01 }), | |
"color_noise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }), | |
"mask_strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01 }), | |
"mask_scale_diff": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }), | |
"noise_strenght": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01 }), | |
"saturation": ("FLOAT", {"default": 2.0, "min": 0.0, "max": 100.0, "step": 0.1 }), | |
"contrast": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1 }), | |
"blur": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1 }), | |
}, | |
"optional": { | |
"noise_mask": ("IMAGE",), | |
} | |
} | |
RETURN_TYPES = ("IMAGE","IMAGE",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, image, noise_size, color_noise, mask_strength, mask_scale_diff, noise_strenght, saturation, contrast, blur, noise_mask=None): | |
torch.manual_seed(0) | |
elastic_alpha = max(image.shape[1], image.shape[2])# * noise_size | |
elastic_sigma = elastic_alpha / 400 * noise_size | |
blur_size = int(6 * blur+1) | |
if blur_size % 2 == 0: | |
blur_size+= 1 | |
if noise_mask is None: | |
noise_mask = image | |
# Ensure noise mask is the same size as the image | |
if noise_mask.shape[1:] != image.shape[1:]: | |
noise_mask = F.interpolate(p(noise_mask), size=(image.shape[1], image.shape[2]), mode='bicubic', align_corners=False) | |
noise_mask = pb(noise_mask) | |
# Ensure we have the same number of masks and images | |
if noise_mask.shape[0] > image.shape[0]: | |
noise_mask = noise_mask[:image.shape[0]] | |
else: | |
noise_mask = torch.cat((noise_mask, noise_mask[-1:].repeat((image.shape[0]-noise_mask.shape[0], 1, 1, 1))), dim=0) | |
# Convert image to grayscale mask | |
noise_mask = noise_mask.mean(dim=3).unsqueeze(-1) | |
# add color noise | |
imgs = p(image.clone()) | |
if color_noise > 0: | |
color_noise = torch.normal(torch.zeros_like(imgs), std=color_noise) | |
#color_noise = torch.rand_like(imgs) * (color_noise * 2) - color_noise | |
color_noise *= (imgs - imgs.min()) / (imgs.max() - imgs.min()) | |
imgs = imgs + color_noise | |
imgs = imgs.clamp(0, 1) | |
# create fine noise | |
fine_noise = [] | |
for n in imgs: | |
avg_color = n.mean(dim=[1,2]) | |
tmp_noise = T.ElasticTransform(alpha=elastic_alpha, sigma=elastic_sigma, fill=avg_color.tolist())(n) | |
#tmp_noise = T.functional.adjust_saturation(tmp_noise, 2.0) | |
tmp_noise = T.GaussianBlur(blur_size, blur)(tmp_noise) | |
tmp_noise = T.ColorJitter(contrast=(contrast,contrast), saturation=(saturation,saturation))(tmp_noise) | |
fine_noise.append(tmp_noise) | |
#tmp_noise = F.interpolate(tmp_noise, scale_factor=.1, mode='bilinear', align_corners=False) | |
#tmp_noise = F.interpolate(tmp_noise, size=(tmp_noise.shape[1], tmp_noise.shape[2]), mode='bilinear', align_corners=False) | |
#tmp_noise = T.ElasticTransform(alpha=elastic_alpha, sigma=elastic_sigma/3, fill=avg_color.tolist())(n) | |
#tmp_noise = T.GaussianBlur(blur_size, blur)(tmp_noise) | |
#tmp_noise = T.functional.adjust_saturation(tmp_noise, saturation) | |
#tmp_noise = T.ColorJitter(contrast=(contrast,contrast), saturation=(saturation,saturation))(tmp_noise) | |
#fine_noise.append(tmp_noise) | |
imgs = None | |
del imgs | |
fine_noise = torch.stack(fine_noise, dim=0) | |
fine_noise = pb(fine_noise) | |
#fine_noise = torch.stack(fine_noise, dim=0) | |
#fine_noise = pb(fine_noise) | |
mask_scale_diff = min(mask_scale_diff, 0.99) | |
if mask_scale_diff > 0: | |
coarse_noise = F.interpolate(p(fine_noise), scale_factor=1-mask_scale_diff, mode='area') | |
coarse_noise = F.interpolate(coarse_noise, size=(fine_noise.shape[1], fine_noise.shape[2]), mode='bilinear', align_corners=False) | |
coarse_noise = pb(coarse_noise) | |
else: | |
coarse_noise = fine_noise | |
#noise_mask = noise_mask * mask_strength + (1 - mask_strength) | |
# merge fine and coarse noise | |
output = (1 - noise_mask) * coarse_noise + noise_mask * fine_noise | |
#noise_mask = noise_mask * mask_strength | |
if mask_strength < 1: | |
noise_mask = noise_mask.pow(mask_strength) | |
noise_mask = torch.nan_to_num(noise_mask).clamp(0, 1) | |
output = noise_mask * output + (1 - noise_mask) * image | |
# apply noise to image | |
output = output * noise_strenght + image * (1 - noise_strenght) | |
output = output.clamp(0, 1) | |
return (output,noise_mask.repeat(1,1,1,3),) | |
class RemoveLatentMask: | |
def INPUT_TYPES(s): | |
return {"required": { "samples": ("LATENT",),}} | |
RETURN_TYPES = ("LATENT",) | |
FUNCTION = "execute" | |
CATEGORY = "essentials" | |
def execute(self, samples): | |
s = samples.copy() | |
if "noise_mask" in s: | |
del s["noise_mask"] | |
return (s,) | |
NODE_CLASS_MAPPINGS = { | |
"GetImageSize+": GetImageSize, | |
"ImageResize+": ImageResize, | |
"ImageCrop+": ImageCrop, | |
"ImageFlip+": ImageFlip, | |
"ImageDesaturate+": ImageDesaturate, | |
"ImagePosterize+": ImagePosterize, | |
"ImageCASharpening+": ImageCAS, | |
"ImageSeamCarving+": ImageSeamCarving, | |
"ImageEnhanceDifference+": ImageEnhanceDifference, | |
"ImageExpandBatch+": ImageExpandBatch, | |
"ImageFromBatch+": ImageFromBatch, | |
"ImageCompositeFromMaskBatch+": ImageCompositeFromMaskBatch, | |
"ExtractKeyframes+": ExtractKeyframes, | |
"ImageApplyLUT+": ImageApplyLUT, | |
"MaskBlur+": MaskBlur, | |
"MaskFlip+": MaskFlip, | |
"MaskPreview+": MaskPreview, | |
"MaskBatch+": MaskBatch, | |
"MaskExpandBatch+": MaskExpandBatch, | |
"TransitionMask+": TransitionMask, | |
"MaskFromColor+": MaskFromColor, | |
"MaskFromBatch+": MaskFromBatch, | |
"SimpleMath+": SimpleMath, | |
"ConsoleDebug+": ConsoleDebug, | |
"DebugTensorShape+": DebugTensorShape, | |
"ModelCompile+": ModelCompile, | |
"BatchCount+": BatchCount, | |
"KSamplerVariationsStochastic+": KSamplerVariationsStochastic, | |
"KSamplerVariationsWithNoise+": KSamplerVariationsWithNoise, | |
"CLIPTextEncodeSDXL+": CLIPTextEncodeSDXLSimplified, | |
"SDXLEmptyLatentSizePicker+": SDXLEmptyLatentSizePicker, | |
"DrawText+": DrawText, | |
"RemBGSession+": RemBGSession, | |
"ImageRemoveBackground+": ImageRemoveBackground, | |
"RemoveLatentMask+": RemoveLatentMask, | |
#"NoiseFromImage~": NoiseFromImage, | |
} | |
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", | |
"ImageSeamCarving+": "π§ Image Seam Carving", | |
"ImageEnhanceDifference+": "π§ Image Enhance Difference", | |
"ImageExpandBatch+": "π§ Image Expand Batch", | |
"ImageFromBatch+": "π§ Image From Batch", | |
"ImageCompositeFromMaskBatch+": "π§ Image Composite From Mask Batch", | |
"ExtractKeyframes+": "π§ Extract Keyframes (experimental)", | |
"ImageApplyLUT+": "π§ Image Apply LUT", | |
"MaskBlur+": "π§ Mask Blur", | |
"MaskFlip+": "π§ Mask Flip", | |
"MaskPreview+": "π§ Mask Preview", | |
"MaskBatch+": "π§ Mask Batch", | |
"MaskExpandBatch+": "π§ Mask Expand Batch", | |
"TransitionMask+": "π§ Transition Mask", | |
"MaskFromColor+": "π§ Mask From Color", | |
"MaskFromBatch+": "π§ Mask From Batch", | |
"SimpleMath+": "π§ Simple Math", | |
"ConsoleDebug+": "π§ Console Debug", | |
"DebugTensorShape+": "π§ Tensor Shape Debug", | |
"ModelCompile+": "π§ Compile Model", | |
"BatchCount+": "π§ Batch Count", | |
"KSamplerVariationsStochastic+": "π§ KSampler Stochastic Variations", | |
"KSamplerVariationsWithNoise+": "π§ KSampler Variations with Noise Injection", | |
"CLIPTextEncodeSDXL+": "π§ SDXLCLIPTextEncode", | |
"SDXLEmptyLatentSizePicker+": "π§ SDXL Empty Latent Size Picker", | |
"DrawText+": "π§ Draw Text", | |
"RemBGSession+": "π§ RemBG Session", | |
"ImageRemoveBackground+": "π§ Image Remove Background", | |
"RemoveLatentMask+": "π§ Remove Latent Mask", | |
#"NoiseFromImage~": "π§ Noise From Image", | |
} | |