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:
+133
-73
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user