diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..dfe0770
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Auto detect text files and perform LF normalization
+* text=auto
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d2795e
--- /dev/null
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f5db99a
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
index b067d10..157f302 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,82 @@
-# Jovi_Measure
-Image metrics nodes for ComfyUI
+
+
+
+
+# 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 virtual environment (venv), 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.
+
+
+
+[](https://github.com/sponsors/Amorano)
+
+[](https://www.paypal.com/paypalme/onarom)
+
+[](https://www.patreon.com/joviex)
+
+[](https://ko-fi.com/alexandermorano)
+
+
+
+
+
+# WHERE TO FIND ME
+
+You can find me on [](https://discord.gg/62TJaZ3Z5r).
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..d64ee2e
--- /dev/null
+++ b/__init__.py
@@ -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()
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 0000000..25e2977
--- /dev/null
+++ b/core/__init__.py
@@ -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
diff --git a/core/metrix.py b/core/metrix.py
new file mode 100644
index 0000000..189863d
--- /dev/null
+++ b/core/metrix.py
@@ -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,
\ No newline at end of file
diff --git a/node_list.json b/node_list.json
new file mode 100644
index 0000000..3057b81
--- /dev/null
+++ b/node_list.json
@@ -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"
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..d9b0788
--- /dev/null
+++ b/pyproject.toml
@@ -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 = ""
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6f863ca
--- /dev/null
+++ b/requirements.txt
@@ -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