assume all tensors and numpy are RGB(A). Convert to BGR on CV2 requirement

Added explicit MASK option for Pixel Split
levels function tweak
This commit is contained in:
Alexander G. Morano
2025-05-10 03:37:06 -04:00
parent 9c8cd9a502
commit 193fe7d2a9
9 changed files with 328 additions and 242 deletions
+133 -73
View File
@@ -22,16 +22,15 @@ from cozy_comfyui.image import \
EnumImageType
from cozy_comfyui.image.convert import \
image_mask, image_convert, tensor_to_cv, cv_to_tensor_full
tensor_to_cv, cv_to_tensor_full
from cozy_comfyui.image.misc import \
image_stack
from ..sup.image.adjust import \
image_contrast, image_equalize, image_gamma, \
image_contrast, image_brightness, image_equalize, image_gamma, \
image_hsv, image_invert, image_pixelate, image_posterize, \
image_quantize, image_sharpen, \
morph_edge_detect, morph_emboss
image_quantize, image_sharpen, morph_edge_detect, morph_emboss
from ..sup.image.channel import \
channel_solid
@@ -41,7 +40,6 @@ from ..sup.image.compose import \
JOV_CATEGORY = "ADJUST"
# ==============================================================================
# === ENUMERATION ===
# ==============================================================================
@@ -62,15 +60,18 @@ class EnumAdjustEdge(Enum):
OPEN = 70
CLOSE = 80
class EnumAdjustLight(Enum):
BRIGHTNESS = 10
CONTRAST = 20
EQUALIZE = 30
EXPOSURE = 40
GAMMA = 50
class EnumAdjustPixel(Enum):
PIXELATE = 10
QUANTIZE = 20
POSTERIZE = 30
class EnumAdjustLight(Enum):
CONTRAST = 10
GAMMA = 20
# ==============================================================================
# === CLASS ===
# ==============================================================================
@@ -140,7 +141,7 @@ Advanced options include pixelation, quantization, and morphological operations
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask, op, radius, val, edges, level, equalize, hsv, contrast, gamma, matte, invert) in enumerate(params):
pA = tensor_to_cv(pA) if pA is not None else channel_solid(chan=EnumImageType.BGR)
pA = tensor_to_cv(pA) if pA is not None else channel_solid()
img_new = image_convert(pA, 3)
match op:
@@ -211,7 +212,7 @@ Advanced options include pixelation, quantization, and morphological operations
img_new = image_levels(img_new, l, h, m, gamma)
if contrast != 0:
img_new = image_contrast(img_new, 1. - contrast)
img_new = image_contrast(img_new, contrast)
if gamma != 0:
img_new = image_gamma(img_new, gamma)
@@ -233,7 +234,7 @@ Advanced options include pixelation, quantization, and morphological operations
return image_stack(images)
'''
class BlurAdjustNode(CozyImageNode):
class AdjustBlurNode(CozyImageNode):
NAME = "BLUR (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
@@ -288,7 +289,7 @@ Enhance and modify images with various blur effects.
pbar.update_absolute(idx)
return image_stack(images)
class EdgeAdjustNode(CozyImageNode):
class AdjustEdgeNode(CozyImageNode):
NAME = "EDGE (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
@@ -363,7 +364,125 @@ Enhanced edge detection.
pbar.update_absolute(idx)
return image_stack(images)
class PixelAdjustNode(CozyImageNode):
class AdjustLevelNode(CozyImageNode):
NAME = "LEVELS (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.MASK: (COZY_TYPE_IMAGE, {}),
Lexicon.LMH: ("VEC3", {
"default": (0,0.5,1), "mij": 0, "maj": 1., "step": 0.01,
"label": ["LOW", "MID", "HIGH"]}),
Lexicon.RANGE: ("VEC2", {
"default": (0, 1), "mij": 0, "maj": 1., "step": 0.01,
"label": ["IN", "OUT"]})
}
})
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.MASK, None)
LMH = parse_param(kw, Lexicon.LMH, EnumConvertType.VEC3, (0,0.5,1))
inout = parse_param(kw, Lexicon.RANGE, EnumConvertType.VEC2, (0,1))
params = list(zip_longest_fill(pA, mask, LMH, inout))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask, LMH, inout) in enumerate(params):
pA = channel_solid() if pA is None else tensor_to_cv(pA)
height, width = pA.shape[:2]
mask = channel_solid(width, height, 255) if mask is None else tensor_to_cv(mask)
'''
h, s, v = hsv
img_new = image_hsv(img_new, h, s, v)
'''
low, mid, high = LMH
start, end = inout
# mid = min(high, max(mid, low))
pA = image_levels(pA, low, mid, high, start, end)
#print(pA.shape, img_new.shape, mask.shape)
#pA = image_blend(pA, img_new, mask)
images.append(cv_to_tensor_full(pA))
pbar.update_absolute(idx)
return image_stack(images)
class AdjustLightNode(CozyImageNode):
NAME = "LIGHT (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.MASK: (COZY_TYPE_IMAGE, {}),
Lexicon.FUNCTION: (EnumAdjustLight._member_names_, {
"default": EnumAdjustLight.CONTRAST.name,}),
Lexicon.VALUE: ("FLOAT", {
"default": 0, "min": -1, "max": 1, "step": 0.001}),
}
})
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.MASK, None)
op = parse_param(kw, Lexicon.FUNCTION, EnumAdjustLight, EnumAdjustLight.CONTRAST.name)
val = parse_param(kw, Lexicon.VALUE, EnumConvertType.FLOAT, 0)
params = list(zip_longest_fill(pA, mask, op, val))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask, op, val) in enumerate(params):
pA = channel_solid() if pA is None else tensor_to_cv(pA)
height, width = pA.shape[:2]
mask = channel_solid(width, height, (255,255,255,255)) if mask is None else tensor_to_cv(mask)
match op:
case EnumAdjustLight.BRIGHTNESS:
img_new = image_contrast(pA, val)
case EnumAdjustLight.CONTRAST:
img_new = image_contrast(pA, val)
case EnumAdjustLight.EQUALIZE:
img_new = image_equalize(pA)
case EnumAdjustLight.EXPOSURE:
img_new = image_contrast(pA, val)
case EnumAdjustLight.GAMMA:
img_new = image_gamma(pA, val)
'''
h, s, v = hsv
img_new = image_hsv(img_new, h, s, v)
l, m, h = level
img_new = image_levels(img_new, l, h, m, gamma)
'''
pA = image_blend(pA, img_new, mask)
images.append(cv_to_tensor_full(pA))
pbar.update_absolute(idx)
return image_stack(images)
class AdjustPixelNode(CozyImageNode):
NAME = "PIXEL (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
@@ -412,62 +531,3 @@ class PixelAdjustNode(CozyImageNode):
images.append(cv_to_tensor_full(pA))
pbar.update_absolute(idx)
return image_stack(images)
class LightAdjustNode(CozyImageNode):
NAME = "LIGHT (JOV)"
CATEGORY = JOV_CATEGORY
DESCRIPTION = """
"""
@classmethod
def INPUT_TYPES(cls) -> InputType:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.MASK: (COZY_TYPE_IMAGE, {}),
Lexicon.FUNCTION: (EnumAdjustLight._member_names_, {
"default": EnumAdjustLight.CONTRAST.name,}),
Lexicon.VALUE: ("FLOAT", {
"default": 1, "min": 0, "step": 0.01}),
}
})
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.MASK, None)
op = parse_param(kw, Lexicon.FUNCTION, EnumAdjustLight, EnumAdjustLight.PIXELATE.name)
val = parse_param(kw, Lexicon.VALUE, EnumConvertType.FLOAT, 0, 0)
params = list(zip_longest_fill(pA, mask, op, val))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask, op, val) in enumerate(params):
pA = channel_solid() if pA is None else tensor_to_cv(pA)
height, width = pA.shape[:2]
mask = channel_solid(width, height, (255,255,255,255)) if mask is None else tensor_to_cv(mask)
match op:
case EnumAdjustLight.CONTRAST:
img_new = image_contrast(pA, 1. - val)
case EnumAdjustLight.GAMMA:
img_new = image_gamma(pA, val)
'''
h, s, v = hsv
img_new = image_hsv(img_new, h, s, v)
if equalize:
img_new = image_equalize(img_new)
l, m, h = level
img_new = image_levels(img_new, l, h, m, gamma)
'''
pA = image_blend(pA, img_new, mask)
images.append(cv_to_tensor_full(pA))
pbar.update_absolute(idx)
return image_stack(images)
+13 -13
View File
@@ -20,9 +20,6 @@ from cozy_comfyui.node import \
COZY_TYPE_IMAGE, \
CozyBaseNode, CozyImageNode
from cozy_comfyui.image import \
EnumImageType
from cozy_comfyui.image.convert import \
image_mask, image_mask_add, tensor_to_cv, \
cv_to_tensor, cv_to_tensor_full
@@ -95,7 +92,7 @@ Simulate color blindness effects on images. You can select various types of colo
images = []
pbar = ProgressBar(len(params))
for idx, (pA, deficiency, simulator, severity) in enumerate(params):
pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor_to_cv(pA)
pA = channel_solid() if pA is None else tensor_to_cv(pA)
pA = color_blind(pA, deficiency, simulator, severity)
images.append(cv_to_tensor_full(pA))
pbar.update_absolute(idx)
@@ -154,7 +151,7 @@ Adjust the color scheme of one image to match another with the Color Match Node.
mask = None
if pA is None:
pA = channel_solid(chan=EnumImageType.BGR)
pA = channel_solid()
else:
pA = tensor_to_cv(pA)
if pA.ndim == 3 and pA.shape[2] == 4:
@@ -162,7 +159,7 @@ Adjust the color scheme of one image to match another with the Color Match Node.
# h, w = pA.shape[:2]
if pB is None:
pB = channel_solid(chan=EnumImageType.BGR)
pB = channel_solid()
else:
pB = tensor_to_cv(pB)
@@ -240,18 +237,21 @@ The top-k colors ordered from most->least used as a strip, tonal palette and 3D
pbar = ProgressBar(len(params) * sum(kcolors))
for idx, (pA, kcolors, nodes, lut_height, wihi) in enumerate(params):
if pA is None:
pA = channel_solid(chan=EnumImageType.BGRA)
pA = channel_solid()
pA = tensor_to_cv(pA)
colors = color_top_used(pA, kcolors)
# size down to 1px strip then expand to 256 for full gradient
top_colors.extend([cv_to_tensor(channel_solid(*wihi, color=c)) for c in colors])
lut_tonal.append(cv_to_tensor(color_lut_tonal(colors, width=pA.shape[1], height=lut_height)))
lut = color_lut_tonal(colors, width=pA.shape[1], height=lut_height)
lut_tonal.append(cv_to_tensor(lut))
full = color_lut_full(colors, nodes)
lut_full.append(torch.from_numpy(full))
lut_visualized.append(cv_to_tensor(color_lut_visualize(full, wihi[1])))
gradient = image_gradient_expand(color_lut_palette(colors, 1))
lut = color_lut_visualize(full, wihi[1])
lut_visualized.append(cv_to_tensor(lut))
palette = color_lut_palette(colors, 1)
gradient = image_gradient_expand(palette)
gradient = cv2.resize(gradient, wihi)
gradients.append(cv_to_tensor(gradient))
pbar.update_absolute(idx)
@@ -298,7 +298,7 @@ Users can customize the angle of separation for color calculations, offering fle
images = []
pbar = ProgressBar(len(params))
for idx, (img, target, user, invert) in enumerate(params):
img = tensor_to_cv(img) if img is not None else channel_solid(chan=EnumImageType.BGRA)
img = tensor_to_cv(img) if img is not None else channel_solid()
img = color_theory(img, user, target)
if invert:
img = (image_invert(s, 1) for s in img)
@@ -353,12 +353,12 @@ The gradient image will be translated into a single row lookup table.
params = list(zip_longest_fill(pA, gradient, reverse, mode, sample, wihi, matte))
pbar = ProgressBar(len(params))
for idx, (pA, gradient, reverse, mode, sample, wihi, matte) in enumerate(params):
pA = channel_solid(chan=EnumImageType.BGR) if pA is None else tensor_to_cv(pA)
pA = channel_solid() if pA is None else tensor_to_cv(pA)
mask = None
if pA.ndim == 3 and pA.shape[2] == 4:
mask = image_mask(pA)
gradient = channel_solid(chan=EnumImageType.BGR) if gradient is None else tensor_to_cv(gradient)
gradient = channel_solid() if gradient is None else tensor_to_cv(gradient)
pA = image_gradient_map(pA, gradient)
if mode != EnumScaleMode.MATTE:
w, h = wihi
+30 -45
View File
@@ -16,12 +16,8 @@ from cozy_comfyui.node import \
COZY_TYPE_IMAGE, \
CozyBaseNode, CozyImageNode
from cozy_comfyui.image import \
EnumImageType
from cozy_comfyui.image.convert import \
image_matte, image_convert, tensor_to_cv, \
cv_to_tensor, cv_to_tensor_full
image_mask_add, image_matte, image_convert, tensor_to_cv, cv_to_tensor, cv_to_tensor_full
from cozy_comfyui.image.misc import \
image_minmax, image_stack
@@ -121,15 +117,16 @@ Combine two input images using various blending modes, such as normal, screen, m
height, width = pA.shape[:2]
if pA is None:
pA = channel_solid(width, height, matte, chan=EnumImageType.BGRA)
pA = channel_solid(width, height, matte,)
else:
pA = tensor_to_cv(pA)
matted = pixel_eval(matte, EnumImageType.BGRA)
matted = pixel_eval(matte)
print("matted", matted)
pA = image_matte(pA, matted)
if pB is None:
clear = list(matte[:3]) + [0]
pB = channel_solid(width, height, clear, chan=EnumImageType.BGRA)
pB = channel_solid(width, height, clear)
else:
pB = tensor_to_cv(pB)
@@ -145,13 +142,9 @@ Combine two input images using various blending modes, such as normal, screen, m
imgs += [mask]
_, w, h = image_by_size(imgs)
print(w, h)
pA = image_scalefit(pA, w, h, inputMode, sample, matte)
pB = image_scalefit(pB, w, h, inputMode, sample, matte)
print(pA.shape)
print(pB.shape)
#if mask is not None:
# mask = image_scalefit(mask, w, h, inputMode, sample)
@@ -228,7 +221,7 @@ class PixelMergeNode(CozyImageNode):
CATEGORY = JOV_CATEGORY
SORT = 45
DESCRIPTION = """
Combines individual color channels (red, green, blue) along with an optional mask channel to create a composite image. This node is useful for merging separate color components into a single image for visualization or further processing.
Combines individual color channels (red, green, blue) along with an optional mask channel to create a composite image.
"""
@classmethod
@@ -241,17 +234,10 @@ Combines individual color channels (red, green, blue) along with an optional mas
Lexicon.CHAN_GREEN: (COZY_TYPE_IMAGE, {}),
Lexicon.CHAN_BLUE: (COZY_TYPE_IMAGE, {}),
Lexicon.CHAN_ALPHA: (COZY_TYPE_IMAGE, {}),
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,}),
Lexicon.FLIP: ("VEC4", {
"default": (0,0,0,0), "mij":0, "maj":1,
"default": (0,0,0,0), "mij":0, "maj":1, "step": 0.01,
"tooltip": "Invert specific input prior to merging. R, G, B, A."}),
Lexicon.INVERT: ("BOOLEAN", {
"default": False,})
@@ -265,16 +251,13 @@ Combines individual color channels (red, green, blue) along with an optional mas
G = parse_param(kw, Lexicon.CHAN_GREEN, EnumConvertType.MASK, None)
B = parse_param(kw, Lexicon.CHAN_BLUE, EnumConvertType.MASK, None)
A = parse_param(kw, Lexicon.CHAN_ALPHA, EnumConvertType.MASK, None)
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)
flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.VEC4, (0, 0, 0, 0), 0., 1.)
invert = parse_param(kw, Lexicon.INVERT, EnumConvertType.BOOLEAN, False)
params = list(zip_longest_fill(rgba, R, G, B, A, mode, wihi, sample, matte, flip, invert))
params = list(zip_longest_fill(rgba, R, G, B, A, matte, flip, invert))
images = []
pbar = ProgressBar(len(params))
for idx, (rgba, r, g, b, a, mode, wihi, sample, matte, flip, invert) in enumerate(params):
for idx, (rgba, r, g, b, a, matte, flip, invert) in enumerate(params):
replace = r, g, b, a
if rgba is not None:
rgba = tensor_to_cv(rgba)
@@ -287,22 +270,19 @@ Combines individual color channels (red, green, blue) along with an optional mas
_, _, w_max, h_max = image_minmax(img)
for i, x in enumerate(img):
if x is None:
x = np.full((h_max, w_max), matte[i], dtype=np.uint8)
x = np.full((h_max, w_max, 1), matte[i], dtype=np.uint8)
else:
x = image_convert(x, 1)
x = image_scalefit(x, w_max, h_max, EnumScaleMode.ASPECT)
if flip[i] > 0:
if flip[i] != 0:
x = image_invert(x, flip[i])
img[i] = x
img = channel_merge(img)
# img = image_invert(img, 1)
if mode != EnumScaleMode.MATTE:
w, h = wihi
img = image_scalefit(img, w, h, mode, sample)
if invert == True:
img = image_invert(img, 1)
#if invert == True:
# img = image_invert(img, 1)
images.append(cv_to_tensor_full(img, matte))
pbar.update_absolute(idx)
@@ -321,7 +301,7 @@ class PixelSplitNode(CozyBaseNode):
)
SORT = 40
DESCRIPTION = """
Takes an input image and splits it into its individual color channels (red, green, blue), along with a mask channel. This node is useful for separating different color components of an image for further processing or analysis.
Split an input into individual color channels (red, green, blue, alpha).
"""
@classmethod
@@ -329,17 +309,22 @@ Takes an input image and splits it into its individual color channels (red, gree
d = super().INPUT_TYPES()
d = deep_merge(d, {
"optional": {
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {})
Lexicon.IMAGE: (COZY_TYPE_IMAGE, {}),
Lexicon.MASK: (COZY_TYPE_IMAGE, {})
}
})
return Lexicon._parse(d)
def run(self, **kw) -> RGBAMaskType:
images = []
pA = parse_param(kw, Lexicon.IMAGE, EnumConvertType.IMAGE, None)
pbar = ProgressBar(len(pA))
for idx, pA in enumerate(pA):
pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor_to_cv(pA)
mask = parse_param(kw, Lexicon.MASK, EnumConvertType.MASK, None)
params = list(zip_longest_fill(pA, mask))
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mask) in enumerate(params):
pA = channel_solid() if pA is None else image_convert(tensor_to_cv(pA), 4)
if mask is not None:
pA = image_mask_add(pA)
images.append([cv_to_tensor(x, True) for x in image_split(pA)])
pbar.update_absolute(idx)
return image_stack(images)
@@ -387,19 +372,19 @@ Swap pixel values between two input images based on specified channel swizzle op
for idx, (pA, pB, swap_r, swap_g, swap_b, swap_a, matte) in enumerate(params):
if pA is None:
if pB is None:
out = channel_solid(chan=EnumImageType.BGRA)
out = channel_solid()
images.append(cv_to_tensor_full(out))
pbar.update_absolute(idx)
continue
h, w = pB.shape[:2]
pA = channel_solid(w, h, chan=EnumImageType.BGRA)
pA = channel_solid(w, h)
else:
h, w = pA.shape[:2]
pA = tensor_to_cv(pA)
pA = image_convert(pA, 4)
pB = tensor_to_cv(pB) if pB is not None else channel_solid(w, h, chan=EnumImageType.BGRA)
pB = tensor_to_cv(pB) if pB is not None else channel_solid(w, h)
pB = image_convert(pB, 4)
pB = image_matte(pB, (0,0,0,0), w, h)
pB = image_scalefit(pB, w, h, EnumScaleMode.CROP)
@@ -449,7 +434,7 @@ Define a range and apply it to an image for segmentation and feature extraction.
images = []
pbar = ProgressBar(len(params))
for idx, (pA, mode, adapt, th, block, invert) in enumerate(params):
pA = tensor_to_cv(pA) if pA is not None else channel_solid(chan=EnumImageType.BGRA)
pA = tensor_to_cv(pA) if pA is not None else channel_solid()
pA = image_threshold(pA, th, mode, adapt, block)
if invert == True:
pA = image_invert(pA, 1)
+2 -2
View File
@@ -99,13 +99,13 @@ Generate a constant image or mask of a specified size and color. It can be used
height, width = mask.shape[:2]
if pA is None:
pA = channel_solid(width, height, (0,0,0,255), EnumImageType.BGRA)
pA = channel_solid(width, height, (0,0,0,255))
else:
pA = tensor_to_cv(pA)
pA = image_convert(pA, 4)
height, width = pA.shape[:2]
pB = channel_solid(width, height, matte, EnumImageType.BGRA)
pB = channel_solid(width, height, matte)
pA = image_blend(pA, pB, mask)
if mode != EnumScaleMode.MATTE:
+1 -4
View File
@@ -18,9 +18,6 @@ from cozy_comfyui.node import \
COZY_TYPE_IMAGE, \
CozyImageNode
from cozy_comfyui.image import \
EnumImageType
from cozy_comfyui.image.crop import \
image_crop, image_crop_center, image_crop_polygonal
@@ -319,7 +316,7 @@ Apply various geometric transformations to images, including translation, rotati
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(chan=EnumImageType.BGRA)
pA = tensor_to_cv(pA) if pA is not None else channel_solid()
if mask is not None:
mask = tensor_to_cv(mask)
pA = image_mask_add(pA, mask)
+68 -26
View File
@@ -12,7 +12,7 @@ from cozy_comfyui import \
from cozy_comfyui.image import \
PixelType, \
Coord2D_Float, EnumImageType, ImageType
Coord2D_Float, ImageType
from cozy_comfyui.image.convert import \
ImageType, \
@@ -95,10 +95,50 @@ class EnumThresholdAdapt(Enum):
# === IMAGE ===
# ==============================================================================
def image_contrast(image: ImageType, value: float) -> ImageType:
mean_value = np.mean(image)
image = (image - mean_value) * value + mean_value
return np.clip(image, 0, 255).astype(np.uint8)
def image_brightness(image: ImageType, brightness: float=0):
if brightness != 0:
brightness = np.clip(brightness, -1, 1) * 255
if brightness > 0:
shadow = brightness
highlight = 255
else:
shadow = 0
highlight = 255 + brightness
alpha_b = (highlight - shadow)/255
gamma_b = shadow
image = cv2.addWeighted(image, alpha_b, image, 0, gamma_b)
return image
def image_contrast(image: ImageType, contrast: float) -> ImageType:
# Map contrast from [-255, 255] to factor
contrast = np.clip(contrast, -1, 1) * 255
factor = (255 * (contrast + 255)) / (255 * (255 - contrast))
def image_contrast_rgb(lab: ImageType) -> ImageType:
"""Adjust contrast in RGB image using LAB color space and standard contrast scaling."""
lab = cv2.cvtColor(lab, cv2.COLOR_RGB2LAB)
L, A, B = cv2.split(lab)
L = L.astype(np.float32)
L = factor * (L - 128) + 128
L = np.clip(L, 0, 255).astype(np.uint8)
lab = cv2.merge([L, A, B])
return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
# Grayscale
if image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 1):
img = image.astype(np.float32)
img = factor * (img - 128) + 128
return np.clip(img, 0, 255).astype(np.uint8)
# RGB
elif image.shape[2] == 3:
return image_contrast_rgb(image)
# RGBA
rgb = image[..., :3]
alpha = image[..., 3:]
rgb = image_contrast_rgb(rgb)
return np.concatenate([rgb, alpha], axis=2)
def image_edge_wrap(image: ImageType, tileX: float=1., tileY: float=1.,
edge:EnumEdge=EnumEdge.WRAP) -> ImageType:
@@ -109,9 +149,9 @@ def image_edge_wrap(image: ImageType, tileX: float=1., tileY: float=1.,
return cv2.copyMakeBorder(image, tileY, tileY, tileX, tileX, cv2.BORDER_WRAP)
def image_equalize(image:ImageType) -> ImageType:
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
image = cv2.equalizeHist(image)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
return image
def image_exposure(image: ImageType, value: float) -> ImageType:
@@ -192,12 +232,12 @@ def image_flatten(image: List[ImageType], width:int=None, height:int=None,
current = cv2.add(current, x)
return current
def image_gamma(image: ImageType, value: float) -> ImageType:
if value <= 0:
def image_gamma(image: ImageType, gamma: float) -> ImageType:
if gamma <= 0:
return np.zeros_like(image, dtype=np.uint8)
inv_gamma = 1.0 / max(1e-6, value)
table = np.power(np.linspace(0, 1, 256), inv_gamma) * 255
gamma = 1.0 / max(1e-6, gamma)
table = np.power(np.linspace(0, 1, 256), gamma) * 255
lookup_table = np.clip(table, 0, 255).astype(np.uint8)
return cv2.LUT(image, lookup_table)
@@ -221,19 +261,19 @@ def image_histogram_normalize(image:ImageType)-> ImageType:
return np.reshape(flatEqualizedImage, image.shape)
def image_hsv(image: ImageType, hue: float, saturation: float, value: float) -> ImageType:
image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)
hue *= 255
image[:, :, 0] = (image[:, :, 0] + hue) % 180
image[:, :, 1] = np.clip(image[:, :, 1] * saturation, 0, 255)
image[:, :, 2] = np.clip(image[:, :, 2] * value, 0, 255)
return cv2.cvtColor(image, cv2.COLOR_HSV2BGR)
return cv2.cvtColor(image, cv2.COLOR_HSV2RGB)
def image_invert(image: ImageType, value: float) -> ImageType:
"""
Invert an Grayscale, RGB or RGBA image using a specified inversion intensity.
Invert a Grayscale, RGB, or RGBA image using a specified inversion intensity.
Parameters:
- image: Input image as a NumPy array (RGB or RGBA).
- image: Input image as a NumPy array (grayscale, RGB, or RGBA).
- value: Float between 0 and 1 representing the intensity of inversion (0: no inversion, 1: full inversion).
Returns:
@@ -241,16 +281,18 @@ def image_invert(image: ImageType, value: float) -> ImageType:
"""
# Clip the value to be within [0, 1] and scale to [0, 255]
value = np.clip(value, 0, 1)
# RGBA
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))
blended_rgb = ((1 - value) * rgb + value * inverted_rgb).astype(np.uint8)
return np.dstack((blended_rgb, alpha))
inverted_image = 255 - image
return ((1 - value) * image + value * inverted_image).astype(np.uint8)
# Grayscale & RGB
inverted = 255 - image
return ((1 - value) * image + value * inverted).astype(np.uint8)
def image_mirror(image: ImageType, mode:EnumMirrorMode, x:float=0.5,
y:float=0.5) -> ImageType:
@@ -405,8 +447,8 @@ def image_scalefit(image: ImageType, width: int, height:int,
case EnumScaleMode.FIT:
image = cv2.resize(image, (width, height), interpolation=sample.value)
if image.ndim == 2:
image = np.expand_dims(image, -1)
#if image.ndim == 2:
# image = np.expand_dims(image, -1)
return image
def image_sharpen(image:ImageType, kernel_size=None, sigma:float=1.0,
@@ -436,7 +478,7 @@ def image_swap_channels(imgA:ImageType, imgB:ImageType,
imgB = image_scalefit(imgB, w, h, EnumScaleMode.CROP)
matte = (matte[2], matte[1], matte[0], matte[3])
out = channel_solid(w, h, matte, EnumImageType.BGRA)
out = channel_solid(w, h, matte)
swap_out = (EnumPixelSwizzle.RED_A,EnumPixelSwizzle.GREEN_A,
EnumPixelSwizzle.BLUE_A,EnumPixelSwizzle.ALPHA_A)
@@ -458,9 +500,9 @@ def image_threshold(image:ImageType, threshold:float=0.5,
const = max(-100, min(100, const))
block = max(3, block if block % 2 == 1 else block + 1)
if adapt != EnumThresholdAdapt.ADAPT_NONE:
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
gray = cv2.adaptiveThreshold(gray, 255, adapt.value, cv2.THRESH_BINARY, block, const)
gray = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
gray = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
# gray = np.stack([gray, gray, gray], axis=-1)
image = cv2.bitwise_and(image, gray)
else:
@@ -525,7 +567,7 @@ def morph_edge_detect(image: ImageType,
low: float=0.27,
high:float=0.6) -> ImageType:
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
#image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
ksize = max(3, ksize)
image = cv2.GaussianBlur(src=image, ksize=(ksize, ksize+2), sigmaX=0.5)
# Perform Canny edge detection
+6 -7
View File
@@ -61,25 +61,24 @@ def channel_add(image:ImageType, color:PixelType=255) -> ImageType:
return np.concatenate([image, new], axis=-1)
def channel_solid(width:int=IMAGE_SIZE_MIN, height:int=IMAGE_SIZE_MIN, color:PixelType=(0, 0, 0, 255),
chan:EnumImageType=EnumImageType.BGR) -> ImageType:
chan:EnumImageType=EnumImageType.RGB) -> ImageType:
if chan == EnumImageType.GRAYSCALE:
color = pixel_eval(color, EnumImageType.GRAYSCALE)
what = np.full((height, width, 1), color, dtype=np.uint8)
return what
return np.full((height, width, 1), color, dtype=np.uint8)
if not type(color) in [list, set, tuple]:
color = [color]
color += (0,) * (3 - len(color))
if chan in [EnumImageType.BGR, EnumImageType.RGB]:
if chan == EnumImageType.RGB:
if chan == EnumImageType.BGR:
color = color[2::-1]
return np.full((height, width, 3), color[:3], dtype=np.uint8)
if len(color) < 4:
color += (255,)
if chan == EnumImageType.RGBA:
if chan == EnumImageType.BGRA:
color = color[2::-1]
return np.full((height, width, 4), color, dtype=np.uint8)
@@ -95,8 +94,8 @@ def channel_merge(channels: List[ImageType]) -> ImageType:
continue
h, w = channel.shape[:2]
if channel.ndim > 2:
channel = channel[..., 0]
if channel.ndim == 3 and channel.shape[2] == 1:
channel = channel[:, :, 0]
pad_top = (max_height - h) // 2
pad_bottom = max_height - h - pad_top
+29 -42
View File
@@ -18,9 +18,13 @@ from cozy_comfyui.image import \
from cozy_comfyui.image.convert import \
ImageType, \
image_mask, image_mask_add, image_convert, hsv_to_bgr, bgr_to_hsv
image_mask, image_mask_add, image_convert
from .compose import image_blend
from cozy_comfyui.image.convert import \
image_grayscale
from .compose import \
image_blend
# ==============================================================================
# === TYPE ===
@@ -158,7 +162,7 @@ def linear2sRGB(image: ImageType) -> ImageType:
# ==============================================================================
def pixel_eval(color: PixelType,
target: EnumImageType=EnumImageType.BGR,
target: EnumImageType=EnumImageType.RGBA,
precision:EnumIntFloat=EnumIntFloat.INT,
crunch:EnumGrayscaleCrunch=EnumGrayscaleCrunch.MEAN) -> tuple[PixelType] | PixelType:
"""Evaluates R(GB)(A) pixels in range (0-255) into target target pixel type."""
@@ -568,57 +572,63 @@ def color_top_used(image: ImageType, top_n: int=8) -> List[tuple[int, int, int]]
# === COLOR ANALYSIS ===
# ==============================================================================
def rgb_to_hsv(bgr_color: PixelType) -> PixelType:
return cv2.cvtColor(np.uint8([[bgr_color]]), cv2.COLOR_RGB2HSV)[0, 0]
def hsv_to_rgb(hsl_color: PixelType) -> PixelType:
return cv2.cvtColor(np.uint8([[hsl_color]]), cv2.COLOR_HSV2RGB)[0, 0]
def color_theory_complementary(color: PixelType) -> PixelType:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 90, 0, 0)
return hsv_to_bgr(color_a)
return hsv_to_rgb(color_a)
def color_theory_monochromatic(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
sat = 255 / 5
val = 255 / 5
color_a = pixel_hsv_adjust(color, 0, -1 * sat, -1 * val, mod_sat=True, mod_value=True)
color_b = pixel_hsv_adjust(color, 0, -2 * sat, -2 * val, mod_sat=True, mod_value=True)
color_c = pixel_hsv_adjust(color, 0, -3 * sat, -3 * val, mod_sat=True, mod_value=True)
color_d = pixel_hsv_adjust(color, 0, -4 * sat, -4 * val, mod_sat=True, mod_value=True)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c), hsv_to_rgb(color_d)
def color_theory_split_complementary(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 75, 0, 0)
color_b = pixel_hsv_adjust(color, 105, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b)
def color_theory_analogous(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 30, 0, 0)
color_b = pixel_hsv_adjust(color, 15, 0, 0)
color_c = pixel_hsv_adjust(color, 165, 0, 0)
color_d = pixel_hsv_adjust(color, 150, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c), hsv_to_rgb(color_d)
def color_theory_triadic(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 60, 0, 0)
color_b = pixel_hsv_adjust(color, 120, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b)
def color_theory_compound(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 90, 0, 0)
color_b = pixel_hsv_adjust(color, 120, 0, 0)
color_c = pixel_hsv_adjust(color, 150, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c)
def color_theory_square(color: PixelType) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
color_a = pixel_hsv_adjust(color, 45, 0, 0)
color_b = pixel_hsv_adjust(color, 90, 0, 0)
color_c = pixel_hsv_adjust(color, 135, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c)
def color_theory_tetrad_custom(color: PixelType, delta:int=0) -> tuple[PixelType, ...]:
color = bgr_to_hsv(color)
color = rgb_to_hsv(color)
# modulus on neg and pos
while delta < 0:
@@ -632,7 +642,7 @@ def color_theory_tetrad_custom(color: PixelType, delta:int=0) -> tuple[PixelType
# just gimme a compliment
color_c = pixel_hsv_adjust(color, 90 - delta, 0, 0)
color_d = pixel_hsv_adjust(color, 90 + delta, 0, 0)
return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d)
return hsv_to_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c), hsv_to_rgb(color_d)
def color_theory(image: ImageType, custom:int=0, scheme: EnumColorTheory=EnumColorTheory.COMPLIMENTARY) -> tuple[ImageType, ...]:
@@ -684,26 +694,3 @@ def image_gradient_map(image:ImageType, color_map:ImageType, reverse:bool=False)
gray = image_grayscale(image)
color_map = image_gradient_expand(color_map)
return cv2.applyColorMap(gray, color_map)
def image_grayscale(image: ImageType, use_alpha: bool = False) -> ImageType:
"""Convert image to grayscale, optionally using the alpha channel if present.
Args:
image (ImageType): Input image, potentially with multiple channels.
use_alpha (bool): If True and the image has 4 channels, multiply the grayscale
values by the alpha channel. Defaults to False.
Returns:
ImageType: Grayscale image, optionally alpha-multiplied.
"""
if image.ndim == 2 or image.shape[2] == 1:
return image
if image.shape[2] == 4:
grayscale = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)
if use_alpha:
alpha_channel = image[:, :, 3] / 255.0
grayscale = (grayscale * alpha_channel).astype(np.uint8)
return grayscale
return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
+46 -30
View File
@@ -7,7 +7,9 @@ from typing import List, Optional
import cv2
import numpy as np
from PIL import Image, ImageDraw
from blendmodes.blend import BlendType, blendLayers
from blendmodes.blend import \
BlendType, \
blendLayers
from cozy_comfyui.image import \
PixelType, \
@@ -127,36 +129,50 @@ def image_blend(background: ImageType, foreground: ImageType, mask:Optional[Imag
image = image_mask_add(image, mask)
return image
def image_levels(image: np.ndarray, black_point:int=0, white_point=255,
mid_point=128, gamma=1.0) -> np.ndarray:
def image_levels(image: ImageType, in_low=0.0, in_mid=0.5, in_high=1.0, out_low=0.0, out_high=1.0):
"""
Adjusts the levels of an image including black, white, midpoints, and gamma correction.
Apply levels adjustment to an image.
Args:
image (numpy.ndarray): Input image tensor in RGB(A) format.
black_point (int): The black point to adjust shadows. Default is 0.
white_point (int): The white point to adjust highlights. Default is 255.
mid_point (int): The mid point for mid-tone adjustment. Default is 128.
gamma (float): Gamma correction value. Default is 1.0.
Parameters:
image (ImageType): Input RGB image in float32 format, range [0, 1].
in_low (float): Input black point.
in_high (float): Input white point.
in_mid (float): Input gamma (midtone).
out_low (float): Output black clamp.
out_high (float): Output white clamp.
Returns:
numpy.ndarray: Adjusted image tensor.
ImageType: Adjusted image in float32 format, range [0, 1].
"""
# Convert points and gamma to float32 for calculations
black = np.array([black_point] * 3, dtype=np.float32)
white = np.array([white_point] * 3, dtype=np.float32)
mid = np.array([mid_point] * 3, dtype=np.float32)
inGamma = np.array([gamma] * 3, dtype=np.float32)
outBlack = np.array([0, 0, 0], dtype=np.float32)
outWhite = np.array([255, 255, 255], dtype=np.float32)
# Separate alpha channel if it exists
has_alpha = image.ndim == 3 and image.shape[2] == 4
if has_alpha:
alpha = image[..., 3:]
image = image[..., :3]
# Apply levels adjustment
image = np.clip((image - black) / (white - black), 0, 1)
image = (image - mid) / (1.0 - mid)
image = (image ** (1 / inGamma)) * (outWhite - outBlack) + outBlack
return np.clip(image, 0, 255).astype(np.uint8)
# Ensure image is float32 and in 0–1 range
image = image.astype(np.float32) / 255.0
# Normalize input range
scale = max(in_high - in_low, 1e-6)
image = (image - in_low) / scale
image = np.clip(image, 0.0, 1.0)
# Apply gamma (midtone)
in_mid = max(in_mid, 1e-6)
gamma = 1.0 / in_mid
image = np.power(image, gamma)
# Scale to output range
image = image * (out_high - out_low) + out_low
image = np.clip(image, 0, 1)
image = (image * 255).round().astype(np.uint8)
# Recombine alpha if present
if has_alpha:
return np.concatenate([image, alpha], axis=2)
return image
def image_mask_binary(image: ImageType) -> ImageType:
"""
@@ -240,16 +256,16 @@ def image_split(image: ImageType) -> tuple[ImageType, ...]:
h, w = image.shape[:2]
# Grayscale image
a = np.full((h, w, 1), 255, dtype=image.dtype)
if image.ndim == 2 or image.shape[2] == 1:
r = g = b = image
a = np.full((h, w), 255, dtype=image.dtype)
# BGR image
elif image.shape[2] == 3:
r, g, b = cv2.split(image)
a = np.full((h, w), 255, dtype=image.dtype)
# RGB(A) image
else:
r, g, b, a = cv2.split(image)
r = image[:, :, 0]
g = image[:, :, 1]
b = image[:, :, 2]
if image.shape[2] == 4:
a = image[:, :, 3]
return r, g, b, a
def image_stacker(image_list: List[ImageType],