diff --git a/core/adjust.py b/core/adjust.py index d205644..af9ec8f 100644 --- a/core/adjust.py +++ b/core/adjust.py @@ -39,6 +39,10 @@ from cozy_comfyui.image.mask import \ from cozy_comfyui.image.misc import \ image_stack +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "ADJUST" # ============================================================================== diff --git a/core/anim.py b/core/anim.py index 4ac840d..5a571f7 100644 --- a/core/anim.py +++ b/core/anim.py @@ -31,6 +31,10 @@ from cozy_comfyui.maths.wave import \ from cozy_comfyui.maths.series import \ seriesLinear +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "ANIMATION" # ============================================================================== diff --git a/core/calc.py b/core/calc.py index 2866813..6ccebc5 100644 --- a/core/calc.py +++ b/core/calc.py @@ -31,6 +31,10 @@ from cozy_comfyui.maths.ease import \ from . import \ EnumFillOperation +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "CALC" # ============================================================================== diff --git a/core/color.py b/core/color.py index b9e309e..5efc21b 100644 --- a/core/color.py +++ b/core/color.py @@ -23,6 +23,13 @@ from cozy_comfyui.node import \ from cozy_comfyui.image.adjust import \ image_invert +from cozy_comfyui.image.color import \ + EnumCBDeficiency, EnumCBSimulator, EnumColorMap, EnumColorTheory, \ + color_lut_full, color_lut_match, color_lut_palette, \ + color_lut_tonal, color_lut_visualize, color_match_reinhard, \ + color_theory, color_blind, color_top_used, image_gradient_expand, \ + image_gradient_map + from cozy_comfyui.image.channel import \ channel_solid @@ -39,12 +46,9 @@ from cozy_comfyui.image.mask import \ from cozy_comfyui.image.misc import \ image_stack -from ..sup.image.color import \ - EnumCBDeficiency, EnumCBSimulator, EnumColorMap, EnumColorTheory, \ - color_lut_full, color_lut_match, color_lut_palette, \ - color_lut_tonal, color_lut_visualize, color_match_reinhard, \ - color_theory, color_blind, color_top_used, image_gradient_expand, \ - image_gradient_map +# ============================================================================== +# === GLOBAL === +# ============================================================================== JOV_CATEGORY = "COLOR" diff --git a/core/compose.py b/core/compose.py index 15d45c4..f72ae66 100644 --- a/core/compose.py +++ b/core/compose.py @@ -40,6 +40,10 @@ from cozy_comfyui.image.misc import \ from cozy_comfyui.image.pixel import \ pixel_eval +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "COMPOSE" # ============================================================================== diff --git a/core/create.py b/core/create.py index 0e15115..4a26c64 100644 --- a/core/create.py +++ b/core/create.py @@ -44,10 +44,14 @@ from cozy_comfyui.image.shape import \ EnumShapes, \ shape_ellipse, shape_polygon, shape_quad -from ..sup.text import \ +from cozy_comfyui.image.text import \ EnumAlignment, EnumJustify, \ font_names, text_autosize, text_draw +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "CREATE" # ============================================================================== diff --git a/core/trans.py b/core/trans.py index df16eaf..4cd5a3d 100644 --- a/core/trans.py +++ b/core/trans.py @@ -35,10 +35,14 @@ from cozy_comfyui.image.mask import \ from cozy_comfyui.image.misc import \ image_stack -from ..sup.image.mapping import \ +from cozy_comfyui.image.mapping import \ EnumProjection, \ remap_fisheye, remap_perspective, remap_polar, remap_sphere +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "TRANSFORM" # ============================================================================== diff --git a/core/vars.py b/core/vars.py index 4f17f5a..8ed88bc 100644 --- a/core/vars.py +++ b/core/vars.py @@ -20,6 +20,10 @@ from cozy_comfyui.node import \ from . import \ EnumFillOperation +# ============================================================================== +# === GLOBAL === +# ============================================================================== + JOV_CATEGORY = "VARIABLE" # ============================================================================== diff --git a/sup/__init__.py b/sup/__init__.py deleted file mode 100644 index c449592..0000000 --- a/sup/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" - ██  ██████  ██  ██ ██ ███  ███ ███████ ████████ ██████  ██ ██  ██  - ██ ██    ██ ██  ██ ██ ████  ████ ██         ██    ██   ██ ██  ██ ██   - ██ ██  ██ ██  ██ ██ ██ ████ ██ █████  ██  ██████  ██   ███   -██ ██ ██  ██  ██  ██  ██ ██  ██  ██ ██     ██  ██   ██ ██  ██ ██  - █████   ██████    ████   ██ ██      ██ ███████  ██  ██  ██ ██ ██   ██  - - Procedural & Compositing Image Manipulation Nodes - Copyright 2023 Alexander Morano (Joviex) -""" \ No newline at end of file diff --git a/sup/image/__init__.py b/sup/image/__init__.py deleted file mode 100644 index ae52a92..0000000 --- a/sup/image/__init__.py +++ /dev/null @@ -1 +0,0 @@ -""" Image Support """ diff --git a/sup/image/color.py b/sup/image/color.py deleted file mode 100644 index 3f157d8..0000000 --- a/sup/image/color.py +++ /dev/null @@ -1,551 +0,0 @@ -""" Jovimetrix - Image Color Support """ - -from enum import Enum -from typing import List - -import cv2 -import numpy as np -from numba import cuda -from scipy.spatial import KDTree -from skimage import exposure -from sklearn.cluster import KMeans -from daltonlens import simulate - -from cozy_comfyui.image import \ - PixelType, ImageType - -from cozy_comfyui.image.compose import \ - EnumBlendType, \ - image_blend - -from cozy_comfyui.image.convert import \ - image_convert, image_grayscale - -from cozy_comfyui.image.mask import \ - image_mask, image_mask_add - -from cozy_comfyui.image.pixel import \ - pixel_hsv_adjust - -from cozy_comfyui.image.space import \ - rgb_to_hsv, hsv_to_rgb - -# ============================================================================== -# === TYPE === -# ============================================================================== - -TYPE_LUT = tuple[int, int, int, int] - -# ============================================================================== -# === ENUMERATION === -# ============================================================================== - -class EnumColorMap(Enum): - AUTUMN = cv2.COLORMAP_AUTUMN - BONE = cv2.COLORMAP_BONE - JET = cv2.COLORMAP_JET - WINTER = cv2.COLORMAP_WINTER - RAINBOW = cv2.COLORMAP_RAINBOW - OCEAN = cv2.COLORMAP_OCEAN - SUMMER = cv2.COLORMAP_SUMMER - SPRING = cv2.COLORMAP_SPRING - COOL = cv2.COLORMAP_COOL - HSV = cv2.COLORMAP_HSV - PINK = cv2.COLORMAP_PINK - HOT = cv2.COLORMAP_HOT - PARULA = cv2.COLORMAP_PARULA - MAGMA = cv2.COLORMAP_MAGMA - INFERNO = cv2.COLORMAP_INFERNO - PLASMA = cv2.COLORMAP_PLASMA - VIRIDIS = cv2.COLORMAP_VIRIDIS - CIVIDIS = cv2.COLORMAP_CIVIDIS - TWILIGHT = cv2.COLORMAP_TWILIGHT - TWILIGHT_SHIFTED = cv2.COLORMAP_TWILIGHT_SHIFTED - TURBO = cv2.COLORMAP_TURBO - DEEPGREEN = cv2.COLORMAP_DEEPGREEN - -class EnumColorTheory(Enum): - COMPLIMENTARY = 0 - MONOCHROMATIC = 1 - SPLIT_COMPLIMENTARY = 2 - ANALOGOUS = 3 - TRIADIC = 4 - # TETRADIC = 5 - SQUARE = 6 - COMPOUND = 8 - # DOUBLE_COMPLIMENTARY = 9 - CUSTOM_TETRAD = 9 - -class EnumCBDeficiency(Enum): - PROTAN = simulate.Deficiency.PROTAN - DEUTAN = simulate.Deficiency.DEUTAN - TRITAN = simulate.Deficiency.TRITAN - -class EnumCBSimulator(Enum): - AUTOSELECT = 0 - BRETTEL1997 = 1 - COBLISV1 = 2 - COBLISV2 = 3 - MACHADO2009 = 4 - VIENOT1999 = 5 - VISCHECK = 6 - -# ============================================================================== -# === SUPPORT === -# ============================================================================== - -@cuda.jit -def kmeans_kernel(pixels, centroids, assignments) -> None: - idx = cuda.grid(1) - if idx < pixels.shape[0]: - min_dist = 1e10 - min_centroid = 0 - for i in range(centroids.shape[0]): - dist = 0 - for j in range(3): - diff = pixels[idx, j] - centroids[i, j] - dist += diff * diff - if dist < min_dist: - min_dist = dist - min_centroid = i - assignments[idx] = min_centroid - -def color_image2lut(image: np.ndarray, num_colors: int = 256) -> np.ndarray: - """Create X sized LUT from an RGB image using GPU acceleration.""" - # Ensure image is in RGB format - if image.shape[2] == 4: # If RGBA, convert to RGB - image = cv2.cvtColor(image, cv2.COLOR_RGBA2RGB) - elif image.shape[2] == 1: # If grayscale, convert to RGB - image = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) - - # Reshape and transfer to GPU - pixels = np.asarray(image.reshape(-1, 3)).astype(np.float32) - # logger.debug("Pixel range:", np.min(pixels), np.max(pixels)) - - # Initialize centroids using random pixels - random_indices = np.random.choice(pixels.shape[0], size=num_colors, replace=False) - centroids = pixels[random_indices] - # logger.debug("Initial centroids range:", np.min(centroids), np.max(centroids)) - - # Prepare for K-means - assignments = np.zeros(pixels.shape[0], dtype=np.int32) - threads_per_block = 256 - blocks = (pixels.shape[0] + threads_per_block - 1) // threads_per_block - - # K-means iterations - for iteration in range(20): # Adjust the number of iterations as needed - kmeans_kernel[blocks, threads_per_block](pixels, centroids, assignments) - new_centroids = np.zeros((num_colors, 3), dtype=np.float32) - for i in range(num_colors): - mask = (assignments == i) - if np.any(mask): - new_centroids[i] = np.mean(pixels[mask], axis=0) - - centroids = new_centroids - - if iteration % 5 == 0: - # logger.debug(f"Iteration {iteration}, Centroids range: {np.min(centroids)} {np.max(centroids)}") - pass - - # Create LUT - lut = np.zeros((256, 1, 3), dtype=np.uint8) - lut[:num_colors] = np.clip(centroids, 0, 255).reshape(-1, 1, 3).astype(np.uint8) - # logger.debug(f"Final LUT range: { np.min(lut)} {np.max(lut)}") - return np.asarray(lut) - -def color_blind(image: ImageType, deficiency:EnumCBDeficiency, - simulator:EnumCBSimulator=EnumCBSimulator.AUTOSELECT, - severity:float=1.0) -> ImageType: - - cc = image.shape[2] if image.ndim == 3 else 1 - if cc == 4: - mask = image_mask(image) - image = image_convert(image, 3) - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - match simulator: - case EnumCBSimulator.AUTOSELECT: - simulator = simulate.Simulator_AutoSelect() - case EnumCBSimulator.BRETTEL1997: - simulator = simulate.Simulator_Brettel1997() - case EnumCBSimulator.COBLISV1: - simulator = simulate.Simulator_CoblisV1() - case EnumCBSimulator.COBLISV2: - simulator = simulate.Simulator_CoblisV2() - case EnumCBSimulator.MACHADO2009: - simulator = simulate.Simulator_Machado2009() - case EnumCBSimulator.VIENOT1999: - simulator = simulate.Simulator_Vienot1999() - case EnumCBSimulator.VISCHECK: - simulator = simulate.Simulator_Vischeck() - image = simulator.simulate_cvd(image, deficiency.value, severity=severity) - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - if cc == 4: - image = image_mask_add(image, mask) - return image - -def color_lut_full(dominant_colors: List[tuple[int, int, int]], nodes:int=33) -> ImageType: - """ - Create a 3D LUT by mapping each RGB value to the closest dominant color. - This version is optimized for speed using vectorization. - - Args: - dominant_colors (List[tuple[int, int, int]]): List of top colors as (R, G, B) tuples. - - Returns: - np.ndarray: 3D LUT with shape (n, n, n, 3). - """ - - kdtree = KDTree(dominant_colors) - r, g, b = np.mgrid[0:nodes, 0:nodes, 0:nodes] - rgb = np.stack([r, g, b], axis=-1).reshape(-1, 3) - _, indices = kdtree.query(rgb) - lut = np.array(dominant_colors)[indices] - lut = lut.reshape(nodes, nodes, nodes, 3).astype(np.uint8) - return lut - -def color_lut_match(image: ImageType, colormap:int=cv2.COLORMAP_JET, - usermap:ImageType=None, num_colors:int=255) -> ImageType: - """Colorize one input based on built in cv2 color maps or a user defined image.""" - cc = image.shape[2] if image.ndim == 3 else 1 - if cc == 4: - alpha = image_mask(image) - - image = image_convert(image, 3) - if usermap is not None: - usermap = image_convert(usermap, 3) - colormap = color_image2lut(usermap, num_colors) - - image = cv2.applyColorMap(image, colormap) - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - - image = cv2.addWeighted(image, 0.5, image, 0.5, 0) - image = image_convert(image, cc) - - if cc == 4: - image[..., 3] = alpha[..., 0] - return image - -def color_lut_palette(colors: List[tuple[int, int, int]], size: int=32) -> ImageType: - """ - Create a color palette LUT as a 2D image from the top colors. - - Args: - colors (List[tuple[int, int, int]]): List of top colors as (R, G, B) tuples. - size (int): Size of each color square in the palette. - - Returns: - np.ndarray: 2D image representing the LUT. - """ - num_colors = len(colors) - width = size * num_colors - lut_image = np.zeros((size, width, 3), dtype=np.uint8) - - for i, color in enumerate(colors): - x_start = i * size - x_end = x_start + size - lut_image[:, x_start:x_end] = color - - return lut_image - -def color_lut_tonal(colors: List[tuple[int, int, int]], width: int=256, height: int=32) -> ImageType: - """ - Create a 2D tonal palette LUT as a grid image from the top colors. - - Args: - colors (List[tuple[int, int, int]]): List of top colors as (R, G, B) tuples. - width (int): Width of each gradient row. - height (int): Height of each color row. - - Returns: - ImageType: 2D image representing the tonal palette LUT. - """ - num_colors = len(colors) - lut_image = np.zeros((height * num_colors, width, 3), dtype=np.uint8) - - for i, color in enumerate(colors): - row_start = i * height - row_end = row_start + height - gradient = np.zeros((height, width, 3), dtype=np.uint8) - - for x in range(width): - factor = x / width - gradient[:, x] = np.array(color) * (1 - factor) + np.array([0, 0, 0]) * factor - - lut_image[row_start:row_end] = gradient - - return lut_image - -def color_lut_visualize(lut: TYPE_LUT, size: int=512) -> ImageType: - """ - Visualize a 3D LUT as a 2D image. - - Args: - lut (np.ndarray): 3D LUT with shape (n, n, n, 3). - size (int): Size of the output image (square). Default is 2048. - - Returns: - PIL.Image.Image: 2D visualization of the 3D LUT. - """ - if len(lut.shape) != 4 or lut.shape[3] != 3 or lut.shape[0] != lut.shape[1] or lut.shape[1] != lut.shape[2]: - raise ValueError("LUT must have shape (n, n, n, 3) where n is the number of nodes per dimension") - - # 8 for a 256^3 LUT - n = lut.shape[0] - vis_n = int(np.ceil(np.cbrt(n))) - - # Calculate the size of each small square, ensuring it's at least 1 pixel - square_size = max(1, size // (vis_n * vis_n)) - - # Recalculate the actual image size based on the square size - actual_size = square_size * vis_n * vis_n - img = np.zeros((actual_size, actual_size, 3), dtype=np.uint8) - - for b in range(n): - # Calculate position of the current slice - slice_y = (b // vis_n) * square_size * vis_n - slice_x = (b % vis_n) * square_size * vis_n - - # Extract the slice from the LUT - slice_data = lut[:, :, b] - slice_resized = cv2.resize(slice_data, (square_size * vis_n, square_size * vis_n), interpolation=cv2.INTER_NEAREST) - - # Ensure we don't go out of bounds - end_y = min(slice_y + square_size * vis_n, actual_size) - end_x = min(slice_x + square_size * vis_n, actual_size) - img[slice_y:end_y, slice_x:end_x] = slice_resized[:end_y-slice_y, :end_x-slice_x] - - return cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - -def color_lut_xport(lut: TYPE_LUT, f_out: str) -> None: - """ - Save a 3D LUT as a .cube file. - - Args: - lut (np.ndarray): 3D LUT with shape (256, 256, 256, 3). - filename (str): Output filename (should end with .cube). - title (str, optional): Title for the LUT. Defaults to "3D LUT". - - Returns: - None - """ - if lut.shape != (256, 256, 256, 3): - raise ValueError("LUT must have shape (256, 256, 256, 3)") - - if not filename.lower().endswith('.cube'): - filename += '.cube' - - with open(f_out, 'w') as f: - f.write(f"TITLE 3D LUT\n") - f.write("LUT_3D_SIZE 256\n") - f.write("DOMAIN_MIN 0 0 0\n") - f.write("DOMAIN_MAX 1 1 1\n\n") - for b in range(256): - for g in range(256): - for r in range(256): - color = lut[r, g, b] - f.write(f"{color[0]/255:.6f} {color[1]/255:.6f} {color[2]/255:.6f}\n") - -def color_match_histogram(image: ImageType, usermap: ImageType) -> ImageType: - """Colorize one input based on the histogram matches.""" - cc = image.shape[2] if image.ndim == 3 else 1 - if cc == 4: - alpha = image_mask(image) - image = image_convert(image, 3) - image = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) - beta = cv2.cvtColor(usermap, cv2.COLOR_BGR2LAB) - image = exposure.match_histograms(image, beta, channel_axis=2) - image = cv2.cvtColor(image, cv2.COLOR_LAB2BGR) - image = image_blend(usermap, image, blendOp=EnumBlendType.LUMINOSITY) - image = image_convert(image, cc) - #if cc == 4: - # image[..., 3] = alpha[..., 0] - return image - -def color_match_reinhard(image: ImageType, target: ImageType) -> ImageType: - """ - Apply Reinhard color matching to an image based on a target image. - Works only for BGR images and returns an BGR image. - - based on https://www.cs.tau.ac.il/~turkel/imagepapers/ColorTransfer. - - Args: - image (ImageType): The input image (BGR or BGRA or Grayscale). - target (ImageType): The target image (BGR or BGRA or Grayscale). - - Returns: - ImageType: The color-matched image in BGR format. - """ - target = image_convert(target, 3) - lab_tar = cv2.cvtColor(target, cv2.COLOR_BGR2Lab) - image = image_convert(image, 3) - lab_ori = cv2.cvtColor(image, cv2.COLOR_BGR2Lab) - mean_tar, std_tar = cv2.meanStdDev(lab_tar) - mean_ori, std_ori = cv2.meanStdDev(lab_ori) - ratio = (std_tar / std_ori).reshape(-1) - offset = (mean_tar - mean_ori * std_tar / std_ori).reshape(-1) - lab_tar = cv2.convertScaleAbs(lab_ori * ratio + offset) - return cv2.cvtColor(lab_tar, cv2.COLOR_Lab2BGR) - -def color_mean(image: ImageType) -> ImageType: - color = [0, 0, 0] - cc = image.shape[2] if image.ndim == 3 else 1 - if cc == 1: - raw = int(np.mean(image)) - color = [raw] * 3 - else: - # each channel.... - color = [ - int(np.mean(image[..., 0])), - int(np.mean(image[:,:,1])), - int(np.mean(image[:,:,2])) ] - return color - -def color_top_used(image: ImageType, top_n: int=8) -> List[tuple[int, int, int]]: - """ - Find dominant colors in an image using k-means clustering. - - Args: - image (np.ndarray): Input image in HxWxC format, assumed to be RGB. - top_n (int): Number of top colors to return. - - Returns: - List[tuple[int, int, int]]: List of top `top_n` colors. - """ - if image.ndim < 3: - image = np.expand_dims(image, axis=-1) - - if image.shape[2] != 3: - image = image_convert(image, 3) - - pixels = image.reshape(-1, 3) - kmeans = KMeans(n_clusters=int(top_n), n_init=10) - kmeans.fit(pixels) - dominant_colors = kmeans.cluster_centers_ - dominant_colors = np.round(dominant_colors).astype(int) - sorted_colors = sorted( - zip(dominant_colors, kmeans.labels_), - key=lambda x: np.sum(kmeans.labels_ == x[1]), - reverse=True - ) - return [tuple(color) for color, _ in sorted_colors] - -# ============================================================================== -# === COLOR ANALYSIS === -# ============================================================================== - -def color_theory_complementary(color: PixelType) -> PixelType: - color = rgb_to_hsv(color) - color_a = pixel_hsv_adjust(color, 90, 0, 0) - return hsv_to_rgb(color_a) - -def color_theory_monochromatic(color: PixelType) -> tuple[PixelType, ...]: - 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_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 = 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_rgb(color_a), hsv_to_rgb(color_b) - -def color_theory_analogous(color: PixelType) -> tuple[PixelType, ...]: - 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_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 = 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_rgb(color_a), hsv_to_rgb(color_b) - -def color_theory_compound(color: PixelType) -> tuple[PixelType, ...]: - 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_rgb(color_a), hsv_to_rgb(color_b), hsv_to_rgb(color_c) - -def color_theory_square(color: PixelType) -> tuple[PixelType, ...]: - 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_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 = rgb_to_hsv(color) - - # modulus on neg and pos - while delta < 0: - delta += 90 - - if delta > 90: - delta = delta % 90 - - color_a = pixel_hsv_adjust(color, -delta, 0, 0) - color_b = pixel_hsv_adjust(color, delta, 0, 0) - # 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_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, ...]: - - b = [0,0,0] - c = [0,0,0] - d = [0,0,0] - color = color_mean(image) - match scheme: - case EnumColorTheory.COMPLIMENTARY: - a = color_theory_complementary(color) - case EnumColorTheory.MONOCHROMATIC: - a, b, c, d = color_theory_monochromatic(color) - case EnumColorTheory.SPLIT_COMPLIMENTARY: - a, b = color_theory_split_complementary(color) - case EnumColorTheory.ANALOGOUS: - a, b, c, d = color_theory_analogous(color) - case EnumColorTheory.TRIADIC: - a, b = color_theory_triadic(color) - case EnumColorTheory.SQUARE: - a, b, c = color_theory_square(color) - case EnumColorTheory.COMPOUND: - a, b, c = color_theory_compound(color) - case EnumColorTheory.CUSTOM_TETRAD: - a, b, c, d = color_theory_tetrad_custom(color, custom) - - h, w = image.shape[:2] - return ( - np.full((h, w, 3), color, dtype=np.uint8), - np.full((h, w, 3), a, dtype=np.uint8), - np.full((h, w, 3), b, dtype=np.uint8), - np.full((h, w, 3), c, dtype=np.uint8), - np.full((h, w, 3), d, dtype=np.uint8), - ) - -# -# -# - -def image_gradient_expand(image: ImageType) -> None: - image = image_convert(image, 3) - image = cv2.resize(image, (256, 256)) - return image[0,:,:].reshape((256, 1, 3)) - -# Adapted from WAS Suite -- gradient_map -# https://github.com/WASasquatch/was-node-suite-comfyui -def image_gradient_map(image:ImageType, color_map:ImageType, reverse:bool=False) -> ImageType: - if reverse: - color_map = color_map[:,:,::-1] - gray = image_grayscale(image) - color_map = image_gradient_expand(color_map) - return cv2.applyColorMap(gray, color_map) diff --git a/sup/image/mapping.py b/sup/image/mapping.py deleted file mode 100644 index 59ddd1a..0000000 --- a/sup/image/mapping.py +++ /dev/null @@ -1,113 +0,0 @@ -""" Jovimetrix - Coordinates and Mapping """ - -from enum import Enum -from typing import List - -import cv2 -import numpy as np - -from cozy_comfyui.image import \ - TAU, \ - Coord2D_Float, ImageType - -# ============================================================================== -# === ENUMERATION === -# ============================================================================== - -class EnumProjection(Enum): - NORMAL = 0 - POLAR = 5 - SPHERICAL = 10 - FISHEYE = 15 - PERSPECTIVE = 20 - -# ============================================================================== -# === COORDINATES === -# ============================================================================== - -def coord_cart2polar(x: float, y: float) -> Coord2D_Float: - r = np.sqrt(x**2 + y**2) - theta = np.arctan2(y, x) - return r, theta - -def coord_polar2cart(r: float, theta: float) -> Coord2D_Float: - x = r * np.cos(theta) - y = r * np.sin(theta) - return x, y - -def coord_default(width:int, height:int, origin:Coord2D_Float=None) -> Coord2D_Float: - """Creates x & y coords for the indicies in a numpy array "data". - "origin" defaults to the center of the image. Specify origin=(0,0) - to set the origin to the lower left corner of the image.""" - if origin is None: - origin_x, origin_y = width // 2, height // 2 - else: - origin_x, origin_y = origin - x, y = np.meshgrid(np.arange(width), np.arange(height)) - x -= origin_x - y -= origin_y - return x, y - -def coord_fisheye(width: int, height: int, distortion: float) -> tuple[ImageType, ImageType]: - map_x, map_y = np.meshgrid(np.linspace(0., 1., width), np.linspace(0., 1., height)) - # normalized - xnd, ynd = (2 * map_x - 1), (2 * map_y - 1) - rd = np.sqrt(xnd**2 + ynd**2) - # fish-eye distortion - condition = (dist := 1 - distortion * (rd**2)) == 0 - xdu, ydu = np.where(condition, xnd, xnd / dist), np.where(condition, ynd, ynd / dist) - xu, yu = ((xdu + 1) * width) / 2, ((ydu + 1) * height) / 2 - return xu.astype(np.float32), yu.astype(np.float32) - -def coord_perspective(width: int, height: int, pts: List[Coord2D_Float]) -> ImageType: - object_pts = np.float32([[0, 0], [width, 0], [width, height], [0, height]]) - pts = np.float32(pts) - pts = np.column_stack([pts[:, 0], pts[:, 1]]) - return cv2.getPerspectiveTransform(object_pts, pts) - -def coord_sphere(width: int, height: int, radius: float) -> tuple[ImageType, ImageType]: - theta, phi = np.meshgrid(np.linspace(0, TAU, width), np.linspace(0, np.pi, height)) - x = radius * np.sin(phi) * np.cos(theta) - y = radius * np.sin(phi) * np.sin(theta) - # z = radius * np.cos(phi) - x_image = (x + 1) * (width - 1) / 2 - y_image = (y + 1) * (height - 1) / 2 - return x_image.astype(np.float32), y_image.astype(np.float32) - -# ============================================================================== -# === MAPPING === -# ============================================================================== - -def remap_fisheye(image: ImageType, distort: float) -> ImageType: - cc = image.shape[2] if image.ndim == 3 else 1 - height, width = image.shape[:2] - if cc == 1: - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - map_x, map_y = coord_fisheye(width, height, distort) - image = cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) - #if cc == 1: - # image = image[..., 0] - return image - -def remap_perspective(image: ImageType, pts: list) -> ImageType: - cc = image.shape[2] if image.ndim == 3 else 1 - height, width = image.shape[:2] - if cc == 1: - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - pts = coord_perspective(width, height, pts) - image = cv2.warpPerspective(image, pts, (width, height)) - #if cc == 1: - # image = image[..., 0] - return image - -def remap_polar(image: ImageType) -> ImageType: - """Re-projects a 3D numpy array ("data") into a polar coordinate system. - "origin" is a tuple of (x0, y0) and defaults to the center of the image.""" - h, w = image.shape[:2] - radius = max(w, h) - return cv2.linearPolar(image, (h // 2, w // 2), radius // 2, cv2.WARP_INVERSE_MAP) - -def remap_sphere(image: ImageType, radius: float) -> ImageType: - height, width = image.shape[:2] - map_x, map_y = coord_sphere(width, height, radius) - return cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT) diff --git a/sup/text.py b/sup/text.py deleted file mode 100644 index 9606c8d..0000000 --- a/sup/text.py +++ /dev/null @@ -1,104 +0,0 @@ -""" Jovimetrix - TEXT support """ - -import textwrap -from enum import Enum -from typing import List - -from matplotlib import font_manager -from PIL import Image, ImageFont, ImageDraw - -from cozy_comfyui import \ - logger - -from cozy_comfyui.image import \ - PixelType, ImageType - -from cozy_comfyui.image.convert import \ - pil_to_cv - -# ============================================================================== - -class EnumAlignment(Enum): - TOP = 10 - CENTER = 0 - BOTTOM = 20 - -class EnumJustify(Enum): - LEFT = 10 - CENTER = 0 - RIGHT = 20 - -# ============================================================================== - -def font_names() -> List[str]: - try: - mgr = font_manager.FontManager() - return {font.name: font.fname for font in mgr.ttflist} - except Exception as e: - logger.warn(e) - return {} - -def text_size(draw: ImageDraw, text:str, font:ImageFont) -> tuple[int, int]: - bbox = draw.textbbox((0, 0), text, font=font) - text_width = bbox[2] - bbox[0] - text_height = bbox[3] - bbox[1] - return text_width, text_height - -def text_autosize(text:str, font:str, width:int, height:int, columns:int=0) -> tuple[str, int, int, int]: - img = Image.new("L", (width, height)) - draw = ImageDraw.Draw(img) - if columns != 0: - text = text.split('\n') - lines = [] - for x in text: - line = textwrap.wrap(x, columns, break_long_words=False) - lines.extend(line) - text = '\n'.join(lines) - - font_size = 1 - test_text = text if columns == 0 else ' ' * columns - while 1: - ttf = ImageFont.truetype(font, font_size) - w, h = text_size(draw, test_text, ttf) - if w >= width or h >= height: - break - font_size += 1 - # * 0.6543 - return text, font_size * 0.33, w, h - -def text_draw(full_text: str, font: ImageFont, - width: int, height: int, - align: EnumAlignment=EnumAlignment.CENTER, - justify: EnumJustify=EnumJustify.CENTER, - margin: int=0, line_spacing: int=0, - color: PixelType=(255,255,255,255)) -> ImageType: - - img = Image.new("RGBA", (width, height)) - draw = ImageDraw.Draw(img) - text_lines = full_text.split('\n') - count = len(text_lines) - height_max = text_size(draw, full_text, font)[1] + line_spacing * (count-1) - height_delta = height_max / count - # find the bounding box of this - - if align == EnumAlignment.TOP: - y = margin - elif align == EnumAlignment.BOTTOM: - y = height - height_max * 1.5 - margin - else: - y = height * 0.5 - height_max - - for line in text_lines: - line_width = text_size(draw, line, font)[0] - if justify == EnumJustify.LEFT: - x = margin - elif justify == EnumJustify.RIGHT: - x = width - line_width - margin - else: - x = (width - line_width) / 2 - - # x = min(width - line_width, max(line_width, x)) - draw.text((x, y), line, fill=color, font=font) - y += height_delta - return pil_to_cv(img) -