diff --git a/core/adjust.py b/core/adjust.py index 9a4d693..ffdb0da 100644 --- a/core/adjust.py +++ b/core/adjust.py @@ -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) diff --git a/core/color.py b/core/color.py index 3fe6f03..c2b29fb 100644 --- a/core/color.py +++ b/core/color.py @@ -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 diff --git a/core/compose.py b/core/compose.py index c5ea9ea..e156bee 100644 --- a/core/compose.py +++ b/core/compose.py @@ -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) diff --git a/core/create.py b/core/create.py index 4ebd917..121e68f 100644 --- a/core/create.py +++ b/core/create.py @@ -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: diff --git a/core/trans.py b/core/trans.py index 453ec0a..6400f43 100644 --- a/core/trans.py +++ b/core/trans.py @@ -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) diff --git a/sup/image/adjust.py b/sup/image/adjust.py index 2f333b7..a770ecd 100644 --- a/sup/image/adjust.py +++ b/sup/image/adjust.py @@ -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 diff --git a/sup/image/channel.py b/sup/image/channel.py index 9f2e565..9999f49 100644 --- a/sup/image/channel.py +++ b/sup/image/channel.py @@ -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 diff --git a/sup/image/color.py b/sup/image/color.py index 52a867a..aefd594 100644 --- a/sup/image/color.py +++ b/sup/image/color.py @@ -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) diff --git a/sup/image/compose.py b/sup/image/compose.py index e04ec54..d558ec7 100644 --- a/sup/image/compose.py +++ b/sup/image/compose.py @@ -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],