*new node: QUEUE TOO -- media specific Q

cozy_menu filter for specific -jov types
MATTE now default for Resample Workflow
This commit is contained in:
Alexander G. Morano
2024-08-31 15:38:25 -07:00
parent 64f78b183e
commit b1e32fa82a
16 changed files with 480 additions and 137 deletions
+5
View File
@@ -62,6 +62,11 @@ If those nodes have descriptions written in HTML or Markdown, they will be conve
## UPDATES
**2024/08/31**:
* Better MASK/ALPHA support for `BLEND`, `ADJUST` and `QUEUE`
* Cleaner Markdown outputs
* Supports ComfyUI 0.1.3+, frontend 1.2.41+
**2024/08/28**:
* New `STRINGER NODE` for string operations: Split, Join, Replace and Slice.
+1 -1
View File
@@ -34,7 +34,7 @@ images, or liner interpolate values and more.
StreamReaderNode, StreamWriterNode, SpoutWriter,
AkashicNode, ArrayNode, BatchLoadNode, DynamicNode, ValueGraphNode, ExportNode, QueueNode,
RouteNode, SaveOutputNode
@version: 1.2.6
@version: 1.2.32
"""
import os
+21 -23
View File
@@ -20,7 +20,7 @@ from Jovimetrix.sup.util import parse_dynamic, parse_param, \
zip_longest_fill, EnumConvertType
from Jovimetrix.sup.image import \
channel_merge, channel_solid, channel_swap, color_match_lut, image_filter, image_flatten_mask, \
channel_merge, channel_solid, channel_swap, color_match_lut, image_filter, \
image_gradient_map, image_minmax, image_quantize, image_scalefit, \
color_match_reinhard, cv2tensor_full, image_color_blind, image_contrast,\
image_crop, image_crop_center, image_crop_polygonal, image_equalize, \
@@ -109,7 +109,7 @@ Enhance and modify images with various effects such as blurring, sharpening, col
pbar = ProgressBar(len(params))
for idx, (pA, mask, op, radius, val, lohi, lmh, hsv, contrast, gamma, matte, invert) in enumerate(params):
pA = tensor2cv(pA) if pA is not None else channel_solid(chan=EnumImageType.BGR)
pA, alpha = image_flatten_mask(pA)
alpha = image_mask(pA) if pA.ndim == 3 and pA.shape[2] == 4 else None
match EnumAdjustOP[op]:
case EnumAdjustOP.INVERT:
@@ -194,8 +194,6 @@ Enhance and modify images with various effects such as blurring, sharpening, col
mask = image_grayscale(mask)
if invert:
mask = 255 - mask
mask = image_convert(mask, 1)
pA = image_blend(pA, img_new, mask)
if alpha is not None:
pA = image_mask_add(pA, alpha)
@@ -224,7 +222,7 @@ Combine two input images using various blending modes, such as normal, screen, m
Lexicon.A: ("FLOAT", {"default": 1, "mij": 0, "maj": 1, "step": 0.01, "tooltips": "Amount of Blending to Perform on the Selected Operation"}),
Lexicon.FLIP: ("BOOLEAN", {"default": False}),
Lexicon.INVERT: ("BOOLEAN", {"default": False, "tooltips": "Invert the mask input"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -239,7 +237,7 @@ Combine two input images using various blending modes, such as normal, screen, m
func = parse_param(kw, Lexicon.FUNC, EnumConvertType.STRING, EnumBlendType.NORMAL.name)
alpha = parse_param(kw, Lexicon.A, EnumConvertType.FLOAT, 1, 0, 1)
flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.BOOLEAN, False)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
@@ -286,7 +284,7 @@ Combine two input images using various blending modes, such as normal, screen, m
img = image_blend(pA, pB, mask, func, alpha)
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
w, h = wihi
sample = EnumInterpolation[sample]
img = image_scalefit(img, w, h, mode, sample)
@@ -582,7 +580,7 @@ Combine multiple input images into a single image by summing their pixel values.
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -599,7 +597,7 @@ Combine multiple input images into a single image by summing their pixel values.
logger.error("no images to flatten")
return ()
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
@@ -608,10 +606,10 @@ Combine multiple input images into a single image by summing their pixel values.
pbar = ProgressBar(len(params))
for idx, (mode, sample, wihi, matte) in enumerate(params):
mode = EnumScaleMode[mode]
h, w = pA[0].shape[:2] if mode == EnumScaleMode.NONE else wihi[::-1]
h, w = pA[0].shape[:2] if mode == EnumScaleMode.MATTE else wihi[::-1]
current = np.full((w, h, 4), (0,0,0,0), dtype=np.uint8)
for x in pA:
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
x = image_scalefit(x, w, h, mode, sample)
x = image_scalefit(x, w, h, EnumScaleMode.CROP, sample)
x = image_convert(x, 4)
@@ -637,7 +635,7 @@ Remaps an input image using a gradient lookup table (LUT). The gradient image wi
Lexicon.PIXEL: (JOV_TYPE_IMAGE, {"tooltips":"Image to remap with gradient input"}),
Lexicon.GRADIENT: (JOV_TYPE_IMAGE, {"tooltips":f"Look up table (LUT) to remap the input image in `{Lexicon.PIXEL}`"}),
Lexicon.FLIP: ("BOOLEAN", {"default":False, "tooltips":"Reverse the gradient from left-to-right "}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -649,7 +647,7 @@ Remaps an input image using a gradient lookup table (LUT). The gradient image wi
pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None)
gradient = parse_param(kw, Lexicon.GRADIENT, EnumConvertType.IMAGE, None)
flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.BOOLEAN, False)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
@@ -666,7 +664,7 @@ Remaps an input image using a gradient lookup table (LUT). The gradient image wi
pA = image_gradient_map(pA, gradient)
# @TODO: pattern o' scale... when make it a lambda?
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
w, h = wihi
sample = EnumInterpolation[sample]
pA = image_scalefit(pA, w, h, mode, sample)
@@ -695,7 +693,7 @@ Combines individual color channels (red, green, blue) along with an optional mas
Lexicon.G: (JOV_TYPE_IMAGE, {}),
Lexicon.B: (JOV_TYPE_IMAGE, {}),
Lexicon.A: (JOV_TYPE_IMAGE, {}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}),
@@ -711,7 +709,7 @@ Combines individual color channels (red, green, blue) along with an optional mas
G = parse_param(kw, Lexicon.G, EnumConvertType.MASK, None)
B = parse_param(kw, Lexicon.B, EnumConvertType.MASK, None)
A = parse_param(kw, Lexicon.A, EnumConvertType.MASK, None)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
@@ -741,7 +739,7 @@ Combines individual color channels (red, green, blue) along with an optional mas
img = channel_merge(img)
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
w, h = wihi
sample = EnumInterpolation[sample]
img = image_scalefit(img, w, h, mode, sample)
@@ -887,7 +885,7 @@ Merge multiple input images into a single composite image by stacking them along
"tooltips":"Choose the direction in which to stack the images. Options include horizontal, vertical, or a grid layout"}),
Lexicon.STEP: ("INT", {"mij": 0, "default": 1,
"tooltips":"Specify the spacing between each stacked image. This determines how far apart the images are from each other"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -907,14 +905,14 @@ Merge multiple input images into a single composite image by stacking them along
axis = parse_param(kw, Lexicon.AXIS, EnumConvertType.STRING, EnumOrientation.GRID.name)[0]
stride = parse_param(kw, Lexicon.STEP, EnumConvertType.INT, 1)[0]
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)[0]
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)[0]
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)[0]
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)[0]
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)[0]
axis = EnumOrientation[axis]
img = image_stack(images, axis, stride) #, matte)
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
w, h = wihi
sample = EnumInterpolation[sample]
img = image_scalefit(img, w, h, mode, sample)
@@ -989,7 +987,7 @@ Apply various geometric transformations to images, including translation, rotati
Lexicon.TLTR: ("VEC4", {"default": (0, 0, 1, 0), "step": 0.005, "label": [Lexicon.TOP, Lexicon.LEFT, Lexicon.TOP, Lexicon.RIGHT]}),
Lexicon.BLBR: ("VEC4", {"default": (0, 1, 1, 1), "step": 0.005, "label": [Lexicon.BOTTOM, Lexicon.LEFT, Lexicon.BOTTOM, Lexicon.RIGHT]}),
Lexicon.STRENGTH: ("FLOAT", {"default": 1, "mij": 0, "step": 0.005}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -1011,7 +1009,7 @@ Apply various geometric transformations to images, including translation, rotati
tltr = parse_param(kw, Lexicon.TLTR, EnumConvertType.VEC4, [(0, 0, 1, 0)], 0, 1)
blbr = parse_param(kw, Lexicon.BLBR, EnumConvertType.VEC4, [(0, 1, 1, 1)], 0, 1)
strength = parse_param(kw, Lexicon.STRENGTH, EnumConvertType.FLOAT, 1, 0, 1)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
@@ -1057,7 +1055,7 @@ Apply various geometric transformations to images, including translation, rotati
pA = image_scalefit(pA, w, h, EnumScaleMode.FIT, sample)
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
w, h = wihi
pA = image_scalefit(pA, w, h, mode, sample)
+3 -3
View File
@@ -54,7 +54,7 @@ Generate a constant image or mask of a specified size and color. It can be used
Lexicon.WH: ("VEC2INT", {"default": (512, 512),
"label": [Lexicon.W, Lexicon.H],
"tooltips": "Desired Width and Height of the Color Output"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
}
})
@@ -64,7 +64,7 @@ Generate a constant image or mask of a specified size and color. It can be used
pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None)
matte = parse_param(kw, Lexicon.RGBA_A, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
images = []
params = list(zip_longest_fill(pA, matte, wihi, mode, sample))
@@ -77,7 +77,7 @@ Generate a constant image or mask of a specified size and color. It can be used
else:
pA = tensor2cv(pA)
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
sample = EnumInterpolation[sample]
pA = image_scalefit(pA, width, height, mode, sample)
images.append(cv2tensor_full(pA, matte))
+4 -4
View File
@@ -91,7 +91,7 @@ class GLSLNodeBase(JOVImageNode):
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -109,7 +109,7 @@ class GLSLNodeBase(JOVImageNode):
delta = parse_param(kw, Lexicon.TIME, EnumConvertType.FLOAT, 0)[0]
# everybody wang comp tonight
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)[0]
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)[0]
mode = EnumScaleMode[mode]
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)[0]
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)[0]
@@ -150,13 +150,13 @@ class GLSLNodeBase(JOVImageNode):
vars[k] = var
w, h = wihi
if firstImage is not None and mode == EnumScaleMode.NONE:
if firstImage is not None and mode == EnumScaleMode.MATTE:
h, w = firstImage.shape[:2]
self.__glsl.size = (w, h)
img = self.__glsl.render(self.__delta, **vars)
if mode != EnumScaleMode.NONE:
if mode != EnumScaleMode.MATTE:
img = image_scalefit(img, w, h, mode, sample)
img = cv2tensor_full(img, matte)
+7 -7
View File
@@ -99,7 +99,7 @@ Capture frames from various sources such as URLs, cameras, monitors, windows, or
Lexicon.BATCH: ("VEC2INT", {"default": (1, 30), "label": ["COUNT", "FPS"], "tooltips": "Number of frames wanted and the FPS"}),
Lexicon.ORIENT: (EnumCanvasOrientation._member_names_, {"default": EnumCanvasOrientation.NORMAL.name}),
Lexicon.ZOOM: ("FLOAT", {"mij": 0, "maj": 1, "step": 0.005, "default": 0.}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -133,7 +133,7 @@ Capture frames from various sources such as URLs, cameras, monitors, windows, or
rate = 1. / rate
width, height = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)])[0]
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0,0,0,255)], 0, 255)[0]
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)[0]
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)[0]
mode = EnumScaleMode[mode]
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)[0]
sample = EnumInterpolation[sample]
@@ -276,7 +276,7 @@ Sends frames to a specified route, typically for live streaming or recording pur
"optional": {
Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}),
Lexicon.ROUTE: ("STRING", {"default": "/stream"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 0), "rgb": True})
@@ -302,7 +302,7 @@ Sends frames to a specified route, typically for live streaming or recording pur
images = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0,0,0,0)], 0, 255)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
params = list(zip_longest_fill(route, images, wihi, matte, mode, sample))
pbar = ProgressBar(len(params))
@@ -349,7 +349,7 @@ Sends frames to a specified Spout receiver application for real-time video shari
Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}),
Lexicon.ROUTE: ("STRING", {"default": "Spout Sender"}),
Lexicon.FPS: ("INT", {"mij": 0, "maj": 60, "default": 30, "tooltips": "@@@ NOT USED @@@"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.NONE.name}),
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
@@ -369,7 +369,7 @@ Sends frames to a specified Spout receiver application for real-time video shari
images = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None)
host = parse_param(kw, Lexicon.ROUTE, EnumConvertType.STRING, "")
#fps = parse_param(kw, Lexicon.FPS, EnumConvertType.INT, 30)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0,0,0,0)], 0, 255)
@@ -381,7 +381,7 @@ Sends frames to a specified Spout receiver application for real-time video shari
matte = pixel_eval(matte, EnumImageType.BGRA)
w, h = wihi
img = channel_solid(w, h, chan=EnumImageType.BGRA) if img is None else tensor2cv(img)
if (mode := EnumScaleMode[mode]) != EnumScaleMode.NONE:
if (mode := EnumScaleMode[mode]) != EnumScaleMode.MATTE:
img = image_scalefit(img, w, h, mode, sample, matte)
img = image_convert(img, 4)
self.__sender.frame = img
+167 -74
View File
@@ -13,7 +13,7 @@ from enum import Enum
from uuid import uuid4
from pathlib import Path
from itertools import zip_longest
from typing import Any, Literal, Tuple
from typing import Any, List, Literal, Tuple
import torch
import numpy as np
@@ -32,7 +32,7 @@ from Jovimetrix import DynamicInputType, deep_merge, comfy_message, parse_reset,
from Jovimetrix.sup.util import parse_dynamic, path_next, \
parse_param, zip_longest_fill, EnumConvertType
from Jovimetrix.sup.image import cv2tensor, image_by_size, image_convert, \
from Jovimetrix.sup.image import EnumInterpolation, EnumScaleMode, cv2tensor, cv2tensor_full, image_by_size, image_convert, \
image_matte, tensor2cv, pil2tensor, image_load, image_formats, tensor2pil, MIN_IMAGE_SIZE
# =============================================================================
@@ -483,46 +483,13 @@ Exports and Displays immediate information about images.
cc = 1
return count, width, height, cc, (width, height), (width, height, cc)
'''
# OLD LOAD BATCH NODE -- add to queue?
def run(self, **kw) -> None:
q = parse_param(kw, Lexicon.QUEUE, EnumConvertType.STRING, "")
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.NONE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
params = list(zip_longest_fill(q, mode, wihi, sample, matte))
images = []
pbar = ProgressBar(len(params))
for idx, (q, mode, wihi, sample, matte) in enumerate(params):
for pA in q.split('\n'):
w, h = wihi
path = Path(pA) if Path(pA).is_file() else Path(ROOT / pA)
if not path.is_file():
logger.error(f"bad file: [{pA}]")
pA = channel_solid(w, h)
elif path.suffix in image_formats():
pA = image_load(str(path))[0]
mode = EnumScaleMode[mode]
if mode != EnumScaleMode.NONE:
pA = image_scalefit(pA, w, h, mode, sample)
else:
pA = channel_solid(w, h)
images.append(cv2tensor_full(pA, matte))
pbar.update_absolute(idx)
return [torch.cat(i, dim=0) for i in zip(*images)]
'''
class QueueNode(JOVBaseNode):
NAME = "QUEUE (JOV) 🗃"
class QueueBaseNode(JOVBaseNode):
CATEGORY = f"JOVIMETRIX 🔺🟩🔵/{JOV_CATEGORY}"
RETURN_TYPES = (JOV_TYPE_ANY, JOV_TYPE_ANY, JOV_TYPE_ANY, "INT", "INT")
RETURN_NAMES = (Lexicon.ANY_OUT, Lexicon.QUEUE, Lexicon.CURRENT, Lexicon.INDEX, Lexicon.TOTAL, )
VIDEO_FORMATS = image_formats() + ['.wav', '.mp3', '.webm', '.mp4', '.avi', '.wmv', '.mkv', '.mov', '.mxf']
SORT = 0
DESCRIPTION = """
Manage a queue of items, such as file paths or data. It supports various formats including images, videos, text files, and JSON files. Users can specify the current index for the queue item, enable pausing the queue, or reset it back to the first index. The node outputs the current item in the queue, the entire queue, the current index, and the total number of items in the queue.
"""
@classmethod
def IS_CHANGED(cls, *arg, **kw) -> float:
return float("nan")
@classmethod
def INPUT_TYPES(cls) -> dict:
@@ -535,30 +502,22 @@ Manage a queue of items, such as file paths or data. It supports various formats
Lexicon.RESET: ("BOOLEAN", {"default": False, "tooltips":"Reset the queue back to index 1"}),
Lexicon.BATCH: ("BOOLEAN", {"default": False, "tooltips":"Load all items, if they are loadable items, i.e. batch load images from the Queue's list"}),
Lexicon.RECURSE: ("BOOLEAN", {"default": False}),
},
"outputs": {
0: (Lexicon.ANY_OUT, {"tooltips":"Current item selected from the Queue list"}),
1: (Lexicon.QUEUE, {"tooltips":"The entire Queue list"}),
2: (Lexicon.CURRENT, {"tooltips":"Current item selected from the Queue list as a string"}),
3: (Lexicon.INDEX, {"tooltips":"Current selected item index in the Queue list"}),
4: (Lexicon.TOTAL, {"tooltips":"Total items in the current Queue List"}),
}
})
return Lexicon._parse(d, cls)
@classmethod
def IS_CHANGED(cls, *arg, **kw) -> float:
return float("nan")
def __init__(self) -> None:
self.__index = 0
self.__q = None
self.__index_last = None
self.__len = 0
self.__current = None
self.__previous = None
self.__ident = None
self.__last_q_value = {}
def __parse(self, data: Any, recurse: bool=False) -> list:
# consume the list into iterable items to load/process
def __parseQ(self, data: Any, recurse: bool=False) -> List[str]:
entries = []
for line in data.strip().split('\n'):
if len(line) == 0:
@@ -602,24 +561,27 @@ Manage a queue of items, such as file paths or data. It supports various formats
entries.extend(ret)
return entries
# turn Q element into actual hard type
def process(self, q_data: Any) -> Tuple[torch.Tensor, torch.Tensor] | str | dict:
# single Q cache to skip loading single entries over and over
# @TODO: MRU cache strategy
if (val := self.__last_q_value.get(q_data, None)) is not None:
return val
if isinstance(q_data, (str,)):
if not os.path.isfile(q_data):
return q_data
_, ext = os.path.splitext(q_data)
if ext in self.VIDEO_FORMATS:
data = image_load(q_data)[0]
self.__last_q_value[q_data] = data
elif ext == '.json':
with open(q_data, 'r', encoding='utf-8') as f:
self.__last_q_value[q_data] = json.load(f)
return self.__last_q_value.get(q_data, q_data)
def run(self, ident, **kw) -> None:
def process(q_data: Any) -> Tuple[torch.Tensor, torch.Tensor] | str | dict:
# single Q cache to skip loading single entries over and over
if (val := self.__last_q_value.get(q_data, None)) is not None:
return val
if isinstance(q_data, (str,)):
if not os.path.isfile(q_data):
return q_data
_, ext = os.path.splitext(q_data)
if ext in self.VIDEO_FORMATS:
data = image_load(q_data)[0]
self.__last_q_value[q_data] = data
elif ext == '.json':
with open(q_data, 'r', encoding='utf-8') as f:
self.__last_q_value[q_data] = json.load(f)
return self.__last_q_value.get(q_data, q_data)
self.__ident = ident
# should work headless as well
if parse_reset(ident) > 0 or parse_param(kw, Lexicon.RESET, EnumConvertType.BOOLEAN, False)[0]:
self.__q = None
@@ -634,21 +596,21 @@ Manage a queue of items, such as file paths or data. It supports various formats
# entry is: data, <filter if folder:*.png,*.jpg>, <repeats:1+>
recurse = parse_param(kw, Lexicon.RECURSE, EnumConvertType.BOOLEAN, False)[0]
q = parse_param(kw, Lexicon.QUEUE, EnumConvertType.STRING, "")[0]
self.__q = self.__parse(q, recurse)
self.__q = self.__parseQ(q, recurse)
self.__len = len(self.__q)
self.__index_last = 0
self.__previous = self.__q[0] if len(self.__q) else None
if self.__previous:
self.__previous = process(self.__previous)
self.__previous = self.process(self.__previous)
if (wait := parse_param(kw, Lexicon.WAIT, EnumConvertType.BOOLEAN, False))[0] == True:
self.__index = self.__index_last
self.__index = max(0, self.__index) % self.__len
current = self.__q[self.__index]
self.__current = self.__q[self.__index]
data = self.__previous
self.__index_last = self.__index
info = f"QUEUE #{ident} [{current}] ({self.__index})"
info = f"QUEUE #{ident} [{self.__current}] ({self.__index})"
if wait == True:
info += f" PAUSED"
else:
@@ -657,7 +619,7 @@ Manage a queue of items, such as file paths or data. It supports various formats
mw, mh, mc = 0, 0, 0
pbar = ProgressBar(self.__len)
for idx in range(self.__len):
ret = process(self.__q[idx])
ret = self.process(self.__q[idx])
if isinstance(ret, (np.ndarray,)):
h, w, c = ret.shape
mw, mh, mc = max(mw, w), max(mh, h), max(mc, c)
@@ -675,11 +637,142 @@ Manage a queue of items, such as file paths or data. It supports various formats
pbar.update_absolute(idx)
data = torch.cat(ret, dim=0)
else:
data = process(self.__q[self.__index])
data = self.process(self.__q[self.__index])
if isinstance(data, (list, np.ndarray,)) and isinstance(data[0], (np.ndarray,)):
data = cv2tensor(data)
self.__index += 1
self.__previous = data
comfy_message(ident, "jovi-queue-ping", self.status)
return data, self.__q, self.__current, self.__index_last+1, self.__len
@property
def status(self) -> dict[str, Any]:
return {
"id": self.__ident,
"c": self.__current,
"i": self.__index_last+1,
"s": self.__len,
"l": self.__q
}
class QueueNode(QueueBaseNode):
NAME = "QUEUE (JOV) 🗃"
RETURN_TYPES = (JOV_TYPE_ANY, JOV_TYPE_ANY, JOV_TYPE_ANY, "INT", "INT")
RETURN_NAMES = (Lexicon.ANY_OUT, Lexicon.QUEUE, Lexicon.CURRENT, Lexicon.INDEX, Lexicon.TOTAL, )
SORT = 450
DESCRIPTION = """
Manage a queue of items, such as file paths or data. Supports various formats including images, videos, text files, and JSON files. You can specify the current index for the queue item, enable pausing the queue, or reset it back to the first index. The node outputs the current item in the queue, the entire queue, the current index, and the total number of items in the queue.
"""
@classmethod
def INPUT_TYPES(cls) -> dict:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"outputs": {
0: (Lexicon.ANY_OUT, {"tooltips":"Current item selected from the Queue list"}),
1: (Lexicon.QUEUE, {"tooltips":"The entire Queue list"}),
2: (Lexicon.CURRENT, {"tooltips":"Current item selected from the Queue list as a string"}),
3: (Lexicon.INDEX, {"tooltips":"Current selected item index in the Queue list"}),
4: (Lexicon.TOTAL, {"tooltips":"Total items in the current Queue List"}),
}
})
return Lexicon._parse(d, cls)
class QueueTooNode(QueueBaseNode):
NAME = "QUEUE TOO (JOV) 🗃"
RETURN_TYPES = ("IMAGE", "IMAGE", "MASK", JOV_TYPE_ANY, "INT", "INT")
RETURN_NAMES = (Lexicon.IMAGE, Lexicon.RGB, Lexicon.MASK, Lexicon.CURRENT, Lexicon.INDEX, Lexicon.TOTAL, )
SORT = 500
DESCRIPTION = """
Manage a queue of specific items: media files. Supports various image and video formats. You can specify the current index for the queue item, enable pausing the queue, or reset it back to the first index. The node outputs the current item in the queue, the entire queue, the current index, and the total number of items in the queue.
"""
@classmethod
def INPUT_TYPES(cls) -> dict:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}),
Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}),
Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True})
},
"outputs": {
0: ("IMAGE", {"tooltips":"Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output."}),
1: ("IMAGE", {"tooltips":"Three channel [RGB] image. There will be no alpha."}),
2: ("MASK", {"tooltips":"Single channel mask output."}),
3: (Lexicon.QUEUE, {"tooltips":"The entire Queue list"}),
4: (Lexicon.CURRENT, {"tooltips":"Current item selected from the Queue list as a string"}),
5: (Lexicon.INDEX, {"tooltips":"Current selected item index in the Queue list"}),
6: (Lexicon.TOTAL, {"tooltips":"Total items in the current Queue List"}),
}
})
return Lexicon._parse(d, cls)
def run(self, ident, **kw) -> None:
if parse_reset(ident) > 0 or parse_param(kw, Lexicon.RESET, EnumConvertType.BOOLEAN, False)[0]:
self.__q = None
self.__index = 0
if (new_val := parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, self.__index)[0]) > 0:
self.__index = new_val
if self.__q is None:
recurse = parse_param(kw, Lexicon.RECURSE, EnumConvertType.BOOLEAN, False)[0]
q = parse_param(kw, Lexicon.QUEUE, EnumConvertType.STRING, "")[0]
self.__q = self.parseQ(q, self.VIDEO_FORMATS, recurse)
self.__len = len(self.__q)
self.__index_last = 0
self.__previous = self.__q[0] if len(self.__q) else None
if self.__previous:
self.__previous = self.process(self.__previous)
if (wait := parse_param(kw, Lexicon.WAIT, EnumConvertType.BOOLEAN, False))[0] == True:
self.__index = self.__index_last
self.__index = max(0, self.__index) % self.__len
current = self.__q[self.__index]
data = self.__previous
self.__index_last = self.__index
info = f"QUEUE #{ident} [{current}] ({self.__index})"
if wait == True:
info += f" PAUSED"
else:
ret = []
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)
mw, mh, mc = 0, 0, 0
if parse_param(kw, Lexicon.BATCH, EnumConvertType.BOOLEAN, False)[0] == True:
data = []
for idx in range(self.__len):
ret = self.process(self.__q[idx])
h, w, c = ret.shape
mw, mh, mc = max(mw, w), max(mh, h), max(mc, c)
data.append(ret)
pbar = ProgressBar(self.__len)
for idx, d in enumerate(data):
d = image_convert(d, mc)
d = image_matte(d, matte, width=mw, height=mh)
d = cv2tensor(d)
ret.append(d)
pbar.update_absolute(idx)
data = torch.cat(ret, dim=0)
else:
data = self.process(self.__q[self.__index])
h, w, c = data.shape
data = cv2tensor_full(data, matte)
self.__index += 1
mode = parse_param(kw, Lexicon.MODE, EnumConvertType.STRING, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)
sample = parse_param(kw, Lexicon.SAMPLE, EnumConvertType.STRING, EnumInterpolation.LANCZOS4.name)
self.__previous = data
msg = {
"id": ident,
+1
View File
@@ -42,6 +42,7 @@
"PIXEL SPLIT (JOV) \ud83d\udc94": "Takes an input image and splits it into its individual color channels (red, green, blue), along with a mask channel",
"PIXEL SWAP (JOV) \ud83d\udd03": "Swap pixel values between two input images based on specified channel swizzle operations",
"QUEUE (JOV) \ud83d\uddc3": "Manage a queue of items, such as file paths or data",
"QUEUE TOO (JOV) \ud83d\uddc3": "Manage a queue of specific items: media files",
"ROUTE (JOV) \ud83d\ude8c": "Routes the input data from the optional input ports to the output port, preserving the order of inputs",
"SAVE OUTPUT (JOV) \ud83d\udcbe": "Save the output image along with its metadata to the specified path",
"SHAPE GEN (JOV) \u2728": "Create n-sided polygons",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "jovimetrix"
description = "Integrates Webcam, MIDI, Spout and GLSL shader support. Animation via tick. Parameter manipulation with wave generator. Math operations with Unary and Binary support. Value conversion for all major types (int, string, list, dict, Image, Mask). Shape mask generation, image stacking and channel ops, batch splitting, merging and randomizing, load images and video from anywhere, dynamic bus routing with a single node, export support for GIPHY, save output anywhere! flatten, crop, transform; check colorblindness, make stereogram or stereoscopic images, or liner interpolate values and more."
version = "1.2.31"
version = "1.2.32"
license = { file = "LICENSE" }
dependencies = [
"aenum>=3.1.15,<4",
+40 -16
View File
@@ -221,9 +221,9 @@ class EnumProjection(Enum):
PERSPECTIVE = 20
class EnumScaleMode(Enum):
NONE = 0
CROP = 20
# NONE = 0
MATTE = 25
CROP = 20
FIT = 10
ASPECT = 30
ASPECT_SHORT = 35
@@ -421,9 +421,8 @@ def cv2tensor(image: TYPE_IMAGE, mask:bool=False) -> torch.Tensor:
def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=0) -> Tuple[torch.Tensor, ...]:
rgba = image_convert(image, 4)
rgb = image_convert(image, 3)
# rgb = image_matte(image, matte)[:,:,:3]
mask = image_mask(image)
rgb = image_matte(rgba, matte)[:,:,:3]
mask = image_mask(rgba)
rgba = torch.from_numpy(rgba.astype(np.float32) / 255.0).unsqueeze(0)
rgb = torch.from_numpy(rgb.astype(np.float32) / 255.0).unsqueeze(0)
mask = torch.from_numpy(mask.astype(np.float32) / 255.0)
@@ -432,7 +431,7 @@ def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=0) -> Tuple[torch.Tensor,
def hsv2bgr(hsl_color: TYPE_PIXEL) -> TYPE_PIXEL:
return cv2.cvtColor(np.uint8([[hsl_color]]), cv2.COLOR_HSV2BGR)[0, 0]
def image2bgr(image: TYPE_IMAGE) -> Tuple[int, TYPE_IMAGE, TYPE_IMAGE]:
def image2bgr(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, TYPE_IMAGE, int]:
"""RGB Helper function.
Return channel count, BGR, and Alpha.
"""
@@ -463,16 +462,22 @@ def pil2tensor(image: Image.Image) -> torch.Tensor:
def tensor2cv(tensor: torch.Tensor) -> TYPE_IMAGE:
"""Convert a torch Tensor to a numpy ndarray."""
tensor = tensor.cpu().squeeze().numpy()
if len(tensor.shape) < 3:
if tensor.ndim == 1:
tensor = np.expand_dims(tensor, -1)
return np.clip(255.0 * tensor, 0, 255).astype(np.uint8)
# return sRGB2Linear(255.0 * tensor)
image = np.clip(255.0 * tensor, 0, 255).astype(np.uint8)
if image.shape[2] == 4:
image_flatten_mask
mask = image_mask(image)
image = image_blend(image, image, mask)
image = image_mask_add(image, mask)
return image
def tensor2pil(tensor: torch.Tensor) -> Image.Image:
"""Convert a torch Tensor to a PIL Image.
Tensor should be HxWxC [no batch].
"""
tensor = np.clip(255. * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
tensor = tensor.cpu().numpy().squeeze()
tensor = np.clip(255. * tensor, 0, 255).astype(np.uint8)
return Image.fromarray(tensor)
def mixlabLayer2cv(layer: dict) -> torch.Tensor:
@@ -757,8 +762,8 @@ def image_blend(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, mask:Optional[TYPE_IMAGE
image = blendLayers(imageA, imageB, blendOp.value, alpha)
image = pil2cv(image)
if mask is not None:
image = image_mask_add(image, mask)
#if mask is not None:
# image = image_mask_add(image, mask)
return image_crop_center(image, w, h)
@@ -1227,6 +1232,7 @@ def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), end:Tuple[int
return tensor2cv(output_image), mask.cpu().numpy().astype(np.uint8) * 255
def image_flatten_mask(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, TYPE_IMAGE|None]:
"""Flatten the image with its own alpha channel, if any."""
mask = image_mask(image)
return image_blend(image, image, mask), mask
@@ -1379,10 +1385,28 @@ def image_hsv(image: TYPE_IMAGE, hue: float, saturation: float, value: float) ->
return bgr2image(image, alpha, cc == 1)
def image_invert(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE:
"""
Invert an Grayscale, RGB or RGBA image using a specified inversion intensity.
Parameters:
- image: Input image as a NumPy array (RGB or RGBA).
- value: Float between 0 and 1 representing the intensity of inversion (0: no inversion, 1: full inversion).
Returns:
- Inverted image.
"""
# Clip the value to be within [0, 1] and scale to [0, 255]
value = np.clip(value, 0, 1)
image, alpha, cc = image2bgr(image)
image = cv2.addWeighted(image, 1 - value, 255 - image, value, 0)
return bgr2image(image, alpha, cc == 1)
if image.ndim == 3 and image.shape[2] == 4:
rgb = image[:, :, :3]
alpha = image[:, :, 3]
mask = alpha > 0
inverted_rgb = 255 - rgb
image = np.where(mask[:, :, None], (1 - value) * rgb + value * inverted_rgb, rgb)
return np.dstack((image.astype(np.uint8), alpha))
inverted_image = 255 - image
return ((1 - value) * image + value * inverted_image).astype(np.uint8)
def image_lerp(imageA: TYPE_IMAGE, imageB:TYPE_IMAGE, mask:TYPE_IMAGE=None,
alpha:float=1.) -> TYPE_IMAGE:
@@ -1775,7 +1799,7 @@ def image_scale(image: TYPE_IMAGE, scale:TYPE_COORD=(1.0, 1.0), sample:EnumInter
return image_affine_edge(image, scale_func, edge)
def image_scalefit(image: TYPE_IMAGE, width: int, height:int,
mode:EnumScaleMode=EnumScaleMode.NONE,
mode:EnumScaleMode=EnumScaleMode.MATTE,
sample:EnumInterpolation=EnumInterpolation.LANCZOS4,
matte:TYPE_PIXEL=(0,0,0,0)) -> TYPE_IMAGE:
+62
View File
@@ -0,0 +1,62 @@
/**
* File: core_cozy_menu.js
* Project: Jovimetrix
*/
import { app } from "../../../scripts/app.js"
import { CONVERTED_TYPE, widgetToInput } from '../util/util_widget.js'
app.registerExtension({
name: "jovimetrix.cozy.menu",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (!nodeData.name.includes("(JOV)")) {
return;
}
let matchingTypes = [];
const inputTypes = nodeData.input;
if (inputTypes) {
matchingTypes = ['required', 'optional']
.flatMap(type => Object.entries(inputTypes[type] || [])
);
if (matchingTypes.length == 0) {
return;
}
}
// MENU CONVERSIONS
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = async function (_, options) {
const me = getExtraMenuOptions?.apply(this, arguments);
if (this.widgets === undefined) {
return me;
}
const widgetToInputArray = [];
const widgets = Object.values(this.widgets);
for (const [widgetName, widgetType] of matchingTypes) {
const widget = widgets.find(m => m.name === widgetName);
if (widget && !widget.type.startsWith(CONVERTED_TYPE) &&
(widget.options?.forceInput === undefined || widget.options?.forceInput === false) &&
widget.options?.menu !== false) {
const widgetToInputObject = {
content: `Convert ${widget.name} to input`,
callback: () => widgetToInput(this, widget, widgetType)
};
widgetToInputArray.push(widgetToInputObject);
}
}
// remove all the options that start with the word "Convert" from the options...
if (options) {
options = options.filter(option => {
return typeof option?.content !== 'string' || !option?.content.startsWith('Convert');
});
}
if (widgetToInputArray.length) {
options.push(...widgetToInputArray, null);
}
return me;
};
}
})
+1
View File
@@ -17,6 +17,7 @@ if (!window.jovimetrixEvents) {
const jovimetrixEvents = window.jovimetrixEvents;
const JOV_HELP_URL = "./api/jovimetrix/doc";
const JOV_WEBWIKI_URL = "https://github.com/Amorano/Jovimetrix/wiki/Z.-REFERENCE#";
async function load_help(name, custom_data) {
// overwrite
-1
View File
@@ -37,7 +37,6 @@ app.registerExtension({
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = async function (message) {
const me = onExecuted?.apply(this, arguments)
let lineCount = 0;
if (this.widgets) {
for (let i = 2; i < this.widgets.length; i++) {
if (this.widgets[i].name.startsWith("jovi_")) {
+159
View File
@@ -0,0 +1,159 @@
/**
* File: queue_too.js
* Project: Jovimetrix
*
*/
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js"
import { apiJovimetrix } from '../util/util_api.js'
import { flashBackgroundColor } from '../util/util_fun.js'
import { nodeFitHeight, TypeSlotEvent, TypeSlot } from '../util/util_node.js'
import { widgetHide, widgetShow } from '../util/util_widget.js'
import { widgetSizeModeHook } from '../util/util_jov.js'
const _id = "QUEUE TOO (JOV) 🗃";
const _prefix = '🦄';
const EVENT_JOVI_PING = "jovi-queue-ping";
const EVENT_JOVI_DONE = "jovi-queue-done";
app.registerExtension({
name: 'jovimetrix.node.' + _id,
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name !== _id) {
return;
}
function update_report(self) {
self.widget_report.value = `[${self.data_index} / ${self.data_all.length}]\n${self.data_current}`;
app.canvas.setDirty(true);
}
function update_list(self, value) {
self.data_count = value.length;
self.data_index = 1;
self.data_current = "";
update_report(self);
apiJovimetrix(self.id, "reset");
}
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = async function () {
const me = onNodeCreated?.apply(this);
const self = this;
this.data_index = 1;
this.data_current = "";
this.data_all = [];
widgetSizeModeHook(this);
const widget_queue = this.widgets.find(w => w.name === 'Q');
const widget_hold = this.widgets.find(w => w.name === '✋🏽');
const widget_reset = this.widgets.find(w => w.name === 'RESET');
const widget_value = this.widgets.find(w => w.name === 'VAL');
widget_value.callback = async() => {
widgetHide(this, widget_hold);
widgetHide(this, widget_reset);
if (widget_value.value == 0) {
widgetShow(widget_reset);
widgetShow(widget_hold);
}
nodeFitHeight(this);
}
widget_queue?.inputEl.addEventListener('input', function () {
const value = widget_queue.value.split('\n');
update_list(self, value);
});
widget_reset.callback = async() => {
widget_reset.value = false;
apiJovimetrix(self.id, "reset");
}
this.widget_report = ComfyWidgets.STRING(this, 'QUEUE IS EMPTY 🔜', [
'STRING', {
multiline: true,
},
], app).widget;
this.widget_report.inputEl.readOnly = true;
this.widget_report.serializeValue = async () => { };
async function python_queue_ping(event) {
if (event.detail.id != self.id) {
return;
}
self.data_index = event.detail.i;
self.data_all = event.detail.l;
self.data_current = event.detail.c;
update_report(self);
}
// Add names to list control that collapses. And counter to see where we are in the overall
async function python_queue_done(event) {
if (event.detail.id != self.id) {
return;
}
await flashBackgroundColor(self.widget_queue.inputEl, 650, 4, "#995242CC");
}
api.addEventListener(EVENT_JOVI_PING, python_queue_ping);
api.addEventListener(EVENT_JOVI_DONE, python_queue_done);
this.onDestroy = () => {
api.removeEventListener(EVENT_JOVI_PING, python_queue_ping);
api.removeEventListener(EVENT_JOVI_DONE, python_queue_done);
};
setTimeout(() => { widget_value.callback(); }, 10);
return me;
}
const onConnectOutput = nodeType.prototype.onConnectOutput;
nodeType.prototype.onConnectOutput = function(outputIndex, inputType, inputSlot, inputNode) {
if (outputIndex == 0 && inputType == "COMBO") {
// can link the "same" list -- user breaks it past that, their problem atm.
const widget_queue = this.widgets.find(w => w.name === 'Q');
const widget = inputNode.widgets.find(w => w.name === inputSlot.name);
const values = widget.options.values.join('\n');
if (this.outputs[0].name != _prefix && widget_queue.value != values) {
return false;
}
}
return onConnectOutput?.apply(this, arguments);
}
const onConnectionsChange = nodeType.prototype.onConnectionsChange;
nodeType.prototype.onConnectionsChange = function (slotType, slot, event, link_info)
//side, slot, connected, link_info
{
if (slotType === TypeSlot.Output && slot == 0 && link_info && event === TypeSlotEvent.Connect) {
const node = app.graph.getNodeById(link_info.target_id);
if (node === undefined || node.inputs === undefined) {
return;
}
const target = node.inputs[link_info.target_slot];
if (target === undefined) {
return;
}
const widget = node.widgets?.find(w => w.name === target.name);
if (widget === undefined) {
return;
}
this.outputs[0].name = widget.name;
if (widget?.origType == "combo" || widget.type == "COMBO") {
const values = widget.options.values;
const widget_queue = this.widgets.find(w => w.name === 'Q');
// remove all connections that don't match the list?
widget_queue.value = values.join('\n');
update_list(this, values);
}
this.outputs[0].name = _prefix;
}
return onConnectionsChange?.apply(this, arguments);
};
}
})
+2 -2
View File
@@ -16,10 +16,10 @@ export function widgetSizeModeHook(node, wh_hide=true) {
widgetHide(node, wh);
widgetHide(node, samp);
if (!['NONE'].includes(mode.value)) {
if (!['MATTE'].includes(mode.value)) {
widgetShow(wh);
}
if (!['NONE', 'CROP', 'MATTE'].includes(mode.value)) {
if (!['CROP', 'MATTE'].includes(mode.value)) {
widgetShow(samp);
}
nodeFitHeight(node);
+6 -5
View File
@@ -38,9 +38,12 @@ export const nodeCleanup = (node) => {
}
export function nodeFitHeight(node) {
node.setSize(node.computeSize());
const size_old = node.size;
const size = node.computeSize();
node.setDirtyCanvas(true, true);
app.graph.setDirtyCanvas(true, true);
node.setSize([size_old[0], size[1]]);
return;
}
/**
@@ -114,6 +117,7 @@ export function nodeAddDynamic(nodeType, prefix, dynamic_type='*', index_start=0
const onConnectionsChange = nodeType.prototype.onConnectionsChange
nodeType.prototype.onConnectionsChange = function (slotType, slot_idx, event, link_info, node_slot) {
let size = this.size;
const me = onConnectionsChange?.apply(this, arguments);
if (slotType === TypeSlot.Input && slot_idx >= index_start) {
if (link_info && event === TypeSlotEvent.Connect) {
@@ -135,11 +139,8 @@ export function nodeAddDynamic(nodeType, prefix, dynamic_type='*', index_start=0
this.addInput(prefix, dynamic_type);
}
}
if (refresh) {
setTimeout(() => {
clean_inputs(this);
}, 5);
clean_inputs(this);
}
nodeFitHeight(this);
return me;