diff --git a/README.md b/README.md index 0b0c508..141e918 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,10 @@ Nodes that have been migrated: [Migrated to Jovi_GLSL](https://github.com/Amorano/Jovi_GLSL) +**2025/04/14** @2.0.1: +* numpy version set for < 2.0.0 +* core supports switched to [cozy_comfyui](https://github.com/cozy-comfyui/cozy_comfyui) + **2025/04/12** @2.0.0: * REMOVED ALL STREAMING, MIDI and GLSL nodes for new packages, HELP System and Node Colorization system: diff --git a/__init__.py b/__init__.py index 21feb3a..3fce27c 100644 --- a/__init__.py +++ b/__init__.py @@ -29,48 +29,23 @@ batch processing, dynamic bus routing. Queue & Load from URLs. QueueTooNode, RouteNode, SaveOutputNode """ -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] -__author__ = """Alexander G. Morano""" +__author__ = "Alexander G. Morano" __email__ = "amorano@gmail.com" -import os -import sys -import time -import json -import inspect -import importlib from pathlib import Path -from types import ModuleType -from typing import Any, Dict, List, Tuple, TypeAlias - -import torch +from typing import Any, Dict from aiohttp import web from server import PromptServer -from loguru import logger +from cozy_comfyui import \ + logger -NODE_CLASS_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS = {} -WEB_DIRECTORY = "./web" +from cozy_comfyui.node import \ + loader -ROOT = Path(__file__).resolve().parent -ROOT_COMFY = ROOT.parent.parent -ROOT_DOC = ROOT / 'res/doc' - -JOV_CONFIG = {} -JOV_WEB = ROOT / 'web' - -# nodes to skip on import; for online systems; skip Export, Streamreader, etc... -JOV_IGNORE_NODE = ROOT / 'ignore.txt' - -logger.add(sys.stdout, level=os.getenv("JOV_LOG_LEVEL", "INFO"), - filter=lambda record: "jovi" in record["extra"]) - -JOV_INTERNAL = os.getenv("JOV_INTERNAL", 'false').strip().lower() in ('true', '1', 't') - -# direct the documentation output -- used to build jovimetrix-examples -JOV_INTERNAL_DOC = os.getenv("JOV_INTERNAL_DOC", str(ROOT / "_doc")) +from cozy_comfyui.api import \ + ComfyAPIMessage JOV_DOCKERENV = False try: @@ -83,48 +58,6 @@ except FileNotFoundError: if JOV_DOCKERENV: logger.info("RUNNING IN A DOCKER") -# The object_info route data -- cached -COMFYUI_OBJ_DATA = {} - -# maximum items to show in help for combo list items -JOV_LIST_MAX = 25 - -# HTML TEMPLATES -TEMPLATE = {} - -# BAD ACTOR NODES -- GITHUB MARKDOWN HATES EMOJI -- SCREW GITHUB MARKDOWN -MARKDOWN = [ - "ADJUST", "BLEND", "CROP", "FLATTEN", "STEREOSCOPIC", "MIDI-MESSAGE", - "MIDI-FILTER", "STREAM-WRITER" -] - -# ============================================================================== -# === TYPE === -# ============================================================================== - -class AnyType(str): - """AnyType input wildcard trick taken from pythongossss's: - - https://github.com/pythongosssss/ComfyUI-Custom-Scripts - """ - def __ne__(self, __value: object) -> bool: - return False - -JOV_TYPE_ANY = AnyType("*") - -TensorType: TypeAlias = torch.Tensor -RGBAMaskType: TypeAlias = Tuple[TensorType, ...] -InputType: TypeAlias = Dict[str, Tuple[str|List[str], Dict[str, Any]]] - -# want to make explicit entries; comfy only looks for single type -JOV_TYPE_NUMBER = "BOOLEAN,FLOAT,INT" -JOV_TYPE_VECTOR = "VEC2,VEC3,VEC4,VEC2INT,VEC3INT,VEC4INT,COORD2D,COORD3D" -JOV_TYPE_NUMERICAL = f"{JOV_TYPE_NUMBER},{JOV_TYPE_VECTOR}" -JOV_TYPE_IMAGE = "IMAGE,MASK" -JOV_TYPE_FULL = f"{JOV_TYPE_NUMBER},{JOV_TYPE_IMAGE}" - -JOV_TYPE_FULL = JOV_TYPE_ANY - # ============================================================================== # === LEXICON === # ============================================================================== @@ -163,19 +96,13 @@ class Lexicon(metaclass=LexiconMeta): ANGLE = '📐', "Rotation Angle" ANY = '🔮', "Any Type" ANY_OUT = 'đŸĻ„', "Any Type" - API = 'API', "API URL route" - ATTRIBUTE = 'ATTRIBUTE', "The token attribute to use for authenticating" - AUTH = 'AUTH', "Authentication Bearer Token" AUTOSIZE = 'AUTOSIZE', "Scale based on Width & Height" AXIS = 'AXIS', "Axis" B = 'đŸŸĻ', "Blue" BATCH = 'BATCH', "Output as a BATCH (all images in a single Tensor) or as a LIST of images (each image processed separately)" BATCH_CHUNK = 'CHUNK', "How many items to put per output. Default (0) is all items" BATCH_MODE = 'MODE', "Make, merge, splice or split a batch or list" - BBOX = '🔲', "Define an inner bounding box using relative coordinates [0..1] as a box region to clip." BI = '💙', "Blue Channel" - BIT = '', "Numerical Bits (0 or 1)" - BLACK = 'âŦ›', "Black Channel" BLBR = 'BL-BR', "Bottom Left - Bottom Right" BLUR = 'BLUR', "Blur" BOOLEAN = '🇴', "Boolean" @@ -186,10 +113,6 @@ class Lexicon(metaclass=LexiconMeta): C3 = 'đŸŸŖ', "Color Scheme Result 3" C4 = 'âšĢī¸', "Color Scheme Result 4" C5 = 'âšĒ', "Color Scheme Result 5" - CAMERA = '📹', "Camera" - C = '🇨', "Image Channels" - CHANNEL = 'CHAN', "Channel" - COLOR = 'ÂŠī¸', "Color Entry for Gradient" COLORMAP = '🇸🇨', "One of two dozen CV2 Built-in Colormap LUT (Look Up Table) Presets" COLORMATCH_MAP = 'MAP', "Custom image that will be transformed into a LUT or a built-in cv2 LUT" COLORMATCH_MODE = 'MODE', "Match colors from an image or built-in (LUT), Histogram lookups or Reinhard method" @@ -198,60 +121,38 @@ class Lexicon(metaclass=LexiconMeta): COMP_B = 'đŸĨĩ', "pass this data on a failure condition" COMPARE = 'đŸ•ĩđŸŊâ€â™€ī¸', "Comparison function. Will pass the data in 😍 on successful comparison" CONTRAST = '🌓', "Contrast" - CONTROL = 'đŸŽšī¸', "Control" COUNT = 'COUNT', 'Number of things' CURRENT = 'CURRENT', "Current" - DATA = '📓', "Data" DEFICIENCY = 'DEFICIENCY', "Type of color deficiency: Red (Protanopia), Green (Deuteranopia), Blue (Tritanopia)" - DELAY = '✋đŸŊ', "Delay" - DELTA = 'đŸ”ē', "Delta" DEPTH = 'DEPTH', "Grayscale image representing a depth map" - DEVICE = '📟', "Device" - DICT = '📖', "Dictionary" - DIFF = 'DIFF', "Difference" - DPI = 'DPI', "Use DPI mode from OS" EASE = 'EASE', "Easing function" EDGE = 'EDGE', "Clip or Wrap the Canvas Edge" - EDGE_X = 'EDGE_X', "Clip or Wrap the Canvas Edge" - EDGE_Y = 'EDGE_Y', "Clip or Wrap the Canvas Edge" ENABLE = 'ENABLE', "Enable or Disable" END = 'END', "End of the range" - FALSE = 'đŸ‡Ģ', "False" - FILEN = '💾', "File Name" - FILTER = '🔎', "Filter" - FIND = 'FIND', "Find" - FIXED = 'FIXED', "Fixed" FLIP = '🙃', "Flip Input A and Input B with each other" FLOAT = '🛟', "Float" FOCAL = 'đŸ“Ŋī¸', "Focal Length" - FOLDER = '📁', "Folder" FONT = 'FONT', "Available System Fonts" FONT_SIZE = 'SIZE', "Text Size" FORMAT = 'FORMAT', "Format" FPS = 'đŸŽī¸', "Frames per second" - FRAME = 'âšī¸', "Frame" FREQ = 'FREQ', "Frequency" FUNC = 'âš’ī¸', "Function" G = '🟩', "Green" GAMMA = '🔆', "Gamma" GI = '💚', "Green Channel" - GLSL_CUSTOM = '🧙đŸŊâ€â™€ī¸', "User GLSL Shader" - GLSL_INTERNAL = '🧙đŸŊ', "Internal GLSL Shader" GRADIENT = '🇲đŸ‡ē', "Gradient" H = '🇭', "Hue" HI = 'HI', "High / Top of range" HSV = 'HSV', "Hue, Saturation and Value" - HOLD = 'âš ī¸', "Hold" IMAGE = 'đŸ–ŧī¸', "RGB-A color image with alpha channel" IN_A = 'đŸ…°ī¸', "Input A" IN_B = 'đŸ…ąī¸', "Input B" INDEX = 'INDEX', "Current item index in the Queue list" INT = '🔟', "Integer" INVERT = 'đŸ”ŗ', "Color Inversion" - IO = '📋', "File I/O" JUSTIFY = 'JUSTIFY', "How to align the text to the side margins of the canvas: Left, Right, or Centered" KEY = '🔑', "Key" - LACUNARITY = 'LACUNARITY', "LACUNARITY" LEFT = 'â—€ī¸', "Left" LENGTH = 'LENGTH', "Length" LENGTH2 = 'FULL SIZE', "All items" @@ -263,86 +164,58 @@ class Lexicon(metaclass=LexiconMeta): LOHI = 'LoHi', "Low and High" LOOP = '🔄', "Loop" LUT = '😎', "Size of each output lut palette square" - M = '🖤', "Alpha Channel" MARGIN = 'MARGIN', "Whitespace padding around canvas" MASK = '😷', "Mask or Image to use as Mask to control where adjustments are applied" MATTE = 'MATTE', "Background color for padding" - MAX = 'MAX', "Maximum" MI = '🤍', "Alpha Channel" MID = 'MID', "Middle" - MIDI = 'đŸŽ›ī¸', "Midi" MIRROR = 'đŸĒž', "Mirror" MODE = 'MODE', "Decide whether the images should be resized to fit a specific dimension. Available modes include scaling to fit within given dimensions or keeping the original size" - MONITOR = 'đŸ–Ĩ', "Monitor" - NORMALIZE = '0-1', "Normalize" NOISE = 'NOISE', "Noise" NOTE = 'đŸŽļ', "Note" - OCTAVES = 'OCTAVES', "OCTAVES" OFFSET = 'OFFSET', "Offset" - ON = '🔛', "On" OPTIMIZE = 'OPT', "Optimize" - ORIENT = '🧭', "Orientation" OVERWRITE = 'OVERWRITE', "Overwrite" - PAD = 'PAD', "Padding" PALETTE = '🎨', "Palette" - PARAM = 'PARAM', "Parameters" PASS_IN = 'đŸ“Ĩ', "Pass In" PASS_OUT = '📤', "Pass Out" - PATH = 'PATH', "Selection path for array element" - PERSISTENCE = 'PERSISTENCE', "PERSISTENCE" - PERSPECTIVE = 'POINT', "Perspective" PHASE = 'PHASE', "Phase" PIVOT = 'PIVOT', "Pivot" PIXEL = '👾', "Pixel Data (RGBA, RGB or Grayscale)" PIXEL_A = '👾A', "Pixel Data (RGBA, RGB or Grayscale)" PIXEL_B = '👾B', "Pixel Data (RGBA, RGB or Grayscale)" PREFIX = 'PREFIX', "Prefix" - PRESET = 'PRESET', "Preset" - PROG_VERT = 'VERTEX', "Select a vertex program to load" - PROG_FRAG = 'FRAGMENT', "Select a fragment program to load" PROJECTION = 'PROJ', "Projection" QUALITY = 'QUALITY', "Quality" QUALITY_M = 'MOTION', "Motion Quality" QUEUE = 'Q', "Current items to process during Queue iteration." R = 'đŸŸĨ', "Red" RADIUS = '🅡', "Radius" - RANDOM = 'RNG', "Random" RANGE = 'RANGE', "start index, ending index (0 means full length) and how many items to skip per step" - RATE = 'RATE', "Rate" - RECORD = 'âē', "Arm record capture from selected device" - REGION = 'REGION', "Region" RECURSE = 'RECURSE', "Search within sub-directories" REPLACE = 'REPLACE', "String to use as replacement" RESET = 'RESET', "Reset" RGB = '🌈', "RGB (no alpha) Color" RGB_A = '🌈A', "RGB (no alpha) Color" RGBA_A = '🌈A', "RGB with Alpha Color" - RGBA_B = '🌈B', "RGB with Alpha Color" RI = 'â¤ī¸', "Red Channel" RIGHT = 'â–ļī¸', "Right" - ROTATE = '🔃', "Rotation Angle" - ROUND = 'ROUND', "Round to the nearest decimal place, or 0 for integer mode" ROUTE = '🚌', "Route" S = '🇸', "Saturation" SAMPLE = 'đŸŽžī¸', "Method for resizing images." SCHEME = 'SCHEME', "Scheme" SEED = 'seed', "Random generator's initial value" - SEGMENT = 'SEGMENT', "Number of parts which the input image should be split" - SELECT = 'SELECT', "Select" SHAPE = 'SHAPE', "Circle, Square or Polygonal forms" SHIFT = 'SHIFT', "Shift" SIDES = 'SIDES', "Number of sides polygon has (3-100)" SIMULATOR = 'SIMULATOR', "Solver to use when translating to new color space" SIZE = '📏', "Scalar by which to scale the input" - SKIP = 'SKIP', "Interval between segments" - SOURCE = 'SRC', "Source" SPACING = 'SPACING', "Line Spacing between Text Lines" START = 'START', "Start of the range" STEP = 'đŸĻļđŸŊ', "Steps/Stride between pulses -- useful to do odd or even batches. If set to 0 will stretch from (VAL -> LOOP) / Batch giving a linear range of values." STOP = 'STOP', "Halt processing" STRENGTH = 'đŸ’ĒđŸŊ', "Strength" STRING = '📝', "String Entry" - STYLE = 'STYLE', "Style" SWAP_A = 'SWAP A', "Replace input Alpha channel with target channel or constant" SWAP_B = 'SWAP B', "Replace input Blue channel with target channel or constant" SWAP_G = 'SWAP G', "Replace input Green channel with target channel or constant" @@ -351,20 +224,16 @@ class Lexicon(metaclass=LexiconMeta): SWAP_X = 'SWAP X', "Replace input Red channel with target channel or constant" SWAP_Y = 'SWAP Y', "Replace input Red channel with target channel or constant" SWAP_Z = 'SWAP Z', "Replace input Red channel with target channel or constant" - THICK = 'THICK', "Thickness" THRESHOLD = '📉', "Threshold" TILE = 'TILE', "How many times to repeat the data in the X and Y" TIME = '🕛', "Time" TIMER = '⏱', "Timer" TLTR = 'TL-TR', "Top Left - Top Right" - TOGGLE = 'TOGGLE', "Toggle" TOP = 'đŸ”ŧ', "Top" TOTAL = 'TOTAL', "Total items in the current Queue List" TRIGGER = '⚡', "Trigger" - TRUE = '🇹', "True" TYPE = '❓', "Type" UNKNOWN = '❔', "Unknown" - URL = '🌐', "URL" V = 'đŸ‡ģ', "Value" VALUE = 'VAL', "Value" VEC = 'VECTOR', "Compound value of type float, vec2, vec3 or vec4" @@ -373,16 +242,10 @@ class Lexicon(metaclass=LexiconMeta): WAVE = '♒', "Wave Function" WH = 'đŸ‡ŧ🇭', "Width and Height as a Vector2 (x,y)" WHC = 'đŸ‡ŧ🇭🇨', "Width, Height and Channel as a Vector3 (x,y,z)" - WINDOW = 'đŸĒŸ', "Window" X = 'đŸ‡Ŋ', "X" - X_RAW = 'X', "X" XY = 'đŸ‡Ŋ🇾', "X and Y" - XYZ = 'đŸ‡Ŋ🇾\u200cđŸ‡ŋ', "X, Y and Z (VEC3)" - XYZW = 'đŸ‡Ŋ🇾\u200cđŸ‡ŋ\u200cđŸ‡ŧ', "X, Y, Z and W (VEC4)" Y = '🇾', "Y" - Y_RAW = 'Y', "Y" Z = 'đŸ‡ŋ', "Z" - ZOOM = '🔎', "ZOOM" @classmethod def _parse(cls, node: dict) -> Dict[str, str]: @@ -401,114 +264,22 @@ class Lexicon(metaclass=LexiconMeta): return node # ============================================================================== -# === THERE CAN BE ONLY ONE === +# === GLOBAL === # ============================================================================== -class Singleton(type): - _instances = {} - - def __call__(cls, *arg, **kw) -> Any: - # If the instance does not exist, create and store it - if cls not in cls._instances: - instance = super().__call__(*arg, **kw) - cls._instances[cls] = instance - return cls._instances[cls] - -# ============================================================================== -# === CORE NODES === -# ============================================================================== - -class JOVBaseNode: - INPUT_IS_LIST = True - NOT_IDEMPOTENT = True - RETURN_TYPES = () - FUNCTION = "run" - - @classmethod - def VALIDATE_INPUTS(cls, input_types) -> bool: - return True - - @classmethod - def INPUT_TYPES(cls, prompt:bool=False, extra_png:bool=False, dynprompt:bool=False) -> Dict[str, str]: - data = { - "optional": {}, - "required": {}, - "hidden": { - "ident": "UNIQUE_ID" - } - } - if prompt: - data["hidden"]["prompt"] = "PROMPT" - if extra_png: - data["hidden"]["extra_pnginfo"] = "EXTRA_PNGINFO" - - if dynprompt: - data["hidden"]["dynprompt"] = "DYNPROMPT" - return data - -class JOVImageNode(JOVBaseNode): - RETURN_TYPES = ("IMAGE", "IMAGE", "MASK") - RETURN_NAMES = (Lexicon.IMAGE, Lexicon.RGB, Lexicon.MASK) - OUTPUT_TOOLTIPS = ( - "Full channel [RGBA] image. If there is an alpha, the image will be masked out with it when using this output.", - "Three channel [RGB] image. There will be no alpha.", - "Single channel mask output." - ) - -def deep_merge(d1: dict, d2: dict) -> Dict[str, str]: - """ - Deep merge multiple dictionaries recursively. - - Args: - *dicts: Variable number of dictionaries to be merged. - - Returns: - dict: Merged dictionary. - """ - for key in d2: - if key in d1: - if isinstance(d1[key], dict) and isinstance(d2[key], dict): - deep_merge(d1[key], d2[key]) - else: - d1[key] = d2[key] - else: - d1[key] = d2[key] - return d1 +PACKAGE = "JOVIMETRIX" +WEB_DIRECTORY = "./web" +ROOT = Path(__file__).resolve().parent +NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS = loader(ROOT, + PACKAGE, + "core", + f"{PACKAGE} đŸ”ē🟩đŸ”ĩ", + False) # ============================================================================== # === API RESPONSE === # ============================================================================== -class TimedOutException(Exception): pass - -class ComfyAPIMessage: - # STASH = {} - MESSAGE = {} - - #@classmethod - #def send(cls, ident, message) -> None: - #cls.MESSAGE[str(ident)] = message - - @classmethod - def poll(cls, ident, period=0.01, timeout=3) -> Any: - _t = time.perf_counter() - if isinstance(ident, (set, list, tuple, )): - ident = ident[0] - sid = str(ident) - logger.debug(f'sid {sid} -- {cls.MESSAGE}') - while not (sid in cls.MESSAGE) and time.perf_counter() - _t < timeout: - time.sleep(period) - - if not (sid in cls.MESSAGE): - # logger.warning(f"message failed {sid}") - raise TimedOutException - dat = cls.MESSAGE.pop(sid) - return dat - -def comfy_api_post(route:str, ident:str, data:dict) -> None: - data['id'] = ident - PromptServer.instance.send_sync(route, data) - @PromptServer.instance.routes.get("/jovimetrix/message") async def jovimetrix_message(req) -> Any: return web.json_response(ComfyAPIMessage.MESSAGE) @@ -521,109 +292,3 @@ async def jovimetrix_message_post(req) -> Any: ComfyAPIMessage.MESSAGE[str(did)] = json_data return web.json_response(json_data) return web.json_response({}) - -# ============================================================================== -# === SUPPORT === -# ============================================================================== - -def parse_reset(ident:str) -> int: - try: - data = ComfyAPIMessage.poll(ident, timeout=0.05) - ret = data.get('cmd', None) - return ret == 'reset' - except TimedOutException as e: - return -1 - except Exception as e: - logger.error(str(e)) - -def configLoad(fname:Path, as_json:bool=True) -> Any | list[str] | None: - try: - with open(fname, 'r', encoding='utf-8') as fn: - if as_json: - return json.load(fn) - return fn.read().splitlines() - except (IOError, FileNotFoundError) as e: - pass - except Exception as e: - logger.error(e) - return [] - -def load_module(name: str) -> None|ModuleType: - module = inspect.getmodule(inspect.stack()[0][0]).__name__ - module = module.replace("\\", "/") - route = str(name).replace("\\", "/") - try: - module = module.split("/")[-1] - route = route.split(f"{module}/")[1] - route = route.split('.')[0] - route = route.replace('/', '.') - module = f"{module}.{route}" - return importlib.import_module(module) - except Exception as e: - logger.warning(f"file failed {name}") - logger.warning(f"module {module}") - logger.warning(str(e)) - -def loader(): - global JOV_CONFIG, JOV_IGNORE_NODE, NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS - NODE_LIST_MAP = {} - - if JOV_IGNORE_NODE.exists(): - JOV_IGNORE_NODE = configLoad(JOV_IGNORE_NODE, False) - else: - JOV_IGNORE_NODE = [] - - for fname in ROOT.glob('core/**/*.py'): - if fname.stem.startswith('_'): - continue - - if fname.stem in JOV_IGNORE_NODE or fname.stem+'.py' in JOV_IGNORE_NODE: - logger.warning(f"💀 [IGNORED] .core.{fname.stem}") - continue - - if (module := load_module(fname)) is None: - continue - - # check if there is a dynamic register function.... - try: - for class_name, class_def in module.import_dynamic(): - setattr(module, class_name, class_def) - except Exception as e: - pass - - classes = inspect.getmembers(module, inspect.isclass) - for class_name, class_object in classes: - # assume both attrs are good enough.... - if not class_name.endswith('BaseNode') and hasattr(class_object, 'NAME') and hasattr(class_object, 'CATEGORY'): - if (name := class_object.NAME) in JOV_IGNORE_NODE: - logger.warning(f"đŸ˜Ĩ {name}") - continue - - name = class_object.NAME - NODE_DISPLAY_NAME_MAPPINGS[name] = name - NODE_CLASS_MAPPINGS[name] = class_object - - if not name.endswith(Lexicon.GLSL_CUSTOM): - desc = class_object.DESCRIPTION if hasattr(class_object, 'DESCRIPTION') else name - NODE_LIST_MAP[name] = desc.split('.')[0].strip('\n') - else: - logger.debug(f"customs {name}") - - NODE_CLASS_MAPPINGS = {x[0] : x[1] for x in sorted(NODE_CLASS_MAPPINGS.items(), - key=lambda item: getattr(item[1], 'SORT', 0))} - - keys = NODE_CLASS_MAPPINGS.keys() - #for name in keys: - # logger.debug(f"✅ {name}") - logger.info(f"{len(keys)} nodes loaded") - - # only do the list on local runs... - if JOV_INTERNAL: - with open(str(ROOT) + "/node_list.json", "w", encoding="utf-8") as f: - json.dump(NODE_LIST_MAP, f, sort_keys=True, indent=4 ) - -# ============================================================================== -# === BOOTSTRAP === -# ============================================================================== - -loader() diff --git a/core/calc.py b/core/calc.py index 68ace8e..4791018 100644 --- a/core/calc.py +++ b/core/calc.py @@ -1,11 +1,9 @@ -""" -Jovimetrix - Calculation -""" +""" Jovimetrix - Calculation """ -import struct import sys import math import random +import struct from enum import Enum from typing import Any, List, Tuple from collections import Counter @@ -13,50 +11,30 @@ from collections import Counter import torch import numpy as np from scipy.special import gamma -from loguru import logger from comfy.utils import ProgressBar -from .. import \ - JOV_TYPE_ANY, JOV_TYPE_FULL, JOV_TYPE_NUMBER, JOV_TYPE_NUMERICAL, \ - InputType, Lexicon, JOVBaseNode, \ - comfy_api_post, deep_merge, parse_reset +from cozy_comfyui import \ + logger, \ + TensorType, InputType, EnumConvertType, \ + deep_merge, parse_dynamic, parse_param, parse_value, zip_longest_fill -from ..sup.util import \ - EnumConvertType, EnumSwizzle, \ - parse_dynamic, parse_param, parse_value, vector_swap, zip_longest_fill +from cozy_comfyui.node import \ + COZY_TYPE_ANY, COZY_TYPE_NUMERICAL, COZY_TYPE_NUMBER, COZY_TYPE_FULL, \ + CozyBaseNode + +from cozy_comfyui.api import \ + comfy_api_post, parse_reset + +from .. import \ + Lexicon from ..sup.anim import \ EnumWave, EnumEase, \ ease_op, wave_op -# ============================================================================== - JOV_CATEGORY = "CALC" -# ============================================================================== -# === SUPPORT === -# ============================================================================== - -LAMBDA_FLATTEN = lambda data: [item for sublist in data for item in sublist] - -def flatten(data): - if isinstance(data, list): - return [a for i in data for a in flatten(i)] - else: - return [data] - -def to_bits(value): - if isinstance(value, int): - return bin(value)[2:] - elif isinstance(value, float): - packed = struct.pack('>d', value) - return ''.join(f'{byte:08b}' for byte in packed) - elif isinstance(value, str): - return ''.join(f'{ord(c):08b}' for c in value) - else: - raise TypeError(f"Unsupported type: {type(value)}") - # ============================================================================== # === ENUMERATION === # ============================================================================== @@ -127,6 +105,17 @@ class EnumNumberType(Enum): INT = 0 FLOAT = 10 +class EnumSwizzle(Enum): + A_X = 0 + A_Y = 10 + A_Z = 20 + A_W = 30 + B_X = 9 + B_Y = 11 + B_Z = 21 + B_W = 31 + CONSTANT = 40 + class EnumUnaryOperation(Enum): ABS = 0 FLOOR = 1 @@ -192,6 +181,47 @@ OP_UNARY = { EnumUnaryOperation.GAMMA: lambda x: gamma(x) if x > 0 else 0, } +# ============================================================================== +# === SUPPORT === +# ============================================================================== + +LAMBDA_FLATTEN = lambda data: [item for sublist in data for item in sublist] + +def flatten(data): + if isinstance(data, list): + return [a for i in data for a in flatten(i)] + else: + return [data] + +def to_bits(value): + if isinstance(value, int): + return bin(value)[2:] + elif isinstance(value, float): + packed = struct.pack('>d', value) + return ''.join(f'{byte:08b}' for byte in packed) + elif isinstance(value, str): + return ''.join(f'{ord(c):08b}' for c in value) + else: + raise TypeError(f"Unsupported type: {type(value)}") + +def vector_swap(pA: Any, pB: Any, swap_x: EnumSwizzle, x:float, swap_y:EnumSwizzle, y:float, + swap_z:EnumSwizzle, z:float, swap_w:EnumSwizzle, w:float) -> List[float]: + """Swap out a vector's values with another vector's values, or a constant fill.""" + def parse(target, targetB, swap, val) -> float: + if swap == EnumSwizzle.CONSTANT: + return val + if swap in [EnumSwizzle.B_X, EnumSwizzle.B_Y, EnumSwizzle.B_Z, EnumSwizzle.B_W]: + target = targetB + swap = int(swap.value / 10) + return target[swap] if swap < len(target) else 0 + + return [ + parse(pA, pB, swap_x, x), + parse(pA, pB, swap_y, y), + parse(pA, pB, swap_z, z), + parse(pA, pB, swap_w, w) + ] + # ============================================================================== # === CLASS === # ============================================================================== @@ -204,11 +234,11 @@ class ResultObject(object): self.trigger = [] self.batch = [] -class BitSplitNode(JOVBaseNode): +class BitSplitNode(CozyBaseNode): NAME = "BIT SPLIT (JOV) ⭄" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY, "BOOLEAN",) - RETURN_NAMES = (Lexicon.BIT, Lexicon.BOOLEAN,) + RETURN_TYPES = (COZY_TYPE_ANY, "BOOLEAN",) + RETURN_NAMES = ("BIT", Lexicon.BOOLEAN,) OUTPUT_TOOLTIPS = ( "Bits as Numerical output (0 or 1)", "Bits as Boolean output (True or False)" @@ -225,7 +255,7 @@ IMAGE and MASK will return a TRUE bit for any non-black pixel, as a stream of bi d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - "VALUE": (JOV_TYPE_FULL, {"default": None, "tooltip":"the value to convert into bits"}), + "VALUE": (COZY_TYPE_FULL, {"default": None, "tooltip":"the value to convert into bits"}), "BITS": ("INT", {"default": 8, "min": 1, "max": 64, "tooltip":"number of output bits requested"}), "MSB": ("BOOLEAN", {"default": False, "tooltip":"return the most signifigant bits (True) or least signifigant bits first"}) } @@ -260,10 +290,10 @@ IMAGE and MASK will return a TRUE bit for any non-black pixel, as a stream of bi pbar.update_absolute(idx) return *list(zip(*results)), -class CalcUnaryOPNode(JOVBaseNode): +class CalcUnaryOPNode(CozyBaseNode): NAME = "OP UNARY (JOV) 🎲" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.UNKNOWN,) OUTPUT_TOOLTIPS = ( "Output type will match the input type" @@ -278,7 +308,7 @@ Perform single function operations like absolute value, mean, median, mode, magn d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_NUMERICAL, {"default": None}), + Lexicon.IN_A: (COZY_TYPE_NUMERICAL, {"default": None}), Lexicon.FUNC: (EnumUnaryOperation._member_names_, {"default": EnumUnaryOperation.ABS.name}) } }) @@ -304,7 +334,7 @@ Perform single function operations like absolute value, mean, median, mode, magn typ = EnumConvertType(len(A) * 10) elif isinstance(A, (dict,)): typ = EnumConvertType.DICT - elif isinstance(A, (torch.Tensor,)): + elif isinstance(A, (TensorType,)): typ = EnumConvertType.IMAGE val = parse_value(A, typ, 0) @@ -361,10 +391,10 @@ Perform single function operations like absolute value, mean, median, mode, magn pbar.update_absolute(idx) return (results,) -class CalcBinaryOPNode(JOVBaseNode): +class CalcBinaryOPNode(CozyBaseNode): NAME = "OP BINARY (JOV) 🌟" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.UNKNOWN,) OUTPUT_TOOLTIPS = ( "Output type will match the input type" @@ -381,9 +411,9 @@ Execute binary operations like addition, subtraction, multiplication, division, d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_NUMERICAL, {"default": None, + Lexicon.IN_A: (COZY_TYPE_NUMERICAL, {"default": None, "tooltip":"Passes a raw value directly, or supplies defaults for any value inputs without connections"}), - Lexicon.IN_B: (JOV_TYPE_NUMERICAL, {"default": None, + Lexicon.IN_B: (COZY_TYPE_NUMERICAL, {"default": None, "tooltip":"Passes a raw value directly, or supplies defaults for any value inputs without connections"}), Lexicon.FUNC: (EnumBinaryOperation._member_names_, {"default": EnumBinaryOperation.ADD.name, "tooltip":"Arithmetic operation to perform"}), Lexicon.TYPE: (names_convert, {"default": names_convert[2], @@ -503,10 +533,10 @@ Execute binary operations like addition, subtraction, multiplication, division, pbar.update_absolute(idx) return results -class ComparisonNode(JOVBaseNode): +class ComparisonNode(CozyBaseNode): NAME = "COMPARISON (JOV) đŸ•ĩđŸŊ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY, JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY, COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.TRIGGER, Lexicon.VALUE,) OUTPUT_TOOLTIPS = ( f"Outputs the input at {Lexicon.IN_A} or {Lexicon.IN_B} depending on which evaluated TRUE", @@ -522,10 +552,10 @@ Evaluates two inputs (A and B) with a specified comparison operators and optiona d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_FULL, {"default": 0, "tooltip":"Master Comparator"}), - Lexicon.IN_B: (JOV_TYPE_FULL, {"default": 0, "tooltip":"Secondary Comparator"}), - Lexicon.COMP_A: (JOV_TYPE_ANY, {"default": 0}), - Lexicon.COMP_B: (JOV_TYPE_ANY, {"default": 0}), + Lexicon.IN_A: (COZY_TYPE_FULL, {"default": 0, "tooltip":"Master Comparator"}), + Lexicon.IN_B: (COZY_TYPE_FULL, {"default": 0, "tooltip":"Secondary Comparator"}), + Lexicon.COMP_A: (COZY_TYPE_ANY, {"default": 0}), + Lexicon.COMP_B: (COZY_TYPE_ANY, {"default": 0}), Lexicon.COMPARE: (EnumComparison._member_names_, {"default": EnumComparison.EQUAL.name}), Lexicon.FLIP: ("BOOLEAN", {"default": False}), Lexicon.INVERT: ("BOOLEAN", {"default": False, "tooltip":"reverse the successful and failure inputs"}), @@ -613,7 +643,7 @@ Evaluates two inputs (A and B) with a specified comparison operators and optiona pbar.update_absolute(idx) outs, vals = zip(*results) - if isinstance(outs[0], (torch.Tensor,)): + if isinstance(outs[0], (TensorType,)): if len(outs) > 1: outs = torch.stack(outs) else: @@ -622,10 +652,10 @@ Evaluates two inputs (A and B) with a specified comparison operators and optiona outs = list(outs) return outs, *vals, -class LerpNode(JOVBaseNode): +class LerpNode(CozyBaseNode): NAME = "LERP (JOV) 🔰" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.ANY_OUT,) OUTPUT_TOOLTIPS = ( f"Output can vary depending on the type chosen in the {Lexicon.TYPE} parameter" @@ -645,8 +675,8 @@ Additionally, you can specify the easing function (EASE) and the desired output names_convert = EnumConvertType._member_names_[:10] d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_FULL, {"tooltip": "Custom Start Point"}), - Lexicon.IN_B: (JOV_TYPE_FULL, {"tooltip": "Custom End Point"}), + Lexicon.IN_A: (COZY_TYPE_FULL, {"tooltip": "Custom Start Point"}), + Lexicon.IN_B: (COZY_TYPE_FULL, {"tooltip": "Custom End Point"}), Lexicon.FLOAT: ("VEC4", {"default": (0.5, 0.5, 0.5, 0.5), "mij": 0., "maj": 1.0, "tooltip": "Blend Amount. 0 = full A, 1 = full B"}), @@ -714,7 +744,7 @@ Additionally, you can specify the easing function (EASE) and the desired output pbar.update_absolute(idx) return [values] -class StringerNode(JOVBaseNode): +class StringerNode(CozyBaseNode): NAME = "STRINGER (JOV) đŸĒ€" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("STRING", "INT",) @@ -739,7 +769,7 @@ Manipulate strings through filtering }) return Lexicon._parse(d) - def run(self, **kw) -> Tuple[torch.Tensor, ...]: + def run(self, **kw) -> Tuple[TensorType, ...]: # turn any all inputs into the data_list = parse_dynamic(kw, Lexicon.UNKNOWN, EnumConvertType.ANY, [""]) if data_list is None: @@ -779,10 +809,10 @@ Manipulate strings through filtering results = [""] return (results, [len(r) for r in results],) if len(results) > 1 else (results[0], len(results[0]),) -class SwizzleNode(JOVBaseNode): +class SwizzleNode(CozyBaseNode): NAME = "SWIZZLE (JOV) đŸ˜ĩ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.ANY_OUT,) SORT = 40 DESCRIPTION = """ @@ -795,8 +825,8 @@ Swap components between two vectors based on specified swizzle patterns and valu names_convert = EnumConvertType._member_names_[3:10] d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_NUMERICAL, {}), - Lexicon.IN_B: (JOV_TYPE_NUMERICAL, {}), + Lexicon.IN_A: (COZY_TYPE_NUMERICAL, {}), + Lexicon.IN_B: (COZY_TYPE_NUMERICAL, {}), Lexicon.TYPE: (names_convert, {"default": names_convert[2], "tooltip":"Output type desired from resultant operation"}), Lexicon.SWAP_X: (EnumSwizzle._member_names_, {"default": EnumSwizzle.A_X.name}), @@ -808,7 +838,7 @@ Swap components between two vectors based on specified swizzle patterns and valu }) return Lexicon._parse(d) - def run(self, **kw) -> Tuple[torch.Tensor, ...]: + def run(self, **kw) -> Tuple[TensorType, ...]: pA = parse_param(kw, Lexicon.IN_A, EnumConvertType.VEC4, [(0,0,0,0)]) pB = parse_param(kw, Lexicon.IN_B, EnumConvertType.VEC4, [(0,0,0,0)]) swap_x = parse_param(kw, Lexicon.SWAP_X, EnumSwizzle, EnumSwizzle.A_X.name) @@ -826,10 +856,10 @@ Swap components between two vectors based on specified swizzle patterns and valu pbar.update_absolute(idx) return results -class TickNode(JOVBaseNode): +class TickNode(CozyBaseNode): NAME = "TICK (JOV) ⏱" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = ("INT", "FLOAT", "FLOAT", JOV_TYPE_ANY, JOV_TYPE_ANY,) + RETURN_TYPES = ("INT", "FLOAT", "FLOAT", COZY_TYPE_ANY, COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.VALUE, Lexicon.LINEAR, Lexicon.FPS, Lexicon.TRIGGER, Lexicon.BATCH,) OUTPUT_IS_LIST = (True, False, False, False, False,) OUTPUT_TOOLTIPS = ( @@ -850,7 +880,7 @@ A timer and frame counter, emitting pulses or signals based on time intervals. I d = deep_merge(d, { "optional": { # data to pass on a pulse of the loop - Lexicon.TRIGGER: (JOV_TYPE_ANY, {"default": None, + Lexicon.TRIGGER: (COZY_TYPE_ANY, {"default": None, "tooltip":"Output to send when beat (BPM setting) is hit"}), # forces a MOD on CYCLE Lexicon.VALUE: ("INT", {"default": 0, "min": 0, "max": sys.maxsize, @@ -923,10 +953,10 @@ A timer and frame counter, emitting pulses or signals based on time intervals. I comfy_api_post("jovi-tick", ident, {"i": self.__frame}) return (results.frame, results.lin, results.fixed, results.trigger, results.batch,) -class ValueNode(JOVBaseNode): +class ValueNode(CozyBaseNode): NAME = "VALUE (JOV) đŸ§Ŧ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY, JOV_TYPE_ANY, JOV_TYPE_ANY, JOV_TYPE_ANY, JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY, COZY_TYPE_ANY, COZY_TYPE_ANY, COZY_TYPE_ANY, COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.ANY_OUT, Lexicon.X, Lexicon.Y, Lexicon.Z, Lexicon.W) SORT = 5 DESCRIPTION = """ @@ -945,17 +975,17 @@ Supplies raw or default values for various data types, supporting vector input w d = deep_merge(d, { "optional": { - Lexicon.IN_A: (JOV_TYPE_ANY, {"default": None, + Lexicon.IN_A: (COZY_TYPE_ANY, {"default": None, "tooltip":"Passes a raw value directly, or supplies defaults for any value inputs without connections"}), Lexicon.TYPE: (typ, {"default": EnumConvertType.BOOLEAN.name, "tooltip":"Take the input and convert it into the selected type."}), - Lexicon.X: (JOV_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, + Lexicon.X: (COZY_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, "maj": sys.maxsize, "step": 0.01, "forceInput": True}), - Lexicon.Y: (JOV_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, + Lexicon.Y: (COZY_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, "maj": sys.maxsize, "step": 0.01, "forceInput": True}), - Lexicon.Z: (JOV_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, + Lexicon.Z: (COZY_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, "maj": sys.maxsize, "step": 0.01, "forceInput": True}), - Lexicon.W: (JOV_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, + Lexicon.W: (COZY_TYPE_NUMERICAL, {"default": 0, "mij": -sys.maxsize, "maj": sys.maxsize, "step": 0.01, "forceInput": True}), Lexicon.IN_A+Lexicon.IN_A: ("VEC4", {"default": (0, 0, 0, 0), #"mij": -sys.maxsize, "maj": sys.maxsize, @@ -1041,7 +1071,7 @@ Supplies raw or default values for various data types, supporting vector input w return results[0] return *list(zip(*results)), -class WaveGeneratorNode(JOVBaseNode): +class WaveGeneratorNode(CozyBaseNode): NAME = "WAVE GEN (JOV) 🌊" NAME_PRETTY = "WAVE GEN (JOV) 🌊" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" @@ -1092,7 +1122,7 @@ Produce waveforms like sine, square, or sawtooth with adjustable frequency, ampl pbar.update_absolute(idx) return *list(zip(*results)), -class Vector2Node(JOVBaseNode): +class Vector2Node(CozyBaseNode): NAME = "VECTOR2 (JOV)" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("VEC2", "VEC2INT", ) @@ -1111,8 +1141,8 @@ Outputs a VEC2 or VEC2INT. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - "X": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), - "Y": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), + "X": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), + "Y": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), } }) return Lexicon._parse(d) @@ -1130,7 +1160,7 @@ Outputs a VEC2 or VEC2INT. pbar.update_absolute(idx) return *list(zip(*results)), -class Vector3Node(JOVBaseNode): +class Vector3Node(CozyBaseNode): NAME = "VECTOR3 (JOV)" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("VEC3", "VEC3INT", ) @@ -1149,9 +1179,9 @@ Outputs a VEC3 or VEC3INT. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - "X": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), - "Y": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), - "Z": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "3rd channel value"}), + "X": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), + "Y": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), + "Z": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "3rd channel value"}), } }) return Lexicon._parse(d) @@ -1171,7 +1201,7 @@ Outputs a VEC3 or VEC3INT. pbar.update_absolute(idx) return *list(zip(*results)), -class Vector4Node(JOVBaseNode): +class Vector4Node(CozyBaseNode): NAME = "VECTOR4 (JOV)" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("VEC4", "VEC4INT", ) @@ -1190,10 +1220,10 @@ Outputs a VEC4 or VEC4INT. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - "X": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), - "Y": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), - "Z": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "3rd channel value"}), - "W": (JOV_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "4th channel value"}), + "X": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "1st channel value"}), + "Y": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "2nd channel value"}), + "Z": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "3rd channel value"}), + "W": (COZY_TYPE_NUMBER, {"default": 0, "min": -sys.maxsize, "max": sys.maxsize, "step": 0.01, "tooltip": "4th channel value"}), } }) return Lexicon._parse(d) @@ -1216,7 +1246,7 @@ Outputs a VEC4 or VEC4INT. return *list(zip(*results)), ''' -class ParameterNode(JOVBaseNode): +class ParameterNode(CozyBaseNode): NAME = "PARAMETER (JOV) âš™ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = () @@ -1231,7 +1261,7 @@ class ParameterNode(JOVBaseNode): d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PASS_IN: (JOV_TYPE_ANY, {"default": None}), + Lexicon.PASS_IN: (COZY_TYPE_ANY, {"default": None}), } }) return Lexicon._parse(d) diff --git a/core/compose.py b/core/compose.py index eee4f8e..8df53d5 100644 --- a/core/compose.py +++ b/core/compose.py @@ -1,32 +1,39 @@ -""" -Jovimetrix - Composition -""" +""" Jovimetrix - Composition """ from enum import Enum -from typing import Any, List, Tuple +from typing import List, Tuple import cv2 import torch import numpy as np -from loguru import logger - from comfy.utils import ProgressBar +from cozy_comfyui import \ + logger, \ + IMAGE_SIZE_MIN, \ + InputType, RGBAMaskType, EnumConvertType, TensorType, \ + deep_merge, parse_param, parse_dynamic, zip_longest_fill + +from cozy_comfyui.node import \ + COZY_TYPE_IMAGE, \ + CozyBaseNode, CozyImageNode + +from cozy_comfyui.image import \ + EnumImageType + +from cozy_comfyui.image.crop import \ + image_crop, image_crop_center, image_crop_polygonal + +from cozy_comfyui.image.convert import \ + image_mask, image_matte, image_mask_add, image_convert, tensor_to_cv, \ + cv_to_tensor, cv_to_tensor_full + +from cozy_comfyui.image.misc import \ + image_minmax + from .. import \ - JOV_TYPE_IMAGE, \ - JOVBaseNode, JOVImageNode, Lexicon, InputType, RGBAMaskType, \ - deep_merge - -from ..sup.util import \ - EnumConvertType, \ - parse_dynamic, parse_param, zip_longest_fill - -from ..sup.image import \ - MIN_IMAGE_SIZE, \ - EnumImageType, \ - image_mask, image_mask_add, image_matte, image_minmax, image_convert, \ - cv2tensor, cv2tensor_full, tensor2cv + Lexicon from ..sup.image.color import \ EnumCBDeficiency, EnumCBSimulator, EnumColorMap, EnumColorTheory, \ @@ -48,17 +55,18 @@ from ..sup.image.channel import \ from ..sup.image.compose import \ EnumAdjustOP, EnumBlendType, EnumOrientation, \ - image_levels, image_split, image_stack, image_blend, \ - image_crop, image_crop_center, image_crop_polygonal + image_levels, image_split, image_stack, image_blend from ..sup.image.mapping import \ EnumProjection, \ remap_fisheye, remap_perspective, remap_polar, remap_sphere -# ============================================================================== - JOV_CATEGORY = "COMPOSE" +# ============================================================================== +# === ENUMERATION === +# ============================================================================== + class EnumColorMatchMode(Enum): REINHARD = 30 LUT = 10 @@ -76,8 +84,10 @@ class EnumCropMode(Enum): BODY = 25 # ============================================================================== +# === CLASS === +# ============================================================================== -class AdjustNode(JOVImageNode): +class AdjustNode(CozyImageNode): NAME = "ADJUST (JOV) đŸ•¸ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -89,8 +99,8 @@ Enhance and modify images with various effects such as blurring, sharpening, col d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), - Lexicon.MASK: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), + Lexicon.MASK: (COZY_TYPE_IMAGE, {}), Lexicon.FUNC: (EnumAdjustOP._member_names_, {"default": EnumAdjustOP.BLUR.name, "tooltip":"Type of adjustment (e.g., blur, sharpen, invert)"}), Lexicon.RADIUS: ("INT", {"default": 3, "min": 3}), @@ -127,7 +137,7 @@ Enhance and modify images with various effects such as blurring, sharpening, col images = [] pbar = ProgressBar(len(params)) for idx, (pA, mask, op, radius, val, lohi, lmh, hsv, contrast, gamma, matte, invert) in enumerate(params): - pA = tensor2cv(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(chan=EnumImageType.BGR) img_new = image_convert(pA, 3) match op: @@ -209,7 +219,7 @@ Enhance and modify images with various effects such as blurring, sharpening, col img_new = cv2.morphologyEx(img_new, cv2.MORPH_CLOSE, (radius, radius), iterations=int(val)) if mask is not None: - mask = tensor2cv(mask) + mask = tensor_to_cv(mask) if invert: mask = 255 - mask @@ -220,11 +230,11 @@ Enhance and modify images with various effects such as blurring, sharpening, col img_new[:,:,3] = mask # img_new = image_mask_add(mask) - images.append(cv2tensor_full(img_new, matte)) + images.append(cv_to_tensor_full(img_new, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class BlendNode(JOVImageNode): +class BlendNode(CozyImageNode): NAME = "BLEND (JOV) âš—ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 10 @@ -237,15 +247,15 @@ Combine two input images using various blending modes, such as normal, screen, m d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL_A: (JOV_TYPE_IMAGE, {"tooltip": "Background Plate"}), - Lexicon.PIXEL_B: (JOV_TYPE_IMAGE, {"tooltip": "Image to Overlay on Background Plate"}), - Lexicon.MASK: (JOV_TYPE_IMAGE, {"tooltip": "Optional Mask to use for Alpha Blend Operation. If empty, will use the ALPHA of B"}), + Lexicon.PIXEL_A: (COZY_TYPE_IMAGE, {"tooltip": "Background Plate"}), + Lexicon.PIXEL_B: (COZY_TYPE_IMAGE, {"tooltip": "Image to Overlay on Background Plate"}), + Lexicon.MASK: (COZY_TYPE_IMAGE, {"tooltip": "Optional Mask to use for Alpha Blend Operation. If empty, will use the ALPHA of B"}), Lexicon.FUNC: (EnumBlendType._member_names_, {"default": EnumBlendType.NORMAL.name, "tooltip": "Blending Operation"}), Lexicon.A: ("FLOAT", {"default": 1, "min": 0, "max": 1, "step": 0.01, "tooltip": "Amount of Blending to Perform on the Selected Operation"}), Lexicon.FLIP: ("BOOLEAN", {"default": False}), Lexicon.INVERT: ("BOOLEAN", {"default": False, "tooltip": "Invert the mask input"}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) } @@ -260,7 +270,7 @@ Combine two input images using various blending modes, such as normal, screen, m alpha = parse_param(kw, Lexicon.A, EnumConvertType.FLOAT, 1, 0, 1) flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.BOOLEAN, False) mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + 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) invert = parse_param(kw, Lexicon.INVERT, EnumConvertType.BOOLEAN, False) @@ -272,7 +282,7 @@ Combine two input images using various blending modes, such as normal, screen, m if flip: pA, pB = pB, pA - width, height = MIN_IMAGE_SIZE, MIN_IMAGE_SIZE + width, height = IMAGE_SIZE_MIN, IMAGE_SIZE_MIN if pA is None: if pB is None: if mask is None: @@ -288,17 +298,17 @@ Combine two input images using various blending modes, such as normal, screen, m if pA is None: pA = channel_solid(width, height, matte, chan=EnumImageType.BGRA) else: - pA = tensor2cv(pA) + pA = tensor_to_cv(pA) matted = pixel_eval(matte, EnumImageType.BGRA) pA = image_matte(pA, matted) if pB is None: pB = channel_solid(width, height, matte, chan=EnumImageType.BGRA) else: - pB = tensor2cv(pB) + pB = tensor_to_cv(pB) if mask is not None: - mask = tensor2cv(mask) + mask = tensor_to_cv(mask) # mask = image_grayscale(mask) if invert: mask = 255 - mask @@ -310,12 +320,12 @@ Combine two input images using various blending modes, such as normal, screen, m width, height = wihi img = image_scalefit(img, width, height, mode, sample) - img = cv2tensor_full(img, matte) + img = cv_to_tensor_full(img, matte) images.append(img) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class ColorBlindNode(JOVImageNode): +class ColorBlindNode(CozyImageNode): NAME = "COLOR BLIND (JOV) 👁‍🗨" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -327,7 +337,7 @@ Simulate color blindness effects on images. You can select various types of colo d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.DEFICIENCY: (EnumCBDeficiency._member_names_, {"default": EnumCBDeficiency.PROTAN.name}), Lexicon.SIMULATOR: (EnumCBSimulator._member_names_, @@ -346,13 +356,13 @@ 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 tensor2cv(pA) + pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor_to_cv(pA) pA = color_blind(pA, deficiency, simulator, severity) - images.append(cv2tensor_full(pA)) + images.append(cv_to_tensor_full(pA)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class ColorMatchNode(JOVImageNode): +class ColorMatchNode(CozyImageNode): NAME = "COLOR MATCH (JOV) 💞" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -364,8 +374,8 @@ Adjust the color scheme of one image to match another with the Color Match Node. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL_A: (JOV_TYPE_IMAGE, {}), - Lexicon.PIXEL_B: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL_A: (COZY_TYPE_IMAGE, {}), + Lexicon.PIXEL_B: (COZY_TYPE_IMAGE, {}), Lexicon.COLORMATCH_MODE: (EnumColorMatchMode._member_names_, {"default": EnumColorMatchMode.REINHARD.name}), Lexicon.COLORMATCH_MAP: (EnumColorMatchMap._member_names_, @@ -402,7 +412,7 @@ Adjust the color scheme of one image to match another with the Color Match Node. if pA is None: pA = channel_solid(chan=EnumImageType.BGR) else: - pA = tensor2cv(pA) + pA = tensor_to_cv(pA) if pA.ndim == 3 and pA.shape[2] == 4: mask = image_mask(pA) @@ -410,7 +420,7 @@ Adjust the color scheme of one image to match another with the Color Match Node. if pB is None: pB = channel_solid(chan=EnumImageType.BGR) else: - pB = tensor2cv(pB) + pB = tensor_to_cv(pB) match mode: case EnumColorMatchMode.LUT: @@ -427,11 +437,11 @@ Adjust the color scheme of one image to match another with the Color Match Node. if mask is not None: pA = image_mask_add(pA, mask) - images.append(cv2tensor_full(pA, matte)) + images.append(cv_to_tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class ColorKMeansNode(JOVBaseNode): +class ColorKMeansNode(CozyBaseNode): NAME = "COLOR MEANS (JOV) ã€°ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "JLUT", "IMAGE",) @@ -452,11 +462,11 @@ The top-k colors ordered from most->least used as a strip, tonal palette and 3D d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.VALUE: ("INT", {"default": 12, "min": 1, "max": 255, "tooltip":"The top K colors to select."}), Lexicon.SIZE: ("INT", {"default": 32, "min": 1, "max": 256, "tooltip":"Height of the tones in the strip. Width is based on input."}), Lexicon.COUNT: ("INT", {"default": 33, "min": 3, "max": 256, "tooltip":"Number of nodes to use in interpolation of full LUT (256 is every pixel)."}), - Lexicon.WH: ("VEC2INT", {"default": (256, 256), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (256, 256), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), } }) return Lexicon._parse(d) @@ -466,7 +476,7 @@ The top-k colors ordered from most->least used as a strip, tonal palette and 3D kcolors = parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, 12, 1, 255) lut_height = parse_param(kw, Lexicon.SIZE, EnumConvertType.INT, 32, 1, 256) nodes = parse_param(kw, Lexicon.COUNT, EnumConvertType.INT, 33, 1, 255) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(256, 256)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(256, 256)], IMAGE_SIZE_MIN) params = list(zip_longest_fill(pA, kcolors, nodes, lut_height, wihi)) top_colors = [] @@ -479,23 +489,23 @@ The top-k colors ordered from most->least used as a strip, tonal palette and 3D if pA is None: pA = channel_solid(chan=EnumImageType.BGRA) - pA = tensor2cv(pA) + 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([cv2tensor(channel_solid(*wihi, color=c)) for c in colors]) - lut_tonal.append(cv2tensor(color_lut_tonal(colors, width=pA.shape[1], height=lut_height))) + 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))) full = color_lut_full(colors, nodes) lut_full.append(torch.from_numpy(full)) - lut_visualized.append(cv2tensor(color_lut_visualize(full, wihi[1]))) + lut_visualized.append(cv_to_tensor(color_lut_visualize(full, wihi[1]))) gradient = image_gradient_expand(color_lut_palette(colors, 1)) gradient = cv2.resize(gradient, wihi) - gradients.append(cv2tensor(gradient)) + gradients.append(cv_to_tensor(gradient)) pbar.update_absolute(idx) return torch.stack(top_colors), torch.stack(lut_tonal), torch.stack(gradients), lut_full, torch.stack(lut_visualized), -class ColorTheoryNode(JOVBaseNode): +class ColorTheoryNode(CozyBaseNode): NAME = "COLOR THEORY (JOV) 🛞" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE") @@ -510,7 +520,7 @@ Generate a color harmony based on the selected scheme. Supported schemes include d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.SCHEME: (EnumColorTheory._member_names_, {"default": EnumColorTheory.COMPLIMENTARY.name}), Lexicon.VALUE: ("INT", {"default": 45, "min": -90, "max": 90, "tooltip": "Custom angle of separation to use when calculating colors"}), @@ -519,7 +529,7 @@ Generate a color harmony based on the selected scheme. Supported schemes include }) return Lexicon._parse(d) - def run(self, **kw) -> Tuple[List[torch.Tensor], List[torch.Tensor]]: + def run(self, **kw) -> Tuple[List[TensorType], List[TensorType]]: pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None) scheme = parse_param(kw, Lexicon.SCHEME, EnumColorTheory, EnumColorTheory.COMPLIMENTARY.name) user = parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, 0, -180, 180) @@ -528,15 +538,15 @@ Generate a color harmony based on the selected scheme. Supported schemes include images = [] pbar = ProgressBar(len(params)) for idx, (img, target, user, invert) in enumerate(params): - img = tensor2cv(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(chan=EnumImageType.BGRA) img = color_theory(img, user, target) if invert: img = (image_invert(s, 1) for s in img) - images.append([cv2tensor(a) for a in img]) + images.append([cv_to_tensor(a) for a in img]) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class CropNode(JOVImageNode): +class CropNode(CozyImageNode): NAME = "CROP (JOV) âœ‚ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 5 @@ -549,10 +559,10 @@ Extract a portion of an input image or resize it. It supports various cropping m d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.FUNC: (EnumCropMode._member_names_, {"default": EnumCropMode.CENTER.name}), Lexicon.XY: ("VEC2", {"default": (0, 0), "mij": 0.5, "maj": 0.5, "step": 0.01, "label": [Lexicon.X, Lexicon.Y]}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij": MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij": IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.TLTR: ("VEC4", {"default": (0, 0, 0, 1), "mij": 0, "maj": 1, "step": 0.01, "label": [Lexicon.TOP, Lexicon.LEFT, Lexicon.TOP, Lexicon.RIGHT]}), Lexicon.BLBR: ("VEC4", {"default": (1, 0, 1, 1), "mij": 0, "maj": 1, "step": 0.01, "label": [Lexicon.BOTTOM, Lexicon.LEFT, Lexicon.BOTTOM, Lexicon.RIGHT]}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) @@ -565,7 +575,7 @@ Extract a portion of an input image or resize it. It supports various cropping m func = parse_param(kw, Lexicon.FUNC, EnumCropMode, EnumCropMode.CENTER.name) # if less than 1 then use as scalar, over 1 = int(size) xy = parse_param(kw, Lexicon.XY, EnumConvertType.VEC2, [(0, 0,)], 0, 1) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN) tltr = parse_param(kw, Lexicon.TLTR, EnumConvertType.VEC4, [(0, 0, 0, 1,)], 0, 1) blbr = parse_param(kw, Lexicon.BLBR, EnumConvertType.VEC4, [(1, 0, 1, 1,)], 0, 1) matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255) @@ -574,7 +584,7 @@ Extract a portion of an input image or resize it. It supports various cropping m pbar = ProgressBar(len(params)) for idx, (pA, func, xy, wihi, tltr, blbr, matte) in enumerate(params): width, height = wihi - pA = tensor2cv(pA) if pA is not None else channel_solid(width, height) + pA = tensor_to_cv(pA) if pA is not None else channel_solid(width, height) alpha = None if pA.ndim == 3 and pA.shape[2] == 4: alpha = image_mask(pA) @@ -596,11 +606,11 @@ Extract a portion of an input image or resize it. It supports various cropping m pass else: pA = image_crop_center(pA, width, height) - images.append(cv2tensor_full(pA, matte)) + images.append(cv_to_tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class FilterMaskNode(JOVImageNode): +class FilterMaskNode(CozyImageNode): NAME = "FILTER MASK (JOV) đŸ¤ŋ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 700 @@ -613,7 +623,7 @@ Create masks based on specific color ranges within an image. Specify the color r d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL_A: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL_A: (COZY_TYPE_IMAGE, {}), Lexicon.START: ("VEC3INT", {"default": (128, 128, 128), "rgb": True}), Lexicon.BOOLEAN: ("BOOLEAN", {"default": False, "tooltip": "use an end point (start->end) when calculating the filter range"}), Lexicon.END: ("VEC3INT", {"default": (128, 128, 128), "rgb": True}), @@ -634,18 +644,18 @@ Create masks based on specific color ranges within an image. Specify the color r images = [] pbar = ProgressBar(len(params)) for idx, (pA, start, use_range, end, fuzz, matte) in enumerate(params): - img = np.zeros((MIN_IMAGE_SIZE, MIN_IMAGE_SIZE, 3), dtype=np.uint8) if pA is None else tensor2cv(pA) + img = np.zeros((IMAGE_SIZE_MIN, IMAGE_SIZE_MIN, 3), dtype=np.uint8) if pA is None else tensor_to_cv(pA) img, mask = image_filter(img, start, end, fuzz, use_range) if img.shape[2] == 3: alpha_channel = np.zeros((img.shape[0], img.shape[1], 1), dtype=img.dtype) img = np.concatenate((img, alpha_channel), axis=2) img[..., 3] = mask[:,:] - images.append(cv2tensor_full(img, matte)) + images.append(cv_to_tensor_full(img, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class Flatten(JOVImageNode): +class Flatten(CozyImageNode): NAME = "FLATTEN (JOV) âŦ‡ī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 500 @@ -659,7 +669,7 @@ Combine multiple input images into a single image by summing their pixel values. d = deep_merge(d, { "optional": { Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) } @@ -673,9 +683,9 @@ Combine multiple input images into a single image by summing their pixel values. return () # be less dumb when merging - pA = [tensor2cv(i) for img in imgs for i in img] + pA = [tensor_to_cv(i) for img in imgs for i in img] mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + 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) @@ -684,11 +694,11 @@ Combine multiple input images into a single image by summing their pixel values. pbar = ProgressBar(len(params)) for idx, (mode, sample, wihi, matte) in enumerate(params): current = image_flatten(pA) - images.append(cv2tensor_full(current, matte)) + images.append(cv_to_tensor_full(current, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class GradientMap(JOVImageNode): +class GradientMap(CozyImageNode): NAME = "GRADIENT MAP (JOV) 🇲đŸ‡ē" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 550 @@ -701,11 +711,11 @@ Remaps an input image using a gradient lookup table (LUT). The gradient image wi d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {"tooltip":"Image to remap with gradient input"}), - Lexicon.GRADIENT: (JOV_TYPE_IMAGE, {"tooltip":f"Look up table (LUT) to remap the input image in `{Lexicon.PIXEL}`"}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {"tooltip":"Image to remap with gradient input"}), + Lexicon.GRADIENT: (COZY_TYPE_IMAGE, {"tooltip":f"Look up table (LUT) to remap the input image in `{Lexicon.PIXEL}`"}), Lexicon.FLIP: ("BOOLEAN", {"default":False, "tooltip":"Reverse the gradient from left-to-right "}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) } @@ -717,30 +727,30 @@ Remaps an input image using a gradient lookup table (LUT). The gradient image wi gradient = parse_param(kw, Lexicon.GRADIENT, EnumConvertType.IMAGE, None) flip = parse_param(kw, Lexicon.FLIP, EnumConvertType.BOOLEAN, False) mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + 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) images = [] params = list(zip_longest_fill(pA, gradient, flip, mode, sample, wihi, matte)) pbar = ProgressBar(len(params)) for idx, (pA, gradient, flip, mode, sample, wihi, matte) in enumerate(params): - pA = channel_solid(chan=EnumImageType.BGR) if pA is None else tensor2cv(pA) + pA = channel_solid(chan=EnumImageType.BGR) 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 tensor2cv(gradient) + gradient = channel_solid(chan=EnumImageType.BGR) if gradient is None else tensor_to_cv(gradient) pA = image_gradient_map(pA, gradient) if mode != EnumScaleMode.MATTE: w, h = wihi pA = image_scalefit(pA, w, h, mode, sample) if mask is not None: pA = image_mask_add(pA, mask) - images.append(cv2tensor_full(pA, matte)) + images.append(cv_to_tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class PixelMergeNode(JOVImageNode): +class PixelMergeNode(CozyImageNode): NAME = "PIXEL MERGE (JOV) đŸĢ‚" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 45 @@ -753,13 +763,13 @@ Combines individual color channels (red, green, blue) along with an optional mas d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), - Lexicon.R: (JOV_TYPE_IMAGE, {}), - Lexicon.G: (JOV_TYPE_IMAGE, {}), - Lexicon.B: (JOV_TYPE_IMAGE, {}), - Lexicon.A: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), + Lexicon.R: (COZY_TYPE_IMAGE, {}), + Lexicon.G: (COZY_TYPE_IMAGE, {}), + Lexicon.B: (COZY_TYPE_IMAGE, {}), + Lexicon.A: (COZY_TYPE_IMAGE, {}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}), Lexicon.FLIP: ("VEC4", {"mij":0, "maj":1, "step": 0.01, "tooltip": "Invert specific input prior to merging. R, G, B, A."}), @@ -775,7 +785,7 @@ Combines individual color channels (red, green, blue) along with an optional mas B = parse_param(kw, Lexicon.B, EnumConvertType.MASK, None) A = parse_param(kw, Lexicon.A, EnumConvertType.MASK, None) mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + 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.) @@ -786,12 +796,12 @@ Combines individual color channels (red, green, blue) along with an optional mas for idx, (rgba, r, g, b, a, mode, wihi, sample, matte, flip, invert) in enumerate(params): replace = r, g, b, a if rgba is not None: - rgba = tensor2cv(rgba) + rgba = tensor_to_cv(rgba) rgba = image_convert(rgba, 4) rgba = image_split(rgba) - img = [tensor2cv(replace[i]) if replace[i] is not None else x for i, x in enumerate(rgba)] + img = [tensor_to_cv(replace[i]) if replace[i] is not None else x for i, x in enumerate(rgba)] else: - img = [tensor2cv(x) if x is not None else x for x in replace] + img = [tensor_to_cv(x) if x is not None else x for x in replace] _, _, w_max, h_max = image_minmax(img) for i, x in enumerate(img): @@ -813,11 +823,11 @@ Combines individual color channels (red, green, blue) along with an optional mas if invert == True: img = image_invert(img, 1) - images.append(cv2tensor_full(img, matte)) + images.append(cv_to_tensor_full(img, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class PixelSplitNode(JOVBaseNode): +class PixelSplitNode(CozyBaseNode): NAME = "PIXEL SPLIT (JOV) 💔" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("MASK", "MASK", "MASK", "MASK",) @@ -838,7 +848,7 @@ Takes an input image and splits it into its individual color channels (red, gree d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}) + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}) } }) return Lexicon._parse(d) @@ -848,12 +858,12 @@ Takes an input image and splits it into its individual color channels (red, gree pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None) pbar = ProgressBar(len(pA)) for idx, pA in enumerate(pA): - pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor2cv(pA) - images.append([cv2tensor(x, True) for x in image_split(pA)]) + pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor_to_cv(pA) + images.append([cv_to_tensor(x, True) for x in image_split(pA)]) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class PixelSwapNode(JOVImageNode): +class PixelSwapNode(CozyImageNode): NAME = "PIXEL SWAP (JOV) 🔃" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 48 @@ -866,8 +876,8 @@ Swap pixel values between two input images based on specified channel swizzle op d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL_A: (JOV_TYPE_IMAGE, {}), - Lexicon.PIXEL_B: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL_A: (COZY_TYPE_IMAGE, {}), + Lexicon.PIXEL_B: (COZY_TYPE_IMAGE, {}), Lexicon.SWAP_R: (EnumPixelSwizzle._member_names_, {"default": EnumPixelSwizzle.RED_A.name}), Lexicon.SWAP_G: (EnumPixelSwizzle._member_names_, @@ -896,7 +906,7 @@ Swap pixel values between two input images based on specified channel swizzle op if pA is None: if pB is None: out = channel_solid(chan=EnumImageType.BGRA) - images.append(cv2tensor_full(out)) + images.append(cv_to_tensor_full(out)) pbar.update_absolute(idx) continue @@ -904,21 +914,21 @@ Swap pixel values between two input images based on specified channel swizzle op pA = channel_solid(w, h, chan=EnumImageType.BGRA) else: h, w = pA.shape[:2] - pA = tensor2cv(pA) + pA = tensor_to_cv(pA) pA = image_convert(pA, 4) - pB = tensor2cv(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, chan=EnumImageType.BGRA) pB = image_convert(pB, 4) pB = image_matte(pB, (0,0,0,0), w, h) pB = image_scalefit(pB, w, h, EnumScaleMode.CROP) out = image_swap_channels(pA, pB, (swap_r, swap_g, swap_b, swap_a), matte) - images.append(cv2tensor_full(out)) + images.append(cv_to_tensor_full(out)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class StackNode(JOVImageNode): +class StackNode(CozyImageNode): NAME = "STACK (JOV) ➕" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 75 @@ -936,7 +946,7 @@ Merge multiple input images into a single composite image by stacking them along Lexicon.STEP: ("INT", {"min": 0, "default": 1, "tooltip":"How many images are placed before a new row starts (stride)."}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) } @@ -948,22 +958,22 @@ Merge multiple input images into a single composite image by stacking them along if len(images) == 0: logger.warning("no images to stack") return - images = [tensor2cv(img) for sublist in images for img in sublist] + images = [tensor_to_cv(img) for sublist in images for img in sublist] axis = parse_param(kw, Lexicon.AXIS, EnumOrientation, EnumOrientation.GRID.name)[0] stride = parse_param(kw, Lexicon.STEP, EnumConvertType.INT, 1)[0] mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name)[0] - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)[0] + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN)[0] sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)[0] matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)[0] img = image_stack(images, axis, stride) #, matte) if mode != EnumScaleMode.MATTE: w, h = wihi img = image_scalefit(img, w, h, mode, sample) - rgba, rgb, mask = cv2tensor_full(img, matte) + rgba, rgb, mask = cv_to_tensor_full(img, matte) return rgba.unsqueeze(0), rgb.unsqueeze(0), mask.unsqueeze(0) -class ThresholdNode(JOVImageNode): +class ThresholdNode(CozyImageNode): NAME = "THRESHOLD (JOV) 📉" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -975,7 +985,7 @@ Define a range and apply it to an image for segmentation and feature extraction. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.ADAPT: ( EnumThresholdAdapt._member_names_, {"default": EnumThresholdAdapt.ADAPT_NONE.name}), Lexicon.FUNC: ( EnumThreshold._member_names_, {"default": EnumThreshold.BINARY.name}), @@ -997,15 +1007,15 @@ 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 = tensor2cv(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(chan=EnumImageType.BGRA) pA = image_threshold(pA, th, mode, adapt, block) if invert == True: pA = image_invert(pA, 1) - images.append(cv2tensor_full(pA)) + images.append(cv_to_tensor_full(pA)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class TransformNode(JOVImageNode): +class TransformNode(CozyImageNode): NAME = "TRANSFORM (JOV) đŸī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" SORT = 0 @@ -1018,8 +1028,8 @@ Apply various geometric transformations to images, including translation, rotati d = super().INPUT_TYPES(prompt=True, dynprompt=True) d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), - Lexicon.MASK: (JOV_TYPE_IMAGE, {"tooltip":"Override Image mask"}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), + Lexicon.MASK: (COZY_TYPE_IMAGE, {"tooltip":"Override Image mask"}), Lexicon.XY: ("VEC2", {"default": (0., 0.,), "mij": -1., "maj": 1., "step": 0.01, "label": [Lexicon.X, Lexicon.Y]}), Lexicon.ANGLE: ("FLOAT", {"default": 0., "step": 0.1}), Lexicon.SIZE: ("VEC2", {"default": (1., 1.), "mij": 0.001, "step": 0.01, "label": [Lexicon.X, Lexicon.Y]}), @@ -1032,7 +1042,7 @@ Apply various geometric transformations to images, including translation, rotati Lexicon.BLBR: ("VEC4", {"default": (0., 1., 1., 1.), "mij": 0., "maj": 1., "step": 0.005, "label": [Lexicon.BOTTOM, Lexicon.LEFT, Lexicon.BOTTOM, Lexicon.RIGHT]}), Lexicon.STRENGTH: ("FLOAT", {"default": 1, "min": 0, "step": 0.005}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), - Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, {"default": EnumInterpolation.LANCZOS4.name}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True}) } @@ -1054,16 +1064,16 @@ Apply various geometric transformations to images, including translation, rotati blbr = parse_param(kw, Lexicon.BLBR, EnumConvertType.VEC4, [(0., 1., 1., 1.)], 0, 1) strength = parse_param(kw, Lexicon.STRENGTH, EnumConvertType.FLOAT, 1, 0, 1) mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN) sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name) matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255) params = list(zip_longest_fill(pA, mask, offset, angle, size, edge, tile_xy, mirror, mirror_pivot, proj, strength, tltr, blbr, mode, wihi, sample, matte)) images = [] pbar = ProgressBar(len(params)) for idx, (pA, mask, offset, angle, size, edge, tile_xy, mirror, mirror_pivot, proj, strength, tltr, blbr, mode, wihi, sample, matte) in enumerate(params): - pA = tensor2cv(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(chan=EnumImageType.BGRA) if mask is not None: - mask = tensor2cv(mask) + mask = tensor_to_cv(mask) pA = image_mask_add(pA, mask) h, w = pA.shape[:2] @@ -1102,7 +1112,7 @@ Apply various geometric transformations to images, including translation, rotati w, h = wihi pA = image_scalefit(pA, w, h, mode, sample) - images.append(cv2tensor_full(pA, matte)) + images.append(cv_to_tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] @@ -1122,7 +1132,7 @@ The Histogram Node generates a histogram representation of the input image, show d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), } }) return Lexicon._parse(d) @@ -1135,7 +1145,7 @@ The Histogram Node generates a histogram representation of the input image, show for idx, (pA, ) in enumerate(params): pA = image_histogram(pA) pA = image_histogram_normalize(pA) - images.append(cv2tensor(pA)) + images.append(cv_to_tensor(pA)) pbar.update_absolute(idx) return list(zip(*images)) ''' diff --git a/core/create.py b/core/create.py index bc714be..2af4e18 100644 --- a/core/create.py +++ b/core/create.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Creation -""" +""" Jovimetrix - Creation """ from typing import Tuple @@ -11,19 +9,24 @@ from skimage.filters import gaussian from comfy.utils import ProgressBar +from cozy_comfyui import \ + IMAGE_SIZE_MIN, \ + InputType, EnumConvertType, RGBAMaskType, TensorType, \ + deep_merge, parse_param, zip_longest_fill + +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_mask_add, image_convert, pil_to_cv, cv_to_tensor, \ + cv_to_tensor_full, tensor_to_cv + from .. import \ - JOV_TYPE_IMAGE, \ - InputType, JOVBaseNode, JOVImageNode, Lexicon, RGBAMaskType, \ - deep_merge - -from ..sup.util import \ - EnumConvertType, \ - parse_param, zip_longest_fill - -from ..sup.image import \ - MIN_IMAGE_SIZE, \ - EnumImageType, \ - image_convert, image_mask_add, image_matte, cv2tensor, cv2tensor_full, tensor2cv, pil2cv + Lexicon from ..sup.image.channel import channel_solid @@ -41,13 +44,13 @@ from ..sup.text import \ EnumAlignment, EnumJustify, \ font_names, text_autosize, text_draw -# ============================================================================== - JOV_CATEGORY = "CREATE" +# ============================================================================== +# === CLASS === # ============================================================================== -class ConstantNode(JOVImageNode): +class ConstantNode(CozyImageNode): NAME = "CONSTANT (JOV) đŸŸĒ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -59,8 +62,8 @@ Generate a constant image or mask of a specified size and color. It can be used d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {"tooltip":"Optional Image to Matte with Selected Color"}), - Lexicon.MASK: (JOV_TYPE_IMAGE, {"tooltip":"Override Image mask"}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {"tooltip":"Optional Image to Matte with Selected Color"}), + Lexicon.MASK: (COZY_TYPE_IMAGE, {"tooltip":"Override Image mask"}), Lexicon.RGBA_A: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True, "tooltip": "Constant Color to Output"}), Lexicon.MODE: (EnumScaleMode._member_names_, {"default": EnumScaleMode.MATTE.name}), @@ -76,7 +79,7 @@ Generate a constant image or mask of a specified size and color. It can be used pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None) mask = parse_param(kw, Lexicon.MASK, EnumConvertType.IMAGE, None) matte = parse_param(kw, Lexicon.RGBA_A, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN) mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name) sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name) images = [] @@ -85,24 +88,24 @@ Generate a constant image or mask of a specified size and color. It can be used for idx, (pA, mask, matte, wihi, mode, sample) in enumerate(params): width, height = wihi if mask is not None: - mask = tensor2cv(mask) + mask = tensor_to_cv(mask) if pA is None: pA = channel_solid(width, height, matte, EnumImageType.BGRA) if mask is not None: pA = image_mask_add(pA, mask) - images.append(cv2tensor_full(pA)) + images.append(cv_to_tensor_full(pA)) else: - pA = tensor2cv(pA) + pA = tensor_to_cv(pA) pA = image_convert(pA, 4) if mask is not None: pA = image_mask_add(pA, mask) if mode != EnumScaleMode.MATTE: pA = image_scalefit(pA, width, height, mode, sample, matte) - images.append(cv2tensor_full(pA, matte)) + images.append(cv_to_tensor_full(pA, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class ShapeNode(JOVImageNode): +class ShapeNode(CozyImageNode): NAME = "SHAPE GEN (JOV) ✨" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -118,7 +121,7 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters Lexicon.SIDES: ("INT", {"default": 3, "min": 3, "max": 100}), Lexicon.RGBA_A: ("VEC4INT", {"default": (255, 255, 255, 255), "rgb": True, "tooltip": "Main Shape Color"}), Lexicon.MATTE: ("VEC4INT", {"default": (0, 0, 0, 255), "rgb": True, "tooltip": "Background Color"}), - Lexicon.WH: ("VEC2INT", {"default": (256, 256), "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + Lexicon.WH: ("VEC2INT", {"default": (256, 256), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.XY: ("VEC2", {"default": (0, 0,), "step": 0.01, "label": [Lexicon.X, Lexicon.Y]}), Lexicon.ANGLE: ("FLOAT", {"default": 0, "min": -180, "max": 180, "step": 0.01}), Lexicon.SIZE: ("VEC2", {"default": (1., 1.), "step": 0.01, "label": [Lexicon.X, Lexicon.Y]}), @@ -135,7 +138,7 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters edge = parse_param(kw, Lexicon.EDGE, EnumEdge, EnumEdge.CLIP.name) offset = parse_param(kw, Lexicon.XY, EnumConvertType.VEC2, [(0, 0)]) size = parse_param(kw, Lexicon.SIZE, EnumConvertType.VEC2, [(1, 1)], zero=0.001) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(256, 256)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(256, 256)], IMAGE_SIZE_MIN) color = parse_param(kw, Lexicon.RGBA_A, EnumConvertType.VEC4INT, [(255, 255, 255, 255)], 0, 255) matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255) blur = parse_param(kw, Lexicon.BLUR, EnumConvertType.FLOAT, 0) @@ -158,7 +161,7 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters case EnumShapes.POLYGON: pA = shape_polygon(width, height, sizeX, sides, fill, back) - pA = pil2cv(pA) + pA = pil_to_cv(pA) pA = image_transform(pA, offset, angle, edge=edge) if blur > 0: # @TODO: Do blur on larger canvas to remove wrap bleed. @@ -169,11 +172,11 @@ Create n-sided polygons. These shapes can be customized by adjusting parameters pB = image_mask_add(pA, mask) matte = image_matte(pB, matte) - images.append([cv2tensor(pB), cv2tensor(matte), cv2tensor(mask, True)]) + images.append([cv_to_tensor(pB), cv_to_tensor(matte), cv_to_tensor(mask, True)]) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class StereogramNode(JOVImageNode): +class StereogramNode(CozyImageNode): NAME = "STEREOGRAM (JOV) đŸ“ģ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" DESCRIPTION = """ @@ -185,8 +188,8 @@ Generates false perception 3D images from 2D input. Set tile divisions, noise, g d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), - Lexicon.DEPTH: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), + Lexicon.DEPTH: (COZY_TYPE_IMAGE, {}), Lexicon.TILE: ("INT", {"default": 8, "min": 1}), Lexicon.NOISE: ("FLOAT", {"default": 0.33, "min": 0, "max": 1, "step": 0.01}), Lexicon.GAMMA: ("FLOAT", {"default": 0.33, "min": 0, "max": 1, "step": 0.01}), @@ -208,17 +211,17 @@ Generates false perception 3D images from 2D input. Set tile divisions, noise, g images = [] pbar = ProgressBar(len(params)) for idx, (pA, depth, divisions, noise, gamma, shift, invert) in enumerate(params): - pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor2cv(pA) + pA = channel_solid(chan=EnumImageType.BGRA) if pA is None else tensor_to_cv(pA) h, w = pA.shape[:2] - depth = channel_solid(w, h, chan=EnumImageType.BGRA) if depth is None else tensor2cv(depth) + depth = channel_solid(w, h, chan=EnumImageType.BGRA) if depth is None else tensor_to_cv(depth) if invert: depth = image_invert(depth, 1.0) pA = image_stereogram(pA, depth, divisions, noise, gamma, shift) - images.append(cv2tensor_full(pA)) + images.append(cv_to_tensor_full(pA)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] -class StereoscopicNode(JOVBaseNode): +class StereoscopicNode(CozyBaseNode): NAME = "STEREOSCOPIC (JOV) đŸ•ļī¸" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("IMAGE", ) @@ -231,14 +234,14 @@ Simulates depth perception in images by generating stereoscopic views. It accept d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {"tooltip":"Optional Image to Matte with Selected Color"}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {"tooltip":"Optional Image to Matte with Selected Color"}), Lexicon.INT: ("FLOAT", {"default": 0.1, "min": 0, "max": 1, "step": 0.01, "tooltip":"Baseline"}), Lexicon.FOCAL: ("FLOAT", {"default": 500, "min": 0, "step": 0.01}), } }) return Lexicon._parse(d) - def run(self, **kw) -> Tuple[torch.Tensor]: + def run(self, **kw) -> Tuple[TensorType]: pA = parse_param(kw, Lexicon.PIXEL, EnumConvertType.IMAGE, None) baseline = parse_param(kw, Lexicon.INT, EnumConvertType.FLOAT, 0, 0.1, 1) focal_length = parse_param(kw, Lexicon.VALUE, EnumConvertType.FLOAT, 500, 0) @@ -246,16 +249,16 @@ Simulates depth perception in images by generating stereoscopic views. It accept params = list(zip_longest_fill(pA, baseline, focal_length)) pbar = ProgressBar(len(params)) for idx, (pA, baseline, focal_length) in enumerate(params): - pA = tensor2cv(pA) if pA is not None else channel_solid(chan=EnumImageType.GRAYSCALE) + pA = tensor_to_cv(pA) if pA is not None else channel_solid(chan=EnumImageType.GRAYSCALE) # Convert depth image to disparity map disparity_map = np.divide(1.0, pA.astype(np.float32), where=pA!=0) # Compute disparity values based on baseline and focal length disparity_map *= baseline * focal_length - images.append(cv2tensor(pA)) + images.append(cv_to_tensor(pA)) pbar.update_absolute(idx) return torch.stack(images) -class TextNode(JOVImageNode): +class TextNode(CozyImageNode): NAME = "TEXT GEN (JOV) 📝" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" FONTS = font_names() @@ -285,7 +288,7 @@ Generates images containing text based on parameters such as font, size, alignme Lexicon.MARGIN: ("INT", {"default": 0, "min": -1024, "max": 1024}), Lexicon.SPACING: ("INT", {"default": 0, "min": -1024, "max": 1024}), Lexicon.WH: ("VEC2INT", {"default": (256, 256), - "mij":MIN_IMAGE_SIZE, "label": [Lexicon.W, Lexicon.H]}), + "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H]}), Lexicon.XY: ("VEC2", {"default": (0, 0,), "mij": -1, "maj": 1, "step": 0.01, "label": [Lexicon.X, Lexicon.Y], "tooltip":"Offset the position"}), @@ -309,7 +312,7 @@ Generates images containing text based on parameters such as font, size, alignme justify = parse_param(kw, Lexicon.JUSTIFY, EnumJustify, EnumJustify.CENTER.name) margin = parse_param(kw, Lexicon.MARGIN, EnumConvertType.INT, 0) line_spacing = parse_param(kw, Lexicon.SPACING, EnumConvertType.INT, 0) - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE) + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN) pos = parse_param(kw, Lexicon.XY, EnumConvertType.VEC2, [(0, 0)], -1, 1) angle = parse_param(kw, Lexicon.ANGLE, EnumConvertType.INT, 0) edge = parse_param(kw, Lexicon.EDGE, EnumEdge, EnumEdge.CLIP.name) @@ -350,6 +353,6 @@ Generates images containing text based on parameters such as font, size, alignme img = image_translate(img, pos, edge=edge) if invert: img = image_invert(img, 1) - images.append(cv2tensor_full(img, matte)) + images.append(cv_to_tensor_full(img, matte)) pbar.update_absolute(idx) return [torch.stack(i) for i in zip(*images)] diff --git a/core/utility/batch.py b/core/utility/batch.py index fffd013..0979c77 100644 --- a/core/utility/batch.py +++ b/core/utility/batch.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Utility -""" +""" Jovimetrix - Utility """ import os import sys @@ -15,26 +13,38 @@ from typing import Any, List, Literal, Tuple import torch import numpy as np -from loguru import logger - from comfy.utils import ProgressBar from nodes import interrupt_processing +from cozy_comfyui import \ + logger, \ + IMAGE_SIZE_MIN, \ + InputType, EnumConvertType, TensorType, \ + deep_merge, parse_dynamic, parse_param + +from cozy_comfyui.node import \ + COZY_TYPE_ANY, \ + CozyBaseNode + +from cozy_comfyui.image import \ + IMAGE_FORMATS + +from cozy_comfyui.image.convert import \ + image_convert, cv_to_tensor, cv_to_tensor_full, tensor_to_cv, image_matte + +from cozy_comfyui.image.misc import \ + EnumInterpolation, \ + image_load + +from cozy_comfyui.api import \ + parse_reset, comfy_api_post + from ... import \ - JOV_TYPE_ANY, ROOT, \ - InputType, Lexicon, JOVBaseNode, \ - deep_merge, comfy_api_post, parse_reset - -from ...sup.util import \ - EnumConvertType, \ - parse_dynamic, parse_param - -from ...sup.image import \ - MIN_IMAGE_SIZE, IMAGE_FORMATS, \ - image_convert, image_matte, image_load, cv2tensor, cv2tensor_full, tensor2cv + ROOT, \ + Lexicon from ...sup.image.adjust import \ - EnumScaleMode, EnumInterpolation, \ + EnumScaleMode, \ image_scalefit # ============================================================================== @@ -55,11 +65,11 @@ class ContainsAnyDict(dict): # ============================================================================== -class ArrayNode(JOVBaseNode): +class ArrayNode(CozyBaseNode): NAME = "ARRAY (JOV) 📚" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" INPUT_IS_LIST = True - RETURN_TYPES = (JOV_TYPE_ANY, "INT", JOV_TYPE_ANY, "INT", JOV_TYPE_ANY) + RETURN_TYPES = (COZY_TYPE_ANY, "INT", COZY_TYPE_ANY, "INT", COZY_TYPE_ANY) RETURN_NAMES = (Lexicon.ANY_OUT, Lexicon.LENGTH, Lexicon.LIST, Lexicon.LENGTH2, Lexicon.LIST) OUTPUT_IS_LIST = (False, False, False, False, True) OUTPUT_TOOLTIPS = ( @@ -143,7 +153,7 @@ Processes a batch of data based on the selected mode, such as merging, picking, data = b["samples"] full_list.extend(data) output_is_latent = True - elif isinstance(b, torch.Tensor): + elif isinstance(b, TensorType): # logger.debug(b.shape) if b.ndim == 4: full_list.extend([i for i in b]) @@ -213,11 +223,11 @@ Processes a batch of data based on the selected mode, such as merging, picking, # _, w, h = image_by_size(data) result = [] for d in data: - d = tensor2cv(d) + d = tensor_to_cv(d) d = image_convert(d, 4) #d = image_matte(d, (0,0,0,0), w, h) # logger.debug(d.shape) - result.append(cv2tensor(d)) + result.append(cv_to_tensor(d)) if len(result) > 1: data = torch.stack(result) @@ -233,9 +243,9 @@ Processes a batch of data based on the selected mode, such as merging, picking, return data, size, full_list, len(full_list), data -class QueueBaseNode(JOVBaseNode): +class QueueBaseNode(CozyBaseNode): CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY, JOV_TYPE_ANY, "STRING", "INT", "INT", "BOOLEAN") + RETURN_TYPES = (COZY_TYPE_ANY, COZY_TYPE_ANY, "STRING", "INT", "INT", "BOOLEAN") RETURN_NAMES = (Lexicon.ANY_OUT, Lexicon.QUEUE, Lexicon.CURRENT, Lexicon.INDEX, Lexicon.TOTAL, Lexicon.TRIGGER, ) VIDEO_FORMATS = ['.wav', '.mp3', '.webm', '.mp4', '.avi', '.wmv', '.mkv', '.mov', '.mxf'] @@ -333,7 +343,7 @@ class QueueBaseNode(JOVBaseNode): return entries # turn Q element into actual hard type - def process(self, q_data: Any) -> torch.Tensor | str | dict: + def process(self, q_data: Any) -> TensorType | str | dict: # single Q cache to skip loading single entries over and over # @TODO: MRU cache strategy if (val := self.__last_q_value.get(q_data, None)) is not None: @@ -411,7 +421,7 @@ class QueueBaseNode(JOVBaseNode): ret = [] mode = parse_param(kw, Lexicon.MODE, EnumScaleMode, EnumScaleMode.MATTE.name)[0] sample = parse_param(kw, Lexicon.SAMPLE, EnumInterpolation, EnumInterpolation.LANCZOS4.name)[0] - wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], MIN_IMAGE_SIZE)[0] + wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], IMAGE_SIZE_MIN)[0] w2, h2 = wihi matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, [(0, 0, 0, 255)], 0, 255)[0] matte = [matte[0], matte[1], matte[2], 0] @@ -423,7 +433,7 @@ class QueueBaseNode(JOVBaseNode): d = image_scalefit(d, w2, h2, mode=mode, sample=sample) else: d = image_matte(d, matte, width=mw, height=mh) - ret.append(cv2tensor(d)) + ret.append(cv_to_tensor(d)) pbar.update_absolute(idx) data = torch.stack(ret) elif wait == True: @@ -431,7 +441,7 @@ class QueueBaseNode(JOVBaseNode): else: data = self.process(self.__q[self.__index]) if isinstance(data, (np.ndarray,)): - data = cv2tensor(data).unsqueeze(0) + data = cv_to_tensor(data).unsqueeze(0) self.__index += 1 self.__previous = data @@ -516,7 +526,7 @@ Manage a queue of specific items: media files. Supports various image and video "default": EnumScaleMode.MATTE.name, "tooltip": "Decide whether the images should be resized to fit"}), Lexicon.WH: ("VEC2INT", { - "default": (512, 512), "mij":MIN_IMAGE_SIZE, + "default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H], "tooltip": "Width and Height"}), Lexicon.SAMPLE: (EnumInterpolation._member_names_, { @@ -530,13 +540,13 @@ Manage a queue of specific items: media files. Supports various image and video }) return Lexicon._parse(d) - def run(self, ident, **kw) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, str, int, int, bool]: + def run(self, ident, **kw) -> Tuple[TensorType, TensorType, TensorType, str, int, int, bool]: data, _, current, index, total, trigger = super().run(ident, **kw) - if not isinstance(data, (torch.Tensor, )): + if not isinstance(data, (TensorType, )): data = [None, None, None] else: matte = parse_param(kw, Lexicon.MATTE, EnumConvertType.VEC4INT, (0, 0, 0, 255), 0, 255)[0] - data = [tensor2cv(d) for d in data] - data = [cv2tensor_full(d, matte) for d in data] + data = [tensor_to_cv(d) for d in data] + data = [cv_to_tensor_full(d, matte) for d in data] data = [torch.stack(d) for d in zip(*data)] return *data, current, index, total, trigger diff --git a/core/utility/info.py b/core/utility/info.py index 4dfbeac..a44ad8f 100644 --- a/core/utility/info.py +++ b/core/utility/info.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Utility -""" +""" Jovimetrix - Utility """ import io import json @@ -12,17 +10,22 @@ from PIL import Image import matplotlib.pyplot as plt from ... import \ - JOV_TYPE_IMAGE, \ - InputType, Lexicon, JOVBaseNode, \ - deep_merge, parse_reset + Lexicon -from ...sup.util import \ - EnumConvertType, \ - parse_dynamic, parse_param +from cozy_comfyui import \ + IMAGE_SIZE_MIN, \ + InputType, EnumConvertType, TensorType, \ + deep_merge, parse_dynamic, parse_param -from ...sup.image import \ - MIN_IMAGE_SIZE, \ - pil2tensor +from cozy_comfyui.node import \ + COZY_TYPE_IMAGE, \ + CozyBaseNode + +from cozy_comfyui.image.convert import \ + pil_to_tensor + +from cozy_comfyui.api import \ + parse_reset # ============================================================================== @@ -32,7 +35,7 @@ JOV_CATEGORY = "UTILITY" # === SUPPORT === # ============================================================================== -def decode_tensor(tensor: torch.Tensor) -> str: +def decode_tensor(tensor: TensorType) -> str: if tensor.ndim > 3: b, h, w, cc = tensor.shape elif tensor.ndim > 2: @@ -57,7 +60,7 @@ class AkashicData: # === CLASS === # ============================================================================== -class AkashicNode(JOVBaseNode): +class AkashicNode(CozyBaseNode): NAME = "AKASHIC (JOV) 📓" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_NAMES = () @@ -112,10 +115,10 @@ Visualize data. It accepts various types of data, including images, text, and ot if isinstance(val, (np.ndarray,)): ret = str(val) typ = "NUMPY ARRAY" - elif isinstance(val[0], (torch.Tensor,)): + elif isinstance(val[0], (TensorType,)): ret = decode_tensor(val[0]) typ = type(val[0]) - elif size == 1 and isinstance(val[0], (list,)) and isinstance(val[0][0], (torch.Tensor,)): + elif size == 1 and isinstance(val[0], (list,)) and isinstance(val[0][0], (TensorType,)): ret = decode_tensor(val[0][0]) typ = "CONDITIONING" elif all(isinstance(i, (tuple, set, list)) for i in val): @@ -127,7 +130,7 @@ Visualize data. It accepts various types of data, including images, text, and ot ret = str(val) elif isinstance(val, bool): ret = "True" if val else "False" - elif isinstance(val, torch.Tensor): + elif isinstance(val, TensorType): ret = decode_tensor(val) else: ret = str(val) @@ -142,7 +145,7 @@ Visualize data. It accepts various types of data, including images, text, and ot output["ui"]["text"].append(data) return output -class GraphNode(JOVBaseNode): +class GraphNode(CozyBaseNode): NAME = "GRAPH (JOV) 📈" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" OUTPUT_NODE = True @@ -168,7 +171,7 @@ Visualize a series of data points over time. It accepts a dynamic number of valu "default": 60, "min": 0, "tooltip":"Number of values to graph and display"}), Lexicon.WH: ("VEC2INT", { - "default": (512, 512), "mij":MIN_IMAGE_SIZE, + "default": (512, 512), "mij":IMAGE_SIZE_MIN, "label": [Lexicon.W, Lexicon.H], "tooltip":"Width and Height of the graph output"}), } @@ -184,7 +187,7 @@ Visualize a series of data points over time. It accepts a dynamic number of valu self.__history = [] self.__fig, self.__ax = plt.subplots(figsize=(5.12, 5.12)) - def run(self, ident, **kw) -> Tuple[torch.Tensor]: + def run(self, ident, **kw) -> Tuple[TensorType]: slice = parse_param(kw, Lexicon.VALUE, EnumConvertType.INT, 60)[0] wihi = parse_param(kw, Lexicon.WH, EnumConvertType.VEC2INT, [(512, 512)], 1)[0] if parse_reset(ident) > 0 or parse_param(kw, Lexicon.RESET, EnumConvertType.BOOLEAN, False)[0]: @@ -217,13 +220,13 @@ Visualize a series of data points over time. It accepts a dynamic number of valu self.__fig.savefig(buffer, format="png") buffer.seek(0) image = Image.open(buffer) - return (pil2tensor(image),) + return (pil_to_tensor(image),) -class ImageInfoNode(JOVBaseNode): +class ImageInfoNode(CozyBaseNode): NAME = "IMAGE INFO (JOV) 📚" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" RETURN_TYPES = ("INT", "INT", "INT", "INT", "VEC2", "VEC3") - RETURN_NAMES = (Lexicon.INT, Lexicon.W, Lexicon.H, Lexicon.C, Lexicon.WH, Lexicon.WHC) + RETURN_NAMES = (Lexicon.INT, Lexicon.W, Lexicon.H, "C", Lexicon.WH, Lexicon.WHC) OUTPUT_TOOLTIPS = ( "Batch count", "Width", @@ -242,7 +245,7 @@ Exports and Displays immediate information about images. d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL_A: (JOV_TYPE_IMAGE, { + Lexicon.PIXEL_A: (COZY_TYPE_IMAGE, { "default": None, "tooltip":"The image to examine"}) } diff --git a/core/utility/io.py b/core/utility/io.py index 4761dd2..c80a6b7 100644 --- a/core/utility/io.py +++ b/core/utility/io.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Utility -""" +""" Jovimetrix - Utility """ import os import json @@ -13,24 +11,32 @@ import numpy as np from PIL import Image from PIL.PngImagePlugin import PngInfo -from loguru import logger - from comfy.utils import ProgressBar from folder_paths import get_output_directory from nodes import interrupt_processing +from cozy_comfyui import \ + logger, \ + InputType, EnumConvertType, \ + deep_merge, parse_param, parse_param_list, zip_longest_fill + +from cozy_comfyui.node import \ + COZY_TYPE_IMAGE, COZY_TYPE_ANY, \ + CozyBaseNode + +from cozy_comfyui.image.convert import \ + tensor_to_pil, tensor_to_cv + +from cozy_comfyui.api import \ + TimedOutException, \ + comfy_api_post + from ... import \ - JOV_TYPE_ANY, JOV_TYPE_IMAGE, \ - InputType, Lexicon, JOVBaseNode, ComfyAPIMessage, TimedOutException, \ - comfy_api_post, deep_merge - -from ...sup.util import \ - EnumConvertType, \ - path_next, parse_param, zip_longest_fill - -from ...sup.image import tensor2cv, tensor2pil + Lexicon, ComfyAPIMessage # ============================================================================== +# === GLOBAL === +# ============================================================================== JOV_CATEGORY = "UTILITY" @@ -57,11 +63,31 @@ else: logger.warning("no gifski support") # ============================================================================== +# === SUPPORT === +# ============================================================================== -class DelayNode(JOVBaseNode): +def path_next(pattern: str) -> str: + """ + Finds the next free path in an sequentially named list of files + """ + i = 1 + while os.path.exists(pattern % i): + i = i * 2 + + a, b = (i // 2, i) + while a + 1 < b: + c = (a + b) // 2 + a, b = (c, b) if os.path.exists(pattern % c) else (a, c) + return pattern % b + +# ============================================================================== +# === CLASS === +# ============================================================================== + +class DelayNode(CozyBaseNode): NAME = "DELAY (JOV) ✋đŸŊ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = (JOV_TYPE_ANY,) + RETURN_TYPES = (COZY_TYPE_ANY,) RETURN_NAMES = (Lexicon.PASS_OUT,) OUTPUT_TOOLTIPS = ( "Pass through data when the delay ends" @@ -76,7 +102,7 @@ Introduce pauses in the workflow that accept an optional input to pass through a d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PASS_IN: (JOV_TYPE_ANY, {"default": None, + Lexicon.PASS_IN: (COZY_TYPE_ANY, {"default": None, "tooltip":"The data that should be held until the timer completes."}), Lexicon.TIMER: ("INT", {"default" : 0, "min": -1, "tooltip":"How long to delay if enabled. 0 means no delay."}), @@ -115,7 +141,7 @@ Introduce pauses in the workflow that accept an optional input to pass through a step += 1 return kw[Lexicon.PASS_IN], -class ExportNode(JOVBaseNode): +class ExportNode(CozyBaseNode): NAME = "EXPORT (JOV) đŸ“Ŋ" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" OUTPUT_NODE = True @@ -130,7 +156,7 @@ Responsible for saving images or animations to disk. It supports various output d = super().INPUT_TYPES() d = deep_merge(d, { "optional": { - Lexicon.PIXEL: (JOV_TYPE_IMAGE, {}), + Lexicon.PIXEL: (COZY_TYPE_IMAGE, {}), Lexicon.PASS_OUT: ("STRING", {"default": get_output_directory(), "default_top":"", "tooltip":"Pass through another route node to pre-populate the outputs."}), @@ -179,7 +205,7 @@ Responsible for saving images or animations to disk. It supports various output path = path_next(path) return path - images = [tensor2pil(i) for i in images] + images = [tensor_to_pil(i) for i in images] if format == "gifski": root = output_dir / f"{suffix}_{uuid4().hex[:16]}" # logger.debug(root) @@ -222,10 +248,10 @@ Responsible for saving images or animations to disk. It supports various output img.save(output(format), optimize=optimize) return () -class RouteNode(JOVBaseNode): +class RouteNode(CozyBaseNode): NAME = "ROUTE (JOV) 🚌" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" - RETURN_TYPES = ("BUS",) + (JOV_TYPE_ANY,) * 127 + RETURN_TYPES = ("BUS",) + (COZY_TYPE_ANY,) * 127 RETURN_NAMES = (Lexicon.ROUTE,) OUTPUT_TOOLTIPS = ( "Pass through for Route node" @@ -247,13 +273,23 @@ Routes the input data from the optional input ports to the output port, preservi return Lexicon._parse(d) def run(self, **kw) -> Tuple[Any, ...]: - inout = parse_param(kw, Lexicon.ROUTE, EnumConvertType.ANY, [None]) + inout = parse_param(kw, Lexicon.ROUTE, EnumConvertType.ANY, None) vars = kw.copy() vars.pop(Lexicon.ROUTE, None) vars.pop('ident', None) - return inout, *vars.values(), -class SaveOutput(JOVBaseNode): + parsed = [] + values = list(vars.values()) + print('values', len(values)) + for x in values: + print(type(x)) + p = parse_param_list(x, EnumConvertType.ANY, None) + parsed.append(p) + junk = *parsed, + print(len(junk)) + return inout, parsed, + +class SaveOutput(CozyBaseNode): NAME = "SAVE OUTPUT (JOV) 💾" CATEGORY = f"JOVIMETRIX đŸ”ē🟩đŸ”ĩ/{JOV_CATEGORY}" OUTPUT_NODE = True @@ -308,7 +344,7 @@ Save the output image along with its metadata to the specified path. Supports sa logger.error(usermeta) metadata["prompt"] = prompt metadata["workflow"] = json.dumps(pnginfo) - image = tensor2cv(image) + image = tensor_to_cv(image) image = Image.fromarray(np.clip(image, 0, 255).astype(np.uint8)) meta_png = PngInfo() for x in metadata: diff --git a/pyproject.toml b/pyproject.toml index 9197a7a..5fe1b66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "jovimetrix" description = "Animation via tick. Parameter manipulation with wave generator. Math operations with Unary and Binary support. Value conversion for all major types (int, string, list, dict, Image, Mask). Shape mask generation, image stacking and channel ops, batch splitting, merging and randomizing, load images and video from anywhere, dynamic bus routing with a single node, export support for GIPHY, save output anywhere! flatten, crop, transform; check colorblindness, make stereogram or stereoscopic images, or liner interpolate values and more." -version = "2.0.0" +version = "2.0.1" license = { file = "LICENSE" } readme = "README.md" authors = [{ name = "Alexander G. Morano", email = "amorano@gmail.com" }] @@ -24,7 +24,7 @@ dependencies = [ "markdownify", "matplotlib", "numba", - "numpy<=1.26.4", + "numpy<2", "opencv-contrib-python", "Pillow", "pywin32; platform_system==\"Windows\"", diff --git a/requirements.txt b/requirements.txt index a3cfdfb..a441a40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ loguru markdownify matplotlib numba -numpy<=1.26.4 +numpy<2 opencv-contrib-python Pillow pywin32; platform_system=="Windows" diff --git a/sup/anim.py b/sup/anim.py index 4ddfc1c..eabfe67 100644 --- a/sup/anim.py +++ b/sup/anim.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Animation Support -""" +""" Jovimetrix - Animation Support """ import inspect from enum import Enum diff --git a/sup/image/__init__.py b/sup/image/__init__.py index 451c167..ae52a92 100644 --- a/sup/image/__init__.py +++ b/sup/image/__init__.py @@ -1,431 +1 @@ -""" -Image Support -""" - -import math -import base64 -import requests -from enum import Enum -from io import BytesIO -from typing import List, Tuple, Union - -import cv2 -import torch -import numpy as np -from PIL import Image, ImageOps - -from ... import RGBAMaskType - -# ============================================================================== -# === GLOBAL === -# ============================================================================== - -MIN_IMAGE_SIZE: int = 32 -HALFPI: float = math.pi / 2 -TAU: float = math.pi * 2 - -IMAGE_FORMATS: List[str] = [ex for ex, f in Image.registered_extensions().items() - if f in Image.OPEN] - -# ============================================================================== -# === TYPE === -# ============================================================================== - -TYPE_fCOORD2D = Tuple[float, float] -TYPE_fCOORD3D = Tuple[float, float, float] -TYPE_iCOORD2D = Tuple[int, int] -TYPE_iCOORD3D = Tuple[int, int, int] - -TYPE_iRGB = Tuple[int, int, int] -TYPE_iRGBA = Tuple[int, int, int, int] -TYPE_fRGB = Tuple[float, float, float] -TYPE_fRGBA = Tuple[float, float, float, float] - -TYPE_PIXEL = Union[int, float, TYPE_iRGB, TYPE_iRGBA, TYPE_fRGB, TYPE_fRGBA] -TYPE_IMAGE = Union[np.ndarray, torch.Tensor] -TYPE_VECTOR = Union[TYPE_IMAGE, TYPE_PIXEL] - -# ============================================================================== -# === ENUMERATION === -# ============================================================================== - -class EnumImageType(Enum): - GRAYSCALE = 0 - RGB = 10 - RGBA = 20 - BGR = 30 - BGRA = 40 - -# ============================================================================== -# === CONVERSION === -# ============================================================================== - -def bgr2hsv(bgr_color: TYPE_PIXEL) -> TYPE_PIXEL: - return cv2.cvtColor(np.uint8([[bgr_color]]), cv2.COLOR_BGR2HSV)[0, 0] - -def bgr2image(image: TYPE_IMAGE, alpha: TYPE_IMAGE=None, gray: bool=False) -> TYPE_IMAGE: - """Restore image with alpha, if any, and converting to grayscale (optional).""" - if gray: - return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - return image_mask_add(image, alpha) - -def b64_2_tensor(base64str: str) -> torch.Tensor: - img = base64.b64decode(base64str) - img = Image.open(BytesIO(img)) - img = ImageOps.exif_transpose(img) - return pil2tensor(img) - -def b64_2_pil(base64_string): - prefix, base64_data = base64_string.split(",", 1) - image_data = base64.b64decode(base64_data) - image_stream = BytesIO(image_data) - return Image.open(image_stream) - -def b64_2_cv(base64_string) -> TYPE_IMAGE: - _, data = base64_string.split(",", 1) - data = base64.b64decode(data) - data = BytesIO(data) - data = Image.open(data) - data = np.array(data) - return cv2.cvtColor(data, cv2.COLOR_RGB2BGR) - -def cv2pil(image: TYPE_IMAGE) -> Image.Image: - """Convert a CV2 image to a PIL Image.""" - if image.ndim > 2: - cc = image.shape[2] - if cc == 3: - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - elif cc == 4: - image = cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) - else: - image = np.squeeze(image, axis=-1) - return Image.fromarray(image) - -def cv2tensor(image: TYPE_IMAGE, grayscale: bool=False) -> torch.Tensor: - """Convert a CV2 image to a torch tensor, with handling for grayscale/mask.""" - if grayscale or image.ndim < 3 or image.shape[2] == 1: - if image.ndim < 3: - image = np.expand_dims(image, -1) - - if image.shape[2] == 4: - image = cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY) - elif image.shape[2] == 3: - image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - - image = np.squeeze(image, axis=-1) - - image = image.astype(np.float32) / 255.0 - return torch.from_numpy(image) - -def cv2tensor_full(image: TYPE_IMAGE, matte:TYPE_PIXEL=(0,0,0,255)) -> RGBAMaskType: - - rgba = image_convert(image, 4) - # rgb = rgba[...,:3] - rgb = image_matte(rgba, matte)[...,:3] - mask = rgba[...,3] - rgba = torch.from_numpy(rgba.astype(np.float32) / 255.0) - rgb = torch.from_numpy(rgb.astype(np.float32) / 255.0) - mask = torch.from_numpy(mask.astype(np.float32) / 255.0) - return rgba, rgb, mask - -def hsv2bgr(hsl_color: TYPE_PIXEL) -> TYPE_PIXEL: - return cv2.cvtColor(np.uint8([[hsl_color]]), cv2.COLOR_HSV2BGR)[0, 0] - -def image2bgr(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, TYPE_IMAGE, int]: - """RGB Helper function. - Return channel count, BGR, and Alpha. - """ - alpha = image_mask(image) - cc = image.shape[2] if image.ndim == 3 else 1 - if cc == 1: - image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - elif cc == 4: - image = cv2.cvtColor(image, cv2.COLOR_BGRA2BGR) - return image, alpha, cc - -def pil2cv(image: Image.Image) -> TYPE_IMAGE: - """Convert a PIL Image to a CV2 Matrix.""" - new_image = np.array(image, dtype=np.uint8) - if new_image.ndim == 2: - pass - elif new_image.shape[2] == 3: - new_image = new_image[:, :, ::-1] - elif new_image.shape[2] == 4: - new_image = new_image[:, :, [2, 1, 0, 3]] - return new_image - -def pil2tensor(image: Image.Image) -> torch.Tensor: - """Convert a PIL Image to a Torch Tensor.""" - image = np.array(image).astype(np.float32) / 255.0 - return torch.from_numpy(image).unsqueeze(0) - -def tensor2cv(tensor: torch.Tensor, invert_mask:bool=True) -> TYPE_IMAGE: - """Convert a torch Tensor to a numpy ndarray.""" - if tensor.ndim > 3: - raise Exception("Tensor is batch of tensors") - - if tensor.ndim < 3: - tensor = tensor.unsqueeze(-1) - - if tensor.shape[2] == 1 and invert_mask: - tensor = 1. - tensor - - tensor = tensor.cpu().numpy() - return np.clip(255.0 * tensor, 0, 255).astype(np.uint8) - -def tensor2pil(tensor: torch.Tensor) -> Image.Image: - """Convert a torch Tensor to a PIL Image. - Tensor should be HxWxC [no batch]. - """ - tensor = tensor.cpu().numpy().squeeze() - tensor = np.clip(255. * tensor, 0, 255).astype(np.uint8) - return Image.fromarray(tensor) - -def mixlabLayer2cv(layer: dict) -> torch.Tensor: - image=layer['image'] - mask=layer['mask'] - if 'type' in layer and layer['type']=='base64' and type(image) == str: - image = b64_2_cv(image) - mask = b64_2_cv(mask) - else: - image = tensor2cv(image) - mask = tensor2cv(mask) - return image_mask_add(image, mask) - -# ============================================================================== -# === IMAGE === -# ============================================================================== - -def image_mask(image: TYPE_IMAGE, color: TYPE_PIXEL = 255) -> TYPE_IMAGE: - """Create a mask from the image, preserving transparency. - - Args: - image (TYPE_IMAGE): Input image, assumed to be 2D or 3D (with or without alpha channel). - color (TYPE_PIXEL): Value to fill the mask (default is 255). - - Returns: - TYPE_IMAGE: Mask of the image, either the alpha channel or a full mask of the given color. - """ - if image.ndim == 3 and image.shape[2] == 4: - return image[..., 3] - - h, w = image.shape[:2] - return np.ones((h, w), dtype=np.uint8) * color - -def image_mask_add(image:TYPE_IMAGE, mask:TYPE_IMAGE=None, alpha:float=255) -> TYPE_IMAGE: - """Put custom mask into an image. If there is no mask, alpha is applied. - Images are expanded to 4 channels. - Existing 4 channel images with no mask input just return themselves. - """ - image = image_convert(image, 4) - mask = image_mask(image, alpha) if mask is None else image_convert(mask, 1) - h, w, c = image.shape - mask = cv2.resize(mask, (w, h)) - image[..., 3] = mask if mask.ndim == 2 else mask[:, :, 0] - return image - -def image_matte(image: TYPE_IMAGE, color: TYPE_iRGBA=(0, 0, 0, 255), width: int=None, height: int=None) -> TYPE_IMAGE: - """ - Puts an RGB(A) image atop a colored matte expanding or clipping the image if requested. - - Args: - image (TYPE_IMAGE): The input RGBA image. - color (TYPE_iRGBA): The color of the matte as a tuple (R, G, B, A). - width (int, optional): The width of the matte. Defaults to the image width. - height (int, optional): The height of the matte. Defaults to the image height. - - Returns: - TYPE_IMAGE: Composited RGBA image on a matte with original alpha channel. - """ - - # Determine the dimensions of the image and the matte - image_height, image_width = image.shape[:2] - width = width or image_width - height = height or image_height - - # Create a solid matte with the specified color - matte = np.full((height, width, 4), color, dtype=image.dtype) - - # Calculate the center position for the image on the matte - x_offset = (width - image_width) // 2 - y_offset = (height - image_height) // 2 - - # Extract the alpha channel from the image if it's RGBA - if image.ndim == 3 and image.shape[2] == 4: - alpha = image[:, :, 3] / 255.0 - - # Blend the RGB channels using the alpha mask - for c in range(3): # Iterate over RGB channels - matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, c] = \ - (1 - alpha) * matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, c] + \ - alpha * image[:, :, c] - - # Set the alpha channel to the image's alpha channel - matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, 3] = image[:, :, 3] - else: - # Handle non-RGBA images (just copy the image onto the matte) - if image.ndim == 2: - image = np.expand_dims(image, axis=-1) - image = np.repeat(image, 3, axis=-1) - matte[y_offset:y_offset + image_height, x_offset:x_offset + image_width, :3] = image[:, :, :3] - - return matte - -def image_convert(image: TYPE_IMAGE, channels: int, width: int=None, height: int=None, - matte: Tuple[int, ...]=(0, 0, 0, 255)) -> TYPE_IMAGE: - """Force image format to a specific number of channels. - Args: - image (TYPE_IMAGE): Input image. - channels (int): Desired number of channels (1, 3, or 4). - width (int): Desired width. `None` means leave unchanged. - height (int): Desired height. `None` means leave unchanged. - matte (tuple): RGBA color to use as background color for transparent areas. - Returns: - TYPE_IMAGE: Image with the specified number of channels. - """ - if image.ndim == 2: - image = np.expand_dims(image, axis=-1) - - if (cc := image.shape[2]) != channels: - if cc == 1 and channels == 3: - image = np.repeat(image, 3, axis=2) - elif cc == 1 and channels == 4: - rgb = np.repeat(image, 3, axis=2) - alpha = np.full(image.shape[:2] + (1,), matte[3], dtype=image.dtype) - image = np.concatenate([rgb, alpha], axis=2) - elif cc == 3 and channels == 1: - image = np.mean(image, axis=2, keepdims=True).astype(image.dtype) - elif cc == 3 and channels == 4: - alpha = np.full(image.shape[:2] + (1,), matte[3], dtype=image.dtype) - image = np.concatenate([image, alpha], axis=2) - elif cc == 4 and channels == 1: - rgb = image[..., :3] - alpha = image[..., 3:4] / 255.0 - image = (np.mean(rgb, axis=2, keepdims=True) * alpha).astype(image.dtype) - elif cc == 4 and channels == 3: - image = image[..., :3] - - # Resize if width or height is specified - h, w = image.shape[:2] - new_width = width if width is not None else w - new_height = height if height is not None else h - if (new_width, new_height) != (w, h): - # Create a new canvas with the specified dimensions and matte color - new_image = np.full((new_height, new_width, channels), matte[:channels], dtype=image.dtype) - - # Calculate the region of the original image to copy over - src_x1 = max(0, (w - new_width) // 2) if new_width < w else 0 - src_y1 = max(0, (h - new_height) // 2) if new_height < h else 0 - src_x2 = src_x1 + min(w, new_width) - src_y2 = src_y1 + min(h, new_height) - - # Calculate the region of the new image to paste onto - dst_x1 = max(0, (new_width - w) // 2) if new_width > w else 0 - dst_y1 = max(0, (new_height - h) // 2) if new_height > h else 0 - dst_x2 = dst_x1 + (src_x2 - src_x1) - dst_y2 = dst_y1 + (src_y2 - src_y1) - - # Place the original image onto the new image - new_image[dst_y1:dst_y2, dst_x1:dst_x2] = image[src_y1:src_y2, src_x1:src_x2] - image = new_image - - return image - -def image_lerp(imageA: TYPE_IMAGE, imageB:TYPE_IMAGE, mask:TYPE_IMAGE=None, - alpha:float=1.) -> TYPE_IMAGE: - - imageA = imageA.astype(np.float32) - imageB = imageB.astype(np.float32) - - # establish mask - alpha = np.clip(alpha, 0, 1) - if mask is None: - height, width = imageA.shape[:2] - mask = np.ones((height, width, 1), dtype=np.float32) - else: - # normalize the mask - mask = mask.astype(np.float32) - mask = (mask - mask.min()) / (mask.max() - mask.min()) * alpha - - # LERP - imageA = cv2.multiply(1. - mask, imageA) - imageB = cv2.multiply(mask, imageB) - imageA = (cv2.add(imageA, imageB) / 255. - 0.5) * 2.0 - imageA = (imageA * 255).astype(imageA.dtype) - return np.clip(imageA, 0, 255) - -def image_load(url: str) -> Tuple[TYPE_IMAGE, ...]: - if url.lower().startswith("http"): - response = requests.get(url, stream=True) - response.raise_for_status() - img_array = np.asarray(bytearray(response.content), dtype=np.uint8) - img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED) - img = image_normalize(img) - if img.ndim == 3: - if img.shape[2] == 4: - img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) - else: - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - elif img.ndim < 3: - img = np.expand_dims(img, -1) - else: - try: - img = cv2.imread(url, cv2.IMREAD_UNCHANGED) - if img is None: - raise ValueError(f"{url} could not be loaded.") - - img = image_normalize(img) - if img.ndim == 3: - if img.shape[2] == 4: - img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA) - else: - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - elif img.ndim < 3: - img = np.expand_dims(img, -1) - - except Exception: - try: - img = Image.open(url) - img = ImageOps.exif_transpose(img) - img = np.array(img) - if img.dtype != np.uint8: - img = np.clip(np.array(img * 255), 0, 255).astype(dtype=np.uint8) - except Exception as e: - raise Exception(f"Error loading image: {e}") - - if img is None: - raise Exception(f"No file found at {url}") - - mask = image_mask(img) - """ - if img.ndim == 3 and img.shape[2] == 4: - alpha = mask / 255.0 - img[..., :3] = img[..., :3] * alpha[..., None] - img[:,:,3] = mask - """ - - return img, mask - -def image_minmax(image:List[TYPE_IMAGE]) -> Tuple[int, ...]: - h_min = w_min = 100000000000 - h_max = w_max = MIN_IMAGE_SIZE - for img in image: - if img is None: - continue - h, w = img.shape[:2] - h_max = max(h, h_max) - w_max = max(w, w_max) - h_min = min(h, h_min) - w_min = min(w, w_min) - - # x,y - x+width, y+height - return w_min, h_min, w_max, h_max - -def image_normalize(image: TYPE_IMAGE) -> TYPE_IMAGE: - image = image.astype(np.float32) - img_min = np.min(image) - img_max = np.max(image) - if img_min == img_max: - return np.zeros_like(image) - image = (image - img_min) / (img_max - img_min) - return (image * 255).astype(np.uint8) +""" Image Support """ diff --git a/sup/image/adjust.py b/sup/image/adjust.py index 926ed2b..ccb0cc7 100644 --- a/sup/image/adjust.py +++ b/sup/image/adjust.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Support -""" +""" Jovimetrix - Support """ from enum import Enum from typing import List, Tuple @@ -9,13 +7,23 @@ import cv2 import torch import numpy as np -from . import \ - TYPE_IMAGE, TYPE_PIXEL, \ - TYPE_fCOORD2D, EnumImageType, \ - image_convert, image_mask_add, image_matte, image_minmax, bgr2image, \ - cv2tensor, image2bgr, tensor2cv +from cozy_comfyui import \ + TensorType -from .compose import image_blend, image_crop_center +from cozy_comfyui.image import \ + PixelType, \ + Coord2D_Float, EnumImageType, ImageType + +from cozy_comfyui.image.convert import \ + ImageType, \ + image_matte, image_mask_add, image_convert, image_to_bgr, bgr_to_image, \ + cv_to_tensor, tensor_to_cv + +from cozy_comfyui.image.misc import \ + image_minmax + +from .compose import \ + image_blend, image_crop_center from .channel import \ EnumPixelSwizzle, \ @@ -77,36 +85,36 @@ class EnumThresholdAdapt(Enum): # === IMAGE === # ============================================================================== -def image_contrast(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: - image, alpha, cc = image2bgr(image) +def image_contrast(image: ImageType, value: float) -> ImageType: + image, alpha, cc = image_to_bgr(image) mean_value = np.mean(image) image = (image - mean_value) * value + mean_value image = np.clip(image, 0, 255).astype(np.uint8) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_edge_wrap(image: TYPE_IMAGE, tileX: float=1., tileY: float=1., - edge:EnumEdge=EnumEdge.WRAP) -> TYPE_IMAGE: +def image_edge_wrap(image: ImageType, tileX: float=1., tileY: float=1., + edge:EnumEdge=EnumEdge.WRAP) -> ImageType: """TILING.""" height, width = image.shape[:2] tileX = int(width * tileX) if edge in [EnumEdge.WRAP, EnumEdge.WRAPX] else 0 tileY = int(height * tileY) if edge in [EnumEdge.WRAP, EnumEdge.WRAPY] else 0 return cv2.copyMakeBorder(image, tileY, tileY, tileX, tileX, cv2.BORDER_WRAP) -def image_equalize(image:TYPE_IMAGE) -> TYPE_IMAGE: - image, alpha, cc = image2bgr(image) +def image_equalize(image:ImageType) -> ImageType: + image, alpha, cc = image_to_bgr(image) image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) image = cv2.equalizeHist(image) image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_exposure(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: - image, alpha, cc = image2bgr(image) +def image_exposure(image: ImageType, value: float) -> ImageType: + image, alpha, cc = image_to_bgr(image) image = np.clip(image * value, 0, 255).astype(np.uint8) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), +def image_filter(image:ImageType, start:Tuple[int]=(128,128,128), end:Tuple[int]=(128,128,128), fuzz:Tuple[float]=(0.5,0.5,0.5), - use_range:bool=False) -> Tuple[TYPE_IMAGE, TYPE_IMAGE]: + use_range:bool=False) -> Tuple[ImageType, ImageType]: """Filter an image based on a range threshold. It can use a start point with fuzziness factor and/or a start and end point with fuzziness on both points. @@ -121,7 +129,7 @@ def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), Tuple[np.ndarray, np.ndarray]: A tuple containing the filtered image and the mask. """ old_alpha = None - new_image = cv2tensor(image) + new_image = cv_to_tensor(image) cc = image.shape[2] if image.ndim > 2 else 1 if cc == 4: old_alpha = new_image[..., 3] @@ -131,9 +139,9 @@ def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), new_image = new_image.unsqueeze(-1) new_image = torch.repeat_interleave(new_image, 3, dim=2) - fuzz = torch.tensor(fuzz, dtype=torch.float64, device="cpu") - start = torch.tensor(start, dtype=torch.float64, device="cpu") / 255. - end = torch.tensor(end, dtype=torch.float64, device="cpu") / 255. + fuzz = TensorType(fuzz, dtype=torch.float64, device="cpu") + start = TensorType(start, dtype=torch.float64, device="cpu") / 255. + end = TensorType(end, dtype=torch.float64, device="cpu") / 255. if not use_range: end = start start -= fuzz @@ -154,11 +162,11 @@ def image_filter(image:TYPE_IMAGE, start:Tuple[int]=(128,128,128), if old_alpha is not None: output_image = torch.cat([output_image, old_alpha.unsqueeze(2)], dim=2) - return tensor2cv(output_image), mask.cpu().numpy().astype(np.uint8) * 255 + return tensor_to_cv(output_image), mask.cpu().numpy().astype(np.uint8) * 255 -def image_flatten(image: List[TYPE_IMAGE], width:int=None, height:int=None, +def image_flatten(image: List[ImageType], width:int=None, height:int=None, mode=EnumScaleMode.MATTE, - sample:EnumInterpolation=EnumInterpolation.LANCZOS4) -> TYPE_IMAGE: + sample:EnumInterpolation=EnumInterpolation.LANCZOS4) -> ImageType: if mode == EnumScaleMode.MATTE: width, height = image_minmax(image)[2:] @@ -179,9 +187,9 @@ def image_flatten(image: List[TYPE_IMAGE], width:int=None, height:int=None, current = cv2.add(current, x) return current -def image_gamma(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: +def image_gamma(image: ImageType, value: float) -> ImageType: # preserve original format - image, alpha, cc = image2bgr(image) + image, alpha, cc = image_to_bgr(image) if value <= 0: image = (image * 0).astype(np.uint8) else: @@ -190,9 +198,9 @@ def image_gamma(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: lookUpTable = np.clip(table, 0, 255).astype(np.uint8) image = cv2.LUT(image, lookUpTable) # now back to the original "format" - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_histogram(image:TYPE_IMAGE, bins=256) -> TYPE_IMAGE: +def image_histogram(image:ImageType, bins=256) -> ImageType: bins = max(image.max(), bins) + 1 flatImage = image.flatten() histogram = np.zeros(bins) @@ -200,7 +208,7 @@ def image_histogram(image:TYPE_IMAGE, bins=256) -> TYPE_IMAGE: histogram[pixel] += 1 return histogram -def image_histogram_normalize(image:TYPE_IMAGE)-> TYPE_IMAGE: +def image_histogram_normalize(image:ImageType)-> ImageType: L = image.max() nonEqualizedHistogram = image_histogram(image, bins=L) sumPixels = np.sum(nonEqualizedHistogram) @@ -211,17 +219,17 @@ def image_histogram_normalize(image:TYPE_IMAGE)-> TYPE_IMAGE: flatEqualizedImage = [transformMap[p] for p in flatNonEqualizedImage] return np.reshape(flatEqualizedImage, image.shape) -def image_hsv(image: TYPE_IMAGE, hue: float, saturation: float, value: float) -> TYPE_IMAGE: - image, alpha, cc = image2bgr(image) +def image_hsv(image: ImageType, hue: float, saturation: float, value: float) -> ImageType: + image, alpha, cc = image_to_bgr(image) image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) 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) image = cv2.cvtColor(image, cv2.COLOR_HSV2BGR) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_invert(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: +def image_invert(image: ImageType, value: float) -> ImageType: """ Invert an Grayscale, RGB or RGBA image using a specified inversion intensity. @@ -245,12 +253,12 @@ def image_invert(image: TYPE_IMAGE, value: float) -> TYPE_IMAGE: inverted_image = 255 - image return ((1 - value) * image + value * inverted_image).astype(np.uint8) -def image_mirror(image: TYPE_IMAGE, mode:EnumMirrorMode, x:float=0.5, - y:float=0.5) -> TYPE_IMAGE: +def image_mirror(image: ImageType, mode:EnumMirrorMode, x:float=0.5, + y:float=0.5) -> ImageType: cc = image.shape[2] if image.ndim == 3 else 1 height, width = image.shape[:2] - def mirror(img:TYPE_IMAGE, axis:int, reverse:bool=False) -> TYPE_IMAGE: + def mirror(img:ImageType, axis:int, reverse:bool=False) -> ImageType: pivot = x if axis == 1 else y flip = cv2.flip(img, axis) pivot = np.clip(pivot, 0, 1) @@ -287,7 +295,7 @@ def image_mirror(image: TYPE_IMAGE, mode:EnumMirrorMode, x:float=0.5, return image -def image_pixelate(image: TYPE_IMAGE, amount:float=1.)-> TYPE_IMAGE: +def image_pixelate(image: ImageType, amount:float=1.)-> ImageType: h, w = image.shape[:2] amount = max(0, min(1, amount)) @@ -315,12 +323,12 @@ def image_pixelate(image: TYPE_IMAGE, amount:float=1.)-> TYPE_IMAGE: return pixelated_image.astype(np.uint8) -def image_posterize(image: TYPE_IMAGE, levels:int=256) -> TYPE_IMAGE: +def image_posterize(image: ImageType, levels:int=256) -> ImageType: divisor = 256 / max(2, min(256, levels)) return (np.floor(image / divisor) * int(divisor)).astype(np.uint8) -def image_quantize(image:TYPE_IMAGE, levels:int=256, iterations:int=10, - epsilon:float=0.2) -> TYPE_IMAGE: +def image_quantize(image:ImageType, levels:int=256, iterations:int=10, + epsilon:float=0.2) -> ImageType: levels = int(max(2, min(256, levels))) pixels = np.float32(image) criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, iterations, epsilon) @@ -328,8 +336,8 @@ def image_quantize(image:TYPE_IMAGE, levels:int=256, iterations:int=10, centers = np.uint8(centers) return centers[labels.flatten()].reshape(image.shape) -def image_rotate(image: TYPE_IMAGE, angle: float, center:TYPE_fCOORD2D=(0.5, 0.5), - edge:EnumEdge=EnumEdge.CLIP) -> TYPE_IMAGE: +def image_rotate(image: ImageType, angle: float, center:Coord2D_Float=(0.5, 0.5), + edge:EnumEdge=EnumEdge.CLIP) -> ImageType: h, w = image.shape[:2] if edge != EnumEdge.CLIP: @@ -343,9 +351,9 @@ def image_rotate(image: TYPE_IMAGE, angle: float, center:TYPE_fCOORD2D=(0.5, 0.5 image = image_crop_center(image, w, h) return image -def image_scale(image: TYPE_IMAGE, scale:TYPE_fCOORD2D=(1.0, 1.0), +def image_scale(image: ImageType, scale:Coord2D_Float=(1.0, 1.0), sample:EnumInterpolation=EnumInterpolation.LANCZOS4, - edge:EnumEdge=EnumEdge.CLIP) -> TYPE_IMAGE: + edge:EnumEdge=EnumEdge.CLIP) -> ImageType: h, w = image.shape[:2] if edge != EnumEdge.CLIP: @@ -360,10 +368,10 @@ def image_scale(image: TYPE_IMAGE, scale:TYPE_fCOORD2D=(1.0, 1.0), image = image_crop_center(image, w, h) return image -def image_scalefit(image: TYPE_IMAGE, width: int, height:int, +def image_scalefit(image: ImageType, width: int, height:int, mode:EnumScaleMode=EnumScaleMode.MATTE, sample:EnumInterpolation=EnumInterpolation.LANCZOS4, - matte:TYPE_PIXEL=(0,0,0,0)) -> TYPE_IMAGE: + matte:PixelType=(0,0,0,0)) -> ImageType: match mode: case EnumScaleMode.MATTE: @@ -394,8 +402,8 @@ def image_scalefit(image: TYPE_IMAGE, width: int, height:int, image = np.expand_dims(image, -1) return image -def image_sharpen(image:TYPE_IMAGE, kernel_size=None, sigma:float=1.0, - amount:float=1.0, threshold:float=0) -> TYPE_IMAGE: +def image_sharpen(image:ImageType, kernel_size=None, sigma:float=1.0, + amount:float=1.0, threshold:float=0) -> ImageType: """Return a sharpened version of the image, using an unsharp mask.""" kernel_size = (kernel_size, kernel_size) if kernel_size else (5, 5) @@ -409,9 +417,9 @@ def image_sharpen(image:TYPE_IMAGE, kernel_size=None, sigma:float=1.0, np.copyto(sharpened, image, where=low_contrast_mask) return sharpened -def image_swap_channels(imgA:TYPE_IMAGE, imgB:TYPE_IMAGE, +def image_swap_channels(imgA:ImageType, imgB:ImageType, swap_in:Tuple[EnumPixelSwizzle, ...], - matte:Tuple[int,...]=(0,0,0,255)) -> TYPE_IMAGE: + matte:Tuple[int,...]=(0,0,0,255)) -> ImageType: """Up-convert and swap all 4-channels of an image with another or a constant.""" imgA = image_convert(imgA, 4) h,w = imgA.shape[:2] @@ -435,14 +443,14 @@ def image_swap_channels(imgA:TYPE_IMAGE, imgB:TYPE_IMAGE, return out -def image_threshold(image:TYPE_IMAGE, threshold:float=0.5, +def image_threshold(image:ImageType, threshold:float=0.5, mode:EnumThreshold=EnumThreshold.BINARY, adapt:EnumThresholdAdapt=EnumThresholdAdapt.ADAPT_NONE, - block:int=3, const:float=0.) -> TYPE_IMAGE: + block:int=3, const:float=0.) -> ImageType: const = max(-100, min(100, const)) block = max(3, block if block % 2 == 1 else block + 1) - image, alpha, cc = image2bgr(image) + image, alpha, cc = image_to_bgr(image) if adapt != EnumThresholdAdapt.ADAPT_NONE: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) gray = cv2.adaptiveThreshold(gray, 255, adapt.value, cv2.THRESH_BINARY, block, const) @@ -452,23 +460,23 @@ def image_threshold(image:TYPE_IMAGE, threshold:float=0.5, else: threshold = int(threshold * 255) _, image = cv2.threshold(image, threshold, 255, mode.value) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_translate(image: TYPE_IMAGE, offset: TYPE_fCOORD2D=(0.0, 0.0), - edge: EnumEdge=EnumEdge.CLIP, border_value:int=0) -> TYPE_IMAGE: +def image_translate(image: ImageType, offset: Coord2D_Float=(0.0, 0.0), + edge: EnumEdge=EnumEdge.CLIP, border_value:int=0) -> ImageType: """ Translates an image by a given offset. Supports various edge handling methods. Args: - image (TYPE_IMAGE): Input image as a numpy array. - offset (TYPE_fCOORD2D): Tuple (offset_x, offset_y) representing the translation offset. + image (ImageType): Input image as a numpy array. + offset (Coord2D_Float): Tuple (offset_x, offset_y) representing the translation offset. edge (EnumEdge): Enum representing edge handling method. Options are 'CLIP', 'WRAP', 'WRAPX', 'WRAPY'. Returns: - TYPE_IMAGE: Translated image. + ImageType: Translated image. """ - def translate(img: TYPE_IMAGE) -> TYPE_IMAGE: + def translate(img: ImageType) -> ImageType: height, width = img.shape[:2] scalarX = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPX] else 1.0 scalarY = 0.333 if edge in [EnumEdge.WRAP, EnumEdge.WRAPY] else 1.0 @@ -485,10 +493,10 @@ def image_translate(image: TYPE_IMAGE, offset: TYPE_fCOORD2D=(0.0, 0.0), return translate(image) -def image_transform(image: TYPE_IMAGE, offset:TYPE_fCOORD2D=(0.0, 0.0), - angle:float=0, scale:TYPE_fCOORD2D=(1.0, 1.0), +def image_transform(image: ImageType, offset:Coord2D_Float=(0.0, 0.0), + angle:float=0, scale:Coord2D_Float=(1.0, 1.0), sample:EnumInterpolation=EnumInterpolation.LANCZOS4, - edge:EnumEdge=EnumEdge.CLIP) -> TYPE_IMAGE: + edge:EnumEdge=EnumEdge.CLIP) -> ImageType: sX, sY = scale if sX < 0: image = cv2.flip(image, 1) @@ -506,10 +514,10 @@ def image_transform(image: TYPE_IMAGE, offset:TYPE_fCOORD2D=(0.0, 0.0), # MORPHOLOGY -def morph_edge_detect(image: TYPE_IMAGE, +def morph_edge_detect(image: ImageType, ksize: int=3, low: float=0.27, - high:float=0.6) -> TYPE_IMAGE: + high:float=0.6) -> ImageType: image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) ksize = max(3, ksize) @@ -517,7 +525,7 @@ def morph_edge_detect(image: TYPE_IMAGE, # Perform Canny edge detection return cv2.Canny(image, int(low * 255), int(high * 255)) -def morph_emboss(image: TYPE_IMAGE, amount: float=1., kernel: int=2) -> TYPE_IMAGE: +def morph_emboss(image: ImageType, amount: float=1., kernel: int=2) -> ImageType: kernel = max(2, kernel) kernel = np.array([ [-kernel, -kernel+1, 0], diff --git a/sup/image/channel.py b/sup/image/channel.py index e27a17c..9f2e565 100644 --- a/sup/image/channel.py +++ b/sup/image/channel.py @@ -1,14 +1,19 @@ -""" -Jovimetrix - Channel Ops -""" +""" Jovimetrix - Channel Ops """ from enum import Enum from typing import List import numpy as np -from . import MIN_IMAGE_SIZE, TYPE_IMAGE, TYPE_PIXEL, \ - EnumImageType +from cozy_comfyui import \ + IMAGE_SIZE_MIN + +from cozy_comfyui.image import \ + PixelType, \ + EnumImageType, ImageType + +from cozy_comfyui.image.convert import \ + ImageType from .color import pixel_eval @@ -32,7 +37,7 @@ class EnumPixelSwizzle(Enum): # === CHANNEL === # ============================================================================== -def channel_add(image:TYPE_IMAGE, color:TYPE_PIXEL=255) -> TYPE_IMAGE: +def channel_add(image:ImageType, color:PixelType=255) -> ImageType: """ This function adds a new channel with a solid color to an image. @@ -40,12 +45,12 @@ def channel_add(image:TYPE_IMAGE, color:TYPE_PIXEL=255) -> TYPE_IMAGE: NumPy array. The function assumes that the image has a shape attribute that returns a tuple representing the dimensions of the image (height, width, and channels if it's a color image) - :type image: TYPE_IMAGE + :type image: ImageType :param color: The `color` parameter in the `channel_add` function represents the color value that will be added as a new channel to the input image. The default value for `color` is 255, which is typically a white color in grayscale images, defaults to 255 - :type color: TYPE_PIXEL (optional) + :type color: PixelType (optional) :return: The function `channel_add` returns a new image with an additional channel appended to the original image. The new channel has a solid color specified by the `color` parameter. @@ -55,8 +60,8 @@ def channel_add(image:TYPE_IMAGE, color:TYPE_PIXEL=255) -> TYPE_IMAGE: new = channel_solid(w, h, color, EnumImageType.GRAYSCALE) return np.concatenate([image, new], axis=-1) -def channel_solid(width:int=MIN_IMAGE_SIZE, height:int=MIN_IMAGE_SIZE, color:TYPE_PIXEL=(0, 0, 0, 255), - chan:EnumImageType=EnumImageType.BGR) -> TYPE_IMAGE: +def channel_solid(width:int=IMAGE_SIZE_MIN, height:int=IMAGE_SIZE_MIN, color:PixelType=(0, 0, 0, 255), + chan:EnumImageType=EnumImageType.BGR) -> ImageType: if chan == EnumImageType.GRAYSCALE: color = pixel_eval(color, EnumImageType.GRAYSCALE) @@ -78,7 +83,7 @@ def channel_solid(width:int=MIN_IMAGE_SIZE, height:int=MIN_IMAGE_SIZE, color:TYP color = color[2::-1] return np.full((height, width, 4), color, dtype=np.uint8) -def channel_merge(channels: List[TYPE_IMAGE]) -> TYPE_IMAGE: +def channel_merge(channels: List[ImageType]) -> ImageType: max_height = max(ch.shape[0] for ch in channels if ch is not None) max_width = max(ch.shape[1] for ch in channels if ch is not None) num_channels = len(channels) @@ -105,8 +110,8 @@ def channel_merge(channels: List[TYPE_IMAGE]) -> TYPE_IMAGE: output = output[..., 0] return output -def channel_swap(imageA:TYPE_IMAGE, swap_ot:EnumPixelSwizzle, - imageB:TYPE_IMAGE, swap_in:EnumPixelSwizzle) -> TYPE_IMAGE: +def channel_swap(imageA:ImageType, swap_ot:EnumPixelSwizzle, + imageB:ImageType, swap_in:EnumPixelSwizzle) -> ImageType: index_out = int(swap_ot.value / 10) cc_out = imageA.shape[2] if imageA.ndim == 3 else 1 diff --git a/sup/image/color.py b/sup/image/color.py index b8693e3..850d205 100644 --- a/sup/image/color.py +++ b/sup/image/color.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Image Color Support -""" +""" Jovimetrix - Image Color Support """ from enum import Enum from typing import List, Tuple @@ -14,9 +12,13 @@ from sklearn.cluster import KMeans from daltonlens import simulate from blendmodes.blend import BlendType -from . import TYPE_IMAGE, TYPE_PIXEL, \ - EnumImageType, \ - bgr2hsv, hsv2bgr, image_convert, image_mask, image_mask_add +from cozy_comfyui.image import \ + PixelType, \ + EnumImageType, ImageType + +from cozy_comfyui.image.convert import \ + ImageType, \ + image_mask, image_mask_add, image_convert, hsv_to_bgr, bgr_to_hsv from .compose import image_blend @@ -93,15 +95,15 @@ class EnumCBSimulator(Enum): # === COLOR SPACE CONVERSION === # ============================================================================== -def gamma2linear(image: TYPE_IMAGE) -> TYPE_IMAGE: +def gamma2linear(image: ImageType) -> ImageType: """Gamma correction for old PCs/CRT monitors""" return np.power(image, 2.2) -def linear2gamma(image: TYPE_IMAGE) -> TYPE_IMAGE: +def linear2gamma(image: ImageType) -> ImageType: """Inverse gamma correction for old PCs/CRT monitors""" return np.power(np.clip(image, 0., 1.), 1.0 / 2.2) -def sRGB2Linear(image: TYPE_IMAGE) -> TYPE_IMAGE: +def sRGB2Linear(image: ImageType) -> ImageType: """Convert sRGB to linearRGB, removing the gamma correction. Works for grayscale, RGB, or RGBA images. """ @@ -126,7 +128,7 @@ def sRGB2Linear(image: TYPE_IMAGE) -> TYPE_IMAGE: image = rgb return (image * 255).astype(np.uint8) -def linear2sRGB(image: TYPE_IMAGE) -> TYPE_IMAGE: +def linear2sRGB(image: ImageType) -> ImageType: """Convert linearRGB to sRGB, applying the gamma correction. Works for grayscale, RGB, or RGBA images. """ @@ -155,13 +157,13 @@ def linear2sRGB(image: TYPE_IMAGE) -> TYPE_IMAGE: # === PIXEL === # ============================================================================== -def pixel_eval(color: TYPE_PIXEL, +def pixel_eval(color: PixelType, target: EnumImageType=EnumImageType.BGR, precision:EnumIntFloat=EnumIntFloat.INT, - crunch:EnumGrayscaleCrunch=EnumGrayscaleCrunch.MEAN) -> Tuple[TYPE_PIXEL] | TYPE_PIXEL: + crunch:EnumGrayscaleCrunch=EnumGrayscaleCrunch.MEAN) -> Tuple[PixelType] | PixelType: """Evaluates R(GB)(A) pixels in range (0-255) into target target pixel type.""" - def parse_single_color(c: TYPE_PIXEL) -> TYPE_PIXEL: + def parse_single_color(c: PixelType) -> PixelType: if not isinstance(c, int): c = np.clip(c, 0, 1) if precision == EnumIntFloat.INT: @@ -212,9 +214,9 @@ def pixel_eval(color: TYPE_PIXEL, color = tuple(color[2::-1]) + tuple([color[-1]]) return color -def pixel_hsv_adjust(color:TYPE_PIXEL, hue:int=0, saturation:int=0, value:int=0, +def pixel_hsv_adjust(color:PixelType, hue:int=0, saturation:int=0, value:int=0, mod_color:bool=True, mod_sat:bool=False, - mod_value:bool=False) -> TYPE_PIXEL: + mod_value:bool=False) -> PixelType: """Adjust an HSV type pixel. OpenCV uses... H: 0-179, S: 0-255, V: 0-255""" hsv = [0, 0, 0] @@ -286,9 +288,9 @@ def color_image2lut(image: np.ndarray, num_colors: int = 256) -> np.ndarray: # logger.debug(f"Final LUT range: { np.min(lut)} {np.max(lut)}") return np.asarray(lut) -def color_blind(image: TYPE_IMAGE, deficiency:EnumCBDeficiency, +def color_blind(image: ImageType, deficiency:EnumCBDeficiency, simulator:EnumCBSimulator=EnumCBSimulator.AUTOSELECT, - severity:float=1.0) -> TYPE_IMAGE: + severity:float=1.0) -> ImageType: cc = image.shape[2] if image.ndim == 3 else 1 if cc == 4: @@ -316,7 +318,7 @@ def color_blind(image: TYPE_IMAGE, deficiency:EnumCBDeficiency, image = image_mask_add(image, mask) return image -def color_lut_full(dominant_colors: List[Tuple[int, int, int]], nodes:int=33) -> TYPE_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. @@ -336,8 +338,8 @@ def color_lut_full(dominant_colors: List[Tuple[int, int, int]], nodes:int=33) -> lut = lut.reshape(nodes, nodes, nodes, 3).astype(np.uint8) return lut -def color_lut_match(image: TYPE_IMAGE, colormap:int=cv2.COLORMAP_JET, - usermap:TYPE_IMAGE=None, num_colors:int=255) -> TYPE_IMAGE: +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: @@ -358,7 +360,7 @@ def color_lut_match(image: TYPE_IMAGE, colormap:int=cv2.COLORMAP_JET, image[..., 3] = alpha[..., 0] return image -def color_lut_palette(colors: List[Tuple[int, int, int]], size: int=32) -> TYPE_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. @@ -380,7 +382,7 @@ def color_lut_palette(colors: List[Tuple[int, int, int]], size: int=32) -> TYPE_ return lut_image -def color_lut_tonal(colors: List[Tuple[int, int, int]], width: int=256, height: int=32) -> TYPE_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. @@ -390,7 +392,7 @@ def color_lut_tonal(colors: List[Tuple[int, int, int]], width: int=256, height: height (int): Height of each color row. Returns: - TYPE_IMAGE: 2D image representing the tonal palette LUT. + ImageType: 2D image representing the tonal palette LUT. """ num_colors = len(colors) lut_image = np.zeros((height * num_colors, width, 3), dtype=np.uint8) @@ -408,7 +410,7 @@ def color_lut_tonal(colors: List[Tuple[int, int, int]], width: int=256, height: return lut_image -def color_lut_visualize(lut: TYPE_LUT, size: int=512) -> TYPE_IMAGE: +def color_lut_visualize(lut: TYPE_LUT, size: int=512) -> ImageType: """ Visualize a 3D LUT as a 2D image. @@ -478,7 +480,7 @@ def color_lut_xport(lut: TYPE_LUT, f_out: str) -> None: 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: TYPE_IMAGE, usermap: TYPE_IMAGE) -> TYPE_IMAGE: +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: @@ -494,7 +496,7 @@ def color_match_histogram(image: TYPE_IMAGE, usermap: TYPE_IMAGE) -> TYPE_IMAGE: # image[..., 3] = alpha[..., 0] return image -def color_match_reinhard(image: TYPE_IMAGE, target: TYPE_IMAGE) -> TYPE_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. @@ -502,11 +504,11 @@ def color_match_reinhard(image: TYPE_IMAGE, target: TYPE_IMAGE) -> TYPE_IMAGE: based on https://www.cs.tau.ac.il/~turkel/imagepapers/ColorTransfer. Args: - image (TYPE_IMAGE): The input image (BGR or BGRA or Grayscale). - target (TYPE_IMAGE): The target image (BGR or BGRA or Grayscale). + image (ImageType): The input image (BGR or BGRA or Grayscale). + target (ImageType): The target image (BGR or BGRA or Grayscale). Returns: - TYPE_IMAGE: The color-matched image in BGR format. + ImageType: The color-matched image in BGR format. """ target = image_convert(target, 3) lab_tar = cv2.cvtColor(target, cv2.COLOR_BGR2Lab) @@ -519,7 +521,7 @@ def color_match_reinhard(image: TYPE_IMAGE, target: TYPE_IMAGE) -> TYPE_IMAGE: lab_tar = cv2.convertScaleAbs(lab_ori * ratio + offset) return cv2.cvtColor(lab_tar, cv2.COLOR_Lab2BGR) -def color_mean(image: TYPE_IMAGE) -> TYPE_IMAGE: +def color_mean(image: ImageType) -> ImageType: color = [0, 0, 0] cc = image.shape[2] if image.ndim == 3 else 1 if cc == 1: @@ -533,7 +535,7 @@ def color_mean(image: TYPE_IMAGE) -> TYPE_IMAGE: int(np.mean(image[:,:,2])) ] return color -def color_top_used(image: TYPE_IMAGE, top_n: int=8) -> List[Tuple[int, int, int]]: +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. @@ -566,57 +568,57 @@ def color_top_used(image: TYPE_IMAGE, top_n: int=8) -> List[Tuple[int, int, int] # === COLOR ANALYSIS === # ============================================================================== -def color_theory_complementary(color: TYPE_PIXEL) -> TYPE_PIXEL: - color = bgr2hsv(color) +def color_theory_complementary(color: PixelType) -> PixelType: + color = bgr_to_hsv(color) color_a = pixel_hsv_adjust(color, 90, 0, 0) - return hsv2bgr(color_a) + return hsv_to_bgr(color_a) -def color_theory_monochromatic(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_monochromatic(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_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 hsv2bgr(color_a), hsv2bgr(color_b), hsv2bgr(color_c), hsv2bgr(color_d) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d) -def color_theory_split_complementary(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_split_complementary(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_to_hsv(color) color_a = pixel_hsv_adjust(color, 75, 0, 0) color_b = pixel_hsv_adjust(color, 105, 0, 0) - return hsv2bgr(color_a), hsv2bgr(color_b) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b) -def color_theory_analogous(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_analogous(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_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 hsv2bgr(color_a), hsv2bgr(color_b), hsv2bgr(color_c), hsv2bgr(color_d) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d) -def color_theory_triadic(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_triadic(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_to_hsv(color) color_a = pixel_hsv_adjust(color, 60, 0, 0) color_b = pixel_hsv_adjust(color, 120, 0, 0) - return hsv2bgr(color_a), hsv2bgr(color_b) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b) -def color_theory_compound(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_compound(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_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 hsv2bgr(color_a), hsv2bgr(color_b), hsv2bgr(color_c) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c) -def color_theory_square(color: TYPE_PIXEL) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_square(color: PixelType) -> Tuple[PixelType, ...]: + color = bgr_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 hsv2bgr(color_a), hsv2bgr(color_b), hsv2bgr(color_c) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c) -def color_theory_tetrad_custom(color: TYPE_PIXEL, delta:int=0) -> Tuple[TYPE_PIXEL, ...]: - color = bgr2hsv(color) +def color_theory_tetrad_custom(color: PixelType, delta:int=0) -> Tuple[PixelType, ...]: + color = bgr_to_hsv(color) # modulus on neg and pos while delta < 0: @@ -630,9 +632,9 @@ def color_theory_tetrad_custom(color: TYPE_PIXEL, delta:int=0) -> Tuple[TYPE_PIX # 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 hsv2bgr(color_a), hsv2bgr(color_b), hsv2bgr(color_c), hsv2bgr(color_d) + return hsv_to_bgr(color_a), hsv_to_bgr(color_b), hsv_to_bgr(color_c), hsv_to_bgr(color_d) -def color_theory(image: TYPE_IMAGE, custom:int=0, scheme: EnumColorTheory=EnumColorTheory.COMPLIMENTARY) -> Tuple[TYPE_IMAGE, ...]: +def color_theory(image: ImageType, custom:int=0, scheme: EnumColorTheory=EnumColorTheory.COMPLIMENTARY) -> Tuple[ImageType, ...]: b = [0,0,0] c = [0,0,0] @@ -669,30 +671,30 @@ def color_theory(image: TYPE_IMAGE, custom:int=0, scheme: EnumColorTheory=EnumCo # # -def image_gradient_expand(image: TYPE_IMAGE) -> None: +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:TYPE_IMAGE, color_map:TYPE_IMAGE, reverse:bool=False) -> TYPE_IMAGE: +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) -def image_grayscale(image: TYPE_IMAGE, use_alpha: bool = False) -> TYPE_IMAGE: +def image_grayscale(image: ImageType, use_alpha: bool = False) -> ImageType: """Convert image to grayscale, optionally using the alpha channel if present. Args: - image (TYPE_IMAGE): Input image, potentially with multiple channels. + 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: - TYPE_IMAGE: Grayscale image, optionally alpha-multiplied. + ImageType: Grayscale image, optionally alpha-multiplied. """ if image.ndim == 2 or image.shape[2] == 1: return image diff --git a/sup/image/compose.py b/sup/image/compose.py index 0fc8236..2732eaf 100644 --- a/sup/image/compose.py +++ b/sup/image/compose.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Image Composition Operation Support -""" +""" Jovimetrix - Image Composition Operation Support """ import sys from enum import Enum @@ -11,9 +9,14 @@ import numpy as np from PIL import Image, ImageDraw from blendmodes.blend import BlendType, blendLayers -from . import TYPE_IMAGE, TYPE_PIXEL, TYPE_fCOORD2D, \ - image_convert, image_mask, image_mask_add, image_matte, \ - bgr2image, cv2pil, image2bgr, pil2cv +from cozy_comfyui.image import \ + PixelType, \ + Coord2D_Float, ImageType + +from cozy_comfyui.image.convert import \ + ImageType, \ + image_matte, image_mask, image_mask_add, \ + image_convert, image_to_bgr, bgr_to_image, cv_to_pil, pil_to_cv # ============================================================================== # === ENUMERATION === @@ -99,7 +102,7 @@ class EnumShapes(Enum): # === PIXEL === # ============================================================================== -def pixel_convert(color:TYPE_PIXEL, size:int=4, alpha:int=255) -> TYPE_PIXEL: +def pixel_convert(color:PixelType, size:int=4, alpha:int=255) -> PixelType: """Convert X channel pixel into Y channel pixel.""" if (cc := len(color)) == size: return color @@ -117,14 +120,14 @@ def pixel_convert(color:TYPE_PIXEL, size:int=4, alpha:int=255) -> TYPE_PIXEL: These are core functions that most of the support image libraries require. """ -def image_blend(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, mask:Optional[TYPE_IMAGE]=None, - blendOp:BlendType=BlendType.NORMAL, alpha:float=1) -> TYPE_IMAGE: +def image_blend(imageA: ImageType, imageB: ImageType, mask:Optional[ImageType]=None, + blendOp:BlendType=BlendType.NORMAL, alpha:float=1) -> ImageType: """Blending that will size to the largest input's background.""" # prep A h, w = imageA.shape[:2] imageA = image_convert(imageA, 4, w, h) - imageA = cv2pil(imageA) + imageA = cv_to_pil(imageA) # prep B cc = imageB.shape[2] if imageB.ndim > 2 else 1 @@ -140,15 +143,15 @@ def image_blend(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, mask:Optional[TYPE_IMAGE mask = cv2.bitwise_and(mask, old_mask) imageB[..., 3] = mask - imageB = cv2pil(imageB) + imageB = cv_to_pil(imageB) alpha = np.clip(alpha, 0, 1) image = blendLayers(imageA, imageB, blendOp.value, alpha) - image = pil2cv(image) + image = pil_to_cv(image) if cc == 4: image = image_mask_add(image, mask) return image -def image_crop_polygonal(image: TYPE_IMAGE, points: List[TYPE_fCOORD2D]) -> TYPE_IMAGE: +def image_crop_polygonal(image: ImageType, points: List[Coord2D_Float]) -> ImageType: cc = image.shape[2] if image.ndim == 3 else 1 height, width = image.shape[:2] point_mask = np.zeros((height, width), dtype=np.uint8) @@ -170,7 +173,7 @@ def image_crop_polygonal(image: TYPE_IMAGE, points: List[TYPE_fCOORD2D]) -> TYPE return image_convert(cropped_image, cc) return cv2.bitwise_and(cropped_image, cropped_image, mask=point_mask_cropped) -def image_crop(image: TYPE_IMAGE, width:int=None, height:int=None, offset:Tuple[float, float]=(0, 0)) -> TYPE_IMAGE: +def image_crop(image: ImageType, width:int=None, height:int=None, offset:Tuple[float, float]=(0, 0)) -> ImageType: h, w = image.shape[:2] width = width if width is not None else w height = height if height is not None else h @@ -182,7 +185,7 @@ def image_crop(image: TYPE_IMAGE, width:int=None, height:int=None, offset:Tuple[ points = [(x, y), (x2, y), (x2, y2), (x, y2)] return image_crop_polygonal(image, points) -def image_crop_center(image: TYPE_IMAGE, width:int=None, height:int=None) -> TYPE_IMAGE: +def image_crop_center(image: ImageType, width:int=None, height:int=None) -> ImageType: """Helper crop function to find the "center" of the area of interest.""" h, w = image.shape[:2] cx = w // 2 @@ -212,7 +215,7 @@ def image_levels(image: np.ndarray, black_point:int=0, white_point=255, numpy.ndarray: Adjusted image tensor. """ - image, alpha, cc = image2bgr(image) + image, alpha, cc = image_to_bgr(image) # Convert points and gamma to float32 for calculations black = np.array([black_point] * 3, dtype=np.float32) @@ -227,18 +230,18 @@ def image_levels(image: np.ndarray, black_point:int=0, white_point=255, image = (image - mid) / (1.0 - mid) image = (image ** (1 / inGamma)) * (outWhite - outBlack) + outBlack image = np.clip(image, 0, 255).astype(np.uint8) - return bgr2image(image, alpha, cc == 1) + return bgr_to_image(image, alpha, cc == 1) -def image_mask_binary(image: TYPE_IMAGE) -> TYPE_IMAGE: +def image_mask_binary(image: ImageType) -> ImageType: """ Convert an image to a binary mask where non-black pixels are 1 and black pixels are 0. Supports BGR, single-channel grayscale, and RGBA images. Args: - image (TYPE_IMAGE): Input image in BGR, grayscale, or RGBA format. + image (ImageType): Input image in BGR, grayscale, or RGBA format. Returns: - TYPE_IMAGE: Binary mask with the same width and height as the input image, where + ImageType: Binary mask with the same width and height as the input image, where pixels are 1 for non-black and 0 for black. """ if image.ndim == 2: @@ -265,8 +268,8 @@ def image_mask_binary(image: TYPE_IMAGE) -> TYPE_IMAGE: mask = np.expand_dims(mask, -1) return mask.astype(np.uint8) -def image_by_size(image_list: List[TYPE_IMAGE], - enumSize: EnumImageBySize=EnumImageBySize.LARGEST) -> Tuple[TYPE_IMAGE, int, int]: +def image_by_size(image_list: List[ImageType], + enumSize: EnumImageBySize=EnumImageBySize.LARGEST) -> Tuple[ImageType, int, int]: img = None mega, width, height = 0, 0, 0 @@ -307,7 +310,7 @@ def image_by_size(image_list: List[TYPE_IMAGE], return img, width, height -def image_split(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, ...]: +def image_split(image: ImageType) -> Tuple[ImageType, ...]: h, w = image.shape[:2] # Grayscale image @@ -323,9 +326,9 @@ def image_split(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, ...]: r, g, b, a = cv2.split(image) return r, g, b, a -def image_stack(image_list: List[TYPE_IMAGE], +def image_stack(image_list: List[ImageType], axis:EnumOrientation=EnumOrientation.HORIZONTAL, - stride:int=0, matte:TYPE_PIXEL=(0,0,0,255)) -> TYPE_IMAGE: + stride:int=0, matte:PixelType=(0,0,0,255)) -> ImageType: _, width, height = image_by_size(image_list) images = [image_matte(image_convert(i, 4), matte, width, height) for i in image_list] @@ -367,7 +370,7 @@ def image_stack(image_list: List[TYPE_IMAGE], # ============================================================================== def shape_ellipse(width: int, height: int, sizeX:float=1., sizeY:float=1., - fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: + fill:PixelType=255, back:PixelType=0) -> Image: sizeX = max(0.5, sizeX / 2 + 0.5) sizeY = max(0.5, sizeY / 2 + 0.5) xy = [(width * (1. - sizeX), height * (1. - sizeY)),(width * sizeX, height * sizeY)] @@ -376,7 +379,7 @@ def shape_ellipse(width: int, height: int, sizeX:float=1., sizeY:float=1., return image def shape_quad(width: int, height: int, sizeX:float=1., sizeY:float=1., - fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: + fill:PixelType=255, back:PixelType=0) -> Image: sizeX = max(0.5, sizeX / 2 + 0.5) sizeY = max(0.5, sizeY / 2 + 0.5) xy = [(width * (1. - sizeX), height * (1. - sizeY)),(width * sizeX, height * sizeY)] @@ -385,7 +388,7 @@ def shape_quad(width: int, height: int, sizeX:float=1., sizeY:float=1., return image def shape_polygon(width: int, height: int, size: float=1., sides: int=3, - fill:TYPE_PIXEL=255, back:TYPE_PIXEL=0) -> Image: + fill:PixelType=255, back:PixelType=0) -> Image: size = max(0.00001, size) r = min(width, height) * size * 0.5 xy = (width * 0.5, height * 0.5, r) diff --git a/sup/image/mapping.py b/sup/image/mapping.py index 42cc7b8..37cef4a 100644 --- a/sup/image/mapping.py +++ b/sup/image/mapping.py @@ -1,6 +1,4 @@ -""" -Jovimetrix - Coordinates and Mapping -""" +""" Jovimetrix - Coordinates and Mapping """ from enum import Enum from typing import Any, List, Tuple @@ -8,8 +6,16 @@ from typing import Any, List, Tuple import cv2 import numpy as np -from . import TAU, TYPE_IMAGE, TYPE_fCOORD2D, \ - image_convert, image_lerp, image_normalize +from cozy_comfyui.image import \ + TAU, \ + Coord2D_Float, ImageType + +from cozy_comfyui.image.convert import \ + ImageType, \ + image_convert + +from cozy_comfyui.image.misc import \ + image_lerp, image_normalize from .color import image_grayscale @@ -41,8 +47,8 @@ def image_mirror_mandela(imageA: np.ndarray, imageB: np.ndarray) -> Tuple[np.nda imageB = np.vstack([top, bottom]) return imageA, imageB -def image_stereogram(image: TYPE_IMAGE, depth: TYPE_IMAGE, divisions:int=8, - mix:float=0.33, gamma:float=0.33, shift:float=1.) -> TYPE_IMAGE: +def image_stereogram(image: ImageType, depth: ImageType, divisions:int=8, + mix:float=0.33, gamma:float=0.33, shift:float=1.) -> ImageType: height, width = depth.shape[:2] out = np.zeros((height, width, 3), dtype=np.uint8) image = cv2.resize(image, (width, height)) @@ -70,17 +76,17 @@ def image_stereogram(image: TYPE_IMAGE, depth: TYPE_IMAGE, divisions:int=8, # === COORDINATES === # ============================================================================== -def coord_cart2polar(x: float, y: float) -> TYPE_fCOORD2D: +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) -> TYPE_fCOORD2D: +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:TYPE_fCOORD2D=None) -> TYPE_fCOORD2D: +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.""" @@ -93,7 +99,7 @@ def coord_default(width:int, height:int, origin:TYPE_fCOORD2D=None) -> TYPE_fCOO y -= origin_y return x, y -def coord_fisheye(width: int, height: int, distortion: float) -> Tuple[TYPE_IMAGE, TYPE_IMAGE]: +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) @@ -104,13 +110,13 @@ def coord_fisheye(width: int, height: int, distortion: float) -> Tuple[TYPE_IMAG 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[TYPE_fCOORD2D]) -> TYPE_IMAGE: +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[TYPE_IMAGE, TYPE_IMAGE]: +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) @@ -123,7 +129,7 @@ def coord_sphere(width: int, height: int, radius: float) -> Tuple[TYPE_IMAGE, TY # === MAPPING === # ============================================================================== -def remap_fisheye(image: TYPE_IMAGE, distort: float) -> TYPE_IMAGE: +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: @@ -134,7 +140,7 @@ def remap_fisheye(image: TYPE_IMAGE, distort: float) -> TYPE_IMAGE: # image = image[..., 0] return image -def remap_perspective(image: TYPE_IMAGE, pts: list) -> TYPE_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: @@ -145,14 +151,14 @@ def remap_perspective(image: TYPE_IMAGE, pts: list) -> TYPE_IMAGE: # image = image[..., 0] return image -def remap_polar(image: TYPE_IMAGE) -> TYPE_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: TYPE_IMAGE, radius: float) -> TYPE_IMAGE: +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) @@ -174,7 +180,7 @@ def depth_from_gradient(grad_x, grad_y): Z /= np.max(Z) return Z -def height_from_normal(image: TYPE_IMAGE, tile:bool=True) -> TYPE_IMAGE: +def height_from_normal(image: ImageType, tile:bool=True) -> ImageType: """Computes a height map from the given normal map.""" image = np.transpose(image, (2, 0, 1)) flip_img = np.flip(image, axis=1) @@ -195,7 +201,7 @@ def height_from_normal(image: TYPE_IMAGE, tile:bool=True) -> TYPE_IMAGE: image = np.transpose(image, (1, 2, 0)) return image -def curvature_from_normal(image: TYPE_IMAGE, blur_radius:int=2)-> TYPE_IMAGE: +def curvature_from_normal(image: ImageType, blur_radius:int=2)-> ImageType: """Computes a curvature map from the given normal map.""" image = np.transpose(image, (2, 0, 1)) blur_factor = 1 / 2 ** min(8, max(2, blur_radius)) @@ -233,7 +239,7 @@ def curvature_from_normal(image: TYPE_IMAGE, blur_radius:int=2)-> TYPE_IMAGE: image = (image - image.min()) / (image.max() - image.min()) * 255 return image.astype(np.uint8) -def roughness_from_normal(image: TYPE_IMAGE) -> TYPE_IMAGE: +def roughness_from_normal(image: ImageType) -> ImageType: """Roughness from a normal map.""" up_vector = np.array([0, 0, 1]) image = 1 - np.dot(image, up_vector) @@ -241,7 +247,7 @@ def roughness_from_normal(image: TYPE_IMAGE) -> TYPE_IMAGE: image = (255 * image).astype(np.uint8) return image_grayscale(image) -def roughness_from_albedo(image: TYPE_IMAGE) -> TYPE_IMAGE: +def roughness_from_albedo(image: ImageType) -> ImageType: """Roughness from an albedo map.""" kernel_size = 3 image = cv2.Laplacian(image, cv2.CV_64F, ksize=kernel_size) @@ -249,8 +255,8 @@ def roughness_from_albedo(image: TYPE_IMAGE) -> TYPE_IMAGE: image = (255 * image).astype(np.uint8) return image_grayscale(image) -def roughness_from_albedo_normal(albedo: TYPE_IMAGE, normal: TYPE_IMAGE, - blur:int=2, blend:float=0.5, iterations:int=3) -> TYPE_IMAGE: +def roughness_from_albedo_normal(albedo: ImageType, normal: ImageType, + blur:int=2, blend:float=0.5, iterations:int=3) -> ImageType: normal = roughness_from_normal(normal) normal = image_normalize(normal) albedo = roughness_from_albedo(albedo) diff --git a/sup/image/zend.py b/sup/image/zend.py index 0f82a2e..7087e73 100644 --- a/sup/image/zend.py +++ b/sup/image/zend.py @@ -1,3 +1,4 @@ +""" Joviemtrix - Ze Supports """ import math import urllib @@ -12,18 +13,58 @@ from scipy import ndimage from skimage.metrics import structural_similarity as ssim from PIL import Image, ImageChops, ImageOps -from loguru import logger +from cozy_comfyui import \ + logger, \ + TensorType -from . import TYPE_IMAGE, TYPE_PIXEL, TYPE_iRGB, \ - image_convert, image_matte, cv2pil, pil2cv +from cozy_comfyui.image import \ + PixelType, \ + ImageType, RGB_Int + +from cozy_comfyui.image.convert import \ + image_convert, pil_to_cv, cv_to_pil, \ + image_matte from .channel import channel_add from .color import image_grayscale -from ..util import grid_make +# ============================================================================== +# === SUPPORT === +# ============================================================================== -def image_crop_head(image: TYPE_IMAGE) -> TYPE_IMAGE: +def grid_make(data: List[Any]) -> Tuple[List[List[Any]], int, int]: + """ + Create a 2D grid from a 1D list. + + Args: + data (List[Any]): Input data. + + Returns: + Tuple[List[List[Any]], int, int]: A tuple containing the 2D grid, number of columns, + and number of rows. + """ + size = len(data) + grid = int(math.sqrt(size)) + if grid * grid < size: + grid += 1 + if grid < 1: + return [], 0, 0 + + rows = size // grid + if size % grid != 0: + rows += 1 + + ret = [] + cols = 0 + for j in range(rows): + end = min((j + 1) * grid, len(data)) + cols = max(cols, end - j * grid) + d = [data[i] for i in range(j * grid, end)] + ret.append(d) + return ret, cols, rows + +def image_crop_head(image: ImageType) -> ImageType: """ Given a file path or np.ndarray image with a face, returns cropped np.ndarray around the largest detected @@ -203,7 +244,7 @@ def image_crop_head(image: TYPE_IMAGE) -> TYPE_IMAGE: return [int(v1), int(v2), int(h1), int(h2)] ''' -def image_detect(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, Tuple[int, ...]]: +def image_detect(image: ImageType) -> Tuple[ImageType, Tuple[int, ...]]: gray = image_grayscale(image) _, thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV) # contours @@ -215,8 +256,8 @@ def image_detect(image: TYPE_IMAGE) -> Tuple[TYPE_IMAGE, Tuple[int, ...]]: cropped_image = image[y:y+h, x:x+w] return cropped_image, (x, y, w, h) -def image_diff(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, threshold:int=0, - color:TYPE_PIXEL=(255, 0, 0)) -> Tuple[TYPE_IMAGE, TYPE_IMAGE, TYPE_IMAGE, TYPE_IMAGE, float]: +def image_diff(imageA: ImageType, imageB: ImageType, threshold:int=0, + color:PixelType=(255, 0, 0)) -> Tuple[ImageType, ImageType, ImageType, ImageType, float]: """imageA, imageB, diff, thresh, score """ h1, w1 = imageA.shape[:2] @@ -259,7 +300,7 @@ def image_disparity(imageA: np.ndarray) -> np.ndarray: disparity_map = np.divide(1.0, imageA, where=imageA != 0) return np.where(imageA == 0, 1, disparity_map) -def image_histogram_statistics(histogram:np.ndarray, L=256)-> TYPE_IMAGE: +def image_histogram_statistics(histogram:np.ndarray, L=256)-> ImageType: sumPixels = np.sum(histogram) normalizedHistogram = histogram/sumPixels mean = 0 @@ -280,7 +321,7 @@ def image_gradient_map2(image, gradient_map): np.take(cmap.reshape(-1, 3), grey_reshaped, axis=0, out=result.reshape(-1, 3)) return result -def image_grid(data: List[TYPE_IMAGE], width: int, height: int) -> TYPE_IMAGE: +def image_grid(data: List[ImageType], width: int, height: int) -> ImageType: #@TODO: makes poor assumption all images are the same dimensions. chunks, col, row = grid_make(data) frame = np.zeros((height * row, width * col, 4), dtype=np.uint8) @@ -297,14 +338,14 @@ def image_grid(data: List[TYPE_IMAGE], width: int, height: int) -> TYPE_IMAGE: return frame -def image_merge(imageA: TYPE_IMAGE, imageB: TYPE_IMAGE, axis: int=0, - flip: bool=False) -> TYPE_IMAGE: +def image_merge(imageA: ImageType, imageB: ImageType, axis: int=0, + flip: bool=False) -> ImageType: if flip: imageA, imageB = imageB, imageA axis = 1 if axis == "HORIZONTAL" else 0 return np.concatenate((imageA, imageB), axis=axis) -def image_recenter(image: TYPE_IMAGE) -> TYPE_IMAGE: +def image_recenter(image: ImageType) -> ImageType: cropped_image = image_detect(image)[0] new_image = np.zeros(image.shape, dtype=np.uint8) paste_x = (new_image.shape[1] - cropped_image.shape[1]) // 2 @@ -312,7 +353,7 @@ def image_recenter(image: TYPE_IMAGE) -> TYPE_IMAGE: new_image[paste_y:paste_y+cropped_image.shape[0], paste_x:paste_x+cropped_image.shape[1]] = cropped_image return new_image -def image_stereo_shift(image: TYPE_IMAGE, depth: TYPE_IMAGE, shift:float=10) -> TYPE_IMAGE: +def image_stereo_shift(image: ImageType, depth: ImageType, shift:float=10) -> ImageType: # Ensure base image has alpha image = image_convert(image, 4) depth = image_convert(depth, 1) @@ -326,7 +367,7 @@ def image_stereo_shift(image: TYPE_IMAGE, depth: TYPE_IMAGE, shift:float=10) -> continue shifted_data[y][x2] = image[y][x] - shifted_image = cv2pil(shifted_data) + shifted_image = cv_to_pil(shifted_data) alphas_image = Image.fromarray( ndimage.binary_fill_holes( ImageChops.invert( @@ -335,11 +376,11 @@ def image_stereo_shift(image: TYPE_IMAGE, depth: TYPE_IMAGE, shift:float=10) -> ) ).convert("1") shifted_image.putalpha(ImageChops.invert(alphas_image)) - return pil2cv(shifted_image) + return pil_to_cv(shifted_image) # KERNELS -def MEDIAN3x3(image: TYPE_IMAGE) -> TYPE_IMAGE: +def MEDIAN3x3(image: ImageType) -> ImageType: height, width = image.shape[:2] out = np.zeros([height, width]) for i in range(1, height-1): @@ -360,7 +401,7 @@ def MEDIAN3x3(image: TYPE_IMAGE) -> TYPE_IMAGE: out[i, j]= temp[4] return out -def kernel(stride: int) -> TYPE_IMAGE: +def kernel(stride: int) -> ImageType: """ Generate a kernel matrix with a specific stride. @@ -371,7 +412,7 @@ def kernel(stride: int) -> TYPE_IMAGE: - stride (int): The size of the square kernel matrix. Returns: - - TYPE_IMAGE: The generated kernel matrix. + - ImageType: The generated kernel matrix. Example: >>> KERNEL(3) @@ -396,7 +437,7 @@ def kernel(stride: int) -> TYPE_IMAGE: # # -def image_load_exr(url: str) -> Tuple[TYPE_IMAGE, TYPE_IMAGE]: +def image_load_exr(url: str) -> Tuple[ImageType, ImageType]: """ exr_file = OpenEXR.InputFile(url) exr_header = exr_file.header() @@ -414,7 +455,7 @@ def image_load_exr(url: str) -> Tuple[TYPE_IMAGE, TYPE_IMAGE]: """ pass -def image_load_from_url(url: str, stream:bool=True) -> TYPE_IMAGE: +def image_load_from_url(url: str, stream:bool=True) -> ImageType: """Creates a CV2 BGR image from a url.""" try: image = urllib.request.urlopen(url) @@ -423,7 +464,7 @@ def image_load_from_url(url: str, stream:bool=True) -> TYPE_IMAGE: except: try: image = Image.open(requests.get(url, stream=stream).raw) - return pil2cv(image) + return pil_to_cv(image) except Exception as e: logger.error(str(e)) @@ -440,11 +481,11 @@ def image_save_gif(fpath:str, images: List[Image.Image], fps: int=0, save_all=True ) -def image_load_data(data: str) -> TYPE_IMAGE: +def image_load_data(data: str) -> ImageType: img = ImageOps.exif_transpose(data) - return pil2cv(img) + return pil_to_cv(img) -def image_gradient(width:int, height:int, color_map:dict=None) -> TYPE_IMAGE: +def image_gradient(width:int, height:int, color_map:dict=None) -> ImageType: if color_map is None: color_map = {0: (0,0,0,255)} else: @@ -458,7 +499,7 @@ def image_gradient(width:int, height:int, color_map:dict=None) -> TYPE_IMAGE: def gaussian(x, a, b, c, d=0) -> Any: return a * math.exp(-(x - b)**2 / (2 * c**2)) + d - def pixel(x, spread:int=1) -> TYPE_iRGB: + def pixel(x, spread:int=1) -> RGB_Int: ws = widthf / (spread * len(color_map)) r = sum([gaussian(x, p[0], k * widthf, ws) for k, p in color_map.items()]) g = sum([gaussian(x, p[1], k * widthf, ws) for k, p in color_map.items()]) @@ -469,9 +510,9 @@ def image_gradient(width:int, height:int, color_map:dict=None) -> TYPE_IMAGE: r, g, b = pixel(x) for y in range(height): draw[x, y] = r, g, b - return pil2cv(image) + return pil_to_cv(image) -def torch_rgb2hsv(rgb: torch.Tensor) -> torch.Tensor: +def torch_rgb2hsv(rgb: TensorType) -> TensorType: cmax, cmax_idx = torch.max(rgb, dim=1, keepdim=True) cmin = torch.min(rgb, dim=1, keepdim=True)[0] delta = cmax - cmin @@ -485,11 +526,11 @@ def torch_rgb2hsv(rgb: torch.Tensor) -> torch.Tensor: hsv_h[cmax_idx == 2] = (((rgb[:, 0:1] - rgb[:, 1:2]) / delta) + 4)[cmax_idx == 2] hsv_h[cmax_idx == 3] = 0. hsv_h /= 6. - hsv_s = torch.where(cmax == 0, torch.tensor(0.).type_as(rgb), delta / cmax) + hsv_s = torch.where(cmax == 0, TensorType(0.).type_as(rgb), delta / cmax) hsv_v = cmax return torch.cat([hsv_h, hsv_s, hsv_v], dim=1) -def torch_hsv2rgb(hsv: torch.Tensor) -> torch.Tensor: +def torch_hsv2rgb(hsv: TensorType) -> TensorType: hsv_h, hsv_s, hsv_l = hsv[:, 0:1], hsv[:, 1:2], hsv[:, 2:3] _c = hsv_l * hsv_s _x = _c * (- torch.abs(hsv_h * 6. % 2. - 1) + 1.) @@ -507,7 +548,7 @@ def torch_hsv2rgb(hsv: torch.Tensor) -> torch.Tensor: rgb += _m return rgb -def torch_rgb2hsl(rgb: torch.Tensor) -> torch.Tensor: +def torch_rgb2hsl(rgb: TensorType) -> TensorType: cmax, cmax_idx = torch.max(rgb, dim=1, keepdim=True) cmin = torch.min(rgb, dim=1, keepdim=True)[0] delta = cmax - cmin @@ -529,7 +570,7 @@ def torch_rgb2hsl(rgb: torch.Tensor) -> torch.Tensor: hsl_s[hsl_l_l0_5] = ((cmax - cmin) / (- hsl_l * 2. + 2.))[hsl_l_l0_5] return torch.cat([hsl_h, hsl_s, hsl_l], dim=1) -def torch_hsl2rgb(hsl: torch.Tensor) -> torch.Tensor: +def torch_hsl2rgb(hsl: TensorType) -> TensorType: hsl_h, hsl_s, hsl_l = hsl[:, 0:1], hsl[:, 1:2], hsl[:, 2:3] _c = (-torch.abs(hsl_l * 2. - 1.) + 1) * hsl_s _x = _c * (-torch.abs(hsl_h * 6. % 2. - 1) + 1.) diff --git a/sup/text.py b/sup/text.py index bcbb348..a28fe77 100644 --- a/sup/text.py +++ b/sup/text.py @@ -1,18 +1,20 @@ -""" -Jovimetrix - TEXT support -""" +""" Jovimetrix - TEXT support """ -from enum import Enum import textwrap +from enum import Enum from typing import List, Tuple from matplotlib import font_manager from PIL import Image, ImageFont, ImageDraw -from loguru import logger +from cozy_comfyui import \ + logger -from .image import TYPE_IMAGE, TYPE_PIXEL, \ - pil2cv +from cozy_comfyui.image import \ + PixelType, ImageType + +from cozy_comfyui.image.convert import \ + pil_to_cv # ============================================================================== @@ -69,7 +71,7 @@ def text_draw(full_text: str, font: ImageFont, align: EnumAlignment=EnumAlignment.CENTER, justify: EnumJustify=EnumJustify.CENTER, margin: int=0, line_spacing: int=0, - color: TYPE_PIXEL=(255,255,255,255)) -> TYPE_IMAGE: + color: PixelType=(255,255,255,255)) -> ImageType: img = Image.new("RGBA", (width, height)) draw = ImageDraw.Draw(img) @@ -99,5 +101,5 @@ def text_draw(full_text: str, font: ImageFont, # x = min(width - line_width, max(line_width, x)) draw.text((x, y), line, fill=color, font=font) y += height_delta - return pil2cv(img) + return pil_to_cv(img) diff --git a/sup/util.py b/sup/util.py deleted file mode 100644 index 0e056d9..0000000 --- a/sup/util.py +++ /dev/null @@ -1,416 +0,0 @@ -""" -Jovimetrix - UTIL support -""" - -import os -import json -import math -from enum import Enum -from typing import Any, List, Generator, Optional, Tuple - -import torch - -from loguru import logger - -MIN_IMAGE_SIZE = 32 - -# ============================================================================== -# === ENUMERATION === -# ============================================================================== - -class EnumConvertType(Enum): - BOOLEAN = 1 - FLOAT = 10 - INT = 12 - VEC2 = 20 - VEC2INT = 25 - VEC3 = 30 - VEC3INT = 35 - VEC4 = 40 - VEC4INT = 45 - COORD2D = 22 - STRING = 0 - LIST = 2 - DICT = 3 - IMAGE = 4 - LATENT = 5 - # ENUM = 6 - ANY = 9 - MASK = 7 - # MIXLAB LAYER - LAYER = 8 - -class EnumSwizzle(Enum): - A_X = 0 - A_Y = 10 - A_Z = 20 - A_W = 30 - B_X = 9 - B_Y = 11 - B_Z = 21 - B_W = 31 - CONSTANT = 40 - -# ============================================================================== -# === SUPPORT === -# ============================================================================== - -def grid_make(data: List[Any]) -> Tuple[List[List[Any]], int, int]: - """ - Create a 2D grid from a 1D list. - - Args: - data (List[Any]): Input data. - - Returns: - Tuple[List[List[Any]], int, int]: A tuple containing the 2D grid, number of columns, - and number of rows. - """ - size = len(data) - grid = int(math.sqrt(size)) - if grid * grid < size: - grid += 1 - if grid < 1: - return [], 0, 0 - - rows = size // grid - if size % grid != 0: - rows += 1 - - ret = [] - cols = 0 - for j in range(rows): - end = min((j + 1) * grid, len(data)) - cols = max(cols, end - j * grid) - d = [data[i] for i in range(j * grid, end)] - ret.append(d) - return ret, cols, rows - -def load_file(fname: str) -> str | None: - try: - with open(fname, 'r', encoding='utf-8') as f: - return f.read() - except Exception as e: - logger.error(e) - -def parse_dynamic(data:dict, prefix:str, typ:EnumConvertType, default: Any) -> List[Any]: - """Convert iterated input field(s) based on a s into a single compound list of entries. - - The default will just look for all keys as integer: - - `#_` or `#__` - - This will return N entries in a list based on the prefix pattern or not. - - """ - vals = [] - fail = 0 - keys = data.keys() - for i in range(100): - if fail > 2: - break - - found = None - for k in keys: - if k.startswith(f"{i}_") or k.startswith(f"{i}_{prefix}_"): - val = parse_param(data, k, typ, default) - if isinstance(val, (list, set, tuple,)): - vals.extend(val) - elif isinstance(val, (torch.Tensor,)): - # a batch of RGB(A) - if val.ndim > 3: - val = [t for t in val] - # a batch of Grayscale - else: - val = [t.unsqueeze(-1) for t in val] - vals.extend(val) - else: - vals.append(val) - found = True - break - - if found is None: - fail += 1 - - return vals - -def parse_value(val:Any, typ:EnumConvertType, default: Any, - clip_min: Optional[float]=None, clip_max: Optional[float]=None, - zero:int=0) -> List[Any]: - """Convert target value into the new specified type.""" - - if typ == EnumConvertType.ANY: - return val - - if isinstance(default, torch.Tensor) and typ not in [EnumConvertType.IMAGE, - EnumConvertType.MASK, - EnumConvertType.LATENT]: - h, w = default.shape[:2] - cc = default.shape[2] if len(default.shape) > 2 else 1 - default = (w, h, cc) - - if val is None: - if default is None: - return None - val = default - - if isinstance(val, dict): - # old jovimetrix index? - if '0' in val or 0 in val: - val = [val.get(i, val.get(str(i), 0)) for i in range(min(len(val), 4))] - # coord2d? - elif 'x' in val: - val = [val.get(c, 0) for c in 'xyzw'] - # wacky color struct? - elif 'r' in val: - val = [val.get(c, 0) for c in 'rgba'] - elif isinstance(val, torch.Tensor) and typ not in [EnumConvertType.IMAGE, - EnumConvertType.MASK, - EnumConvertType.LATENT]: - h, w = val.shape[:2] - cc = val.shape[2] if len(val.shape) > 2 else 1 - val = (w, h, cc) - - new_val = val - if typ in [EnumConvertType.FLOAT, EnumConvertType.INT, - EnumConvertType.VEC2, EnumConvertType.VEC2INT, - EnumConvertType.VEC3, EnumConvertType.VEC3INT, - EnumConvertType.VEC4, EnumConvertType.VEC4INT, - EnumConvertType.COORD2D]: - - if not isinstance(val, (list, tuple, torch.Tensor)): - val = [val] - - size = max(1, int(typ.value / 10)) - new_val = [] - for idx in range(size): - try: - d = default[idx] if idx < len(default) else 0 - except: - try: - d = default.get(str(idx), 0) - except: - d = default - - v = d if val is None else val[idx] if idx < len(val) else d - if isinstance(v, (str, )): - v = v.strip('\n').strip() - if v == '': - v = 0 - - try: - if typ in [EnumConvertType.FLOAT, EnumConvertType.VEC2, EnumConvertType.VEC3, EnumConvertType.VEC4]: - v = round(float(v or 0), 16) - else: - v = int(v) - if clip_min is not None: - v = max(v, clip_min) - if clip_max is not None: - v = min(v, clip_max) - except Exception as e: - logger.exception(e) - logger.error(f"Error converting value: {val} -- {v}") - v = 0 - - if v == 0: - v = zero - new_val.append(v) - new_val = new_val[0] if size == 1 else tuple(new_val) - elif typ == EnumConvertType.DICT: - try: - if isinstance(new_val, (str,)): - try: - new_val = json.loads(new_val) - except json.decoder.JSONDecodeError: - new_val = {} - else: - if not isinstance(new_val, (list, tuple,)): - new_val = [new_val] - new_val = {i: v for i, v in enumerate(new_val)} - except Exception as e: - logger.exception(e) - elif typ == EnumConvertType.LIST: - new_val = list(new_val) - elif typ == EnumConvertType.STRING: - if isinstance(new_val, (str, list, int, float,)): - new_val = [new_val] - new_val = ", ".join(map(str, new_val)) if not isinstance(new_val, str) else new_val - elif typ == EnumConvertType.BOOLEAN: - if isinstance(new_val, (torch.Tensor,)): - new_val = True - elif isinstance(new_val, (dict,)): - new_val = len(new_val.keys()) > 0 - elif isinstance(new_val, (list, tuple,)) and len(new_val) > 0 and (nv := new_val[0]) is not None: - if isinstance(nv, (bool, str,)): - new_val = bool(nv) - elif isinstance(nv, (int, float,)): - new_val = nv > 0 - elif typ == EnumConvertType.LATENT: - # covert image into latent - if isinstance(new_val, (torch.Tensor,)): - new_val = {'samples': new_val.unsqueeze(0)} - else: - # convert whatever into a latent sample... - new_val = torch.empty((4, 64, 64), dtype=torch.uint8).unsqueeze(0) - new_val = {'samples': new_val} - elif typ == EnumConvertType.IMAGE: - # covert image into image? just skip if already an image - if not isinstance(new_val, (torch.Tensor,)): - color = parse_value(new_val, EnumConvertType.VEC4INT, (0,0,0,255), 0, 255) - color = torch.tensor(color, dtype=torch.int32).tolist() - new_val = torch.empty((MIN_IMAGE_SIZE, MIN_IMAGE_SIZE, 4), dtype=torch.uint8) - new_val[0,:,:] = color[0] - new_val[1,:,:] = color[1] - new_val[2,:,:] = color[2] - new_val[3,:,:] = color[3] - elif typ == EnumConvertType.MASK: - # @TODO: FIX FOR MULTI-CHAN? - if not isinstance(new_val, (torch.Tensor,)): - color = parse_value(new_val, EnumConvertType.INT, 0, 0, 255) - color = torch.tensor(color, dtype=torch.int32).tolist() - new_val = torch.empty((MIN_IMAGE_SIZE, MIN_IMAGE_SIZE, 1), dtype=torch.uint8) - new_val[0,:,:] = color - - elif issubclass(typ, Enum): - new_val = typ[val] - - if typ == EnumConvertType.COORD2D: - new_val = {'x': new_val[0], 'y': new_val[1]} - return new_val - -def parse_param(data:dict, key:str, typ:EnumConvertType, default: Any, - clip_min: Optional[float]=None, clip_max: Optional[float]=None, - zero:int=0) -> List[Any]: - """Convenience because of the dictionary parameters.""" - values = data.get(key, default) - if typ == EnumConvertType.ANY: - if values is None: - return [default] - return parse_param_list(values, typ, default, clip_min, clip_max, zero) - -def parse_param_list(values:Any, typ:EnumConvertType, default: Any, - clip_min: Optional[float]=None, clip_max: Optional[float]=None, - zero:int=0) -> List[Any]: - """Convert list of values into a list of specified type.""" - - if not isinstance(values, (list,)): - values = [values] - - value_array = [] - for val in values: - if isinstance(val, (str,)): - try: val = json.loads(val.replace("'", '"')) - except json.JSONDecodeError: pass - value_array.append(val) - # see if we are a Jovimetrix hacked vector blob... {0:x, 1:y, 2:z, 3:w} - elif isinstance(val, dict): - # mixlab layer? - if (image := val.get('image', None)) is not None: - ret = image - if (mask := val.get('mask', None)) is not None: - while len(mask.shape) < len(image.shape): - mask = mask.unsqueeze(-1) - ret = torch.cat((image, mask), dim=-1) - if ret.ndim > 3: - val = [t for t in ret] - elif ret.ndim == 3: - val = [v.unsqueeze(-1) for v in ret] - value_array.extend(val) - # vector patch.... - elif 'xyzw' in val: - val = tuple(x for x in val["xyzw"]) - # latents.... - elif 'samples' in val: - val = tuple(x for x in val["samples"]) - elif ('0' in val) or (0 in val): - val = tuple(val.get(i, val.get(str(i), 0)) for i in range(min(len(val), 4))) - elif 'x' in val and 'y' in val: - val = tuple(val.get(c, 0) for c in 'xyzw') - elif 'r' in val and 'g' in val: - val = tuple(val.get(c, 0) for c in 'rgba') - elif len(val) == 0: - val = tuple() - value_array.append(val) - elif isinstance(val, (torch.Tensor,)): - # a batch of RGB(A) - if val.ndim > 3: - val = [t for t in val] - # a batch of Grayscale - else: - val = [t.unsqueeze(-1) for t in val] - value_array.extend(val) - elif isinstance(val, (list, tuple, set)): - if isinstance(val, (tuple, set,)): - val = list(val) - value_array.append(val) - elif issubclass(type(val), (Enum,)): - val = str(val.name) - value_array.append(val) - else: - value_array.append(val) - - return [parse_value(v, typ, default, clip_min, clip_max, zero) for v in value_array] - -def path_next(pattern: str) -> str: - """ - Finds the next free path in an sequentially named list of files - """ - i = 1 - while os.path.exists(pattern % i): - i = i * 2 - - a, b = (i // 2, i) - while a + 1 < b: - c = (a + b) // 2 - a, b = (c, b) if os.path.exists(pattern % c) else (a, c) - return pattern % b - -def vector_swap(pA: Any, pB: Any, swap_x: EnumSwizzle, x:float, swap_y:EnumSwizzle, y:float, - swap_z:EnumSwizzle, z:float, swap_w:EnumSwizzle, w:float) -> List[float]: - """Swap out a vector's values with another vector's values, or a constant fill.""" - def parse(target, targetB, swap, val) -> float: - if swap == EnumSwizzle.CONSTANT: - return val - if swap in [EnumSwizzle.B_X, EnumSwizzle.B_Y, EnumSwizzle.B_Z, EnumSwizzle.B_W]: - target = targetB - swap = int(swap.value / 10) - return target[swap] if swap < len(target) else 0 - - return [ - parse(pA, pB, swap_x, x), - parse(pA, pB, swap_y, y), - parse(pA, pB, swap_z, z), - parse(pA, pB, swap_w, w) - ] - -def zip_longest_fill(*iterables: Any) -> Generator[Tuple[Any, ...], None, None]: - """ - Zip longest with fill value. - - This function behaves like itertools.zip_longest, but it fills the values - of exhausted iterators with their own last values instead of None. - """ - try: - iterators = [iter(iterable) for iterable in iterables] - except Exception as e: - logger.error(iterables) - logger.error(str(e)) - else: - while True: - values = [next(iterator, None) for iterator in iterators] - - # Check if all iterators are exhausted - if all(value is None for value in values): - break - - # Fill in the last values of exhausted iterators with their own last values - for i, _ in enumerate(iterators): - if values[i] is None: - iterator_copy = iter(iterables[i]) - while True: - current_value = next(iterator_copy, None) - if current_value is None: - break - values[i] = current_value - - yield tuple(values)