first pass

This commit is contained in:
Alexander G. Morano
2025-01-11 17:23:19 -05:00
parent cde0b57a4d
commit dcc8d35539
10 changed files with 446 additions and 2 deletions
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+25
View File
@@ -0,0 +1,25 @@
# Python-specific
__pycache__/
*.pyc
# Virtual environments
.env/
venv/
# IDE files
.idea/
.vscode/
*.code-workspace
# Build artifacts
build/
dist/
*.egg-info/
# Other
.DS_Store
# user junk
__user/
.coverage
.env
+23
View File
@@ -0,0 +1,23 @@
MIT License
Copyright (c) 2025 Alexander G. Morano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
GO NUTS; JUST TRY NOT TO DO IT IN YOUR HEAD.
+82 -2
View File
@@ -1,2 +1,82 @@
# Jovi_Measure
Image metrics nodes for ComfyUI
<div align="center">
<picture>
<source srcset="https://github.com/Amorano/Jovi_Measure-examples/blob/master/res/Jovi_Measure.png">
<img alt="Image metrics nodes for ComfyUI" width="256" height="256">
</picture>
</div>
<div align="center">
<a href="https://github.com/comfyanonymous/ComfyUI">COMFYUI</a> Nodes for image metrics
</div>
<div align="center">
![KNIVES!](https://badgen.net/github/open-issues/Amorano/Jovi_Measure)
![FORKS!](https://badgen.net/github/forks/Amorano/Jovi_Measure)
</div>
<!---------------------------------------------------------------------------->
# HIGHLIGHTS
![image](https://github.com/user-attachments/assets/d8d13740-8dbc-4cc3-ad52-072a32c91517)
## UPDATES
**2024/01/07** @1.0.0:
* initial release
# INSTALLATION
## COMFYUI MANAGER
If you have [ComfyUI Manager](https://github.com/ltdrdata/ComfyUI-Manager) installed, simply search for Jovi_Measure and install from the manager's database.
## MANUAL INSTALL
Clone the repository into your ComfyUI custom_nodes directory. You can clone the repository with the command:
```
git clone https://github.com/Amorano/Jovi_Measure.git
```
You can then install the requirements by using the command:
```
.\python_embed\python.exe -s -m pip install -r .\ComfyUI\custom_nodes\Jovi_Measure\requirements.txt
```
If you are using a <code>virtual environment</code> (<code><i>venv</i></code>), make sure it is activated before installation. Then install the requirements with the command:
```
pip install -r .\ComfyUI\custom_nodes\Jovi_Measure\requirements.txt
```
## ENVIRONMENT
<!---------------------------------------------------------------------------->
<!---------------------------------------------------------------------------->
# SPONSORSHIP
Please consider sponsoring me if you enjoy the results of my work, code or documentation or otherwise. A good way to keep code development open and free is through sponsorship.
<div align="center">
[![BE A GITHUB SPONSOR ❤️](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/Amorano)
[![DIRECTLY SUPPORT ME VIA PAYPAL](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://www.paypal.com/paypalme/onarom)
[![PATREON SUPPORTER](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://www.patreon.com/joviex)
[![SUPPORT ME ON KO-FI!](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/alexandermorano)
</div>
<!---------------------------------------------------------------------------->
# WHERE TO FIND ME
You can find me on [![DISCORD](https://dcbadge.vercel.app/api/server/62TJaZ3Z5r?style=flat-square)](https://discord.gg/62TJaZ3Z5r).
+152
View File
@@ -0,0 +1,152 @@
"""
██╗ ██████╗ ██╗ ██╗██╗ ███╗ ███╗███████╗ █████╗ ███████╗██╗ ██╗██████╗ ███████╗
██║██╔═══██╗██║ ██║██║ ████╗ ████║██╔════╝██╔══██╗██╔════╝██║ ██║██╔══██╗██╔════╝
██║██║ ██║██║ ██║██║ ██╔████╔██║█████╗ ███████║███████╗██║ ██║██████╔╝█████╗
██ ██║██║ ██║╚██╗ ██╔╝██║ ██║╚██╔╝██║██╔══╝ ██╔══██║╚════██║██║ ██║██╔══██╗██╔══╝
╚█████╔╝╚██████╔╝ ╚████╔╝ ██║ ██║ ╚═╝ ██║███████╗██║ ██║███████║╚██████╔╝██║ ██║███████╗
╚════╝ ╚═════╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
Image metrics nodes for ComfyUI
http://www.github.com/amorano/Jovi_Measure
"""
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
__author__ = """Alexander G. Morano"""
__email__ = "amorano@gmail.com"
__version__ = "1.0.0"
import os
import sys
import json
import inspect
import importlib
from pathlib import Path
from types import ModuleType
from typing import Any
from loguru import logger
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
WEB_DIRECTORY = "./web"
ROOT = Path(__file__).resolve().parent
ROOT_COMFY = ROOT.parent.parent
JOV_WEB = ROOT / 'web'
JOV_LOG_LEVEL = os.getenv("JOV_LOG_LEVEL", "INFO")
logger.configure(handlers=[{"sink": sys.stdout, "level": JOV_LOG_LEVEL}])
JOV_INTERNAL = os.getenv("JOV_INTERNAL", 'false').strip().lower() in ('true', '1', 't')
JOV_PACKAGE = "JOV_MEASURE"
# ==============================================================================
# === CORE NODES ===
# ==============================================================================
class JOVBaseNode:
NOT_IDEMPOTENT = True
CATEGORY = f"{JOV_PACKAGE} 📺"
RETURN_TYPES = ()
FUNCTION = "run"
@classmethod
def IS_CHANGED(cls, **kw) -> float:
return float('nan')
@classmethod
def VALIDATE_INPUTS(cls, *arg, **kw) -> bool:
return True
@classmethod
def INPUT_TYPES(cls, prompt:bool=False, extra_png:bool=False, dynprompt:bool=False) -> dict:
data = {
"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
# ==============================================================================
# === 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("*")
# ==============================================================================
# === NODE LOADER ===
# ==============================================================================
def load_module(name: str) -> None|ModuleType:
module = inspect.getmodule(inspect.stack()[0][0]).__name__
try:
route = str(name).replace("\\", "/")
route = route.split(f"{module}/core/")[1]
route = route.split('.')[0].replace('/', '.')
except Exception as e:
logger.warning(f"module failed {name}")
logger.warning(str(e))
return
try:
module = f"{module}.core.{route}"
module = importlib.import_module(module)
except Exception as e:
logger.warning(f"module failed {module}")
logger.warning(str(e))
return
return module
def loader():
global NODE_DISPLAY_NAME_MAPPINGS, NODE_CLASS_MAPPINGS
NODE_LIST_MAP = {}
for fname in ROOT.glob('core/**/*.py'):
if fname.stem.startswith('_'):
continue
if (module := load_module(fname)) is None:
continue
classes = inspect.getmembers(module, inspect.isclass)
for class_name, class_object in classes:
if not class_name.endswith('BaseNode') and hasattr(class_object, 'NAME') and hasattr(class_object, 'CATEGORY'):
name = f"{class_object.NAME} ({JOV_PACKAGE})"
NODE_DISPLAY_NAME_MAPPINGS[name] = name
NODE_CLASS_MAPPINGS[name] = class_object
desc = class_object.DESCRIPTION if hasattr(class_object, 'DESCRIPTION') else name
NODE_LIST_MAP[name] = desc.split('.')[0].strip('\n')
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 )
loader()
+30
View File
@@ -0,0 +1,30 @@
"""
Jovi_Measure - http://www.github.com/Amorano/Jovi_Measure
Core
"""
from .. import JOVBaseNode
# ==============================================================================
# === SUPPORT ===
# ==============================================================================
def deep_merge(d1: dict, d2: dict) -> dict:
"""
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
+105
View File
@@ -0,0 +1,105 @@
"""
Jovi_Measure - http://www.github.com/amorano/Jovi_Measure
Image Metrics
"""
import torch
import numpy as np
import skimage.measure as skm
from comfy.utils import ProgressBar
from .. import JOVBaseNode
from . import deep_merge
# ==============================================================================
# === SUPPORT ===
# ==============================================================================
def tensor2cv(tensor: torch.Tensor, invert_mask:bool=True) -> np.ndarray:
"""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)
# ==============================================================================
# === COMFYUI NODE ===
# ==============================================================================
class ShannonEntropyNode(JOVBaseNode):
NAME = "SHANNON ENTROPY"
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
SORT = 50
DESCRIPTION = """
Calculate the Shannon entropy of an image.
"""
@classmethod
def INPUT_TYPES(cls) -> dict:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"required": {
'image': ("IMAGE", {"default": None}),
}
})
return d
def run(self, image, **kw) -> float:
vals = []
images = [i for i in image]
pbar = ProgressBar(len(images))
for idx, image in enumerate(images):
image = tensor2cv(image)
val = skm.shannon_entropy(image)
vals.append(val)
print(val)
pbar.update_absolute(idx)
return vals,
class BlurEffectNode(JOVBaseNode):
NAME = "BLUR EFFECT"
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("FLOAT",)
SORT = 50
DESCRIPTION = """
Calculate the Shannon entropy of an image.
"""
@classmethod
def INPUT_TYPES(cls) -> dict:
d = super().INPUT_TYPES()
d = deep_merge(d, {
"required": {
'image': ("IMAGE", {"default": None}),
},
"optional": {
'h_size': ("INT", {"default": 11, "tooltip": "Size of the re-blurring filter."}),
# 'channel_axis': ("INT", {"default": 0, "min": 0, "max": 3}),
}
})
return d
def run(self, image, h_size, **kw) -> float:
vals = []
images = [i for i in image]
pbar = ProgressBar(len(images))
for idx, image in enumerate(images):
image = tensor2cv(image)
channel_axis = 2
if len(hwc := image.shape) == 2 or hwc[2] == 1:
channel_axis = None
val = skm.blur_effect(image, h_size=h_size, channel_axis=channel_axis)
vals.append(val)
print(val)
pbar.update_absolute(idx)
return vals,
+4
View File
@@ -0,0 +1,4 @@
{
"BLUR EFFECT (JOV_MEASURE)": "Calculate the Shannon entropy of an image",
"SHANNON ENTROPY (JOV_MEASURE)": "Calculate the Shannon entropy of an image"
}
+19
View File
@@ -0,0 +1,19 @@
[project]
name = "Jovi_Measure"
description = "ComfyUI Nodes for using Spout streams "
version = "1.0.0"
license = { file = "LICENSE" }
dependencies = [
"loguru",
"numpy>=1.26.4,<2.0.0; python_version < '3.12'",
"numpy>=2.0.0; python_version > '3.11'",
"scikit-image",
]
[project.urls]
Repository = "https://github.com/Amorano/Jovi_Measure"
[tool.comfy]
PublisherId = "amorano"
DisplayName = "Jovi_Measure"
Icon = ""
+4
View File
@@ -0,0 +1,4 @@
loguru
numpy>=1.26.4,<2.0.0; python_version < '3.12'
numpy>=2.0.0; python_version > '3.11'
scikit-image