From 5fad86d77b3e4682463de2d1e03a7050c1335b3e Mon Sep 17 00:00:00 2001 From: Alexandre 'Kidev' Poumaroux <1204936+Kidev@users.noreply.github.com> Date: Mon, 27 Jan 2025 16:28:26 +0100 Subject: [PATCH] Add parameters to set center, add entire image parameter to Defisheye --- README.md | 20 ++++++++------- base.py | 1 + defisheye.py | 63 +++++++++++++++++++++++++++++++++++++++++------- fisheye.py | 38 ++++++++++++++++++++++------- install.py | 19 ++++++++++----- nodes.py | 9 +++---- pyproject.toml | 4 +-- requirements.txt | 7 +++--- 8 files changed, 117 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index fbe324a..1423264 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Provides tools for applying and removing fisheye lens effects from images. - Multiple distortion mappings: equidistant, equisolid, orthographic, stereographic - Format options: fullframe and circular - Adjustable FOV and PFOV parameters - -### Tips -- The `orthographic` mapping keeps the entirety of the image data, so going back and forth works well, see `examples/BackAndForth.json` +- Adjustable center +- Option to always display the full image uncropped ## Installation @@ -47,12 +46,15 @@ The nodes will appear in the node menu under the "image/processing" category: ### Parameters - image: Input image -- mapping: Distortion mapping mode (equidistant, equisolid, orthographic, stereographic) -- format: Output format (fullframe, circular) -- fov: Field of view in degrees (0-360) -- pfov: Perspective field of view in degrees (0-360) +- mapping: Distortion mapping mode (`equidistant`, `equisolid`, `orthographic`, `stereographic`) +- format: Output format (`fullframe`, `circular`) +- fov: Field of view in degrees (`0.0`-`360.0`) +- pfov: Perspective field of view in degrees (`0.0`-`360.0`) +- entire_image: Always show the full image uncropped not matter the FOV/PFOV values (`True`-`False`) +- wcenter: Horizontal center of the effect (`0.0`-`1.0`) +- hcenter: Vertical center of the effect (`0.0`-`1.0`) -## Example Workflows +## Examples -Check the examples folder for sample [workflows](examples/BackAndForth.json) demonstrating various use cases. +Check the examples folder for a sample [workflow](examples/BackAndForth.json) and images demonstrating various use cases. ![Example](examples/BackAndForthFullframe.png) diff --git a/base.py b/base.py index a702985..fe815f4 100644 --- a/base.py +++ b/base.py @@ -1,6 +1,7 @@ import numpy as np import torch + class FisheyeBase: def setup_parameters(self, fov, pfov, mapping, format): self.fov = fov diff --git a/defisheye.py b/defisheye.py index b684c58..9245684 100644 --- a/defisheye.py +++ b/defisheye.py @@ -1,7 +1,9 @@ -import numpy as np import cv2 +import numpy as np +from numpy import arange, arctan, hypot, meshgrid, pi, sin, sqrt, tan + from .base import FisheyeBase -from numpy import arange, sqrt, arctan, sin, tan, meshgrid, pi, hypot + class DefisheyeNode(FisheyeBase): @classmethod @@ -9,10 +11,27 @@ def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), - "mapping": (["equidistant", "equisolid", "orthographic", "stereographic"],), + "mapping": ( + ["equidistant", "equisolid", "orthographic", "stereographic"], + ), "format": (["fullframe", "circular"],), - "fov": ("FLOAT", {"default": 180.0, "min": 0.0, "max": 360.0, "step": 10.0}), - "pfov": ("FLOAT", {"default": 120.0, "min": 0.0, "max": 360.0, "step": 10.0}), + "fov": ( + "FLOAT", + {"default": 180.0, "min": 0.0, "max": 360.0, "step": 10.0}, + ), + "pfov": ( + "FLOAT", + {"default": 120.0, "min": 0.0, "max": 360.0, "step": 10.0}, + ), + "entire_image": ("BOOLEAN", {"default": False}), + "wcenter": ( + "FLOAT", + {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}, + ), + "hcenter": ( + "FLOAT", + {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}, + ), }, } @@ -20,6 +39,25 @@ def INPUT_TYPES(cls): FUNCTION = "remove_fisheye" CATEGORY = "image/processing" + def calculate_zoom_factor(self, fov_in, fov_out, mapping): + fov_in_rad = fov_in * pi / 180.0 + fov_out_rad = fov_out * pi / 180.0 + + if mapping == "equidistant": + max_in = fov_in_rad / 2.0 + max_out = tan(fov_out_rad / 2.0) + elif mapping == "equisolid": + max_in = 2 * sin(fov_in_rad / 4.0) + max_out = tan(fov_out_rad / 2.0) + elif mapping == "orthographic": + max_in = sin(fov_in_rad / 2.0) + max_out = tan(fov_out_rad / 2.0) + elif mapping == "stereographic": + max_in = 2 * tan(fov_in_rad / 4.0) + max_out = tan(fov_out_rad / 2.0) + + return max_out / max_in if max_in > 0 else 1.0 + def map_defisheye(self, i, j, width, height, dim, xcenter, ycenter): xd = i - xcenter yd = j - ycenter @@ -45,6 +83,10 @@ def map_defisheye(self, i, j, width, height, dim, xcenter, ycenter): ifoc = dim / (2.0 * tan(self.fov * pi / 720)) rr = ifoc * tan(phiang / 2) + if self.entire_image: + zoom = self.calculate_zoom_factor(self.fov, self.pfov, self.mapping) + rr *= zoom + rdmask = rd != 0 xs = xd.astype(np.float32).copy() ys = yd.astype(np.float32).copy() @@ -57,19 +99,22 @@ def map_defisheye(self, i, j, width, height, dim, xcenter, ycenter): return xs, ys - def remove_fisheye(self, image, mapping, format, fov, pfov): + def remove_fisheye( + self, image, mapping, format, fov, pfov, entire_image, wcenter, hcenter + ): self.setup_parameters(fov, pfov, mapping, format) + self.entire_image = entire_image image_np = self.process_image_tensor(image) height, width = image_np.shape[:2] - xcenter = width // 2 - ycenter = height // 2 + xcenter = width * wcenter + ycenter = height * hcenter if format == "circular": dim = min(width, height) else: # fullframe - dim = sqrt(width ** 2 + height ** 2) + dim = sqrt(width**2 + height**2) i = arange(width) j = arange(height) diff --git a/fisheye.py b/fisheye.py index 918780a..2258e3f 100644 --- a/fisheye.py +++ b/fisheye.py @@ -1,19 +1,37 @@ -import numpy as np import cv2 -from numpy import arange, sqrt, arctan, sin, tan, meshgrid, pi, hypot +import numpy as np +from numpy import arange, arctan, hypot, meshgrid, pi, sin, sqrt, tan + from .base import FisheyeBase + class FisheyeNode(FisheyeBase): @classmethod def INPUT_TYPES(cls): return { "required": { "image": ("IMAGE",), - "mapping": (["equidistant", "equisolid", "orthographic", "stereographic"],), + "mapping": ( + ["equidistant", "equisolid", "orthographic", "stereographic"], + ), "format": (["fullframe", "circular"],), - "fov": ("FLOAT", {"default": 180.0, "min": 0.0, "max": 360.0, "step": 10.0}), - "pfov": ("FLOAT", {"default": 120.0, "min": 0.0, "max": 360.0, "step": 10.0}), + "fov": ( + "FLOAT", + {"default": 180.0, "min": 0.0, "max": 360.0, "step": 10.0}, + ), + "pfov": ( + "FLOAT", + {"default": 120.0, "min": 0.0, "max": 360.0, "step": 10.0}, + ), "entire_image": ("BOOLEAN", {"default": False}), + "wcenter": ( + "FLOAT", + {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}, + ), + "hcenter": ( + "FLOAT", + {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.1}, + ), }, } @@ -88,20 +106,22 @@ def map_fisheye(self, i, j, width, height, dim, xcenter, ycenter): return xs, ys - def apply_fisheye(self, image, mapping, format, fov, pfov, entire_image): + def apply_fisheye( + self, image, mapping, format, fov, pfov, entire_image, wcenter, hcenter + ): self.setup_parameters(fov, pfov, mapping, format) self.entire_image = entire_image image_np = self.process_image_tensor(image) height, width = image_np.shape[:2] - xcenter = width // 2 - ycenter = height // 2 + xcenter = width * wcenter + ycenter = height * hcenter if format == "circular": dim = min(width, height) else: # fullframe - dim = sqrt(width ** 2 + height ** 2) + dim = sqrt(width**2 + height**2) i = arange(width) j = arange(height) diff --git a/install.py b/install.py index cd4d145..ed96923 100644 --- a/install.py +++ b/install.py @@ -1,23 +1,30 @@ +import importlib.metadata import os -import sys import subprocess -import importlib.metadata +import sys + def install_requirements(): - requirements_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt") + requirements_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "requirements.txt" + ) with open(requirements_path) as f: required = f.read().splitlines() - installed = {dist.metadata["Name"]: dist.version for dist in importlib.metadata.distributions()} - missing = [pkg for pkg in required if pkg.split('>=')[0] not in installed] + installed = { + dist.metadata["Name"]: dist.version + for dist in importlib.metadata.distributions() + } + missing = [pkg for pkg in required if pkg.split(">=")[0] not in installed] if missing: print(f"Installing missing packages: {', '.join(missing)}") - subprocess.check_call([sys.executable, '-m', 'pip', 'install', *missing]) + subprocess.check_call([sys.executable, "-m", "pip", "install", *missing]) print("All requirements installed successfully!") else: print("All requirements already satisfied!") + if __name__ == "__main__": install_requirements() diff --git a/nodes.py b/nodes.py index 905a82a..c2ee3b6 100644 --- a/nodes.py +++ b/nodes.py @@ -1,12 +1,9 @@ -from .fisheye import FisheyeNode from .defisheye import DefisheyeNode +from .fisheye import FisheyeNode -NODE_CLASS_MAPPINGS = { - "Fisheye": FisheyeNode, - "Defisheye": DefisheyeNode -} +NODE_CLASS_MAPPINGS = {"Fisheye": FisheyeNode, "Defisheye": DefisheyeNode} NODE_DISPLAY_NAME_MAPPINGS = { "Fisheye": "Apply Fisheye Effect", - "Defisheye": "Remove Fisheye Effect" + "Defisheye": "Remove Fisheye Effect", } diff --git a/pyproject.toml b/pyproject.toml index 17428e6..16d67b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "fisheye-effects" description = "Provides tools for applying and removing fisheye lens effects from images." -version = "0.0.3" +version = "1.0.0" license = {file = "LICENSE"} -dependencies = ["numpy>=1.22.0", "opencv-python>=4.8.0", "Pillow>=9.5.0"] +dependencies = ["numpy>=1.22.0", "opencv-python>=4.8.0", "Pillow>=9.5.0", "torch>=2.5.1"] [project.urls] Repository = "https://github.com/Kidev/ComfyUI-Fisheye-effects" diff --git a/requirements.txt b/requirements.txt index 366c436..b253ff7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -numpy==2.2.2 -opencv-python==4.11.0.86 -torch==2.5.1 +numpy>=2.2.2 +opencv-python>=4.11.0 +torch>=2.5.1 +Pillow>=9.5.0