6 Commits
Author SHA1 Message Date
Jacob Segal 432cb4d146 Recommend alternative node packs in the README 2024-06-18 21:16:26 -07:00
Jacob Segal 69a944969c Add docstrings for some nodes 2024-02-25 20:21:52 -08:00
Jacob Segal 240209bc25 Add missing line. Oops. 2023-07-08 22:26:45 -07:00
Jacob Segal 5c7bfe05a1 Add an Incrementer node 2023-07-08 19:33:10 -07:00
Jacob Segal 758b0069e9 Add several new nodes 2023-07-08 18:05:42 -07:00
Jacob Segal ef6e78ba01 Support pasting to multiple images with one mask
Also adds a node for creating batches.
2023-05-07 20:30:06 -07:00
3 changed files with 383 additions and 8 deletions
+340 -6
View File
@@ -1,4 +1,3 @@
from PIL import Image, ImageFilter, ImageOps
import os
import torch
import numpy as np
@@ -22,8 +21,10 @@ def update_package_list():
global package_list
package_list = [r.decode().split('==')[0] for r in subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']).split()]
def ensure_package(package_name, import_path):
def ensure_package(package_name, import_path=None):
global package_list
if import_path == None:
import_path = package_name
if package_list == None:
update_package_list()
@@ -110,6 +111,9 @@ def tensors2common(t1: torch.Tensor, t2: torch.Tensor) -> (torch.Tensor, torch.T
return t1, t2
class ClipSegNode:
"""
Automatically calculates a mask based on the text prompt
"""
def __init__(self):
pass
@@ -126,6 +130,7 @@ class ClipSegNode:
}
RETURN_TYPES = ("IMAGE","IMAGE",)
RETURN_NAMES = ("thresholded_mask", "raw_mask",)
FUNCTION = "get_mask"
CATEGORY = "Masquerade Nodes"
@@ -311,7 +316,7 @@ class UnaryMaskOp:
return {
"required": {
"image": ("IMAGE",),
"op": (["invert", "average", "round", "clamp"],),
"op": (["invert", "average", "round", "clamp", "abs"],),
},
}
@@ -331,6 +336,40 @@ class UnaryMaskOp:
return (torch.round(image),)
elif op == "clamp":
return (torch.min(torch.max(image, torch.tensor(0.)), torch.tensor(1.)),)
elif op == "abs":
return (torch.abs(image),)
class UnaryImageOp:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"op": (["invert", "average", "round", "clamp", "abs"],),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "op_image"
CATEGORY = "Masquerade Nodes"
def op_image(self, image, op):
image = tensor2rgb(image)
if op == "invert":
return (1. - image,)
elif op == "average":
mean = torch.mean(torch.mean(image,dim=2),dim=1)
return (mean.unsqueeze(1).unsqueeze(2).repeat(1, image.shape[1], image.shape[2], 1),)
elif op == "round":
return (torch.round(image),)
elif op == "clamp":
return (torch.min(torch.max(image, torch.tensor(0.)), torch.tensor(1.)),)
elif op == "abs":
return (torch.abs(image),)
class BlurNode:
@@ -463,6 +502,7 @@ class MixColorByMask:
CATEGORY = "Masquerade Nodes"
def mix(self, image, r, g, b, mask):
r, g, b = r / 255., g / 255., b / 255.
image_size = image.size()
image2 = torch.tensor([r, g, b]).to(device=image.device).unsqueeze(0).unsqueeze(0).unsqueeze(0).repeat(image_size[0], image_size[1], image_size[2], 1)
image, image2 = tensors2common(image, image2)
@@ -470,6 +510,9 @@ class MixColorByMask:
return (image * (1. - mask) + image2 * mask,)
class CreateRectMask:
"""
Creates a rectangle mask. If copy_image_size is provided, the image_width and image_height parameters are ignored and the size of the given images will be used instead.
"""
def __init__(self):
pass
@@ -522,6 +565,9 @@ class CreateRectMask:
return (mask.unsqueeze(0),)
class MaskToRegion:
"""
Given a mask, returns a rectangular region that fits the mask with the given constraints
"""
def __init__(self):
pass
@@ -634,6 +680,9 @@ class MaskToRegion:
return (region,)
class CutByMask:
"""
Cuts the image to the bounding box of the mask. If force_resize_width or force_resize_height are provided, the image will be resized to those dimensions. The `mask_mapping_optional` input can be provided from a 'Separate Mask Components' node to cut multiple pieces out of a single image in a batch.
"""
def __init__(self):
pass
@@ -726,6 +775,9 @@ class CutByMask:
return (result,)
class SeparateMaskComponents:
"""
Separates a mask into multiple contiguous components. Returns the individual masks created as well as a MASK_MAPPING which can be used in other nodes when dealing with batches.
"""
def __init__(self):
pass
@@ -738,6 +790,7 @@ class SeparateMaskComponents:
}
RETURN_TYPES = ("IMAGE","MASK_MAPPING")
RETURN_NAMES = ("mask", "mask_mappings")
FUNCTION = "separate"
CATEGORY = "Masquerade Nodes"
@@ -776,6 +829,9 @@ class SeparateMaskComponents:
class PasteByMask:
"""
Pastes `image_to_paste` onto `image_base` using `mask` to determine the location. The `resize_behavior` parameter determines how the image to paste is resized to fit the mask. If `mask_mapping_optional` obtained from a 'Separate Mask Components' node is used, it will control which image gets pasted onto which base image.
"""
def __init__(self):
pass
@@ -806,9 +862,21 @@ class PasteByMask:
# Scale the mask to be a matching size if it isn't
B, H, W, C = image_base.shape
MB = mask.shape[0]
if mask_mapping_optional is None and MB < B:
assert(B % MB == 0)
mask = mask.repeat(B // MB, 1, 1)
PB = image_to_paste.shape[0]
if mask_mapping_optional is None:
if B < PB:
assert(PB % B == 0)
image_base = image_base.repeat(PB // B, 1, 1, 1)
B, H, W, C = image_base.shape
if MB < B:
assert(B % MB == 0)
mask = mask.repeat(B // MB, 1, 1)
elif B < MB:
assert(MB % B == 0)
image_base = image_base.repeat(MB // B, 1, 1, 1)
if PB < B:
assert(B % PB == 0)
image_to_paste = image_to_paste.repeat(B // PB, 1, 1, 1)
mask = torch.nn.functional.interpolate(mask.unsqueeze(1), size=(H, W), mode='nearest')[:,0,:,:]
MB, MH, MW = mask.shape
@@ -916,6 +984,7 @@ class GetImageSize:
}
RETURN_TYPES = ("INT","INT",)
RETURN_NAMES = ("width", "height")
FUNCTION = "get_size"
CATEGORY = "Masquerade Nodes"
@@ -955,6 +1024,9 @@ class ChangeChannelCount:
return (tensor2rgb(image),)
class ConstantMask:
"""
Creates a mask filled with a constant value. If copy_image_size is provided, the explicit_height and explicit_width parameters are ignored and the size of the given images will be used instead.
"""
def __init__(self):
pass
@@ -993,6 +1065,9 @@ class ConstantMask:
return (result,)
class PruneByMask:
"""
Filters out the images in a batch that don't have an associated mask with an average pixel value of at least 0.5.
"""
def __init__(self):
pass
@@ -1016,11 +1091,241 @@ class PruneByMask:
mean = torch.mean(torch.mean(mask,dim=2),dim=1)
return (image[mean >= 0.5],)
class MakeImageBatch:
"""
Creates a batch of images from multiple individual images or batches.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image1": ("IMAGE",),
},
"optional": {
"image2": ("IMAGE",),
"image3": ("IMAGE",),
"image4": ("IMAGE",),
"image5": ("IMAGE",),
"image6": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "append"
CATEGORY = "Masquerade Nodes"
def append(self, image1, image2 = None, image3 = None, image4 = None, image5 = None, image6 = None):
result = image1
if image2 is not None:
result = torch.cat((result, image2), 0)
if image3 is not None:
result = torch.cat((result, image3), 0)
if image4 is not None:
result = torch.cat((result, image4), 0)
if image5 is not None:
result = torch.cat((result, image5), 0)
if image6 is not None:
result = torch.cat((result, image6), 0)
return (result,)
class CreateQRCodeNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"multiline": True}),
"size": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64}),
"qr_version": ("INT", {"default": 1, "min": 1, "max": 40, "step": 1}),
"error_correction": (["L", "M", "Q", "H"], {"default": "H"}),
"box_size": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1}),
"border": ("INT", {"default": 4, "min": 0, "max": 100, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "create_qr_code"
CATEGORY = "Masquerade Nodes"
def create_qr_code(self, text, size, qr_version, error_correction, box_size, border):
ensure_package("qrcode")
import qrcode
if error_correction =="L":
error_level = qrcode.constants.ERROR_CORRECT_L
elif error_correction =="M":
error_level = qrcode.constants.ERROR_CORRECT_M
elif error_correction =="Q":
error_level = qrcode.constants.ERROR_CORRECT_Q
else:
error_level = qrcode.constants.ERROR_CORRECT_H
qr = qrcode.QRCode(
version=qr_version,
error_correction=error_level,
box_size=box_size,
border=border)
qr.add_data(text)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img = img.resize((size,size))
# Convert img (a PIL Image) into a torch tensor
tensor = torch.from_numpy(np.array(img))
return (tensor2rgb(tensor.unsqueeze(0)),)
def rgb2hsv(rgb):
# rgb is a tensor in the form [B, H, W, C]
r = rgb[...,0]
g = rgb[...,1]
b = rgb[...,2]
hsv = torch.zeros_like(rgb)
hsv_h = hsv[...,0]
hsv_s = hsv[...,1]
hsv_v = hsv[...,2]
# Value
hsv_v[:], max_idx = torch.max(rgb, dim=3)
chroma = hsv_v - torch.min(rgb, dim=3).values
# Hue
sixth = 1.0 / 6.0
hsv_h[max_idx == 0] = (sixth * ((g - b) / chroma % 6))[max_idx == 0]
hsv_h[max_idx == 1] = (sixth * ((b - r) / chroma + 2))[max_idx == 1]
hsv_h[max_idx == 2] = (sixth * ((r - g) / chroma + 4))[max_idx == 2]
hsv_h[chroma == 0] = 0
# Saturation
hsv_s[chroma != 0] = chroma[chroma != 0] / hsv_v[chroma != 0]
return hsv
def hsv2rgb(hsv):
# hsv is a tensor in the form [B, H, W, C] where C is (h,s,v)
h = hsv[...,0]
h = h % 1.0
s = hsv[...,1]
v = hsv[...,2]
rgb = torch.zeros_like(hsv)
chroma = v * s
hp = (h * 6.0).type(torch.uint8)
x = chroma * (1 - torch.abs((h * 6.0) % 2 - 1))
zeros = torch.zeros_like(x)
rgb[hp == 0] = torch.stack([chroma, x, zeros], dim=3)[hp == 0]
rgb[hp == 1] = torch.stack([x, chroma, zeros], dim=3)[hp == 1]
rgb[hp == 2] = torch.stack([zeros, chroma, x], dim=3)[hp == 2]
rgb[hp == 3] = torch.stack([zeros, x, chroma], dim=3)[hp == 3]
rgb[hp == 4] = torch.stack([x, zeros, chroma], dim=3)[hp == 4]
rgb[hp == 5] = torch.stack([chroma, zeros, x], dim=3)[hp == 5]
rgb += (v - chroma).unsqueeze(3).repeat(1,1,1,3)
return rgb
def hsv2hsl(hsv):
hsl = torch.zeros_like(hsv)
h = hsv[...,0]
s = hsv[...,1]
v = hsv[...,2]
hsl[...,0] = h
hsl[...,2] = v * (1. - s / 2.)
l = hsl[...,2]
defined = (l != 0) & (l != 1)
hsl[...,1][defined] = ((v - l) / torch.min(l, 1. - l))[defined]
return hsl
def hsl2hsv(hsl):
hsv = torch.zeros_like(hsl)
h = hsl[...,0]
s = hsl[...,1]
l = hsl[...,2]
hsv[...,0] = h
hsv[...,2] = l + s * torch.min(l, 1. - l)
v = hsv[...,2]
defined = (v != 0)
hsv[...,1][defined] = (2. * (1. - l / v))[defined]
return hsv
class ConvertColorSpace:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"in_space": (["RGB", "HSV", "HSL"],),
"out_space": (["RGB", "HSV", "HSL"],),
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "convert_color_space"
CATEGORY = "Masquerade Nodes"
def convert_color_space(self, in_space, out_space, image):
if in_space == out_space:
return (image,)
image = tensor2rgb(image)
if in_space == "HSV":
hsv = image
if in_space == "RGB":
hsv = rgb2hsv(image)
elif in_space == "HSL":
hsv = hsl2hsv(image)
# We are now in RGB or HSV
if out_space == "HSV":
return (hsv,)
elif out_space == "RGB":
return (hsv2rgb(hsv),)
else:
assert out_space == "HSL"
return (hsv2hsl(hsv),)
class MaqueradeIncrementerNode:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"seed": ("INT", {"default": 0, "min": -1, "max": 0xffffffffffffffff, "step": 1}),
"max_value": ("INT", {"default": 1, "min": 1, "max": 0xffffffffffffffff, "step": 1}),
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "increment"
CATEGORY = "Masquerade Nodes"
def increment(self, seed, max_value):
return (seed % max_value,)
NODE_CLASS_MAPPINGS = {
"Mask By Text": ClipSegNode,
"Mask Morphology": MaskMorphologyNode,
"Combine Masks": MaskCombineOp,
"Unary Mask Op": UnaryMaskOp,
"Unary Image Op": UnaryImageOp,
"Blur": BlurNode,
"Image To Mask": ImageToMask,
"Mix Images By Mask": MixByMask,
@@ -1034,4 +1339,33 @@ NODE_CLASS_MAPPINGS = {
"Prune By Mask": PruneByMask,
"Separate Mask Components": SeparateMaskComponents,
"Create Rect Mask": CreateRectMask,
"Make Image Batch": MakeImageBatch,
"Create QR Code": CreateQRCodeNode,
"Convert Color Space": ConvertColorSpace,
"MasqueradeIncrementer": MaqueradeIncrementerNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Mask By Text": "Mask By Text",
"Mask Morphology": "Mask Morphology",
"Combine Masks": "Combine Masks",
"Unary Mask Op": "Unary Mask Op",
"Unary Image Op": "Unary Image Op",
"Blur": "Blur",
"Image To Mask": "Image To Mask",
"Mix Images By Mask": "Mix Images By Mask",
"Mix Color By Mask": "Mix Color By Mask",
"Mask To Region": "Mask To Region",
"Cut By Mask": "Cut By Mask",
"Paste By Mask": "Paste By Mask",
"Get Image Size": "Get Image Size",
"Change Channel Count": "Change Channel Count",
"Constant Mask": "Constant Mask",
"Prune By Mask": "Prune By Mask",
"Separate Mask Components": "Separate Mask Components",
"Create Rect Mask": "Create Rect Mask",
"Make Image Batch": "Make Image Batch",
"Create QR Code": "Create QR Code",
"Convert Color Space": "Convert Color Space",
"MasqueradeIncrementer": "Incrementer",
}
+41
View File
@@ -1,3 +1,8 @@
# Recommended Alternatives
This node pack was created as a dependency-free library before the [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) made installing dependencies easy for end-users.
Unless you specifically need a library without dependencies, I recommend using [Impact Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack) instead. It's a more feature-rich and well-maintained alternative for dealing with masks and segmentation.
# Masquerade Nodes
This is a node pack for ComfyUI, primarily dealing with masks. Some example workflows this pack enables are:
@@ -80,6 +85,7 @@ The lynchpin of these workflows is the [Mask by Text](#mask-by-text) node. This
* `average` - Sets the value of all pixels in the mask to be equal to the value of the average pixel.
* `round` - Rounds all pixels to the nearest integer (generally 0 or 1).
* `clamp` - Clamps all values between 0 and 1.
* `abs` - Takes the absolute value of all pixels.
#### Outputs
1. The resultant mask.
@@ -87,6 +93,20 @@ The lynchpin of these workflows is the [Mask by Text](#mask-by-text) node. This
#### Tips
* Average is deceptively useful. For example, you can use two Mask to Text nodes with different concepts (like 'New York' and 'Chicago') and average the raw ClipSeg result to see which city the image is more likely to depict.
## Unary Image Op
This node is the same as Unary Mask Op, but will operate across all channels of an image individually. This can be particularly useful after converting to HSV colorspace.
#### Inputs
* `image` - The image to operate on.
* `op` - The operation to perform.
* `invert` - Inverts all channels of the image.
* `average` - Sets the value of all pixels in the image to be equal to the value of the average pixel in that channel.
* `round` - Rounds all pixels to the nearest integer (generally 0 or 1).
* `clamp` - Clamps all values between 0 and 1.
* `abs` - Takes the absolute value of all pixels.
#### Outputs
1. The resultant image.
## Blur
#### Inputs
* `image` - The image to blur.
@@ -258,3 +278,24 @@ The lynchpin of these workflows is the [Mask by Text](#mask-by-text) node. This
* `image_width` - The width of the overall image to use. Will only be used if `copy_image_size` is empty.
* `image_height` - The height of the overall image to use. Will only be used if `copy_image_size` is empty.
* `copy_image_size` - If specified, the mask will have the same size as the given image.
## Create QR Code
#### Inputs
* `text` - The content to embed in the QR Code
* `size` - The size of the QR Code (across height and width) in pixels.
* `qr_version` - The version of QR Code to use. Higher versions can encode more data, but are larger.
* `error_correction` - The level of error correction to use.
* `box_size` - The size of each box in the QR Code in pixels.
* `border` - The size of the border around the QR Code in pixels.
## Convert Color Space
#### Inputs
* `image` - The image to convert.
* `in_space` - The color space of the input image -- valid values are `RGB`, `HSV`, and `HSL`.
* `out_space` - The color space of the output image -- valid values are `RGB`, `HSV`, and `HSL`.
## Incrementer
#### Inputs
* `seed` - The current value.
* `control_after_generate` - Set to "Increment" to actually increment.
* `max_value` - The value to perform modulo against
+2 -2
View File
@@ -1,3 +1,3 @@
from .MaskNodes import NODE_CLASS_MAPPINGS
from .MaskNodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ['NODE_CLASS_MAPPINGS']
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']