Files

426 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
""" Jovimetrix - Transform """
import sys
from enum import Enum
from comfy.utils import ProgressBar
from cozy_comfyui import \
logger, \
IMAGE_SIZE_MIN, \
InputType, RGBAMaskType, EnumConvertType, \
deep_merge, parse_param, parse_dynamic, zip_longest_fill
from cozy_comfyui.lexicon import \
Lexicon
from cozy_comfyui.node import \
COZY_TYPE_IMAGE, \
CozyImageNode, CozyBaseNode
from cozy_comfyui.image.channel import \
channel_solid
from cozy_comfyui.image.convert import \
tensor_to_cv, cv_to_tensor_full, cv_to_tensor, image_mask, image_mask_add
from cozy_comfyui.image.compose import \
EnumOrientation, EnumEdge, EnumMirrorMode, EnumScaleMode, EnumInterpolation, \
image_edge_wrap, image_mirror, image_scalefit, image_transform, \
image_crop, image_crop_center, image_crop_polygonal, image_stacker, \
image_flatten
from cozy_comfyui.image.misc import \
image_stack
from cozy_comfyui.image.mapping import \
EnumProjection, \
remap_fisheye, remap_perspective, remap_polar, remap_sphere
# ==============================================================================
# === GLOBAL ===
# ==============================================================================
JOV_CATEGORY = "TRANSFORM"
# ==============================================================================
# === ENUMERATION ===
# ==============================================================================
class EnumCropMode(Enum):
CENTER = 20
XY = 0
FREE = 10
# ==============================================================================
# === CLASS ===
# ==============================================================================
class CropNode(CozyImageNode):
NAME = "CROP (JOV) ✂️"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
Extract a portion of an input image or resize it. It supports various cropping modes, including center cropping, custom XY cropping, and free-form polygonal cropping. This node is useful for preparing image data for specific tasks or extracting regions of interest.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.FUNCTION: (EnumCropMode._member_names_, {
"default": EnumCropMode.CENTER.name}),
Lexicon.XY: ("VEC2", {
"default": (0, 0), "mij": 0, "maj": 1,
"label": ["X", "Y"]}),
Lexicon.WH: ("VEC2", {
"default": (512, 512), "mij": IMAGE_SIZE_MIN, "int": True,
"label": ["W", "H"]}),
Lexicon.TLTR: ("VEC4", {
"default": (0, 0, 0, 1), "mij": 0, "maj": 1,
"label": ["TOP", "LEFT", "TOP", "RIGHT"],}),
Lexicon.BLBR: ("VEC4", {
"default": (1, 0, 1, 1), "mij": 0, "maj": 1,
"label": ["BOTTOM", "LEFT", "BOTTOM", "RIGHT"],}),
Lexicon.MATTE: ("VEC4", {
"default": (0, 0, 0, 255), "rgb": True,})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
pA = parse_param(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
func = parse_param(kw, Lexicon.FUNCTION, EnumCropMode, EnumCropMode.CENTER.name)
# if less than 1 then use as scalar, over 1 = int(size)
xy = parse_param(kw, Lexicon.XY, EnumConvertType.VEC2, (0, 0,))
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, (512, 512), IMAGE_SIZE_MIN)
tltr = parse_param(kw, Lexicon.TLTR, EnumConvertType.VEC4, (0, 0, 0, 1,))
blbr = parse_param(kw, Lexicon.BLBR, EnumConvertType.VEC4, (1, 0, 1, 1,))
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)
params = list(zip_longest_fill(pA, func, xy, wihi, tltr, blbr, matte))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, func, xy, wihi, tltr, blbr, matte) in enumerate(params):
width, height = wihi
pA = tensor_to_cv(pA) if pA is not None else channel_solid(width, height)
alpha = None
if pA.ndim == 3 and pA.shape[2] == 4:
alpha = image_mask(pA)
if func == EnumCropMode.FREE:
x1, y1, x2, y2 = tltr
x4, y4, x3, y3 = blbr
points = (x1 * width, y1 * height), (x2 * width, y2 * height), \
(x3 * width, y3 * height), (x4 * width, y4 * height)
pA = image_crop_polygonal(pA, points)
if alpha is not None:
alpha = image_crop_polygonal(alpha, points)
pA[..., 3] = alpha[..., 0][:,:]
elif func == EnumCropMode.XY:
pA = image_crop(pA, width, height, xy)
else:
pA = image_crop_center(pA, width, height)
images.append(cv_to_tensor_full(pA, matte))
pbar.update_absolute(idx)
return image_stack(images)
class FlattenNode(CozyImageNode):
NAME = "FLATTEN (JOV) ⬇️"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
Combine multiple input images into a single image by summing their pixel values. This operation is useful for merging multiple layers or images into one composite image, such as combining different elements of a design or merging masks. Users can specify the blending mode and interpolation method to control how the images are combined. Additionally, a matte can be applied to adjust the transparency of the final composite image.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.MODE: (EnumScaleMode._member_names_, {
"default": EnumScaleMode.MATTE.name,}),
Lexicon.WH: ("VEC2", {
"default": (512, 512), "mij":1, "int": True,
"label": ["W", "H"]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {
"default": EnumInterpolation.LANCZOS4.name,}),
Lexicon.MATTE: ("VEC4", {
"default": (0, 0, 0, 255), "rgb": True,}),
Lexicon.OFFSET: ("VEC2", {
"default": (0, 0), "mij":0, "int": True,
"label": ["X", "Y"]}),
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
imgs = parse_dynamic(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
if imgs is None:
logger.warning("no images to flatten")
return ()
# be less dumb when merging
pA = [tensor_to_cv(i) for i in imgs]
mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name)[0]
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, (512, 512), 1)[0]
sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)[0]
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)[0]
offset = parse_param(kw, Lexicon.OFFSET, EnumConvertType.VEC2INT, (0, 0), 0)[0]
w, h = wihi
x, y = offset
pA = image_flatten(pA, x, y, w, h, mode=mode, sample=sample)
pA = [cv_to_tensor_full(pA, matte)]
return image_stack(pA)
class SplitNode(CozyBaseNode):
NAME = "SPLIT (JOV) 🎭"
CATEGORY = JOV_CATEGORY
RETURN_TYPES = ("IMAGE", "IMAGE",)
RETURN_NAMES = ("IMAGEA", "IMAGEB",)
OUTPUT_TOOLTIPS = (
"Left/Top image",
"Right/Bottom image"
)
DESCRIPTION = """
Split an image into two or four images based on the percentages for width and height.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.VALUE: ("FLOAT", {
"default": 0.5, "min": 0, "max": 1, "step": 0.001
}),
Lexicon.FLIP: ("BOOLEAN", {
"default": False,
"tooltip": "Horizontal split (False) or Vertical split (True)"
}),
Lexicon.MODE: (EnumScaleMode._member_names_, {
"default": EnumScaleMode.MATTE.name,}),
Lexicon.WH: ("VEC2", {
"default": (512, 512), "mij":IMAGE_SIZE_MIN, "int": True,
"label": ["W", "H"]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {
"default": EnumInterpolation.LANCZOS4.name,}),
Lexicon.MATTE: ("VEC4", {
"default": (0, 0, 0, 255), "rgb": True,})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
pA = parse_param(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
percent = parse_param(kw, Lexicon.VALUE, EnumConvertType.FLOAT, 0.5, 0, 1)
flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.BOOLEAN, False)
mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, (512, 512), IMAGE_SIZE_MIN)
sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)
params = list(zip_longest_fill(pA, percent, flip, mode, wihi, sample, matte))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, percent, flip, mode, wihi, sample, matte) in enumerate(params):
w, h = wihi
pA = channel_solid(w, h, matte) if pA is None else tensor_to_cv(pA)
if flip:
size = pA.shape[1]
percent = max(1, min(size-1, int(size * percent)))
image_a = pA[:, :percent]
image_b = pA[:, percent:]
else:
size = pA.shape[0]
percent = max(1, min(size-1, int(size * percent)))
image_a = pA[:percent, :]
image_b = pA[percent:, :]
if mode != EnumScaleMode.MATTE:
image_a = image_scalefit(image_a, w, h, mode, sample)
image_b = image_scalefit(image_b, w, h, mode, sample)
images.append([cv_to_tensor(img) for img in [image_a, image_b]])
pbar.update_absolute(idx)
return image_stack(images)
class StackNode(CozyImageNode):
NAME = "STACK (JOV) ➕"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
Merge multiple input images into a single composite image by stacking them along a specified axis.
Options include axis, stride, scaling mode, width and height, interpolation method, and matte color.
The axis parameter allows for horizontal, vertical, or grid stacking of images, while stride controls the spacing between them.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.AXIS: (EnumOrientation._member_names_, {
"default": EnumOrientation.GRID.name,}),
Lexicon.STEP: ("INT", {
"default": 1, "min": 0,
"tooltip":"How many images are placed before a new row starts (stride)"}),
Lexicon.MODE: (EnumScaleMode._member_names_, {
"default": EnumScaleMode.MATTE.name,}),
Lexicon.WH: ("VEC2", {
"default": (512, 512), "mij": IMAGE_SIZE_MIN, "int": True,
"label": ["W", "H"]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {
"default": EnumInterpolation.LANCZOS4.name,}),
Lexicon.MATTE: ("VEC4", {
"default": (0, 0, 0, 255), "rgb": True,})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
images = parse_dynamic(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
if len(images) == 0:
logger.warning("no images to stack")
return
images = [tensor_to_cv(i) for i in images]
axis = parse_param(kw, Lexicon.AXIS, EnumOrientation, EnumOrientation.GRID.name)[0]
stride = parse_param(kw, Lexicon.STEP, EnumConvertType.INT, 1, 0)[0]
mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name)[0]
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, (512, 512), IMAGE_SIZE_MIN)[0]
sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)[0]
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)[0]
img = image_stacker(images, axis, stride) #, matte)
if mode != EnumScaleMode.MATTE:
w, h = wihi
img = image_scalefit(img, w, h, mode, sample)
rgba, rgb, mask = cv_to_tensor_full(img, matte)
return rgba.unsqueeze(0), rgb.unsqueeze(0), mask.unsqueeze(0)
class TransformNode(CozyImageNode):
NAME = "TRANSFORM (JOV) 🏝️"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
Apply various geometric transformations to images, including translation, rotation, scaling, mirroring, tiling and perspective projection. It offers extensive control over image manipulation to achieve desired visual effects.
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES(prompt=True, dynprompt=True)
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.MASK: (COZY_TYPE_IMAGE, {
"tooltip": "Override Image mask"}),
Lexicon.XY: ("VEC2", {
"default": (0, 0,), "mij": -1, "maj": 1,
"label": ["X", "Y"]}),
Lexicon.ANGLE: ("FLOAT", {
"default": 0, "min": -sys.float_info.max, "max": sys.float_info.max, "step": 0.1,}),
Lexicon.SIZE: ("VEC2", {
"default": (1, 1), "mij": 0.001,
"label": ["X", "Y"]}),
Lexicon.TILE: ("VEC2", {
"default": (1, 1), "mij": 1,
"label": ["X", "Y"]}),
Lexicon.EDGE: (EnumEdge._member_names_, {
"default": EnumEdge.CLIP.name}),
Lexicon.MIRROR: (EnumMirrorMode._member_names_, {
"default": EnumMirrorMode.NONE.name}),
Lexicon.PIVOT: ("VEC2", {
"default": (0.5, 0.5), "mij": 0, "maj": 1, "step": 0.01,
"label": ["X", "Y"]}),
Lexicon.PROJECTION: (EnumProjection._member_names_, {
"default": EnumProjection.NORMAL.name}),
Lexicon.TLTR: ("VEC4", {
"default": (0, 0, 1, 0), "mij": 0, "maj": 1, "step": 0.005,
"label": ["TOP", "LEFT", "TOP", "RIGHT"],}),
Lexicon.BLBR: ("VEC4", {
"default": (0, 1, 1, 1), "mij": 0, "maj": 1, "step": 0.005,
"label": ["BOTTOM", "LEFT", "BOTTOM", "RIGHT"],}),
Lexicon.STRENGTH: ("FLOAT", {
"default": 1, "min": 0, "max": 1, "step": 0.005}),
Lexicon.MODE: (EnumScaleMode._member_names_, {
"default": EnumScaleMode.MATTE.name,}),
Lexicon.WH: ("VEC2", {
"default": (512, 512), "mij": IMAGE_SIZE_MIN, "int": True,
"label": ["W", "H"]}),
Lexicon.SAMPLE: (EnumInterpolation._member_names_, {
"default": EnumInterpolation.LANCZOS4.name,}),
Lexicon.MATTE: ("VEC4", {
"default": (0, 0, 0, 255), "rgb": True,})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
pA = parse_param(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
mask = parse_param(kw, Lexicon.MASK, EnumConvertType.IMAGE, None)
offset = parse_param(kw, Lexicon.XY, EnumConvertType.VEC2, (0, 0), -1, 1)
angle = parse_param(kw, Lexicon.ANGLE, EnumConvertType.FLOAT, 0)
size = parse_param(kw, Lexicon.SIZE, EnumConvertType.VEC2, (1, 1), 0.001)
edge = parse_param(kw, Lexicon.EDGE, EnumEdge, EnumEdge.CLIP.name)
mirror = parse_param(kw, Lexicon.MIRROR, EnumMirrorMode, EnumMirrorMode.NONE.name)
mirror_pivot = parse_param(kw, Lexicon.PIVOT, EnumConvertType.VEC2, (0.5, 0.5), 0, 1)
tile_xy = parse_param(kw, Lexicon.TILE, EnumConvertType.VEC2, (1, 1), 1)
proj = parse_param(kw, Lexicon.PROJECTION, EnumProjection, EnumProjection.NORMAL.name)
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, EnumScaleMode, EnumScaleMode.MATTE.name)
wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, (512, 512), IMAGE_SIZE_MIN)
sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)
matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)
params = list(zip_longest_fill(pA, mask, offset, angle, size, edge, tile_xy, mirror, mirror_pivot, proj, strength, tltr, blbr, mode, wihi, sample, matte))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask, offset, angle, size, edge, tile_xy, mirror, mirror_pivot, proj, strength, tltr, blbr, mode, wihi, sample, matte) in enumerate(params):
pA = tensor_to_cv(pA) if pA is not None else channel_solid()
if mask is None:
mask = image_mask(pA, 255)
else:
mask = tensor_to_cv(mask)
pA = image_mask_add(pA, mask)
h, w = pA.shape[:2]
pA = image_transform(pA, offset, angle, size, sample, edge)
pA = image_crop_center(pA, w, h)
if mirror != EnumMirrorMode.NONE:
mpx, mpy = mirror_pivot
pA = image_mirror(pA, mirror, mpx, mpy)
pA = image_scalefit(pA, w, h, EnumScaleMode.FIT, sample)
tx, ty = tile_xy
if tx != 1. or ty != 1.:
pA = image_edge_wrap(pA, tx / 2 - 0.5, ty / 2 - 0.5)
pA = image_scalefit(pA, w, h, EnumScaleMode.FIT, sample)
match proj:
case EnumProjection.PERSPECTIVE:
x1, y1, x2, y2 = tltr
x4, y4, x3, y3 = blbr
sh, sw = pA.shape[:2]
x1, x2, x3, x4 = map(lambda x: x * sw, [x1, x2, x3, x4])
y1, y2, y3, y4 = map(lambda y: y * sh, [y1, y2, y3, y4])
pA = remap_perspective(pA, [[x1, y1], [x2, y2], [x3, y3], [x4, y4]])
case EnumProjection.SPHERICAL:
pA = remap_sphere(pA, strength)
case EnumProjection.FISHEYE:
pA = remap_fisheye(pA, strength)
case EnumProjection.POLAR:
pA = remap_polar(pA)
if proj != EnumProjection.NORMAL:
pA = image_scalefit(pA, w, h, EnumScaleMode.FIT, sample)
if mode != EnumScaleMode.MATTE:
w, h = wihi
pA = image_scalefit(pA, w, h, mode, sample)
images.append(cv_to_tensor_full(pA, matte))
pbar.update_absolute(idx)
return image_stack(images)