* core supports switched to [cozy_comfyui](https://github.com/cozy-comfyui/cozy_comfyui)

This commit is contained in:
Alexander G. Morano
2025-04-14 02:04:37 -04:00
parent 869001110c
commit 5e9e0e9f9d
20 changed files with 771 additions and 1791 deletions
+4
View File
@@ -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:
+17 -352
View File
@@ -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()
+122 -92
View File
@@ -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)
+139 -129
View File
@@ -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))
'''
+48 -45
View File
@@ -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)]
+43 -33
View File
@@ -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
+27 -24
View File
@@ -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"})
}
+62 -26
View File
@@ -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":"<comfy output dir>",
"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:
+2 -2
View File
@@ -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\"",
+1 -1
View File
@@ -6,7 +6,7 @@ loguru
markdownify
matplotlib
numba
numpy<=1.26.4
numpy<2
opencv-contrib-python
Pillow
pywin32; platform_system=="Windows"
+1 -3
View File
@@ -1,6 +1,4 @@
"""
Jovimetrix - Animation Support
"""
""" Jovimetrix - Animation Support """
import inspect
from enum import Enum
+1 -431
View File
@@ -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 """
+79 -71
View File
@@ -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],
+18 -13
View File
@@ -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
+63 -61
View File
@@ -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
+31 -28
View File
@@ -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)
+29 -23
View File
@@ -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)
+73 -32
View File
@@ -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.)
+11 -9
View File
@@ -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)
-416
View File
@@ -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:
`#_<field name>` or `#_<prefix>_<field name>`
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)