*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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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_")) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
})
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user