first pass
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
+25
@@ -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
|
||||
@@ -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.
|
||||
@@ -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">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------------------------------------->
|
||||
|
||||
# HIGHLIGHTS
|
||||
|
||||

|
||||
|
||||
## 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">
|
||||
|
||||
[](https://github.com/sponsors/Amorano)
|
||||
|
||||
[](https://www.paypal.com/paypalme/onarom)
|
||||
|
||||
[](https://www.patreon.com/joviex)
|
||||
|
||||
[](https://ko-fi.com/alexandermorano)
|
||||
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------------------------------------->
|
||||
|
||||
# WHERE TO FIND ME
|
||||
|
||||
You can find me on [](https://discord.gg/62TJaZ3Z5r).
|
||||
|
||||
+152
@@ -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()
|
||||
@@ -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
@@ -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,
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 = ""
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user