├── __init__.py ├── nodes ├── __init__.py ├── filters │ ├── __init__.py │ ├── algorithms │ │ ├── __init__.py │ │ ├── phycv │ │ │ ├── __init__.py │ │ │ ├── pst.py │ │ │ ├── vevid.py │ │ │ ├── utils.py │ │ │ ├── vevid_gpu.py │ │ │ ├── pst_gpu.py │ │ │ ├── page.py │ │ │ └── page_gpu.py │ │ ├── light_enhancement.py │ │ └── edge_detection.py │ ├── node_code_snippet.py │ ├── node_light_enhancement.py │ └── node_convolution.py ├── inputs │ ├── __init__.py │ ├── node_image.py │ ├── objects │ │ └── video_objects.py │ ├── node_webcam.py │ ├── node_screen_recorder.py │ ├── node_image_folder.py │ └── node_video.py ├── outputs │ ├── __init__.py │ ├── node_image_output.py │ └── node_video_output.py ├── viewers │ ├── __init__.py │ ├── objects │ │ ├── __init__.py │ │ └── composer_objects.py │ └── node_image_view.py ├── adjustments │ ├── __init__.py │ ├── node_mask.py │ ├── node_flip.py │ ├── node_resize.py │ ├── node_normalize.py │ ├── node_rotate.py │ ├── node_threshold.py │ └── node_crop.py └── node.py ├── res ├── __init__.py ├── fonts │ ├── __init__.py │ ├── JetBrainsMono-Medium.ttf │ └── JetBrainsMono-Regular.ttf └── icons │ ├── __init__.py │ ├── app_icon.ico │ └── app_icon.png ├── node_editor ├── __init__.py ├── connection_objects.py └── editor.py ├── .gitignore ├── github_readme_files ├── crop_node.png ├── flip_node.png ├── input_node.png ├── mask_node.png ├── video_node.png ├── canvas_node.png ├── resize_node.png ├── rotate_node.png ├── webcam_node.png ├── 2d_shape_node.png ├── image_view_node.png ├── normalize_node.png ├── threshold_node.png ├── code_snippet_node.png ├── convolution_node.png ├── image_folder_node.png ├── image_writer_node.png ├── nodiumpy_app_icon.png ├── video_writer_node.png ├── edge_detection_node.png ├── screen_recorder_node.png ├── light_enhancement_node.png ├── nodiumpy_demo_image_1.png └── smoothing_sharpening_node.png ├── project_resources.py ├── main.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /node_editor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/fonts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /res/icons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/filters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/inputs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/outputs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/viewers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/adjustments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/viewers/objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /venv/ 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | *.pyc 7 | *.so 8 | -------------------------------------------------------------------------------- /res/icons/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/res/icons/app_icon.ico -------------------------------------------------------------------------------- /res/icons/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/res/icons/app_icon.png -------------------------------------------------------------------------------- /github_readme_files/crop_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/crop_node.png -------------------------------------------------------------------------------- /github_readme_files/flip_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/flip_node.png -------------------------------------------------------------------------------- /github_readme_files/input_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/input_node.png -------------------------------------------------------------------------------- /github_readme_files/mask_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/mask_node.png -------------------------------------------------------------------------------- /github_readme_files/video_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/video_node.png -------------------------------------------------------------------------------- /res/fonts/JetBrainsMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/res/fonts/JetBrainsMono-Medium.ttf -------------------------------------------------------------------------------- /github_readme_files/canvas_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/canvas_node.png -------------------------------------------------------------------------------- /github_readme_files/resize_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/resize_node.png -------------------------------------------------------------------------------- /github_readme_files/rotate_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/rotate_node.png -------------------------------------------------------------------------------- /github_readme_files/webcam_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/webcam_node.png -------------------------------------------------------------------------------- /res/fonts/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/res/fonts/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /github_readme_files/2d_shape_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/2d_shape_node.png -------------------------------------------------------------------------------- /github_readme_files/image_view_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/image_view_node.png -------------------------------------------------------------------------------- /github_readme_files/normalize_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/normalize_node.png -------------------------------------------------------------------------------- /github_readme_files/threshold_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/threshold_node.png -------------------------------------------------------------------------------- /github_readme_files/code_snippet_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/code_snippet_node.png -------------------------------------------------------------------------------- /github_readme_files/convolution_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/convolution_node.png -------------------------------------------------------------------------------- /github_readme_files/image_folder_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/image_folder_node.png -------------------------------------------------------------------------------- /github_readme_files/image_writer_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/image_writer_node.png -------------------------------------------------------------------------------- /github_readme_files/nodiumpy_app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/nodiumpy_app_icon.png -------------------------------------------------------------------------------- /github_readme_files/video_writer_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/video_writer_node.png -------------------------------------------------------------------------------- /github_readme_files/edge_detection_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/edge_detection_node.png -------------------------------------------------------------------------------- /github_readme_files/screen_recorder_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/screen_recorder_node.png -------------------------------------------------------------------------------- /github_readme_files/light_enhancement_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/light_enhancement_node.png -------------------------------------------------------------------------------- /github_readme_files/nodiumpy_demo_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/nodiumpy_demo_image_1.png -------------------------------------------------------------------------------- /github_readme_files/smoothing_sharpening_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farzadshayanfar/nodiumpy/HEAD/github_readme_files/smoothing_sharpening_node.png -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/__init__.py: -------------------------------------------------------------------------------- 1 | from .page import * 2 | from .page_gpu import * 3 | from .pst import * 4 | from .pst_gpu import * 5 | from .utils import * 6 | from .vevid import * 7 | from .vevid_gpu import * 8 | -------------------------------------------------------------------------------- /project_resources.py: -------------------------------------------------------------------------------- 1 | from importlib.abc import Traversable 2 | from importlib.resources import files 3 | 4 | import res.icons 5 | import res.fonts 6 | 7 | icons_paths: Traversable = files(package=res.icons) 8 | AppIconPath: str = str(icons_paths.joinpath("app_icon.ico")) 9 | 10 | fonts_paths: Traversable = files(package=res.fonts) 11 | JetBrainsMonoRegularPath: str = str(fonts_paths.joinpath("JetBrainsMono-Regular.ttf")) 12 | JetBrainsMonoMediumPath: str = str(fonts_paths.joinpath("JetBrainsMono-Medium.ttf")) 13 | -------------------------------------------------------------------------------- /nodes/node.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Union 2 | 3 | import dearpygui.dearpygui as dpg 4 | 5 | from node_editor.connection_objects import NodeAttribute 6 | from node_editor.editor import NodeEditor 7 | from settings import AppSettings 8 | 9 | 10 | class NodeBase: 11 | def __init__(self, 12 | tag: int, 13 | editor: NodeEditor): 14 | self._tag: int = tag 15 | self._editor: NodeEditor = editor 16 | self._settings: AppSettings = editor.settings 17 | self._inAttrs: list[NodeAttribute] = list() 18 | self._outAttrs: list[NodeAttribute] = list() 19 | self._updateFcn: Union[Callable, None] = None 20 | 21 | @property 22 | def tag(self): 23 | return self._tag 24 | 25 | @property 26 | def editor(self): 27 | return self._editor 28 | 29 | @property 30 | def settings(self): 31 | return self._settings 32 | 33 | @property 34 | def inAttrs(self): 35 | return self._inAttrs 36 | 37 | @property 38 | def outAttrs(self): 39 | return self._outAttrs 40 | 41 | @property 42 | def updateFcn(self): 43 | return self._updateFcn 44 | 45 | def update(self): 46 | pass 47 | 48 | def close(self): 49 | dpg.delete_item(item=self._tag) 50 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | import cv2 5 | import dearpygui.dearpygui as dpg 6 | 7 | from project_resources import AppIconPath 8 | from node_editor.editor import NodeEditor 9 | from settings import AppSettings 10 | 11 | 12 | def main(): 13 | settings = AppSettings() 14 | 15 | cv2.setUseOptimized(True) 16 | 17 | dpg.create_context() 18 | 19 | settings.addFonts() 20 | dpg.setup_dearpygui() 21 | 22 | dpg.create_viewport(title="NodiumPy", 23 | small_icon=AppIconPath, 24 | large_icon=AppIconPath, 25 | width=settings.windowWidth, 26 | height=settings.windowHeight, 27 | vsync=False) 28 | 29 | menu_dict = { 30 | "Inputs": "inputs", 31 | "Adjustments": "adjustments", 32 | "Filters": "filters", 33 | "Viewers": "viewers", 34 | "Outputs": "outputs" 35 | } 36 | 37 | editor = NodeEditor(settings=settings, 38 | menuDict=menu_dict, 39 | nodeDir="./nodes") 40 | 41 | dpg.set_primary_window(window=editor.windowTag, value=True) 42 | dpg.bind_item_theme(item=editor.windowTag, theme=settings.createTheme()) 43 | dpg.bind_theme(theme=settings.createDialogTheme()) 44 | dpg.show_viewport() 45 | 46 | def asyncMain(): 47 | editor.update() 48 | 49 | loop = asyncio.new_event_loop() 50 | loop.run_in_executor(None, asyncMain) 51 | 52 | while dpg.is_dearpygui_running(): 53 | dpg.render_dearpygui_frame() 54 | time.sleep(0.033) 55 | 56 | editor.terminate() 57 | dpg.destroy_context() 58 | 59 | 60 | if __name__ == '__main__': 61 | main() 62 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/light_enhancement.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from numpy.fft import fft2, fftshift, ifft2 4 | 5 | 6 | def vevid(img: np.ndarray, 7 | phaseStrength: float, 8 | spectralPhaseFcnVariance: float, 9 | regularizationTerm: float, 10 | phaseActivationGain: float, 11 | enhanceColor: bool, 12 | liteMode: bool): 13 | img = cv2.cvtColor(img, cv2.COLOR_RGBA2RGB) 14 | img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV) 15 | 16 | height = img.shape[0] 17 | width = img.shape[1] 18 | 19 | # set the frequency grid 20 | u = np.linspace(-0.5, 0.5, height) 21 | v = np.linspace(-0.5, 0.5, width) 22 | U, V = np.meshgrid(u, v, indexing="ij") 23 | 24 | # construct the kernel 25 | theta = np.arctan2(V, U) 26 | rho = np.hypot(U, V) 27 | 28 | kernel = np.exp(-rho ** 2 / spectralPhaseFcnVariance) 29 | kernel = (kernel / np.max(abs(kernel))) * phaseStrength 30 | 31 | if enhanceColor: 32 | channel_idx = 1 33 | else: 34 | channel_idx = 2 35 | 36 | vevid_input = img[:, :, channel_idx] 37 | 38 | if liteMode: 39 | vevid_phase = np.arctan2(-phaseActivationGain * (vevid_input + regularizationTerm), vevid_input) 40 | else: 41 | vevid_input_f = fft2(vevid_input + regularizationTerm) 42 | img_vevid = ifft2(vevid_input_f * fftshift(np.exp(-1j * kernel))) 43 | vevid_phase = np.arctan2(phaseActivationGain * np.imag(img_vevid), vevid_input) 44 | vevid_phase_norm = (vevid_phase - vevid_phase.min()) / (vevid_phase.max() - vevid_phase.min()) 45 | img[:, :, channel_idx] = vevid_phase_norm 46 | img = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX) 47 | 48 | img = cv2.cvtColor(img, cv2.COLOR_HSV2RGB) 49 | vevid_output = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA) 50 | 51 | return vevid_output 52 | -------------------------------------------------------------------------------- /nodes/inputs/node_image.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Image" 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth 21 | self._frameSizeTextTag: int = editorHandle.getUniqueTag() 22 | 23 | self._currentImage: Union[np.ndarray, None] = None 24 | 25 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 26 | parentNodeTag=self._tag, 27 | attrType=AttributeType.Image) 28 | self.outAttrs.append(self._attrImageOutput) 29 | 30 | with dpg.node(tag=self._tag, 31 | parent=editorHandle.tag, 32 | label=self.nodeLabel, 33 | pos=pos): 34 | fileDialogTag = editorHandle.getUniqueTag() 35 | editorHandle.createImageFileSelectionDialog(tag=fileDialogTag, callback=self.__callbackOpenFile) 36 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 37 | attribute_type=dpg.mvNode_Attr_Static): 38 | dpg.add_button(label='select image', 39 | width=self._width, 40 | callback=lambda: dpg.show_item(item=fileDialogTag)) 41 | 42 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 43 | attribute_type=dpg.mvNode_Attr_Output, 44 | shape=dpg.mvNode_PinShape_Triangle): 45 | dpg.add_text(tag=self._frameSizeTextTag, 46 | indent=self._width - 100) 47 | 48 | def update(self): 49 | return None 50 | 51 | def __callbackOpenFile(self, sender: str, data: dict): 52 | # data is a dictionary with some keys being "file_path_name", \ 53 | # "file_name", "current_path", "current_filter" 54 | img = cv2.imread(filename=data['file_path_name'], flags=cv2.IMREAD_UNCHANGED) 55 | if img.ndim == 3 and img.shape[2] == 4: 56 | img = cv2.cvtColor(src=img, code=cv2.COLOR_BGRA2RGBA) 57 | else: 58 | img = cv2.cvtColor(src=img, code=cv2.COLOR_BGR2RGBA) 59 | img = img.astype(np.float32) / 255 60 | self._attrImageOutput.data = img 61 | dpg.set_value(item=self._frameSizeTextTag, value=img.shape[:2]) 62 | -------------------------------------------------------------------------------- /nodes/inputs/objects/video_objects.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union, Iterator 3 | 4 | import cv2 5 | import numpy as np 6 | 7 | 8 | class VideoFile: 9 | def __init__(self, inputFile: Union[Path, str]): 10 | filePath = Path(inputFile) 11 | assert filePath.exists() and filePath.is_file() 12 | self._VC = cv2.VideoCapture(str(filePath.resolve())) 13 | self._fps: int = self._VC.get(cv2.CAP_PROP_FPS) 14 | self._frameCount: int = self._VC.get(cv2.CAP_PROP_FRAME_COUNT) 15 | self._currentFrame: int = 0 16 | 17 | @property 18 | def videoCapture(self): 19 | return self._VC 20 | 21 | @property 22 | def fps(self): 23 | return self._fps 24 | 25 | @property 26 | def frameCount(self): 27 | return self._frameCount 28 | 29 | @property 30 | def currentFrame(self): 31 | return self.videoCapture.get(cv2.CAP_PROP_POS_FRAMES) 32 | 33 | @currentFrame.setter 34 | def currentFrame(self, value): 35 | self._currentFrame = value 36 | self.videoCapture.set(cv2.CAP_PROP_POS_FRAMES, value) 37 | 38 | @property 39 | def isOpened(self): 40 | return self.videoCapture.isOpened() 41 | 42 | def readCurrentFrame(self) -> np.ndarray: 43 | success, frame = self.videoCapture.read() 44 | if success: 45 | # frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) 46 | return frame 47 | 48 | def readNextFrame(self) -> np.ndarray: 49 | self.currentFrame += 1 50 | return self.readCurrentFrame() 51 | 52 | def retrieveFrame(self, frameIndex: int) -> np.ndarray: 53 | self.currentFrame = frameIndex 54 | return self.readCurrentFrame() 55 | 56 | def getFramesEveryNSeconds(self, n: float) -> Iterator[np.ndarray]: 57 | numberOfFramesToReturn = int(self.frameCount / self.fps / n) 58 | frameIndices = np.linspace(start=0, 59 | stop=self.frameCount, 60 | num=numberOfFramesToReturn, 61 | endpoint=False, 62 | dtype=np.int) 63 | 64 | return (self.retrieveFrame(idx) for idx in frameIndices) 65 | 66 | def getFrameEveryNFrame(self, n: int) -> Iterator[np.ndarray]: 67 | numberOfFramesToReturn = int(self.frameCount / n) 68 | frameIndices = np.linspace(start=0, 69 | stop=self.frameCount, 70 | num=numberOfFramesToReturn, 71 | endpoint=False, 72 | dtype=np.int) 73 | return (self.retrieveFrame(idx) for idx in frameIndices) 74 | 75 | def getInterval(self, startFrame: int, endFrame: int) -> Iterator[np.ndarray]: 76 | """ 77 | 78 | :param startFrame: startFrame is included 79 | :param endFrame: startFrame is included 80 | :return: 81 | """ 82 | assert startFrame < endFrame 83 | if endFrame + 1 < self.frameCount: 84 | results = (self.retrieveFrame(frameIndex=idx) for idx in range(startFrame, endFrame + 1)) 85 | else: 86 | results = (self.retrieveFrame(frameIndex=idx) for idx in range(startFrame, self.frameCount - 1)) 87 | return results 88 | 89 | def closeVideoFile(self): 90 | if self.isOpened: 91 | self.videoCapture.release() 92 | -------------------------------------------------------------------------------- /nodes/adjustments/node_mask.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel: str = "Mask" 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth 21 | self._currentImage: Union[np.ndarray, None] = None 22 | self._currentMask: Union[np.ndarray, None] = None 23 | 24 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 25 | parentNodeTag=self._tag, 26 | attrType=AttributeType.Image) 27 | self._attrMaskInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 28 | parentNodeTag=self._tag, 29 | attrType=AttributeType.Image) 30 | self.inAttrs.extend([self._attrImageInput, self._attrMaskInput]) 31 | 32 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 33 | parentNodeTag=self._tag, 34 | attrType=AttributeType.Image) 35 | self.outAttrs.append(self._attrImageOutput) 36 | 37 | with dpg.node(tag=self._tag, 38 | parent=editorHandle.tag, 39 | label=self.nodeLabel, 40 | pos=pos): 41 | with dpg.node_attribute(tag=self._attrImageInput.tag, 42 | attribute_type=dpg.mvNode_Attr_Input, 43 | shape=dpg.mvNode_PinShape_QuadFilled): 44 | dpg.add_text(default_value="input image") 45 | 46 | with dpg.node_attribute(tag=self._attrMaskInput.tag, 47 | attribute_type=dpg.mvNode_Attr_Input, 48 | shape=dpg.mvNode_PinShape_QuadFilled): 49 | dpg.add_text(default_value="mask (binary image)") 50 | 51 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 52 | attribute_type=dpg.mvNode_Attr_Output, 53 | shape=dpg.mvNode_PinShape_Triangle): 54 | dpg.add_spacer(width=self._width) 55 | 56 | def update(self): 57 | inputImage = self._attrImageInput.data 58 | maskImage = self._attrMaskInput.data 59 | if inputImage is None or maskImage is None: 60 | return 61 | if np.array_equal(inputImage, self._currentImage) and np.array_equal(maskImage, self._currentMask): 62 | return 63 | self._currentImage = inputImage 64 | self._currentMask = maskImage 65 | self.__applyMask() 66 | 67 | def __applyMask(self): 68 | if self._currentImage is None: 69 | return 70 | img = self._currentImage.copy() 71 | mask = self._currentMask.copy() 72 | height, width = img.shape[:2] 73 | mask = cv2.resize(src=mask, dsize=(width, height)) 74 | img[mask == 0] = 0 75 | self._attrImageOutput.data = img 76 | -------------------------------------------------------------------------------- /nodes/filters/node_code_snippet.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Union 3 | 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Code Snippet" 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth * 1.3 21 | self._currentImage: Union[np.ndarray, None] = None 22 | self._snippetTextInputTag: int = editorHandle.getUniqueTag() 23 | self._defaultText: str = "outImg = inImg" 24 | 25 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 26 | parentNodeTag=self._tag, 27 | attrType=AttributeType.Image) 28 | self.inAttrs.append(self._attrImageInput) 29 | 30 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 31 | parentNodeTag=self._tag, 32 | attrType=AttributeType.Image) 33 | self.outAttrs.append(self._attrImageOutput) 34 | 35 | with dpg.node(tag=self._tag, 36 | parent=editorHandle.tag, 37 | label=self.nodeLabel, 38 | pos=pos): 39 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 40 | attribute_type=dpg.mvNode_Attr_Input, 41 | shape=dpg.mvNode_PinShape_QuadFilled) 42 | 43 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 44 | attribute_type=dpg.mvNode_Attr_Static): 45 | dpg.add_input_text(tag=self._snippetTextInputTag, 46 | default_value=self._defaultText, 47 | width=self._width, 48 | height=230, 49 | multiline=True) 50 | with dpg.group(horizontal=True, indent=70): 51 | dpg.add_button(label="reset", 52 | width=80, 53 | callback=self.__callbackReset) 54 | dpg.add_button(label="apply", 55 | width=80, 56 | callback=self.__applySnippet) 57 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 58 | attribute_type=dpg.mvNode_Attr_Output, 59 | shape=dpg.mvNode_PinShape_Triangle) 60 | 61 | def update(self): 62 | data = self._attrImageInput.data 63 | if data is None: 64 | return 65 | if np.array_equal(data, self._currentImage): 66 | return 67 | self._currentImage = data 68 | self.__applySnippet() 69 | 70 | def __applySnippet(self): 71 | if self._currentImage is None: 72 | return 73 | img = self._currentImage.copy() 74 | execLocals = {"outImg": None, "inImg": img} 75 | snippet = dpg.get_value(item=self._snippetTextInputTag) 76 | try: 77 | exec(snippet, globals(), execLocals) 78 | except: 79 | traceback.print_exc() 80 | finally: 81 | self._attrImageOutput.data = execLocals["outImg"] 82 | 83 | def __callbackReset(self): 84 | dpg.set_value(item=self._snippetTextInputTag, 85 | value=self._defaultText) 86 | self.__applySnippet() 87 | -------------------------------------------------------------------------------- /nodes/adjustments/node_flip.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Flip" 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth 21 | self._currentImage: Union[np.ndarray, None] = None 22 | self._verticalFlipCheckBoxTag: int = editorHandle.getUniqueTag() 23 | self._verticalFlip: bool = False 24 | self._horizontalFlipCheckBoxTag: int = editorHandle.getUniqueTag() 25 | self._horizontalFlip: bool = True 26 | 27 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 28 | parentNodeTag=self._tag, 29 | attrType=AttributeType.Image) 30 | self.inAttrs.append(self._attrImageInput) 31 | 32 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 33 | parentNodeTag=self._tag, 34 | attrType=AttributeType.Image) 35 | self.outAttrs.append(self._attrImageOutput) 36 | 37 | with dpg.node(tag=self._tag, 38 | parent=editorHandle.tag, 39 | label=self.nodeLabel, 40 | pos=pos): 41 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 42 | attribute_type=dpg.mvNode_Attr_Input, 43 | shape=dpg.mvNode_PinShape_QuadFilled) 44 | 45 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 46 | attribute_type=dpg.mvNode_Attr_Static): 47 | dpg.add_checkbox(tag=self._verticalFlipCheckBoxTag, 48 | label="vertical", 49 | default_value=self._verticalFlip, 50 | callback=self.__callbackVerticalFlipChange) 51 | dpg.add_checkbox(tag=self._horizontalFlipCheckBoxTag, 52 | label="horizontal", 53 | default_value=self._horizontalFlip, 54 | callback=self.__callbackHorizontalFlipChange) 55 | dpg.add_spacer(width=self._width) 56 | 57 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 58 | attribute_type=dpg.mvNode_Attr_Output, 59 | shape=dpg.mvNode_PinShape_Triangle) 60 | 61 | def update(self): 62 | data = self._attrImageInput.data 63 | if data is None: 64 | self._currentImage = None 65 | return 66 | if np.array_equal(data, self._currentImage): 67 | return 68 | self._currentImage = data 69 | 70 | self.__flip() 71 | 72 | def __flip(self): 73 | if self._currentImage is None: 74 | return 75 | img = self._currentImage.copy() 76 | if self._verticalFlip: 77 | img = cv2.flip(src=img, flipCode=0) 78 | if self._horizontalFlip: 79 | img = cv2.flip(src=img, flipCode=1) 80 | self._attrImageOutput.data = img 81 | 82 | def __callbackVerticalFlipChange(self, sender, data): 83 | self._verticalFlip = data 84 | self.__flip() 85 | 86 | def __callbackHorizontalFlipChange(self, sender, data): 87 | self._horizontalFlip = data 88 | self.__flip() 89 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/pst.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from numpy.fft import fft2, fftshift, ifft2 4 | 5 | from .utils import cart2pol, denoise, morph, normalize 6 | 7 | 8 | class PST: 9 | def __init__(self, h=None, w=None): 10 | """initialize the PST CPU version class 11 | 12 | Args: 13 | h (int, optional): height of the image to be processed. Defaults to None. 14 | w (int, optional): width of the image to be processed. Defaults to None. 15 | """ 16 | self.h = h 17 | self.w = w 18 | 19 | def load_img(self, img_file=None, img_array=None): 20 | """load the image from an ndarray or from an image file 21 | 22 | Args: 23 | img_file (str, optional): path to the image. Defaults to None. 24 | img_array (np.ndarray, optional): image in the form of np.ndarray. Defaults to None. 25 | """ 26 | if img_array is not None: 27 | self.img = img_array 28 | self.img = img_array.shape[0] 29 | self.img = img_array.shape[1] 30 | else: 31 | self.img = cv2.imread(img_file) 32 | if not self.h and not self.w: 33 | self.h = self.img.shape[0] 34 | self.w = self.img.shape[1] 35 | else: 36 | self.img = cv2.imresize(self.img, [self.h, self.w]) 37 | # convert to grayscale if it is RGB 38 | if self.img.ndim == 3: 39 | self.img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY) 40 | 41 | def init_kernel(self, S, W): 42 | """initialize the phase kernel of PST 43 | 44 | Args: 45 | S (float): phase strength of PST 46 | W (float): warp strength of PST 47 | """ 48 | # set the frequency grid 49 | u = np.linspace(-0.5, 0.5, self.h) 50 | v = np.linspace(-0.5, 0.5, self.w) 51 | [U, V] = np.meshgrid(u, v, indexing="ij") 52 | [self.THETA, self.RHO] = cart2pol(U, V) 53 | # construct the PST Kernel 54 | self.pst_kernel = W * self.RHO * np.arctan(W * self.RHO) - 0.5 * np.log( 55 | 1 + (W * self.RHO) ** 2 56 | ) 57 | self.pst_kernel = S * self.pst_kernel / np.max(self.pst_kernel) 58 | 59 | def apply_kernel(self, sigma_LPF, thresh_min, thresh_max, morph_flag): 60 | """apply the phase kernel onto the image 61 | 62 | Args: 63 | sigma_LPF (float): std of the low pass filter 64 | thresh_min (float): minimum thershold, we keep features < thresh_min 65 | thresh_max (float): maximum thershold, we keep features > thresh_max 66 | morph_flag (boolean): whether apply morphological operation 67 | """ 68 | self.img_denoised = denoise(img=self.img, rho=self.RHO, sigma_LPF=sigma_LPF) 69 | self.img_pst = ifft2( 70 | fft2(self.img_denoised) * fftshift(np.exp(-1j * self.pst_kernel)) 71 | ) 72 | self.pst_feature = normalize(np.angle(self.img_pst)) 73 | if morph_flag == 0: 74 | self.pst_output = self.pst_feature 75 | else: 76 | self.pst_output = morph( 77 | img=self.img, 78 | feature=self.pst_feature, 79 | thresh_max=thresh_max, 80 | thresh_min=thresh_min, 81 | ) 82 | 83 | def run( 84 | self, 85 | img_file, 86 | S, 87 | W, 88 | sigma_LPF, 89 | thresh_min, 90 | thresh_max, 91 | morph_flag, 92 | ): 93 | """wrap all steps of PST into a single run method 94 | 95 | Args: 96 | img_file (str): path to the image. 97 | S (float): phase strength of PST 98 | W (float): warp strength of PST 99 | sigma_LPF (float): std of the low pass filter 100 | thresh_min (float): minimum thershold, we keep features < thresh_min 101 | thresh_max (float): maximum thershold, we keep features > thresh_max 102 | morph_flag (boolean): whether apply morphological operation 103 | 104 | Returns: 105 | np.ndarray: PST output 106 | """ 107 | self.load_img(img_file=img_file) 108 | self.init_kernel(S, W) 109 | self.apply_kernel(sigma_LPF, thresh_min, thresh_max, morph_flag) 110 | 111 | return self.pst_output 112 | -------------------------------------------------------------------------------- /nodes/inputs/node_webcam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import threading 4 | from typing import Union 5 | 6 | import cv2 7 | import dearpygui.dearpygui as dpg 8 | import numpy as np 9 | 10 | from node_editor.connection_objects import NodeAttribute, AttributeType 11 | from node_editor.editor import NodeEditor 12 | from nodes.node import NodeBase 13 | 14 | 15 | class Node(NodeBase): 16 | nodeLabel = "Webcam" 17 | 18 | def __init__(self, 19 | tag: int, 20 | pos: tuple[int, int], 21 | editorHandle: NodeEditor): 22 | super().__init__(tag=tag, editor=editorHandle) 23 | self._width: int = self._settings.nodeWidth 24 | self._deviceIndexList: list[int] = list() 25 | self._currentVideoCapture: Union[cv2.VideoCapture, None] = None 26 | self._currentDevice: int = 0 27 | self._loadingAttrTag: int = editorHandle.getUniqueTag() 28 | self._loadingTextTag: int = editorHandle.getUniqueTag() 29 | self._loadingIndicatorTag: int = editorHandle.getUniqueTag() 30 | self._deviceComboTag: int = editorHandle.getUniqueTag() 31 | 32 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 33 | parentNodeTag=self._tag, 34 | attrType=AttributeType.Image) 35 | self.outAttrs.append(self._attrImageOutput) 36 | 37 | with dpg.node(tag=self._tag, 38 | parent=editorHandle.tag, 39 | label=self.nodeLabel, 40 | pos=pos): 41 | with dpg.node_attribute(tag=self._loadingAttrTag, 42 | attribute_type=dpg.mvNode_Attr_Static): 43 | with dpg.group(): 44 | dpg.add_text(tag=self._loadingTextTag, 45 | default_value="Checking available cameras:\n(this can take a while)") 46 | dpg.add_loading_indicator(tag=self._loadingIndicatorTag, indent=80) 47 | dpg.add_spacer(width=self._width) 48 | 49 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 50 | attribute_type=dpg.mvNode_Attr_Output, 51 | shape=dpg.mvNode_PinShape_Triangle, 52 | show=False): 53 | dpg.add_spacer(width=self._width) 54 | threading.Thread(target=self.__checkAndAddCameraDevices).start() 55 | 56 | def update(self): 57 | if self._currentVideoCapture is None: 58 | return 59 | ret, frame = self._currentVideoCapture.read() 60 | if ret: 61 | frame = cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2RGBA) 62 | frame = frame.astype(np.float32) / 255 63 | self._attrImageOutput.data = frame 64 | 65 | def close(self): 66 | if self._currentVideoCapture is not None: 67 | self._currentVideoCapture.release() 68 | dpg.delete_item(item=self._tag) 69 | 70 | def __checkAndAddCameraDevices(self): 71 | for i in range(5): 72 | cap = cv2.VideoCapture(i) 73 | if cap is None or not cap.isOpened(): 74 | count = len(self._deviceIndexList) 75 | dpg.set_value(item=self._loadingTextTag, value=f"{count} cameras were found") 76 | dpg.hide_item(item=self._loadingIndicatorTag) 77 | break 78 | self._deviceIndexList.append(i) 79 | cap.release() 80 | if self._deviceIndexList: 81 | dpg.hide_item(item=self._loadingTextTag) 82 | dpg.hide_item(item=self._loadingIndicatorTag) 83 | items = [f"device {x}" for x in self._deviceIndexList] 84 | dpg.add_combo(parent=self._loadingAttrTag, 85 | tag=self._deviceComboTag, 86 | width=self._width, 87 | items=items, 88 | default_value=items[0], 89 | callback=self.__callbackDeviceChange) 90 | dpg.show_item(item=self._attrImageOutput.tag) 91 | self.__useWebcam(deviceIndex=0) 92 | 93 | def __callbackDeviceChange(self, sender, data): 94 | index = int(data.split(" ")[-1]) 95 | self.__useWebcam(deviceIndex=index) 96 | 97 | def __useWebcam(self, deviceIndex: int): 98 | if self._currentVideoCapture is not None: 99 | self._currentVideoCapture.release() 100 | self._currentVideoCapture = cv2.VideoCapture(deviceIndex, cv2.CAP_DSHOW) 101 | self._currentVideoCapture.set(cv2.CAP_PROP_FRAME_WIDTH, self._settings.webcamWidth) 102 | self._currentVideoCapture.set(cv2.CAP_PROP_FRAME_HEIGHT, self._settings.windowHeight) 103 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/vevid.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from numpy.fft import fft2, fftshift, ifft2 4 | 5 | from .utils import cart2pol 6 | 7 | 8 | class VEVID: 9 | def __init__(self, h=None, w=None): 10 | """initialize the VEVID CPU version class 11 | 12 | Args: 13 | h (int, optional): height of the image to be processed. Defaults to None. 14 | w (int, optional): width of the image to be processed. Defaults to None. 15 | """ 16 | self.h = h 17 | self.w = w 18 | 19 | def load_img(self, img_file=None, img_array=None): 20 | """load the image from an ndarray or from an image file 21 | 22 | Args: 23 | img_file (str, optional): path to the image. Defaults to None. 24 | img_array (np.ndarray, optional): image in the form of np.ndarray. Defaults to None. 25 | """ 26 | if img_array is not None: 27 | # directly load the image from numpy array 28 | self.img_bgr = img_array 29 | self.h = img_array.shape[0] 30 | self.w = img_array.shape[1] 31 | else: 32 | # load the image from the image file 33 | self.img_bgr = cv2.imread(img_file) 34 | if not self.h and not self.w: 35 | self.h = self.img_bgr.shape[0] 36 | self.w = self.img_bgr.shape[1] 37 | else: 38 | self.img_bgr = cv2.resize(self.img_bgr, [self.w, self.h]) 39 | 40 | self.img_hsv = cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2HSV) / 255.0 41 | 42 | def init_kernel(self, S, T): 43 | """initialize the phase kernel of VEViD 44 | 45 | Args: 46 | S (float): phase strength 47 | T (float): variance of the spectral phase function 48 | """ 49 | # create the frequency grid 50 | u = np.linspace(-0.5, 0.5, self.h) 51 | v = np.linspace(-0.5, 0.5, self.w) 52 | [U, V] = np.meshgrid(u, v, indexing="ij") 53 | # construct the kernel 54 | [self.THETA, self.RHO] = cart2pol(U, V) 55 | self.vevid_kernel = np.exp(-self.RHO ** 2 / T) 56 | self.vevid_kernel = (self.vevid_kernel / np.max(abs(self.vevid_kernel))) * S 57 | 58 | def apply_kernel(self, b, G, color=False, lite=False): 59 | """apply the phase kernel onto the image 60 | 61 | Args: 62 | b (float): regularization term 63 | G (float): phase activation gain 64 | color (bool, optional): whether to run color enhancement. Defaults to False. 65 | lite (bool, optional): whether to run VEViD lite. Defaults to False. 66 | """ 67 | if color: 68 | channel_idx = 1 69 | else: 70 | channel_idx = 2 71 | vevid_input = self.img_hsv[:, :, channel_idx] 72 | if lite: 73 | vevid_phase = np.arctan2(-G * (vevid_input + b), vevid_input) 74 | else: 75 | vevid_input_f = fft2(vevid_input + b) 76 | img_vevid = ifft2(vevid_input_f * fftshift(np.exp(-1j * self.vevid_kernel))) 77 | vevid_phase = np.arctan2(G * np.imag(img_vevid), vevid_input) 78 | vevid_phase_norm = (vevid_phase - vevid_phase.min()) / ( 79 | vevid_phase.max() - vevid_phase.min() 80 | ) 81 | self.img_hsv[:, :, channel_idx] = vevid_phase_norm 82 | self.img_hsv = cv2.normalize( 83 | self.img_hsv, None, 0, 255, cv2.NORM_MINMAX 84 | ).astype(np.uint8) 85 | self.vevid_output = cv2.cvtColor(self.img_hsv, cv2.COLOR_HSV2RGB) 86 | 87 | def run(self, img_file, S, T, b, G, color=False): 88 | """run the full VEViD algorithm 89 | 90 | Args: 91 | img_file (str): path to the image 92 | S (float): phase strength 93 | T (float): variance of the spectral phase function 94 | b (float): regularization term 95 | G (float): phase activation gain 96 | color (bool, optional): whether to run color enhancement. Defaults to False. 97 | 98 | Returns: 99 | np.ndarray: enhanced image 100 | """ 101 | self.load_img(img_file=img_file) 102 | self.init_kernel(S, T) 103 | self.apply_kernel(b, G, color, lite=False) 104 | 105 | return self.vevid_output 106 | 107 | def run_lite(self, img_file, b, G, color=False): 108 | """run the VEViD lite algorithm 109 | 110 | Args: 111 | img_file (str): path to the image 112 | b (float): regularization term 113 | G (float): phase activation gain 114 | color (bool, optional): whether to run color enhancement. Defaults to False. 115 | 116 | Returns: 117 | np.ndarray: enhanced image 118 | """ 119 | self.load_img(img_file=img_file) 120 | self.apply_kernel(b, G, color, lite=True) 121 | 122 | return self.vevid_output 123 | -------------------------------------------------------------------------------- /nodes/inputs/node_screen_recorder.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | from PIL import ImageGrab 7 | 8 | from node_editor.connection_objects import NodeAttribute, AttributeType 9 | from node_editor.editor import NodeEditor 10 | from nodes.node import NodeBase 11 | 12 | 13 | class Node(NodeBase): 14 | nodeLabel = "Screen Recorder" 15 | _modes = ["only main screen", "all screens"] 16 | 17 | def __init__(self, 18 | tag: int, 19 | pos: tuple[int, int], 20 | editorHandle: NodeEditor): 21 | super().__init__(tag=tag, editor=editorHandle) 22 | self._width: int = self._settings.nodeWidth 23 | self._captureMode: str = self._modes[0] 24 | self._keepCapturing: bool = True 25 | self._captureInterval: float = 0.033 26 | self._keepTicking: bool = False 27 | self._t1: float = 0 28 | self._t2: float = 0 29 | 30 | self._captureIntervalGroupTag: int = editorHandle.getUniqueTag() 31 | self._frameSizeTextTag: int = editorHandle.getUniqueTag() 32 | self._currentFrameSize: tuple[int, int] = (0, 0) 33 | 34 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 35 | parentNodeTag=self._tag, 36 | attrType=AttributeType.Image) 37 | 38 | self.outAttrs.append(self._attrImageOutput) 39 | 40 | with dpg.node(tag=self._tag, 41 | parent=editorHandle.tag, 42 | label=self.nodeLabel, 43 | pos=pos): 44 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 45 | attribute_type=dpg.mvNode_Attr_Static): 46 | dpg.add_combo(items=self._modes, 47 | default_value=self._captureMode, 48 | width=self._width, 49 | callback=self.__callbackCaptureModeChange) 50 | dpg.add_button(label="capture", 51 | width=self._width, 52 | callback=self.__callbackCapture) 53 | dpg.add_checkbox(label="keep capturing", 54 | default_value=self._keepCapturing, 55 | callback=self.__callbackKeepCapturingChange) 56 | with dpg.group(tag=self._captureIntervalGroupTag, horizontal=True, show=self._keepCapturing): 57 | dpg.add_text(default_value="interval") 58 | dpg.add_input_float(width=140, 59 | min_value=0, 60 | min_clamped=True, 61 | step=0.01, 62 | default_value=self._captureInterval) 63 | 64 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 65 | attribute_type=dpg.mvNode_Attr_Output, 66 | shape=dpg.mvNode_PinShape_Triangle): 67 | dpg.add_text(tag=self._frameSizeTextTag, 68 | wrap=self._width, 69 | indent=self._width - 100) 70 | self.__capture() 71 | 72 | def update(self): 73 | if not self._keepCapturing: 74 | return 75 | if not self._keepTicking: 76 | self._keepTicking = True 77 | self._t1 = time.perf_counter() 78 | self._t2 = time.perf_counter() 79 | if self._t2 - self._t1 > self._captureInterval: 80 | self.__capture() 81 | self._keepTicking = False 82 | 83 | def __capture(self): 84 | if self._captureMode == "all screens": 85 | img = ImageGrab.grab(all_screens=True, include_layered_windows=True) 86 | else: 87 | img = ImageGrab.grab(all_screens=False, include_layered_windows=True) 88 | 89 | img = np.asarray(a=img) 90 | img = cv2.cvtColor(src=img, code=cv2.COLOR_RGB2RGBA) 91 | img = (img / 255).astype(np.float32) 92 | self._attrImageOutput.data = img 93 | if self._currentFrameSize != img.shape[:2]: 94 | self._currentFrameSize = img.shape[:2] 95 | dpg.set_value(item=self._frameSizeTextTag, value=img.shape[:2]) 96 | 97 | def __callbackCapture(self): 98 | if self._keepCapturing: 99 | return 100 | self.__capture() 101 | 102 | def __callbackCaptureModeChange(self, _, data): 103 | self._captureMode = data 104 | self.__capture() 105 | 106 | def __callbackKeepCapturingChange(self, _, data): 107 | self._keepCapturing = data 108 | if data: 109 | dpg.show_item(item=self._captureIntervalGroupTag) 110 | else: 111 | dpg.hide_item(item=self._captureIntervalGroupTag) 112 | self.__capture() 113 | -------------------------------------------------------------------------------- /nodes/adjustments/node_resize.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Resize" 14 | 15 | _modes = dict(nearest=cv2.INTER_NEAREST, 16 | linear=cv2.INTER_LINEAR, 17 | area=cv2.INTER_AREA, 18 | cubic=cv2.INTER_CUBIC, 19 | LANCZOS4=cv2.INTER_LANCZOS4) 20 | 21 | def __init__(self, 22 | tag: int, 23 | pos: tuple[int, int], 24 | editorHandle: NodeEditor): 25 | super().__init__(tag=tag, editor=editorHandle) 26 | self._width: int = self._settings.nodeWidth 27 | self._currentImage: Union[np.ndarray, None] = None 28 | self._currentMode = list(self._modes.keys())[0] 29 | 30 | self._desiredWidthInputTag: int = editorHandle.getUniqueTag() 31 | self._desiredHeightInputTag: int = editorHandle.getUniqueTag() 32 | self._desiredWidth: int = 1280 33 | self._desiredHeight: int = 720 34 | 35 | self._inputImageSizeLabelTag: int = editorHandle.getUniqueTag() 36 | 37 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 38 | parentNodeTag=self._tag, 39 | attrType=AttributeType.Image) 40 | self.inAttrs.append(self._attrImageInput) 41 | 42 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 43 | parentNodeTag=self._tag, 44 | attrType=AttributeType.Image) 45 | self.outAttrs.append(self._attrImageOutput) 46 | 47 | with dpg.node(tag=self._tag, 48 | parent=editorHandle.tag, 49 | label=self.nodeLabel, 50 | pos=pos): 51 | with dpg.node_attribute(tag=self._attrImageInput.tag, 52 | attribute_type=dpg.mvNode_Attr_Input, 53 | shape=dpg.mvNode_PinShape_QuadFilled): 54 | dpg.add_text(tag=self._inputImageSizeLabelTag) 55 | 56 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 57 | attribute_type=dpg.mvNode_Attr_Static): 58 | dpg.add_combo(items=list(self._modes.keys()), 59 | default_value=self._currentMode, 60 | width=self._width, 61 | callback=self.__callbackComboChange) 62 | with dpg.group(indent=20): 63 | with dpg.group(horizontal=True): 64 | dpg.add_text(default_value="width", indent=8) 65 | dpg.add_input_int(tag=self._desiredWidthInputTag, 66 | width=self._width - 100, 67 | default_value=self._desiredWidth, 68 | callback=self.__callbackDesiredWidthChange) 69 | 70 | with dpg.group(horizontal=True): 71 | dpg.add_text(default_value="height") 72 | dpg.add_input_int(tag=self._desiredHeightInputTag, 73 | width=self._width - 100, 74 | default_value=self._desiredHeight, 75 | callback=self.__callbackDesiredHeightChange) 76 | 77 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 78 | attribute_type=dpg.mvNode_Attr_Output, 79 | shape=dpg.mvNode_PinShape_Triangle) 80 | 81 | def update(self): 82 | data = self._attrImageInput.data 83 | if data is None: 84 | return 85 | if np.array_equal(data, self._currentImage): 86 | return 87 | dpg.set_value(item=self._inputImageSizeLabelTag, value=data.shape[:2]) 88 | self._currentImage = data 89 | 90 | self.__resize() 91 | 92 | def __resize(self): 93 | if self._currentImage is None: 94 | return 95 | img = self._currentImage.copy() 96 | img = cv2.resize(src=img, 97 | dsize=(self._desiredWidth, self._desiredHeight), 98 | interpolation=self._modes[self._currentMode]) 99 | 100 | self._attrImageOutput.data = img 101 | 102 | def __callbackComboChange(self, _, data): 103 | self._currentMode = data 104 | self.__resize() 105 | 106 | def __callbackDesiredWidthChange(self, _, data): 107 | self._desiredWidth = data 108 | self.__resize() 109 | 110 | def __callbackDesiredHeightChange(self, _, data): 111 | self._desiredHeight = data 112 | self.__resize() 113 | -------------------------------------------------------------------------------- /nodes/adjustments/node_normalize.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import dearpygui.dearpygui as dpg 4 | import numpy as np 5 | 6 | from node_editor.connection_objects import NodeAttribute, AttributeType 7 | from node_editor.editor import NodeEditor 8 | from nodes.node import NodeBase 9 | 10 | 11 | class Node(NodeBase): 12 | nodeLabel = "Normalize" 13 | _defaultMean: list[float, float, float] = [0.485, 0.456, 0.406] 14 | _defaultStd: list[float, float, float] = [0.229, 0.224, 0.225] 15 | 16 | def __init__(self, 17 | tag: int, 18 | pos: tuple[int, int], 19 | editorHandle: NodeEditor): 20 | super().__init__(tag=tag, editor=editorHandle) 21 | self._width: int = self._settings.nodeWidth 22 | self._currentImage: Union[np.ndarray, None] = None 23 | self._meanInputTag: int = editorHandle.getUniqueTag() 24 | self._stdInputTag: int = editorHandle.getUniqueTag() 25 | self._mean: list[float, float, float] = self._defaultMean 26 | self._std: list[float, float, float] = self._defaultStd 27 | 28 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 29 | parentNodeTag=self._tag, 30 | attrType=AttributeType.Image) 31 | self.inAttrs.append(self._attrImageInput) 32 | 33 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 34 | parentNodeTag=self._tag, 35 | attrType=AttributeType.Image) 36 | self.outAttrs.append(self._attrImageOutput) 37 | 38 | with dpg.node(tag=self._tag, 39 | parent=editorHandle.tag, 40 | label=self.nodeLabel, 41 | pos=pos): 42 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 43 | attribute_type=dpg.mvNode_Attr_Input, 44 | shape=dpg.mvNode_PinShape_QuadFilled) 45 | 46 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 47 | attribute_type=dpg.mvNode_Attr_Static): 48 | with dpg.group(horizontal=True): 49 | dpg.add_text(default_value="mean") 50 | dpg.add_drag_floatx(tag=self._meanInputTag, 51 | size=3, 52 | default_value=self._mean, 53 | format="%.3f", 54 | width=self._width - 41, 55 | min_value=0, 56 | clamped=True, 57 | speed=0.005, 58 | callback=self.__callbackMeanChange) 59 | with dpg.group(horizontal=True): 60 | dpg.add_text(default_value="std", indent=7) 61 | dpg.add_drag_floatx(tag=self._stdInputTag, 62 | size=3, 63 | default_value=self._std, 64 | format="%.3f", 65 | width=self._width - 41, 66 | min_value=0, 67 | clamped=True, 68 | speed=0.005, 69 | callback=self.__callbackStdChange) 70 | dpg.add_button(label="R", 71 | width=32, 72 | height=32, 73 | indent=self._width // 2 - 16, 74 | callback=self.__callbackReset) 75 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 76 | attribute_type=dpg.mvNode_Attr_Output, 77 | shape=dpg.mvNode_PinShape_Triangle) 78 | 79 | def update(self): 80 | data = self._attrImageInput.data 81 | if data is None: 82 | return 83 | if np.array_equal(data, self._currentImage): 84 | return 85 | self._currentImage = data 86 | 87 | self.__normalize() 88 | 89 | def __normalize(self): 90 | if self._currentImage is None: 91 | return 92 | img = self._currentImage.copy() 93 | mean = np.array(self._mean) 94 | std = np.array(self._std) 95 | img[:, :, :3] = (img[:, :, :3] - mean) / std 96 | self._attrImageOutput.data = img 97 | 98 | def __callbackMeanChange(self, sender, data): 99 | self._mean = data[:3] 100 | self.__normalize() 101 | 102 | def __callbackStdChange(self, sender, data): 103 | self._std = data[:3] 104 | self.__normalize() 105 | 106 | def __callbackReset(self): 107 | self._mean = self._defaultMean 108 | self._std = self._defaultStd 109 | dpg.set_value(item=self._meanInputTag, value=self._mean) 110 | dpg.set_value(item=self._stdInputTag, value=self._std) 111 | self.__normalize() 112 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torch.fft 4 | 5 | 6 | def normalize(x): 7 | """normalize the input to 0-1 8 | 9 | Args: 10 | x (np.ndarray or torch.Tensor): input array or tensor 11 | 12 | Returns: 13 | np.ndarray or torch.Tensor 14 | """ 15 | return (x - x.min()) / (x.max() - x.min()) 16 | 17 | 18 | def cart2pol(x, y): 19 | """convert cartesian coordiates to polar coordinates 20 | 21 | Args: 22 | x (np.ndarray): cartesian coordinates in x direction 23 | y (np.ndarray): cartesian coordinates in y direction 24 | 25 | Returns: 26 | tuple: polar coordinates theta and rho 27 | """ 28 | theta = np.arctan2(y, x) 29 | rho = np.hypot(x, y) 30 | return (theta, rho) 31 | 32 | 33 | def cart2pol_torch(x, y): 34 | """convert cartesian coordiates to polar coordinates with PyTorch 35 | 36 | Args: 37 | x (torch.Tensor): cartesian coordinates in x direction 38 | y (torch.Tensor): cartesian coordinates in x direction 39 | 40 | Returns: 41 | tuple: polar coordinates theta and rho 42 | """ 43 | theta = torch.atan2(y, x) 44 | rho = torch.hypot(x, y) 45 | return (theta, rho) 46 | 47 | 48 | def denoise(img, rho, sigma_LPF): 49 | """apply a low pass filter to denoise the image 50 | 51 | Args: 52 | img (np.ndarray): original image 53 | rho (np.ndarray): polar coordinates 54 | sigma_LPF (float): variance of the low pass filter 55 | 56 | Returns: 57 | np.ndarray: denoised image 58 | """ 59 | img_orig_f = np.fft.fft2(img) 60 | expo = np.fft.fftshift( 61 | np.exp( 62 | -0.5 * np.power((np.divide(rho, np.sqrt((sigma_LPF ** 2) / np.log(2)))), 2) 63 | ) 64 | ) 65 | img_filtered = np.real(np.fft.ifft2((np.multiply(img_orig_f, expo)))) 66 | 67 | return img_filtered 68 | 69 | 70 | def denoise_torch(img, rho, sigma_LPF): 71 | """apply a low pass filter to denoise the image with PyTorch 72 | 73 | Args: 74 | img (torch.Tensor): original image 75 | rho (torch.Tensor): polar coordinates 76 | sigma_LPF (float): std of the low pass filter 77 | 78 | Returns: 79 | torch.Tensor: denoised image 80 | """ 81 | img_orig_f = torch.fft.fft2(img) 82 | expo = torch.fft.fftshift( 83 | torch.exp( 84 | -0.5 85 | * torch.pow((torch.divide(rho, np.sqrt((sigma_LPF ** 2) / np.log(2)))), 2) 86 | ) 87 | ) 88 | img_filtered = torch.real(torch.fft.ifft2((torch.mul(img_orig_f, expo)))) 89 | 90 | return img_filtered 91 | 92 | 93 | def morph(img, feature, thresh_min, thresh_max): 94 | """apply morphological operation to transform analog features to digial features 95 | 96 | Args: 97 | img (np.ndarray): original image 98 | feature (np.ndarray): analog feature 99 | thresh_min (0<= float <=1): minimum thershold, we keep features < quantile(feature, thresh_min) 100 | thresh_max (0<= float <=1): maximum thershold, we keep features < quantile(feature, thresh_min) 101 | 102 | Returns: 103 | np.ndarray: digital features (binary edge) 104 | """ 105 | # downsample feature to reduce computational time of np.quantile() for large arrays 106 | if len(feature.shape) == 3: 107 | quantile_max = np.quantile(feature[::4, ::4, ::4], thresh_max) 108 | quantile_min = np.quantile(feature[::4, ::4, ::4], thresh_min) 109 | elif len(feature.shape) == 2: 110 | quantile_max = np.quantile(feature[::4, ::4], thresh_max) 111 | quantile_min = np.quantile(feature[::4, ::4], thresh_min) 112 | 113 | digital_feature = np.zeros(feature.shape) 114 | digital_feature[feature > quantile_max] = 1 115 | digital_feature[feature < quantile_min] = 1 116 | digital_feature[img < (np.amax(img) / 20)] = 0 117 | 118 | return digital_feature.astype(np.float32) 119 | 120 | 121 | def morph_torch(img, feature, thresh_min, thresh_max, device): 122 | """apply morphological operation to transform analog features to digial features in PyTorch 123 | 124 | Args: 125 | img (torch.Tensor): original image 126 | feature (torch.Tensor): analog feature 127 | thresh_min (0<= float <=1): minimum thershold, we keep features < quantile(feature, thresh_min) 128 | thresh_max (0<= float <=1): maximum thershold, we keep features < quantile(feature, thresh_min) 129 | device (torch.device) 130 | 131 | Returns: 132 | torch.Tensor: digital features (binary edge) 133 | """ 134 | # downsample feature to reduce computational time of torch.quantile() for large tensors 135 | if len(feature.shape) == 3: 136 | quantile_max = torch.quantile(feature[::4, ::4, ::4], thresh_max) 137 | quantile_min = torch.quantile(feature[::4, ::4, ::4], thresh_min) 138 | elif len(feature.shape) == 2: 139 | quantile_max = torch.quantile(feature[::4, ::4], thresh_max) 140 | quantile_min = torch.quantile(feature[::4, ::4], thresh_min) 141 | 142 | digital_feature = torch.zeros(feature.shape).to(device) 143 | digital_feature[feature > quantile_max] = 1 144 | digital_feature[feature < quantile_min] = 1 145 | digital_feature[img < (torch.max(img) / 20)] = 0 146 | 147 | return torch.squeeze(digital_feature) 148 | -------------------------------------------------------------------------------- /nodes/adjustments/node_rotate.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Rotate" 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth 21 | self._currentImage: Union[np.ndarray, None] = None 22 | 23 | self._desiredRotationInputTag: int = editorHandle.getUniqueTag() 24 | self._desiredAngle: float = 0 25 | self._reshape: bool = True 26 | 27 | self._inputImageSizeLabelTag: int = editorHandle.getUniqueTag() 28 | self._outputImageSizeLabelTag: int = editorHandle.getUniqueTag() 29 | 30 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 31 | parentNodeTag=self._tag, 32 | attrType=AttributeType.Image) 33 | self.inAttrs.append(self._attrImageInput) 34 | 35 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 36 | parentNodeTag=self._tag, 37 | attrType=AttributeType.Image) 38 | self.outAttrs.append(self._attrImageOutput) 39 | 40 | with dpg.node(tag=self._tag, 41 | parent=editorHandle.tag, 42 | label=self.nodeLabel, 43 | pos=pos): 44 | with dpg.node_attribute(tag=self._attrImageInput.tag, 45 | attribute_type=dpg.mvNode_Attr_Input, 46 | shape=dpg.mvNode_PinShape_QuadFilled): 47 | dpg.add_text(tag=self._inputImageSizeLabelTag) 48 | 49 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 50 | attribute_type=dpg.mvNode_Attr_Static): 51 | with dpg.group(horizontal=True, indent=10): 52 | dpg.add_text(default_value="angle") 53 | dpg.add_input_float(tag=self._desiredRotationInputTag, 54 | width=self._width - 80, 55 | default_value=self._desiredAngle, 56 | max_value=360, 57 | min_value=-360, 58 | min_clamped=True, 59 | max_clamped=True, 60 | callback=self.__callbackDesiredRotationChange) 61 | 62 | dpg.add_checkbox(label="reshape", 63 | default_value=self._reshape, 64 | indent=10, 65 | callback=self.__callbackReshapeChange) 66 | dpg.add_spacer(width=self._width) 67 | 68 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 69 | attribute_type=dpg.mvNode_Attr_Output, 70 | shape=dpg.mvNode_PinShape_Triangle): 71 | dpg.add_text(tag=self._outputImageSizeLabelTag, indent=self._width - 85) 72 | 73 | def update(self): 74 | data = self._attrImageInput.data 75 | if data is None: 76 | return 77 | if np.array_equal(data, self._currentImage): 78 | return 79 | dpg.set_value(item=self._inputImageSizeLabelTag, value=data.shape[:2]) 80 | self._currentImage = data 81 | self.__rotate() 82 | 83 | def __rotate(self): 84 | if self._currentImage is None: 85 | return 86 | img = self._currentImage.copy() 87 | shape = img.shape 88 | pivot = (shape[1] // 2, shape[0] // 2) 89 | if self._reshape: 90 | img = self.__rotateAndReshape(mat=img, angle=self._desiredAngle) 91 | else: 92 | rotMat = cv2.getRotationMatrix2D(center=pivot, angle=self._desiredAngle, scale=1) 93 | img = cv2.warpAffine(src=img, M=rotMat, dsize=(shape[1], shape[0])) 94 | 95 | dpg.set_value(item=self._outputImageSizeLabelTag, value=img.shape[:2]) 96 | self._attrImageOutput.data = img 97 | 98 | def __callbackDesiredRotationChange(self, _, data): 99 | self._desiredAngle = data 100 | self.__rotate() 101 | 102 | def __callbackReshapeChange(self, _, data): 103 | self._reshape = data 104 | self.__rotate() 105 | 106 | def __rotateAndReshape(self, mat, angle): 107 | """ 108 | Rotates an image (angle in degrees) and expands image to avoid cropping 109 | """ 110 | 111 | height, width = mat.shape[:2] 112 | image_center = (width / 2, 113 | height / 2) 114 | 115 | rotation_mat = cv2.getRotationMatrix2D(center=image_center, angle=angle, scale=1) 116 | 117 | # rotation calculates the cos and sin, taking absolutes of those. 118 | abs_cos = abs(rotation_mat[0, 0]) 119 | abs_sin = abs(rotation_mat[0, 1]) 120 | 121 | # find the new width and height bounds 122 | bound_w = int(height * abs_sin + width * abs_cos) 123 | bound_h = int(height * abs_cos + width * abs_sin) 124 | 125 | # subtract old image center (bringing image back to origo) and adding the new image center coordinates 126 | rotation_mat[0, 2] += bound_w / 2 - image_center[0] 127 | rotation_mat[1, 2] += bound_h / 2 - image_center[1] 128 | 129 | # rotate image with the new bounds and translated rotation matrix 130 | rotated_mat = cv2.warpAffine(src=mat, M=rotation_mat, dsize=(bound_w, bound_h), borderValue=0) 131 | return rotated_mat 132 | -------------------------------------------------------------------------------- /nodes/outputs/node_image_output.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import cv2 5 | import dearpygui.dearpygui as dpg 6 | import numpy as np 7 | 8 | from node_editor.connection_objects import NodeAttribute, AttributeType 9 | from node_editor.editor import NodeEditor 10 | from nodes.node import NodeBase 11 | 12 | 13 | class Node(NodeBase): 14 | nodeLabel = "Image Writer" 15 | 16 | _formats = [".jpg", ".png"] 17 | _settings = None 18 | 19 | def __init__(self, 20 | tag: int, 21 | pos: tuple[int, int], 22 | editorHandle: NodeEditor): 23 | super().__init__(tag=tag, editor=editorHandle) 24 | self._width: int = self.settings.nodeWidth 25 | self._folderDialogTag: int = editorHandle.getUniqueTag() 26 | self._outDirPath: Union[Path, None] = None 27 | self._outDirTextInputTag: int = editorHandle.getUniqueTag() 28 | self._outDirBrowseBtnTag: int = editorHandle.getUniqueTag() 29 | self._fileBaseName: str = str() 30 | self._nameChangerInt: int = 1 31 | self._nameChangeIntTextTag: int = editorHandle.getUniqueTag() 32 | self._baseNameTextInputTag: int = editorHandle.getUniqueTag() 33 | self._fileFormat: str = self._formats[0] 34 | self._currentImage: np.ndarray = np.zeros(shape=(2, 2)) 35 | self._isWriting: bool = True 36 | self._overwrite: bool = True 37 | 38 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 39 | parentNodeTag=self._tag, 40 | attrType=AttributeType.Image) 41 | 42 | with dpg.node(tag=self._tag, 43 | parent=editorHandle.tag, 44 | label=self.nodeLabel, 45 | pos=pos): 46 | with dpg.node_attribute(tag=self._attrImageInput.tag, 47 | attribute_type=dpg.mvNode_Attr_Input, 48 | user_data=self._attrImageInput, 49 | shape=dpg.mvNode_PinShape_QuadFilled, 50 | indent=5): 51 | editorHandle.createFolderSelectionDialog(tag=self._folderDialogTag, callback=self.__callbackSetOutDir) 52 | dpg.add_button(label="select out dir", 53 | width=self._width - 10, 54 | callback=lambda: dpg.show_item(item=self._folderDialogTag)) 55 | with dpg.group(horizontal=True): 56 | dpg.add_text(default_value="out dir", indent=15) 57 | dpg.add_input_text(tag=self._outDirTextInputTag, 58 | width=self._width - 90, 59 | readonly=True) 60 | 61 | with dpg.group(horizontal=True): 62 | dpg.add_text(default_value="base name") 63 | dpg.add_input_text(tag=self._baseNameTextInputTag, 64 | width=self._width - 90, 65 | callback=self.__callbackBaseNameChange) 66 | with dpg.group(horizontal=True): 67 | dpg.add_text(default_value="format", indent=23) 68 | dpg.add_combo(items=self._formats, 69 | default_value=self._fileFormat, 70 | width=self._width - 90, 71 | callback=self.__callbackFileFormatChange) 72 | 73 | dpg.add_checkbox(label="write", 74 | default_value=self._isWriting, 75 | callback=self.__callbackWriteStateChange) 76 | 77 | dpg.add_checkbox(label="overwrite existing", 78 | default_value=self._overwrite, 79 | callback=self.__callbackOverWriteStateChange) 80 | 81 | with dpg.group(horizontal=True, horizontal_spacing=5, indent=50): 82 | dpg.add_text(tag=self._nameChangeIntTextTag, default_value="unq int: 1") 83 | dpg.add_button(label="R", width=30, height=30, callback=self.__callbackResetNameChanger) 84 | 85 | def update(self): 86 | if self._outDirPath is None or not self._isWriting: 87 | return 88 | data = self._attrImageInput.data 89 | if data is not None: 90 | if np.array_equal(data, self._currentImage): 91 | return 92 | filename = self._outDirPath.joinpath(self._fileBaseName + "_" 93 | + str(self._nameChangerInt) 94 | + self._fileFormat) 95 | if filename.exists() and not self._overwrite: 96 | return 97 | self._currentImage = data.copy() 98 | self._nameChangerInt += 1 99 | cv2.imwrite(filename=str(filename.resolve()), img=cv2.cvtColor(self._currentImage, cv2.COLOR_BGR2RGB)) 100 | dpg.set_value(item=self._nameChangeIntTextTag, value=f"unq int: {self._nameChangerInt}") 101 | 102 | def __callbackSetOutDir(self, sender, data): 103 | self._outDirPath = Path(data["file_path_name"]) 104 | dpg.set_value(item=self._outDirTextInputTag, value=str(self._outDirPath.resolve())) 105 | self.__callbackResetNameChanger() 106 | 107 | def __callbackWriteStateChange(self, sender, data): 108 | self._isWriting = data 109 | 110 | def __callbackBaseNameChange(self, sender, data): 111 | self._fileBaseName = data 112 | 113 | def __callbackOverWriteStateChange(self, sender, data): 114 | self._overwrite = data 115 | 116 | def __callbackFileFormatChange(self, sender, data): 117 | self._fileFormat = data 118 | 119 | def __callbackResetNameChanger(self): 120 | self._nameChangerInt = 1 121 | dpg.set_value(item=self._nameChangeIntTextTag, value="unq int: 1") 122 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/vevid_gpu.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import torch 4 | from kornia.color import hsv_to_rgb, rgb_to_hsv 5 | from torch.fft import fft2, fftshift, ifft2 6 | from torchvision.io import read_image 7 | from torchvision.transforms.functional import resize 8 | 9 | from .utils import cart2pol_torch 10 | 11 | 12 | class VEVID_GPU: 13 | def __init__(self, device, h=None, w=None): 14 | """initialize the VEVID GPU version class 15 | 16 | Args: 17 | device (torch.device) 18 | h (int, optional): height of the image to be processed. Defaults to None. 19 | w (int, optional): width of the image to be processed. Defaults to None. 20 | """ 21 | self.h = h 22 | self.w = w 23 | self.device = device 24 | 25 | def load_img(self, img_file=None, img_array=None): 26 | """load the image from an ndarray or from an image file 27 | 28 | Args: 29 | img_file (str, optional): path to the image. Defaults to None. 30 | img_array (torch.Tensor, optional): image in the form of torch.Tensor. Defaults to None. 31 | """ 32 | if img_array is not None: 33 | # directly load the image from the array instead of the file 34 | if img_array.get_device() == self.device: 35 | self.img_rgb = img_array 36 | else: 37 | self.img_rgb = img_array.to(self.device) 38 | if not self.h and not self.w: 39 | self.h = self.img_rgb.shape[-2] 40 | self.w = self.img_rgb.shape[-1] 41 | # convert from RGB to HSV 42 | self.img_hsv = rgb_to_hsv(self.img_rgb) 43 | 44 | else: 45 | # load the image from the image file 46 | # torchvision read_image currently only supports 'jpg' and 'png' 47 | # use opencv to read other image formats 48 | if img_file.split(".")[-1] in ["jpg", "png", "jpeg"]: 49 | self.img_rgb = read_image(img_file).to(self.device) 50 | else: 51 | self.img_bgr = cv2.imread(img_file) 52 | self.img_rgb = cv2.cvtColor(self.img_bgr, cv2.COLOR_BGR2RGB) 53 | self.img_rgb = torch.from_numpy( 54 | np.transpose(self.img_rgb, (2, 0, 1)) 55 | ).to(self.device) 56 | if not self.h and not self.w: 57 | self.h = self.img_rgb.shape[-2] 58 | self.w = self.img_rgb.shape[-1] 59 | else: 60 | self.img_rgb = resize(self.img_rgb, [self.h, self.w]) 61 | # convert from RGB to HSV 62 | # rgb_to_hsv in kornia requires the input RGB image to be in the range of 0-1 63 | self.img_hsv = rgb_to_hsv((self.img_rgb.float()) / 255.0) 64 | 65 | def init_kernel(self, S, T): 66 | """initialize the phase kernel of VEViD 67 | 68 | Args: 69 | S (float): phase strength 70 | T (float): variance of the spectral phase function 71 | """ 72 | # create the frequency grid 73 | u = torch.linspace(-0.5, 0.5, self.h, device=self.device).float() 74 | v = torch.linspace(-0.5, 0.5, self.w, device=self.device).float() 75 | [U, V] = torch.meshgrid(u, v, indexing="ij") 76 | # construct the kernel 77 | [self.THETA, self.RHO] = cart2pol_torch(U, V) 78 | self.vevid_kernel = torch.exp(-self.RHO ** 2 / T) 79 | self.vevid_kernel = (self.vevid_kernel / torch.max(abs(self.vevid_kernel))) * S 80 | 81 | def apply_kernel(self, b, G, color=False, lite=False): 82 | """apply the phase kernel onto the image 83 | 84 | Args: 85 | b (float): regularization term 86 | G (float): phase activation gain 87 | color (bool, optional): whether to run color enhancement. Defaults to False. 88 | lite (bool, optional): whether to run VEViD lite. Defaults to False. 89 | """ 90 | if color: 91 | channel_idx = 1 92 | else: 93 | channel_idx = 2 94 | vevid_input = self.img_hsv[channel_idx, :, :] 95 | if lite: 96 | vevid_phase = torch.atan2(-G * (vevid_input + b), vevid_input) 97 | else: 98 | vevid_input_f = fft2(vevid_input + b) 99 | img_vevid = ifft2( 100 | vevid_input_f * fftshift(torch.exp(-1j * self.vevid_kernel)) 101 | ) 102 | vevid_phase = torch.atan2(G * torch.imag(img_vevid), vevid_input) 103 | vevid_phase_norm = (vevid_phase - vevid_phase.min()) / ( 104 | vevid_phase.max() - vevid_phase.min() 105 | ) 106 | self.img_hsv[channel_idx, :, :] = vevid_phase_norm 107 | self.vevid_output = hsv_to_rgb(self.img_hsv) 108 | 109 | def run(self, img_file, S, T, b, G, color=False): 110 | """run the full VEViD algorithm 111 | 112 | Args: 113 | img_file (str): path to the image 114 | S (float): phase strength 115 | T (float): variance of the spectral phase function 116 | b (float): regularization term 117 | G (float): phase activation gain 118 | color (bool, optional): whether to run color enhancement. Defaults to False. 119 | 120 | Returns: 121 | torch.Tensor: enhanced image 122 | """ 123 | self.load_img(img_file=img_file) 124 | self.init_kernel(S, T) 125 | self.apply_kernel(b, G, color, lite=False) 126 | 127 | return self.vevid_output 128 | 129 | def run_lite(self, img_file, b, G, color=False): 130 | """run the VEViD lite algorithm 131 | 132 | Args: 133 | img_file (str): path to the image 134 | b (float): regularization term 135 | G (float): phase activation gain 136 | color (bool, optional): whether to run color enhancement. Defaults to False. 137 | 138 | Returns: 139 | torch.Tensor: enhanced image 140 | """ 141 | self.load_img(img_file=img_file) 142 | self.apply_kernel(b, G, color, lite=True) 143 | 144 | return self.vevid_output 145 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/edge_detection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.fft import fft2, fftshift, ifft2 3 | 4 | 5 | def pst(img: np.ndarray, 6 | phaseStrength: float, 7 | warpStrength: float, 8 | lpfSigma: float, 9 | minThreshold: float, 10 | maxThreshold: float, 11 | useMorph: bool) -> np.ndarray: 12 | height = img.shape[0] 13 | width = img.shape[1] 14 | u = np.linspace(-0.5, 0.5, height, dtype=np.float32) 15 | v = np.linspace(-0.5, 0.5, width, dtype=np.float32) 16 | U, V = np.meshgrid(u, v, indexing="ij") 17 | theta = np.arctan2(V, U) 18 | rho = np.hypot(U, V) 19 | kernel = warpStrength * rho * np.arctan(warpStrength * rho) - 0.5 * np.log(1 + (warpStrength * rho) ** 2) 20 | kernel = phaseStrength * kernel / np.max(kernel) 21 | 22 | # denoising 23 | img_orig = np.fft.fft2(img) 24 | expo = np.fft.fftshift(np.exp(-0.5 * np.power((np.divide(rho, np.sqrt((lpfSigma ** 2) / np.log(2)))), 2))) 25 | img_denoised = np.real(np.fft.ifft2((np.multiply(img_orig, expo)))).astype(np.float32) 26 | 27 | img_pst = ifft2(fft2(img_denoised) * fftshift(np.exp(-1j * kernel))) 28 | x = np.angle(img_pst).astype(np.float32) 29 | pst_feature = (x - x.min()) / (x.max() - x.min()) 30 | 31 | if useMorph: 32 | if len(pst_feature.shape) == 3: 33 | quantile_max = np.quantile(pst_feature[::4, ::4, ::4], maxThreshold) 34 | quantile_min = np.quantile(pst_feature[::4, ::4, ::4], minThreshold) 35 | elif len(pst_feature.shape) == 2: 36 | quantile_max = np.quantile(pst_feature[::4, ::4], maxThreshold) 37 | quantile_min = np.quantile(pst_feature[::4, ::4], minThreshold) 38 | 39 | digital_feature = np.zeros(pst_feature.shape) 40 | digital_feature[pst_feature > quantile_max] = 1 41 | digital_feature[pst_feature < quantile_min] = 1 42 | digital_feature[img < (np.amax(img) / 20)] = 0 43 | digital_feature = digital_feature.real.astype(np.float32) 44 | return digital_feature 45 | 46 | pst_feature = pst_feature.real 47 | return pst_feature 48 | 49 | 50 | def page(img: np.ndarray, 51 | directionBins: int, 52 | mu1: float, 53 | mu2: float, 54 | sigma1: float, 55 | sigma2: float, 56 | phaseStrength1: float, 57 | phaseStrength2: float, 58 | lpfSigma: float, 59 | minThreshold: float, 60 | maxThreshold: float, 61 | useMorph: bool) -> np.ndarray: 62 | height = img.shape[0] 63 | width = img.shape[1] 64 | 65 | # set the frequency grid 66 | u = np.linspace(-0.5, 0.5, height) 67 | v = np.linspace(-0.5, 0.5, width) 68 | U, V = np.meshgrid(u, v, indexing="ij") 69 | 70 | theta = np.arctan2(V, U) 71 | rho = np.hypot(U, V) 72 | 73 | minDirection = np.pi / 180 74 | directionSpan = np.pi / directionBins 75 | directions = np.arange(start=minDirection, stop=np.pi, step=directionSpan) 76 | 77 | # create PAGE kernels channel by channel 78 | kernel = np.zeros(shape=(height, width, directionBins)) 79 | 80 | for i in range(directionBins): 81 | tetav = directions[i] 82 | 83 | # Project onto new directionality basis for PAGE filter creation 84 | Uprime = U * np.cos(tetav) + V * np.sin(tetav) 85 | Vprime = -U * np.sin(tetav) + V * np.cos(tetav) 86 | 87 | # Create Normal component of PAGE filter 88 | Phi_1 = np.exp(-0.5 * ((abs(Uprime) - mu1) / sigma1) ** 2) / (1 * np.sqrt(2 * np.pi) * sigma1) 89 | Phi_1 = (Phi_1 / np.max(Phi_1[:])) * phaseStrength1 90 | 91 | # Create Log-Normal component of PAGE filter 92 | Phi_2 = np.exp(-0.5 * ((np.log(abs(Vprime)) - mu2) / sigma2) ** 2) / (abs(Vprime) * np.sqrt(2 * np.pi) * sigma2) 93 | Phi_2 = (Phi_2 / np.max(Phi_2[:])) * phaseStrength2 94 | 95 | # Add overall directional filter to PAGE filter array 96 | kernel[:, :, i] = Phi_1 * Phi_2 97 | 98 | # denoise on the loaded image 99 | img_orig = np.fft.fft2(img) 100 | expo = np.fft.fftshift(np.exp(-0.5 * np.power((np.divide(rho, np.sqrt((lpfSigma ** 2) / np.log(2)))), 2))) 101 | img_denoised = np.real(np.fft.ifft2((np.multiply(img_orig, expo)))) 102 | page_output = np.zeros(shape=(height, width, directionBins)) 103 | 104 | # apply the kernel channel by channel 105 | for i in range(directionBins): 106 | img_page = ifft2(fft2(img_denoised) * fftshift(np.exp(-1j * kernel[:, :, i]))) 107 | x = np.angle(img_page) 108 | page_feature = (x - x.min()) / (x.max() - x.min()) 109 | 110 | # apply morphological operation if applicable 111 | if useMorph: 112 | if len(page_feature.shape) == 3: 113 | quantile_max = np.quantile(page_feature[::4, ::4, ::4], maxThreshold) 114 | quantile_min = np.quantile(page_feature[::4, ::4, ::4], minThreshold) 115 | elif len(page_feature.shape) == 2: 116 | quantile_max = np.quantile(page_feature[::4, ::4], maxThreshold) 117 | quantile_min = np.quantile(page_feature[::4, ::4], minThreshold) 118 | 119 | digital_feature = np.zeros(page_feature.shape) 120 | digital_feature[page_feature > quantile_max] = 1 121 | digital_feature[page_feature < quantile_min] = 1 122 | digital_feature[img < (np.amax(img) / 20)] = 0 123 | page_output[:, :, i] = digital_feature 124 | 125 | else: 126 | page_output[:, :, i] = page_feature 127 | 128 | # Create a weighted color image of PAGE output to visualize directionality of edges 129 | weight_step = 255 * 3 / directionBins 130 | color_weight = np.arange(0, 255, weight_step) 131 | page_edge = np.zeros(shape=(height, width, 3)) 132 | # step_edge = int(round(self.direction_bins/3)) 133 | step_edge = directionBins // 3 134 | for i in range(step_edge): 135 | page_edge[:, :, 0] = (color_weight[i] * page_output[:, :, i] + page_edge[:, :, 0]) 136 | page_edge[:, :, 1] = (color_weight[i] * page_output[:, :, i + step_edge] + page_edge[:, :, 1]) 137 | page_edge[:, :, 2] = (color_weight[i] * page_output[:, :, i + (2 * step_edge)] + page_edge[:, :, 2]) 138 | 139 | page_edge = (page_edge - np.min(page_edge)) / (np.max(page_edge) - np.min(page_edge)) 140 | return page_edge.astype(np.float32) 141 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/pst_gpu.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import torch 3 | import torchvision 4 | from torch.fft import fft2, fftshift, ifft2 5 | from torchvision.transforms.functional import resize, rgb_to_grayscale 6 | 7 | from .utils import cart2pol_torch, denoise_torch, morph_torch, normalize 8 | 9 | 10 | class PST_GPU: 11 | def __init__(self, device, h=None, w=None): 12 | """initialize the PST GPU version class 13 | 14 | Args: 15 | device (torch.device) 16 | h (int, optional): height of the image to be processed. Defaults to None. 17 | w (int, optional): width of the image to be processed. Defaults to None. 18 | """ 19 | self.h = h 20 | self.w = w 21 | self.device = device 22 | 23 | def load_img(self, img_file=None, img_array=None): 24 | """load the image from an ndarray or from an image file 25 | 26 | Args: 27 | img_file (str, optional): path to the image. Defaults to None. 28 | img_array (torch.Tensor, optional): image in the form of torch.Tensor. Defaults to None. 29 | """ 30 | if img_array is not None: 31 | # directly load the image from the array instead of the file 32 | if img_array.get_device() == self.device: 33 | self.img = img_array 34 | else: 35 | self.img = img_array.to(self.device) 36 | # convert to grayscale if it is RGB 37 | if self.img.dim() == 3 and self.img.shape[0] != 1: 38 | self.img = rgb_to_grayscale(self.img) 39 | # read the image size or resize to the indicated size (height x width) 40 | if not self.h and not self.w: 41 | self.img = torch.squeeze(self.img) 42 | self.h = self.img.shape[0] 43 | self.w = self.img.shape[1] 44 | else: 45 | self.img = torch.squeeze(resize(self.img, [self.h, self.w])) 46 | else: 47 | # load the image from the image file 48 | # torchvision read_image only supports 'jpg' and 'png' 49 | if img_file.split(".")[-1] in ["jpg", "png", "jpeg"]: 50 | self.img = torchvision.io.read_image(img_file).to(self.device) 51 | # convert to grayscale if it is RGB 52 | if self.img.dim() == 3 and self.img.shape[0] != 1: 53 | self.img = rgb_to_grayscale(self.img) 54 | # read the image size or resize to the indicated size (height x width) 55 | if not self.h and not self.w: 56 | self.img = torch.squeeze(self.img) 57 | self.h = self.img.shape[0] 58 | self.w = self.img.shape[1] 59 | else: 60 | self.img = torch.squeeze(resize(self.img, [self.h, self.w])) 61 | else: 62 | # use opencv to load other format of image 63 | self.img = cv2.imread(img_file) 64 | if self.img.ndim == 3: 65 | self.img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY) 66 | if not self.h and not self.w: 67 | self.h = self.img.shape[0] 68 | self.w = self.img.shape[1] 69 | else: 70 | self.img = cv2.imresize(self.img, [self.h, self.w]) 71 | 72 | self.img = torch.from_numpy(self.img).to(self.device) 73 | 74 | def init_kernel(self, S, W): 75 | """initialize the phase kernel of PST 76 | 77 | Args: 78 | S (float): phase strength of PST 79 | W (float): warp of PST 80 | """ 81 | # set the frequency grid 82 | u = torch.linspace(-0.5, 0.5, self.h, device=self.device).float() 83 | v = torch.linspace(-0.5, 0.5, self.w, device=self.device).float() 84 | [U, V] = torch.meshgrid(u, v, indexing="ij") 85 | [self.THETA, self.RHO] = cart2pol_torch(U, V) 86 | # construct the PST Kernel 87 | self.pst_kernel = W * self.RHO * torch.arctan(W * self.RHO) - 0.5 * torch.log( 88 | 1 + (W * self.RHO) ** 2 89 | ) 90 | self.pst_kernel = S * self.pst_kernel / torch.max(self.pst_kernel) 91 | 92 | def apply_kernel(self, sigma_LPF, thresh_min, thresh_max, morph_flag): 93 | 94 | """apply the phase kernel onto the image 95 | 96 | Args: 97 | sigma_LPF (float): std of the low pass filter 98 | thresh_min (float): minimum thershold, we keep features < thresh_min 99 | thresh_max (float): maximum thershold, we keep features > thresh_max 100 | morph_flag (boolean): whether apply morphological operation 101 | """ 102 | # denoise on the loaded image 103 | self.img_denoised = denoise_torch( 104 | img=self.img, rho=self.RHO, sigma_LPF=sigma_LPF 105 | ) 106 | # apply the pst kernel 107 | self.img_pst = ifft2( 108 | fft2(self.img_denoised) * fftshift(torch.exp(-1j * self.pst_kernel)) 109 | ) 110 | self.pst_feature = normalize(torch.angle(self.img_pst)) 111 | # apply morphological operation if applicable 112 | if morph_flag == 0: 113 | self.pst_output = self.pst_feature 114 | else: 115 | self.pst_output = morph_torch( 116 | img=self.img, 117 | feature=self.pst_feature, 118 | thresh_max=thresh_max, 119 | thresh_min=thresh_min, 120 | device=self.device, 121 | ) 122 | 123 | def run( 124 | self, 125 | img_file, 126 | S, 127 | W, 128 | sigma_LPF, 129 | thresh_min, 130 | thresh_max, 131 | morph_flag, 132 | ): 133 | """wrap all steps of PST into a single run method 134 | 135 | Args: 136 | img_file (str): _description_ 137 | S (float): _description_ 138 | W (float): _description_ 139 | sigma_LPF (float): _description_ 140 | thresh_min (float): _description_ 141 | thresh_max (float): _description_ 142 | morph_flag (boolean): _description_ 143 | 144 | Returns: 145 | torch.Tensor: PST output 146 | """ 147 | # wrap load_img, init_kernel, apply_kernel in one run 148 | self.load_img(img_file=img_file) 149 | self.init_kernel(S, W) 150 | self.apply_kernel(sigma_LPF, thresh_min, thresh_max, morph_flag) 151 | 152 | return self.pst_output 153 | -------------------------------------------------------------------------------- /nodes/inputs/node_image_folder.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import cv2 5 | import dearpygui.dearpygui as dpg 6 | import numpy as np 7 | 8 | from node_editor.connection_objects import NodeAttribute, AttributeType 9 | from node_editor.editor import NodeEditor 10 | from nodes.node import NodeBase 11 | 12 | 13 | class Node(NodeBase): 14 | nodeLabel = "Image Folder" 15 | _filePatterns = ["*.png", "*.PNG", "*.jpg", "*.jpeg", "*.JPEG"] 16 | 17 | def __init__(self, 18 | tag: int, 19 | pos: tuple[int, int], 20 | editorHandle: NodeEditor): 21 | super().__init__(tag=tag, editor=editorHandle) 22 | self._width: int = self._settings.nodeWidth 23 | self._searchSubDirs: bool = False 24 | self._currentImageIndex: int = 0 25 | self._currentPath: Union[Path, None] = None 26 | self._pathList: list[Path] = list() 27 | self._intDragTag = editorHandle.getUniqueTag() 28 | self._frameSizeTextTag: int = editorHandle.getUniqueTag() 29 | self._fileCount: int = 0 30 | self._iterate: bool = False 31 | self._loop: bool = False 32 | 33 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 34 | parentNodeTag=self._tag, 35 | attrType=AttributeType.Image) 36 | 37 | self.outAttrs.append(self._attrImageOutput) 38 | 39 | with dpg.node(tag=self._tag, 40 | parent=editorHandle.tag, 41 | label=self.nodeLabel, 42 | pos=pos): 43 | folderDialogTag = editorHandle.getUniqueTag() 44 | editorHandle.createFolderSelectionDialog(tag=folderDialogTag, callback=self.__callbackGetImages) 45 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 46 | attribute_type=dpg.mvNode_Attr_Static): 47 | with dpg.group(): 48 | dpg.add_button(label='select folder', 49 | width=self._width, 50 | callback=lambda: dpg.show_item(item=folderDialogTag)) 51 | dpg.add_checkbox(label="subdirs", callback=self.__callbackSearchSubDirs) 52 | 53 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 54 | attribute_type=dpg.mvNode_Attr_Static): 55 | dpg.add_drag_int(tag=self._intDragTag, 56 | width=self._width, 57 | min_value=0, 58 | clamped=True, 59 | format="0 / 0", 60 | callback=self.__callbackCurrentImageChange) 61 | with dpg.group(horizontal=True): 62 | dpg.add_checkbox(label="iterate", 63 | default_value=self._iterate, 64 | callback=self.__callbackIterate) 65 | dpg.add_checkbox(label="loop", 66 | default_value=self._loop, 67 | callback=self.__callbackLoop) 68 | 69 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 70 | attribute_type=dpg.mvNode_Attr_Output, 71 | shape=dpg.mvNode_PinShape_Triangle): 72 | dpg.add_text(tag=self._frameSizeTextTag, 73 | wrap=self._width, 74 | indent=self._width - 100) 75 | 76 | def update(self): 77 | if self._iterate: 78 | if self._loop and self._currentImageIndex + 1 == self._fileCount: 79 | self._currentImageIndex = -1 80 | if self._fileCount == 0 or self._currentImageIndex + 1 == self._fileCount: 81 | return 82 | self._currentImageIndex += 1 83 | dpg.set_value(item=self._intDragTag, value=self._currentImageIndex) 84 | self.__loadCurrentIndexImage() 85 | 86 | def __callbackGetImages(self, sender: str, data: dict): 87 | # data is a dictionary with some keys being "file_path_name", \ 88 | # "file_name", "current_path", "current_filter" 89 | self._currentPath = Path(data['file_path_name']) 90 | self._pathList.clear() 91 | if self._searchSubDirs: 92 | for pattern in self._filePatterns: 93 | self._pathList.extend(list(self._currentPath.rglob(pattern=pattern))) 94 | else: 95 | for pattern in self._filePatterns: 96 | self._pathList.extend(list(self._currentPath.glob(pattern=pattern))) 97 | 98 | if not self._pathList: 99 | dpg.configure_item(item=self._intDragTag, 100 | format="0 / 0") 101 | return 102 | 103 | self._currentImageIndex = 0 104 | dpg.configure_item(item=self._intDragTag, 105 | format=f"index %f / {len(self._pathList) - 1}", 106 | default_value=0, 107 | max_value=len(self._pathList) - 1) 108 | 109 | self._fileCount = len(self._pathList) 110 | self.__loadCurrentIndexImage() 111 | 112 | def __loadCurrentIndexImage(self): 113 | img = cv2.imread(filename=str(self._pathList[self._currentImageIndex].resolve()), flags=cv2.IMREAD_UNCHANGED) 114 | if img is None: 115 | print(f"can't properly open this file:\n{self._pathList[self._currentImageIndex].resolve()}") 116 | return 117 | if img.ndim == 3 and img.shape[2] == 4: 118 | img = cv2.cvtColor(src=img, code=cv2.COLOR_BGRA2RGBA) 119 | else: 120 | img = cv2.cvtColor(src=img, code=cv2.COLOR_BGR2RGBA) 121 | img = img.astype(np.float32) / 255 122 | self._attrImageOutput.data = img 123 | dpg.set_value(item=self._frameSizeTextTag, value=img.shape[:2]) 124 | 125 | def __callbackSearchSubDirs(self, _, data): 126 | self._searchSubDirs = data 127 | if self._currentPath is not None: 128 | self.__callbackGetImages(sender=str(), data={"file_path_name": str(self._currentPath.resolve())}) 129 | 130 | def __callbackIterate(self, _, data): 131 | self._iterate = data 132 | 133 | def __callbackLoop(self, _, data): 134 | self._loop = data 135 | 136 | def __callbackCurrentImageChange(self, _, data): 137 | self._currentImageIndex = data 138 | self.__loadCurrentIndexImage() 139 | -------------------------------------------------------------------------------- /nodes/inputs/node_video.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.inputs.objects.video_objects import VideoFile 10 | from nodes.node import NodeBase 11 | 12 | 13 | class Node(NodeBase): 14 | nodeLabel = "Video" 15 | 16 | def __init__(self, 17 | tag: int, 18 | pos: tuple[int, int], 19 | editorHandle: NodeEditor): 20 | super().__init__(tag=tag, editor=editorHandle) 21 | self._width: int = self._settings.nodeWidth 22 | self._editorHandle = editorHandle 23 | 24 | self._cvf: Union[VideoFile, None] = None 25 | 26 | self._frameSizeTextTag: int = editorHandle.getUniqueTag() 27 | self._isPlayingTag: int = editorHandle.getUniqueTag() 28 | self._seekSliderTag: int = editorHandle.getUniqueTag() 29 | self._controlAttrTag: int = editorHandle.getUniqueTag() 30 | 31 | self._loop: bool = True 32 | self._play: bool = False 33 | self._skipRange = (1, 15) 34 | self._skipValue = self._skipRange[0] 35 | self._seekRange: tuple[int, int] = (0, 999) 36 | 37 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 38 | parentNodeTag=self._tag, 39 | attrType=AttributeType.Image) 40 | self.outAttrs.append(self._attrImageOutput) 41 | 42 | with dpg.node(tag=self._tag, parent=editorHandle.tag, label=self.nodeLabel, pos=pos): 43 | self.fileDialogTag = editorHandle.getUniqueTag() 44 | editorHandle.createVideoFileSelectionDialog(tag=self.fileDialogTag, callback=self.__callbackOpenFile) 45 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 46 | attribute_type=dpg.mvNode_Attr_Static): 47 | dpg.add_button(label='select video', 48 | width=self._width, 49 | callback=self.__callbackSelectVideo) 50 | 51 | with dpg.node_attribute(tag=self._controlAttrTag, 52 | attribute_type=dpg.mvNode_Attr_Static): 53 | dpg.add_drag_int(tag=editorHandle.getUniqueTag(), 54 | width=self._width, 55 | format="x %f", 56 | default_value=self._skipValue, 57 | min_value=self._skipRange[0], 58 | max_value=self._skipRange[1], 59 | clamped=True, 60 | callback=self.__callbackSkipRate) 61 | dpg.add_drag_int(tag=self._seekSliderTag, 62 | width=self._width, 63 | format="pos %f", 64 | default_value=0, 65 | min_value=self._seekRange[0], 66 | max_value=self._seekRange[1], 67 | clamped=True, 68 | callback=self.__callbackSeekFrame) 69 | 70 | with dpg.group(tag=editorHandle.getUniqueTag(), horizontal=True): 71 | dpg.add_checkbox(label='loop', 72 | callback=self.__callbackLooping, 73 | default_value=self._loop) 74 | dpg.add_checkbox(label='play', 75 | tag=self._isPlayingTag, 76 | callback=self.__callbackPlaying, 77 | default_value=self._play) 78 | 79 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 80 | attribute_type=dpg.mvNode_Attr_Output, 81 | shape=dpg.mvNode_PinShape_Triangle): 82 | dpg.add_text(tag=self._frameSizeTextTag, 83 | wrap=self._width, 84 | indent=self._width - 100) 85 | 86 | def __callbackSelectVideo(self): 87 | self._editorHandle.pause() 88 | dpg.show_item(item=self.fileDialogTag) 89 | 90 | def update(self): 91 | if self._cvf is None: 92 | return 93 | if not self._play: 94 | return 95 | dpg.set_value(item=self._seekSliderTag, value=self._cvf.currentFrame) 96 | if not self._cvf.currentFrame + self._skipValue >= self._cvf.frameCount: 97 | self._cvf.currentFrame += self._skipValue 98 | else: 99 | if self._loop: 100 | self._cvf.currentFrame = 0 101 | else: 102 | self._cvf.currentFrame = self._cvf.frameCount - 1 103 | dpg.set_value(item=self._isPlayingTag, value=False) 104 | self._play = False 105 | dpg.enable_item(item=self._seekSliderTag) 106 | return 107 | frame = self._cvf.readCurrentFrame() 108 | frame = cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2RGBA) 109 | frame = frame.astype(np.float32) / 255 110 | self._attrImageOutput.data = frame 111 | 112 | def close(self): 113 | if self._cvf is not None: 114 | self._cvf.closeVideoFile() 115 | dpg.delete_item(item=self._tag) 116 | 117 | def __callbackOpenFile(self, _, data): 118 | # data is a dictionary with some keys being "file_path_name", \ 119 | # "file_name", "current_path", "current_filter" 120 | self._cvf = VideoFile(inputFile=data["file_path_name"]) 121 | self._seekRange = (0, self._cvf.frameCount - 1) 122 | dpg.configure_item(item=self._seekSliderTag, 123 | default_value=0, 124 | min_value=self._seekRange[0], 125 | max_value=self._seekRange[1]) 126 | frame = self._cvf.readCurrentFrame() 127 | frame = cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2RGBA) 128 | frame = frame.astype(np.float32) / 255 129 | self._attrImageOutput.data = frame 130 | 131 | dpg.set_value(item=self._frameSizeTextTag, value=frame.shape[:2]) 132 | self._editorHandle.resume() 133 | 134 | def __callbackLooping(self, _, data): 135 | self._loop = data 136 | 137 | def __callbackPlaying(self, _, data): 138 | self._play = data 139 | if data: 140 | dpg.disable_item(item=self._seekSliderTag) 141 | else: 142 | dpg.enable_item(item=self._seekSliderTag) 143 | 144 | def __callbackSkipRate(self, _, data): 145 | self._skipValue = data 146 | 147 | def __callbackSeekFrame(self, _, data): 148 | if self._cvf is None: 149 | return 150 | if self._play: 151 | return 152 | frame = self._cvf.retrieveFrame(frameIndex=data) 153 | frame = cv2.cvtColor(src=frame, code=cv2.COLOR_BGR2RGBA) 154 | frame = frame.astype(np.float32) / 255 155 | self._attrImageOutput.data = frame 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # NodiumPy 4 | 5 | This is a very simple and minimal python application for node based image processing. The structure is very lean and 6 | highly modular and allows for the existing nodes to be easily modified or for new nodes to be conveniently added. 7 | 8 | My goal in writing this was to learn about dearpygui and flow based programming. The use case for this simple 9 | application is merely educational and will benefit students or other enthusiasts wishing to develop a visceral and 10 | practical understanding of how some image algorithms affect the input image and what the combination of these algorithms 11 | will look like. 12 | 13 | ![demo_image_1](./github_readme_files/nodiumpy_demo_image_1.png) 14 | 15 |
16 |
17 | 18 | # Avalilable Nodes 19 | 20 |
21 | Input Nodes 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 70 | 71 | 72 |
Image 28 | This node opens a single image file (common image formats are supported). 29 |
Image Folder 36 | This node allows retrieving a sequence of images in a specified directory. Also makes it possible to iterate and loop over the sequence. 37 |
Screen Recorder 44 | Using this node, it is possible to capture the main screen or get all available screens (as a single large image). We can capture screen(s) as a single snapshot or a stream of snapshots. 45 |
2D Shape 52 | This node creates several primitive 2D shapes with control over size, fill color, stroke color, and stroke weight. 53 |
Video 60 | This node retrieve a video file and allows common video play functionality (playing, seeking, frame skip, and looping) 61 |
Webcam 68 | As the name denotes, this node is for capturing image streams from connected webcams. 69 |
73 |
74 | 75 |
76 | Adjustment Nodes 77 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 128 | 129 | 132 | 133 | 134 |
Crop 82 | Node for cropping an input image. It offers two modes for cropping: (1) center crop, and (2) two corner crop 83 |
Flip 90 | Flips the input image either horizontally, vertically or both. 91 |
Mask 98 | Applies a binary mask to an input image. 99 |
Normalize 106 | Normalizes the input image using provided mean and std. 107 |
Resize 114 | Node for resizing the input image. 115 |
Rotate 122 | Rotates the input image. There is an option for allowing reshaping of the rotated image, if off the rotated image is truncated, otherwise it will resize the rotated image such that no truncation happens. 123 |
Threshold 130 | Node for thresholding input images. 131 |
135 |
136 | 137 |
138 | Filter Nodes 139 | 140 | 141 | 142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 154 | 155 | 156 | 157 | 158 | 159 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | 178 | 179 |
Convolution 144 | This node applies convolution operation for a parameterized kernel and the input image. 145 |
Edge Detection 152 | This node allows for application of several edge detection algorithms on the input image. 153 |
Light Enhancement 160 | Currently this node only offers an implementation of the VEVID algorithm from PhyCV library. 161 |
Smoothing / Sharpening 168 | This node contains some of the common algorithms for smoothing and sharpening of the input image. 169 |
Code Snippet 176 | We can use this node to write our own custom python filter code snippet. The variable "inImg" holds the reference to the input image and the variable "outImg" is the reference to the output image. 177 |
180 |
181 | 182 |
183 | Viewer Nodes 184 | 185 | 186 | 187 | 188 | 191 | 192 | 193 | 194 | 195 | 196 | 199 | 200 |
Canvas 189 | This node allows for composition of several input images into an output image. It supports layering, transforming, and several blending modes. 190 |
Image View 197 | This node is used to visulize the output image or output image sequence. It has a context menu with entries for resizing and saving the displayed image. 198 |
201 |
202 | 203 |
204 | Output Nodes 205 | 206 | 207 | 208 | 209 | 212 | 213 | 214 | 215 | 216 | 217 | 220 | 221 |
Image Writer 210 | Node for writing image files to disk. 211 |
Video Writer 218 | Node for writing video files to disk. 219 |
222 |
223 |
224 | 225 | # Requirements 226 | 227 | Python 3.11.3 and above is supported. All the required libraries can be installed using this line: 228 | 229 | ``` 230 | pip install dearpygui opencv-python Pillow 231 | ``` 232 | 233 |
234 | 235 | # Running 236 | 237 | After setting up a python environment and installing the requirements, you can launch nodiumpy by running *main.py* 238 | 239 | ``` 240 | python main.py 241 | ``` 242 | 243 |
244 | 245 | # License 246 | 247 | NodiumPy is licensed under Apache 2.0 License. 248 |
249 |
250 | 251 | # Credits 252 | 253 | + Developed by Farzad Shayanfar 254 | + *[Jonathan Hoffstadt](https://github.com/hoffstadt)* and *[Preston Cothren](https://github.com/Pcothren)* and others 255 | for their work on *[DearPyGui](https://github.com/hoffstadt/DearPyGui)* 256 | + This work is highly inspired by 257 | *[Image-Processing-Node-Editor](https://github.com/Kazuhito00/Image-Processing-Node-Editor)* by 258 | *[Kazuhito Takahashi](https://github.com/Kazuhito00)* -------------------------------------------------------------------------------- /nodes/adjustments/node_threshold.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel: str = "Threshold" 14 | _modes2FcnMap: dict = {"binary": cv2.THRESH_BINARY, 15 | "inverted binary": cv2.THRESH_BINARY_INV, 16 | "trunc": cv2.THRESH_TRUNC, 17 | "tozero": cv2.THRESH_TOZERO, 18 | "inverted tozero": cv2.THRESH_TOZERO_INV, 19 | "adaptive mean": cv2.ADAPTIVE_THRESH_MEAN_C, 20 | "adaptive gaussian": cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 21 | "Otsu": cv2.THRESH_OTSU} 22 | _modes = list(_modes2FcnMap.keys()) 23 | 24 | def __init__(self, 25 | tag: int, 26 | pos: tuple[int, int], 27 | editorHandle: NodeEditor): 28 | super().__init__(tag=tag, editor=editorHandle) 29 | self._width: int = self._settings.nodeWidth 30 | self._currentImage: Union[np.ndarray, None] = None 31 | self._currentGrayImage: Union[np.ndarray, None] = None 32 | self._currentMode = self._modes[0] 33 | self._thresholdGroupTag: int = editorHandle.getUniqueTag() 34 | self._threshold: float = 0.5 35 | self._adaptiveGroupTag: int = editorHandle.getUniqueTag() 36 | self._adaptiveBlockSize: int = 11 37 | self._adaptiveConstant: int = 2 38 | 39 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 40 | parentNodeTag=self._tag, 41 | attrType=AttributeType.Image) 42 | self.inAttrs.append(self._attrImageInput) 43 | 44 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 45 | parentNodeTag=self._tag, 46 | attrType=AttributeType.Image) 47 | self.outAttrs.append(self._attrImageOutput) 48 | 49 | with dpg.node(tag=self._tag, 50 | parent=editorHandle.tag, 51 | label=self.nodeLabel, 52 | pos=pos): 53 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 54 | attribute_type=dpg.mvNode_Attr_Input, 55 | shape=dpg.mvNode_PinShape_QuadFilled) 56 | 57 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 58 | attribute_type=dpg.mvNode_Attr_Static): 59 | dpg.add_combo(items=self._modes, 60 | default_value=self._currentMode, 61 | width=self._width, 62 | callback=self.__callbackComboChange) 63 | with dpg.group(tag=self._thresholdGroupTag, horizontal=True, indent=10): 64 | dpg.add_text(default_value="threshold") 65 | dpg.add_input_float(width=self._width - 100, 66 | default_value=self._threshold, 67 | min_value=0, 68 | min_clamped=True, 69 | max_value=1, 70 | max_clamped=True, 71 | step=0.05, 72 | callback=self.__callbackThresholdChange) 73 | 74 | with dpg.group(tag=self._adaptiveGroupTag, show=False): 75 | with dpg.group(horizontal=True, indent=6): 76 | dpg.add_text(default_value="block size") 77 | dpg.add_input_int(width=self._width - 100, 78 | default_value=self._adaptiveBlockSize, 79 | min_value=3, 80 | min_clamped=True, 81 | max_value=149, 82 | max_clamped=True, 83 | callback=self.__callbackAdaptiveBlockSizeChange) 84 | 85 | with dpg.group(horizontal=True, indent=6): 86 | dpg.add_text(default_value="constant", indent=16) 87 | dpg.add_input_int(width=self._width - 100, 88 | default_value=self._adaptiveConstant, 89 | callback=self.__callbackAdaptiveConstantChange) 90 | 91 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 92 | attribute_type=dpg.mvNode_Attr_Output, 93 | shape=dpg.mvNode_PinShape_Triangle) 94 | 95 | def update(self): 96 | data = self._attrImageInput.data 97 | if data is None: 98 | return 99 | if np.array_equal(data, self._currentImage): 100 | return 101 | self._currentImage = data 102 | self._currentGrayImage = cv2.cvtColor(src=data, code=cv2.COLOR_RGBA2GRAY) 103 | self.__applyThreshold() 104 | 105 | def __applyThreshold(self): 106 | if self._currentImage is None: 107 | return 108 | img = self._currentGrayImage.copy() 109 | if self._currentMode in ["binary", "inverted binary", "trunc", "tozero", "inverted tozero"]: 110 | _, img = cv2.threshold(src=img, 111 | thresh=self._threshold, 112 | maxval=1, 113 | type=self._modes2FcnMap[self._currentMode]) 114 | elif self._currentMode in ["adaptive mean", "adaptive gaussian"]: 115 | img = (img * 255).astype(np.uint8) 116 | img = cv2.adaptiveThreshold(src=img, 117 | maxValue=255, 118 | adaptiveMethod=self._modes2FcnMap[self._currentMode], 119 | thresholdType=cv2.THRESH_BINARY, 120 | blockSize=self._adaptiveBlockSize, 121 | C=self._adaptiveConstant) 122 | img = (img / 255).astype(np.float32) 123 | elif self._currentMode == "Otsu": 124 | img = (img * 255).astype(np.uint8) 125 | _, img = cv2.threshold(src=img, 126 | thresh=self._threshold, 127 | maxval=255, 128 | type=cv2.THRESH_BINARY + cv2.THRESH_OTSU) 129 | img = (img / 255).astype(np.float32) 130 | self._attrImageOutput.data = cv2.cvtColor(src=img, code=cv2.COLOR_GRAY2RGBA) 131 | 132 | def __callbackComboChange(self, _, data): 133 | self._currentMode = data 134 | if data in ["adaptive mean", "adaptive gaussian"]: 135 | dpg.hide_item(item=self._thresholdGroupTag) 136 | dpg.show_item(item=self._adaptiveGroupTag) 137 | elif data == "Otsu": 138 | dpg.hide_item(item=self._thresholdGroupTag) 139 | dpg.hide_item(item=self._adaptiveGroupTag) 140 | else: 141 | dpg.show_item(item=self._thresholdGroupTag) 142 | dpg.hide_item(item=self._adaptiveGroupTag) 143 | self.__applyThreshold() 144 | 145 | def __callbackThresholdChange(self, _, data): 146 | self._threshold = data 147 | self.__applyThreshold() 148 | 149 | def __callbackAdaptiveBlockSizeChange(self, sender, data): 150 | if data > self._adaptiveBlockSize: 151 | self._adaptiveBlockSize = data if data % 2 != 0 else data + 1 152 | else: 153 | self._adaptiveBlockSize = data if data % 2 != 0 else data - 1 154 | dpg.set_value(item=sender, value=self._adaptiveBlockSize) 155 | self.__applyThreshold() 156 | 157 | def __callbackAdaptiveConstantChange(self, sender, data): 158 | self._adaptiveConstant = data 159 | self.__applyThreshold() 160 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/page.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | from numpy.fft import fft2, fftshift, ifft2 4 | 5 | from .utils import cart2pol, denoise, morph, normalize 6 | 7 | 8 | class PAGE: 9 | def __init__(self, direction_bins, h=None, w=None): 10 | """initialize the PAGE CPU version class 11 | 12 | Args: 13 | direction_bins (int): number of different diretions of edge to be extracted 14 | h (int, optional): height of the image to be processed. Defaults to None. 15 | w (int, optional): width of the image to be processed. Defaults to None. 16 | """ 17 | self.h = h 18 | self.w = w 19 | self.direction_bins = direction_bins 20 | 21 | def load_img(self, img_file=None, img_array=None): 22 | """load the image from an ndarray or from an image file 23 | 24 | Args: 25 | img_file (str, optional): path to the image. Defaults to None. 26 | img_array (np.ndarray, optional): image in the form of np.ndarray. Defaults to None. 27 | """ 28 | if img_array is not None: 29 | # directly load the image from numpy array 30 | self.img = img_array 31 | self.h = img_array.shape[0] 32 | self.w = img_array.shape[1] 33 | else: 34 | # load the image from the image file 35 | self.img = cv2.imread(img_file) 36 | # read the image size or resize to the indicated size (height x width) 37 | if not self.h and not self.w: 38 | self.h = self.img.shape[0] 39 | self.w = self.img.shape[1] 40 | else: 41 | self.img = cv2.imresize(self.img, [self.h, self.w]) 42 | # convert to grayscale if it is RGB 43 | if self.img.ndim == 3: 44 | self.img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY) 45 | 46 | def init_kernel(self, mu_1, mu_2, sigma_1, sigma_2, S1, S2): 47 | """initialize the phase kernel of PST 48 | 49 | Args: 50 | mu_1 (float): Center frequency of a normal distributed passband filter ϕ1 51 | mu_2 (float): Center frequency of log-normal distributed passband filter ϕ2 52 | sigma_1 (float): Standard deviation of normal distributed passband filter ϕ1 53 | sigma_2 (float): Standard deviation of log-normal distributed passband filter ϕ2 54 | S1 (float): Phase strength of ϕ1 55 | S2 (float): Phase strength of ϕ2 56 | """ 57 | # set the frequency grid 58 | u = np.linspace(-0.5, 0.5, self.h) 59 | v = np.linspace(-0.5, 0.5, self.w) 60 | [U, V] = np.meshgrid(u, v, indexing="ij") 61 | [self.THETA, self.RHO] = cart2pol(U, V) 62 | 63 | min_direction = np.pi / 180 64 | direction_span = np.pi / self.direction_bins 65 | directions = np.arange(min_direction, np.pi, direction_span) 66 | 67 | # create PAGE kernels channel by channel 68 | self.page_kernel = np.zeros([self.h, self.w, self.direction_bins]) 69 | for i in range(self.direction_bins): 70 | tetav = directions[i] 71 | # Project onto new directionality basis for PAGE filter creation 72 | Uprime = U * np.cos(tetav) + V * np.sin(tetav) 73 | Vprime = -U * np.sin(tetav) + V * np.cos(tetav) 74 | 75 | # Create Normal component of PAGE filter 76 | Phi_1 = np.exp(-0.5 * ((abs(Uprime) - mu_1) / sigma_1) ** 2) / ( 77 | 1 * np.sqrt(2 * np.pi) * sigma_1 78 | ) 79 | Phi_1 = (Phi_1 / np.max(Phi_1[:])) * S1 80 | 81 | # Create Log-Normal component of PAGE filter 82 | Phi_2 = np.exp(-0.5 * ((np.log(abs(Vprime)) - mu_2) / sigma_2) ** 2) / ( 83 | abs(Vprime) * np.sqrt(2 * np.pi) * sigma_2 84 | ) 85 | Phi_2 = (Phi_2 / np.max(Phi_2[:])) * S2 86 | 87 | # Add overall directional filter to PAGE filter array 88 | self.page_kernel[:, :, i] = Phi_1 * Phi_2 89 | 90 | def apply_kernel(self, sigma_LPF, thresh_min, thresh_max, morph_flag): 91 | """apply the phase kernel onto the image 92 | Args: 93 | sigma_LPF (float): std of the low pass filter 94 | thresh_min (float): minimum thershold, we keep features < thresh_min 95 | thresh_max (float): maximum thershold, we keep features > thresh_max 96 | morph_flag (boolean): whether apply morphological operation 97 | """ 98 | 99 | # denoise on the loaded image 100 | self.img_denoised = denoise(img=self.img, rho=self.RHO, sigma_LPF=sigma_LPF) 101 | self.page_output = np.zeros([self.h, self.w, self.direction_bins]) 102 | # apply the kernel channel by channel 103 | for i in range(self.direction_bins): 104 | self.img_page = ifft2( 105 | fft2(self.img_denoised) 106 | * fftshift(np.exp(-1j * self.page_kernel[:, :, i])) 107 | ) 108 | self.page_feature = normalize(np.angle(self.img_page)) 109 | # apply morphological operation if applicable 110 | if morph_flag == 0: 111 | self.page_output[:, :, i] = self.page_feature 112 | else: 113 | self.page_output[:, :, i] = morph( 114 | img=self.img, 115 | feature=self.page_feature, 116 | thresh_max=thresh_max, 117 | thresh_min=thresh_min, 118 | ) 119 | 120 | def create_page_edge(self): 121 | """create results which color-coded directional edges""" 122 | # Create a weighted color image of PAGE output to visualize directionality of edges 123 | weight_step = 255 * 3 / self.direction_bins 124 | color_weight = np.arange(0, 255, weight_step) 125 | self.page_edge = np.zeros([self.h, self.w, 3]) 126 | # step_edge = int(round(self.direction_bins/3)) 127 | step_edge = self.direction_bins // 3 128 | for i in range(step_edge): 129 | self.page_edge[:, :, 0] = ( 130 | color_weight[i] * self.page_output[:, :, i] + self.page_edge[:, :, 0] 131 | ) 132 | self.page_edge[:, :, 1] = ( 133 | color_weight[i] * self.page_output[:, :, i + step_edge] 134 | + self.page_edge[:, :, 1] 135 | ) 136 | self.page_edge[:, :, 2] = ( 137 | color_weight[i] * self.page_output[:, :, i + (2 * step_edge)] 138 | + self.page_edge[:, :, 2] 139 | ) 140 | 141 | self.page_edge = (self.page_edge - np.min(self.page_edge)) / ( 142 | np.max(self.page_edge) - np.min(self.page_edge) 143 | ) 144 | 145 | def run( 146 | self, 147 | img_file, 148 | mu_1, 149 | mu_2, 150 | sigma_1, 151 | sigma_2, 152 | S1, 153 | S2, 154 | sigma_LPF, 155 | thresh_min, 156 | thresh_max, 157 | morph_flag, 158 | ): 159 | """wrap all steps of PAGE into a single run method 160 | 161 | Args: 162 | img_file (str): path to the image. 163 | mu_1 (float): Center frequency of a normal distributed passband filter ϕ1 164 | mu_2 (float): Center frequency of log-normal distributed passband filter ϕ2 165 | sigma_1 (float): Standard deviation of normal distributed passband filter ϕ1 166 | sigma_2 (float): Standard deviation of log-normal distributed passband filter ϕ2 167 | S1 (float): Phase strength of ϕ1 168 | S2 (float): Phase strength of ϕ2 169 | sigma_LPF (float): std of the low pass filter 170 | thresh_min (float): minimum thershold, we keep features < thresh_min 171 | thresh_max (float): maximum thershold, we keep features > thresh_max 172 | morph_flag (boolean): whether apply morphological operation 173 | 174 | Returns: 175 | np.ndarray: color-coded directional edge 176 | """ 177 | # wrap load_img, init_kernel, apply_kernel, create_page_edge in one run 178 | self.load_img(img_file=img_file) 179 | self.init_kernel(mu_1, mu_2, sigma_1, sigma_2, S1, S2) 180 | self.apply_kernel(sigma_LPF, thresh_min, thresh_max, morph_flag) 181 | self.create_page_edge() 182 | 183 | return self.page_edge 184 | -------------------------------------------------------------------------------- /nodes/filters/node_light_enhancement.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import dearpygui.dearpygui as dpg 4 | import numpy as np 5 | 6 | from node_editor.connection_objects import NodeAttribute, AttributeType 7 | from node_editor.editor import NodeEditor 8 | from nodes.filters.algorithms.light_enhancement import vevid 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Light Enhancement" 14 | 15 | _filters = ["VEVID"] 16 | 17 | def __init__(self, 18 | tag: int, 19 | pos: tuple[int, int], 20 | editorHandle: NodeEditor): 21 | super().__init__(tag=tag, editor=editorHandle) 22 | self._width: int = self._settings.nodeWidth 23 | self._currentImage: Union[np.ndarray, None] = None 24 | self._currentFilter = self._filters[0] 25 | 26 | self._vevidGroupTag: int = editorHandle.getUniqueTag() 27 | self._vevidGenericRange: tuple[float, float] = (0, 1) 28 | self._vevidPhaseStrength: float = 0.3 29 | self._vevidSpectralPhaseFcnVariance: float = 0.001 30 | self._vevidRegularizationTerm: float = 0.17 31 | self._vevidPhaseActivationGainRange: tuple[float, float] = (1, 10) 32 | self._vevidPhaseActivationGain: float = 1.4 33 | self._vevidEnhanceColor: bool = False 34 | self._vevidLiteMode: bool = True 35 | 36 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 37 | parentNodeTag=self._tag, 38 | attrType=AttributeType.Image) 39 | self.inAttrs.append(self._attrImageInput) 40 | 41 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 42 | parentNodeTag=self._tag, 43 | attrType=AttributeType.Image) 44 | self.outAttrs.append(self._attrImageOutput) 45 | 46 | with dpg.node(tag=self._tag, 47 | parent=editorHandle.tag, 48 | label=self.nodeLabel, 49 | pos=pos): 50 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 51 | attribute_type=dpg.mvNode_Attr_Input, 52 | shape=dpg.mvNode_PinShape_QuadFilled) 53 | 54 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 55 | attribute_type=dpg.mvNode_Attr_Static): 56 | dpg.add_combo(items=self._filters, 57 | default_value=self._filters[0], 58 | width=self._width, 59 | callback=self.__callbackComboChange) 60 | 61 | with dpg.group(tag=self._vevidGroupTag, indent=25): 62 | with dpg.group(horizontal=True): 63 | dpg.add_text(default_value="ps", indent=16) 64 | dpg.add_drag_float(tag=editorHandle.getUniqueTag(), 65 | default_value=self._vevidPhaseStrength, 66 | width=self._width - 85, 67 | callback=self.__callbackVEVIDPhaseStrengthChange, 68 | min_value=self._vevidGenericRange[0], 69 | max_value=self._vevidGenericRange[1], 70 | no_input=False, 71 | speed=0.01) 72 | 73 | with dpg.group(horizontal=True): 74 | dpg.add_text(default_value="b", indent=24) 75 | dpg.add_drag_float(tag=editorHandle.getUniqueTag(), 76 | default_value=self._vevidRegularizationTerm, 77 | width=self._width - 85, 78 | callback=self.__callbackVEVIDRegTermChange, 79 | min_value=self._vevidGenericRange[0], 80 | max_value=self._vevidGenericRange[1], 81 | no_input=False, 82 | speed=0.01) 83 | 84 | with dpg.group(horizontal=True): 85 | dpg.add_text(default_value="gain") 86 | dpg.add_drag_float(tag=editorHandle.getUniqueTag(), 87 | default_value=self._vevidPhaseActivationGain, 88 | width=self._width - 85, 89 | callback=self.__callbackVEVIDGainChange, 90 | min_value=self._vevidPhaseActivationGainRange[0], 91 | max_value=self._vevidPhaseActivationGainRange[1], 92 | no_input=False, 93 | speed=0.01) 94 | 95 | with dpg.group(horizontal=True): 96 | dpg.add_text(default_value="variance") 97 | dpg.add_drag_float(tag=editorHandle.getUniqueTag(), 98 | default_value=self._vevidSpectralPhaseFcnVariance, 99 | width=self._width - 118, 100 | callback=self.__callbackVEVIDVarianceChange, 101 | min_value=self._vevidGenericRange[0], 102 | max_value=self._vevidGenericRange[1], 103 | no_input=False, 104 | speed=0.01) 105 | 106 | dpg.add_checkbox(label="enhance color", 107 | default_value=self._vevidEnhanceColor, 108 | callback=self.__callbackVEVIDEnhanceColor) 109 | 110 | dpg.add_checkbox(label="lite mode", 111 | default_value=self._vevidLiteMode, 112 | callback=self.__callbackVEVIDLiteMode) 113 | 114 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 115 | attribute_type=dpg.mvNode_Attr_Output, 116 | shape=dpg.mvNode_PinShape_Triangle) 117 | 118 | def update(self): 119 | data = self._attrImageInput.data 120 | if data is None: 121 | return 122 | if np.array_equal(data, self._currentImage): 123 | return 124 | self._currentImage = data 125 | self.__applyFilter() 126 | 127 | def __applyFilter(self): 128 | if self._currentImage is None: 129 | return 130 | img = self._currentImage.copy() 131 | if self._currentFilter == "VEVID": 132 | img = vevid(img=img, 133 | phaseStrength=self._vevidPhaseStrength, 134 | spectralPhaseFcnVariance=self._vevidSpectralPhaseFcnVariance, 135 | regularizationTerm=self._vevidRegularizationTerm, 136 | phaseActivationGain=self._vevidPhaseActivationGain, 137 | enhanceColor=self._vevidEnhanceColor, 138 | liteMode=self._vevidLiteMode) 139 | self._attrImageOutput.data = img 140 | 141 | def __callbackComboChange(self, _, data): 142 | self._currentFilter = data 143 | if data == "VEVID": 144 | dpg.show_item(item=self._vevidGroupTag) 145 | self.__applyFilter() 146 | 147 | def __callbackVEVIDPhaseStrengthChange(self, _, data): 148 | self._vevidPhaseStrength = data 149 | self.__applyFilter() 150 | 151 | def __callbackVEVIDVarianceChange(self, _, data): 152 | self._vevidSpectralPhaseFcnVariance = data 153 | self.__applyFilter() 154 | 155 | def __callbackVEVIDRegTermChange(self, _, data): 156 | self._vevidRegularizationTerm = data 157 | self.__applyFilter() 158 | 159 | def __callbackVEVIDGainChange(self, _, data): 160 | self._vevidPhaseActivationGain = data 161 | self.__applyFilter() 162 | 163 | def __callbackVEVIDEnhanceColor(self, _, data): 164 | self._vevidEnhanceColor = data 165 | self.__applyFilter() 166 | 167 | def __callbackVEVIDLiteMode(self, _, data): 168 | self._vevidLiteMode = data 169 | self.__applyFilter() 170 | -------------------------------------------------------------------------------- /node_editor/connection_objects.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import Union, Callable 3 | 4 | import numpy as np 5 | 6 | 7 | class AttributeType(enum.Enum): 8 | Image = 0 9 | InferenceResult = 1 10 | AnyArray = 2 11 | TorchModelFeatures = 3 12 | 13 | 14 | class NodeAttribute: 15 | def __init__(self, *, 16 | tag: int, 17 | parentNodeTag: int, 18 | attrType: AttributeType): 19 | self._tag: int = tag 20 | self._parentNodeTag: int = parentNodeTag 21 | self._blocked: bool = False 22 | self._attrType = attrType 23 | self._data: Union[np.ndarray, None] = None 24 | self._connections: list[Connection] = list() 25 | 26 | @property 27 | def tag(self): 28 | return self._tag 29 | 30 | @property 31 | def parentNodeTag(self): 32 | return self._parentNodeTag 33 | 34 | @property 35 | def data(self) -> Union[np.ndarray, None]: 36 | return self._data 37 | 38 | @data.setter 39 | def data(self, value: Union[np.ndarray, None]): 40 | if value is not None: 41 | self._data = value 42 | 43 | @property 44 | def blocked(self): 45 | return self._blocked 46 | 47 | @blocked.setter 48 | def blocked(self, value: bool): 49 | self._blocked = value 50 | 51 | @property 52 | def attrType(self): 53 | return self._attrType 54 | 55 | @property 56 | def connections(self): 57 | return self._connections 58 | 59 | 60 | class TreeNode: 61 | def __init__(self, 62 | tag: int, 63 | inAttrs: list[NodeAttribute], 64 | outAttrs: list[NodeAttribute], 65 | updateFcn: Union[None, Callable] = None): 66 | self._tag = tag 67 | self._updateFcn: Union[None, Callable] = updateFcn 68 | self._connections: list[Connection] = list() 69 | self._inAttrs: list[NodeAttribute] = inAttrs 70 | self._outAttrs: list[NodeAttribute] = outAttrs 71 | 72 | @property 73 | def tag(self): 74 | return self._tag 75 | 76 | @property 77 | def updateFcn(self): 78 | return self._updateFcn 79 | 80 | @property 81 | def connections(self): 82 | return self._connections 83 | 84 | @property 85 | def inAttrs(self): 86 | return self._inAttrs 87 | 88 | @property 89 | def outAttrs(self): 90 | return self._outAttrs 91 | 92 | 93 | class Connection: 94 | def __init__(self, *, 95 | originNode: TreeNode, 96 | originAttr: NodeAttribute, 97 | targetNode: TreeNode, 98 | targetAttr: NodeAttribute, 99 | tag: int): 100 | self._originNode: TreeNode = originNode 101 | self._originAttr: NodeAttribute = originAttr 102 | self._targetNode: TreeNode = targetNode 103 | self._targetAttr: NodeAttribute = targetAttr 104 | self._tag: int = tag 105 | 106 | @property 107 | def originNode(self): 108 | return self._originNode 109 | 110 | @property 111 | def originAttr(self): 112 | return self._originAttr 113 | 114 | @property 115 | def targetNode(self): 116 | return self._targetNode 117 | 118 | @property 119 | def targetAttr(self): 120 | return self._targetAttr 121 | 122 | @property 123 | def tag(self): 124 | return self._tag 125 | 126 | 127 | class Tree: 128 | def __init__(self): 129 | self._levels: list[list[TreeNode]] = list() # levels of nodes 130 | self._nodes: list[TreeNode] = list() 131 | self._connections: list[Connection] = list() 132 | self._tagToEntityMap: dict = dict() 133 | 134 | @property 135 | def levels(self): 136 | return self._levels 137 | 138 | @property 139 | def nodes(self): 140 | return self._nodes 141 | 142 | @property 143 | def connections(self): 144 | return self._connections 145 | 146 | def updateLevels(self): 147 | if len(self._connections) == 0: 148 | return 149 | self._levels.clear() 150 | 151 | # first get the upstream nodes (output-only nodes) 152 | topLevel = list() 153 | for connection in self._connections: 154 | if not connection.originNode.inAttrs: 155 | topLevel.append(connection.originNode) 156 | else: 157 | checkList = list() 158 | for inAttr in connection.originNode.inAttrs: 159 | checkList.append(False if inAttr.blocked else True) 160 | if np.all(a=np.array(checkList)): 161 | topLevel.append(connection.originNode) 162 | 163 | aCounter = 0 164 | levelsDict = dict() 165 | levelsDict[aCounter] = topLevel 166 | for node in topLevel: 167 | self.__traceBranch(node=node, levelsDict=levelsDict, counter=aCounter) 168 | 169 | self._levels = [list(set(x)) for x in levelsDict.values()] 170 | 171 | def updateConnections(self): 172 | for level in self._levels: 173 | for node in level: 174 | for outAttr in node.outAttrs: 175 | for connection in outAttr.connections: 176 | connection.targetAttr.data = \ 177 | None if connection.originAttr.data is None else connection.originAttr.data.copy() 178 | 179 | def updateNodes(self): 180 | newLevels = list() 181 | self._levels.reverse() 182 | alreadyConsideredNodes = list() 183 | for level in self._levels: 184 | aLevel = list() 185 | for node in level: 186 | if node not in alreadyConsideredNodes: 187 | aLevel.append(node) 188 | alreadyConsideredNodes.append(node) 189 | newLevels.append(aLevel) 190 | newLevels.reverse() 191 | for level in newLevels: 192 | for node in level: 193 | node.updateFcn() 194 | 195 | def __traceBranch(self, node: TreeNode, levelsDict: dict, counter: int): 196 | counter += 1 197 | for connection in node.connections: 198 | if counter not in levelsDict: 199 | levelsDict[counter] = list() 200 | levelsDict[counter].append(connection.targetNode) 201 | 202 | for connection in node.connections: 203 | if connection.targetNode.outAttrs: 204 | for outAttr in connection.targetNode.outAttrs: 205 | for outConnection in outAttr.connections: 206 | self.__traceBranch(node=outConnection.targetNode, levelsDict=levelsDict, counter=counter) 207 | 208 | def addNode(self, 209 | node: TreeNode): 210 | self._nodes.append(node) 211 | self._tagToEntityMap[node.tag] = node 212 | 213 | def removeNodeByTag(self, tag: int): 214 | node = self._tagToEntityMap[tag] 215 | for connection in node.connections: 216 | self.removeConnectionByObject(connection=connection) 217 | self._nodes.remove(node) 218 | del self._tagToEntityMap[tag] 219 | 220 | def removeNodeByObject(self, node: TreeNode): 221 | for connection in node.connections: 222 | self.removeConnectionByObject(connection=connection) 223 | self._nodes.remove(node) 224 | del self._tagToEntityMap[node.tag] 225 | 226 | def getNodeByTag(self, tag: int) -> TreeNode: 227 | return self._tagToEntityMap[tag] 228 | 229 | def addConnection(self, 230 | connection: Connection): 231 | connection.originAttr.connections.append(connection) 232 | connection.originNode.connections.append(connection) 233 | connection.targetAttr.blocked = True 234 | connection.targetAttr.connections.append(connection) 235 | connection.targetNode.connections.append(connection) 236 | self._connections.append(connection) 237 | self._tagToEntityMap[connection.tag] = connection 238 | 239 | def removeConnectionByTag(self, tag: int): 240 | connection = self._tagToEntityMap[tag] 241 | self.removeConnectionByObject(connection=connection) 242 | 243 | def removeConnectionByObject(self, connection: Connection): 244 | connection.originAttr.connections.remove(connection) 245 | connection.originNode.connections.remove(connection) 246 | connection.targetAttr.blocked = False 247 | connection.targetAttr.data = None 248 | connection.targetAttr.connections.remove(connection) 249 | connection.targetNode.connections.remove(connection) 250 | self._connections.remove(connection) 251 | del self._tagToEntityMap[connection.tag] 252 | 253 | def getConnectionByTag(self, tag: int) -> Union[Connection, None]: 254 | return self._tagToEntityMap[tag] 255 | 256 | def getAttrByTag(self, tag: int) -> Union[NodeAttribute, None]: 257 | for node in self._nodes: 258 | for outAttr in node.outAttrs: 259 | if outAttr.tag == tag: 260 | return outAttr 261 | 262 | for inAttr in node.inAttrs: 263 | if inAttr.tag == tag: 264 | return inAttr 265 | -------------------------------------------------------------------------------- /nodes/adjustments/node_crop.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import dearpygui.dearpygui as dpg 4 | import numpy as np 5 | 6 | from node_editor.connection_objects import NodeAttribute, AttributeType 7 | from node_editor.editor import NodeEditor 8 | from nodes.node import NodeBase 9 | 10 | 11 | class Node(NodeBase): 12 | nodeLabel = "Crop" 13 | _modes = ["center crop", "two corner crop"] 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._width: int = self._settings.nodeWidth 21 | self._currentMode: str = self._modes[0] 22 | self._currentImage: Union[np.ndarray, None] = None 23 | 24 | self._inputImageSizeLabelTag: int = editorHandle.getUniqueTag() 25 | self._outputImageSizeLabelTag: int = editorHandle.getUniqueTag() 26 | self._centerCropGroupTag: int = editorHandle.getUniqueTag() 27 | self._twoCenterCropGroupTag: int = editorHandle.getUniqueTag() 28 | 29 | self._cropWidth: int = 100 30 | self._cropHeight: int = 100 31 | self._corner1Left: int = 0 32 | self._corner1Top: int = 0 33 | self._corner2Left: int = 50 34 | self._corner2Top: int = 50 35 | 36 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 37 | parentNodeTag=self._tag, 38 | attrType=AttributeType.Image) 39 | self.inAttrs.append(self._attrImageInput) 40 | 41 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 42 | parentNodeTag=self._tag, 43 | attrType=AttributeType.Image) 44 | self.outAttrs.append(self._attrImageOutput) 45 | 46 | with dpg.node(tag=self._tag, 47 | parent=editorHandle.tag, 48 | label=self.nodeLabel, 49 | pos=pos): 50 | with dpg.node_attribute(tag=self._attrImageInput.tag, 51 | attribute_type=dpg.mvNode_Attr_Input, 52 | shape=dpg.mvNode_PinShape_QuadFilled): 53 | dpg.add_text(tag=self._inputImageSizeLabelTag) 54 | 55 | with dpg.node_attribute(tag=editorHandle.getUniqueTag(), 56 | attribute_type=dpg.mvNode_Attr_Static): 57 | dpg.add_combo(width=self._width, 58 | items=self._modes, 59 | default_value=self._currentMode, 60 | callback=self.__callbackCropModeChange) 61 | with dpg.group(tag=self._centerCropGroupTag, indent=50): 62 | dpg.add_drag_int(default_value=self._cropWidth, 63 | speed=10, 64 | format="width %f", 65 | width=self._width - 100, 66 | min_value=0, 67 | max_value=3840, 68 | clamped=True, 69 | callback=self.__callbackCenterWidthChange) 70 | dpg.add_drag_int(default_value=self._cropHeight, 71 | speed=10, 72 | format="height %f", 73 | width=self._width - 100, 74 | min_value=0, 75 | max_value=3840, 76 | clamped=True, 77 | callback=self.__callbackCenterHeightChange) 78 | with dpg.group(tag=self._twoCenterCropGroupTag, show=False): 79 | with dpg.group(horizontal=True, indent=18): 80 | dpg.add_drag_int(default_value=self._corner1Left, 81 | speed=10, 82 | format="c1 left %f", 83 | width=self._width - 150, 84 | min_value=0, 85 | max_value=3840, 86 | clamped=True, 87 | callback=self.__callbackCorner1LeftChange) 88 | dpg.add_drag_int(default_value=self._corner1Top, 89 | speed=10, 90 | format="c1 top %f", 91 | width=self._width - 150, 92 | min_value=0, 93 | max_value=3840, 94 | clamped=True, 95 | callback=self.__callbackCorner2TopChange) 96 | with dpg.group(horizontal=True, indent=18): 97 | dpg.add_drag_int(default_value=self._corner2Left, 98 | speed=10, 99 | format="c2 left %f", 100 | width=self._width - 150, 101 | min_value=0, 102 | max_value=3840, 103 | clamped=True, 104 | callback=self.__callbackCorner2LeftChange) 105 | dpg.add_drag_int(default_value=self._corner2Top, 106 | speed=10, 107 | format="c2 top %f", 108 | width=self._width - 150, 109 | min_value=0, 110 | max_value=3840, 111 | clamped=True, 112 | callback=self.__callbackCorner2TopChange) 113 | dpg.add_spacer(width=self._width) 114 | 115 | with dpg.node_attribute(tag=self._attrImageOutput.tag, 116 | attribute_type=dpg.mvNode_Attr_Output, 117 | shape=dpg.mvNode_PinShape_Triangle): 118 | dpg.add_text(tag=self._outputImageSizeLabelTag, indent=self._width - 85) 119 | 120 | def update(self): 121 | data = self._attrImageInput.data 122 | if data is None: 123 | dpg.set_value(item=self._inputImageSizeLabelTag, value="") 124 | self._currentImage = None 125 | return 126 | if np.array_equal(data, self._currentImage): 127 | return 128 | dpg.set_value(item=self._inputImageSizeLabelTag, value=data.shape[:2]) 129 | self._currentImage = data 130 | 131 | self.__crop() 132 | 133 | def __crop(self): 134 | if self._currentImage is None: 135 | return 136 | img = self._currentImage.copy() 137 | height, width = img.shape[:2] 138 | if self._currentMode == "center crop": 139 | centerRow = height // 2 140 | centerCol = width // 2 141 | rowStart = centerRow - self._cropHeight // 2 142 | rowEnd = centerRow + self._cropHeight // 2 143 | colStart = centerCol - self._cropWidth // 2 144 | colEnd = centerCol + self._cropWidth // 2 145 | else: 146 | rowStart = self._corner1Top 147 | rowEnd = self._corner2Top 148 | colStart = self._corner1Left 149 | colEnd = self._corner2Left 150 | if rowEnd < rowStart or colEnd < colStart: 151 | return 152 | if rowStart < 0: 153 | rowStart = 0 154 | if colStart < 0: 155 | colStart = 0 156 | if rowEnd > height: 157 | rowEnd = height 158 | if colEnd > width: 159 | colEnd = width 160 | 161 | outImg = img[rowStart:rowEnd, colStart:colEnd] 162 | dpg.set_value(item=self._outputImageSizeLabelTag, value=outImg.shape[:2]) 163 | self._attrImageOutput.data = outImg 164 | 165 | def __callbackCropModeChange(self, _, data): 166 | self._currentMode = data 167 | if data == "center crop": 168 | dpg.show_item(item=self._centerCropGroupTag) 169 | dpg.hide_item(item=self._twoCenterCropGroupTag) 170 | else: 171 | dpg.hide_item(item=self._centerCropGroupTag) 172 | dpg.show_item(item=self._twoCenterCropGroupTag) 173 | self.__crop() 174 | 175 | def __callbackCenterWidthChange(self, _, data): 176 | self._cropWidth = data 177 | self.__crop() 178 | 179 | def __callbackCenterHeightChange(self, _, data): 180 | self._cropHeight = data 181 | self.__crop() 182 | 183 | def __callbackCorner1LeftChange(self, _, data): 184 | self._corner1Left = data 185 | self.__crop() 186 | 187 | def __callbackCorner1TopChange(self, _, data): 188 | self._corner1Top = data 189 | self.__crop() 190 | 191 | def __callbackCorner2LeftChange(self, _, data): 192 | self._corner2Left = data 193 | self.__crop() 194 | 195 | def __callbackCorner2TopChange(self, _, data): 196 | self._corner2Top = data 197 | self.__crop() 198 | -------------------------------------------------------------------------------- /nodes/filters/algorithms/phycv/page_gpu.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import torch 4 | import torchvision 5 | from torch.fft import fft2, fftshift, ifft2 6 | from torchvision.transforms.functional import resize, rgb_to_grayscale 7 | 8 | from .utils import cart2pol_torch, denoise_torch, morph_torch, normalize 9 | 10 | 11 | class PAGE_GPU: 12 | def __init__(self, direction_bins, device, h=None, w=None): 13 | 14 | """initialize the PAGE GPU version class 15 | 16 | Args: 17 | direction_bins (int): number of different diretions of edge to be extracted 18 | device(torch.device) 19 | h (int, optional): height of the image to be processed. Defaults to None. 20 | w (int, optional): width of the image to be processed. Defaults to None. 21 | """ 22 | self.h = h 23 | self.w = w 24 | self.direction_bins = direction_bins 25 | self.device = device 26 | 27 | def load_img(self, img_file=None, img_array=None): 28 | """load the image from an ndarray or from an image file 29 | 30 | Args: 31 | img_file (str, optional): path to the image. Defaults to None. 32 | img_array (torch.Tensor, optional): image in the form of torch.Tensor. Defaults to None. 33 | """ 34 | if img_array is not None: 35 | # directly load the image from the array instead of the file 36 | if img_array.get_device() == self.device: 37 | self.img = img_array 38 | else: 39 | self.img = img_array.to(self.device) 40 | # convert to grayscale if it is RGB 41 | if self.img.dim() == 3 and self.img.shape[0] != 1: 42 | self.img = rgb_to_grayscale(self.img) 43 | # read the image size or resize to the indicated size (height x width) 44 | if not self.h and not self.w: 45 | self.img = torch.squeeze(self.img) 46 | self.h = self.img.shape[0] 47 | self.w = self.img.shape[1] 48 | else: 49 | self.img = torch.squeeze(resize(self.img, [self.h, self.w])) 50 | 51 | else: 52 | # load the image from the image file 53 | # torchvision read_image only supports 'jpg' and 'png' 54 | if img_file.split(".")[-1] in ["jpg", "png", "jpeg"]: 55 | self.img = torchvision.io.read_image(img_file).to(self.device) 56 | # convert to grayscale if it is RGB 57 | if self.img.dim() == 3 and self.img.shape[0] != 1: 58 | self.img = rgb_to_grayscale(self.img) 59 | # read the image size or resize to the indicated size (height x width) 60 | if not self.h and not self.w: 61 | self.img = torch.squeeze(self.img) 62 | self.h = self.img.shape[0] 63 | self.w = self.img.shape[1] 64 | else: 65 | self.img = torch.squeeze(resize(self.img, [self.h, self.w])) 66 | 67 | else: 68 | # use opencv to load other format of image 69 | self.img = cv2.imread(img_file) 70 | if self.img.ndim == 3: 71 | self.img = cv2.cvtColor(self.img, cv2.COLOR_BGR2GRAY) 72 | if not self.h and not self.w: 73 | self.h = self.img.shape[0] 74 | self.w = self.img.shape[1] 75 | else: 76 | self.img = cv2.imresize(self.img, [self.h, self.w]) 77 | 78 | self.img = torch.from_numpy(self.img).to(self.device) 79 | 80 | def init_kernel(self, mu_1, mu_2, sigma_1, sigma_2, S1, S2): 81 | """initialize the phase kernel of PST 82 | 83 | Args: 84 | mu_1 (float): Center frequency of a normal distributed passband filter ϕ1 85 | mu_2 (float): Center frequency of log-normal distributed passband filter ϕ2 86 | sigma_1 (float): Standard deviation of normal distributed passband filter ϕ1 87 | sigma_2 (float): Standard deviation of log-normal distributed passband filter ϕ2 88 | S1 (float): Phase strength of ϕ1 89 | S2 (float): Phase strength of ϕ2 90 | """ 91 | 92 | # set the frequency grid 93 | u = torch.linspace(-0.5, 0.5, self.h, device=self.device).float() 94 | v = torch.linspace(-0.5, 0.5, self.w, device=self.device).float() 95 | [U, V] = torch.meshgrid(u, v, indexing="ij") 96 | [self.THETA, self.RHO] = cart2pol_torch(U, V) 97 | 98 | min_direction = np.pi / 180 99 | direction_span = np.pi / self.direction_bins 100 | directions = torch.arange(min_direction, np.pi, direction_span) 101 | 102 | # create PAGE kernels in parallel by broadcasting 103 | tetavs = torch.unsqueeze(directions, dim=0).to(self.device) 104 | Us = torch.unsqueeze(U, dim=-1) 105 | Vs = torch.unsqueeze(V, dim=-1) 106 | Uprimes = Us * torch.cos(tetavs) + Vs * torch.sin(tetavs) 107 | Vprimes = -Us * torch.sin(tetavs) + Vs * torch.cos(tetavs) 108 | 109 | Phi_1s = torch.exp(-0.5 * ((torch.abs(Uprimes) - mu_1) / sigma_1) ** 2) / ( 110 | 1 * np.sqrt(2 * np.pi) * sigma_1 111 | ) 112 | Phi_1s = ( 113 | Phi_1s / torch.max(Phi_1s.view(-1, self.direction_bins), dim=0)[0] 114 | ) * S1 115 | 116 | Phi_2s = torch.exp( 117 | -0.5 * ((torch.log(torch.abs(Vprimes)) - mu_2) / sigma_2) ** 2 118 | ) / (abs(Vprimes) * np.sqrt(2 * np.pi) * sigma_2) 119 | Phi_2s = ( 120 | Phi_2s / torch.max(Phi_2s.view(-1, self.direction_bins), dim=0)[0] 121 | ) * S2 122 | self.page_kernel = Phi_1s * Phi_2s 123 | 124 | def apply_kernel(self, sigma_LPF, thresh_min, thresh_max, morph_flag): 125 | 126 | """apply the phase kernel onto the image 127 | Args: 128 | sigma_LPF (float): std of the low pass filter 129 | thresh_min (float): minimum thershold, we keep features < thresh_min 130 | thresh_max (float): maximum thershold, we keep features > thresh_max 131 | morph_flag (boolean): whether apply morphological operation 132 | """ 133 | # denoise on the loaded image 134 | self.img_denoised = denoise_torch( 135 | img=self.img, rho=self.RHO, sigma_LPF=sigma_LPF 136 | ) 137 | # apply the page kernel 138 | self.img_page = ifft2( 139 | fft2(self.img_denoised).unsqueeze(-1) 140 | * fftshift(torch.exp(-1j * self.page_kernel), dim=(0, 1)), 141 | dim=(0, 1), 142 | ) 143 | self.page_feature = normalize(torch.angle(self.img_page)) 144 | # apply morphological operation if applicable 145 | if morph_flag == 0: 146 | self.page_output = self.page_feature 147 | else: 148 | self.page_output = morph_torch( 149 | img=self.img, 150 | feature=self.page_feature, 151 | thresh_min=thresh_min, 152 | thresh_max=thresh_max, 153 | device=self.device, 154 | ) 155 | 156 | def create_page_edge(self): 157 | """create results which color-coded directional edges""" 158 | # Create a weighted color image of PAGE output to visualize directionality of edges 159 | weight_step = 255 * 3 / self.direction_bins 160 | color_weight = torch.arange(0, 255, weight_step).to(self.device) 161 | self.page_edge = torch.zeros([self.h, self.w, 3]).to(self.device) 162 | # step_edge = int(round(self.direction_bins/3)) 163 | step_edge = self.direction_bins // 3 164 | for i in range(step_edge): 165 | self.page_edge[:, :, 0] = ( 166 | color_weight[i] * self.page_output[:, :, i] + self.page_edge[:, :, 0] 167 | ) 168 | self.page_edge[:, :, 1] = ( 169 | color_weight[i] * self.page_output[:, :, i + step_edge] 170 | + self.page_edge[:, :, 1] 171 | ) 172 | self.page_edge[:, :, 2] = ( 173 | color_weight[i] * self.page_output[:, :, i + (2 * step_edge)] 174 | + self.page_edge[:, :, 2] 175 | ) 176 | 177 | self.page_edge = (self.page_edge - torch.min(self.page_edge)) / ( 178 | torch.max(self.page_edge) - torch.min(self.page_edge) 179 | ) 180 | 181 | def run( 182 | self, 183 | img_file, 184 | mu_1, 185 | mu_2, 186 | sigma_1, 187 | sigma_2, 188 | S1, 189 | S2, 190 | sigma_LPF, 191 | thresh_min, 192 | thresh_max, 193 | morph_flag, 194 | ): 195 | """wrap all steps of PAGE into a single run method 196 | 197 | Args: 198 | img_file (str): path to the image. 199 | mu_1 (float): Center frequency of a normal distributed passband filter ϕ1 200 | mu_2 (float): Center frequency of log-normal distributed passband filter ϕ2 201 | sigma_1 (float): Standard deviation of normal distributed passband filter ϕ1 202 | sigma_2 (float): Standard deviation of log-normal distributed passband filter ϕ2 203 | S1 (float): Phase strength of ϕ1 204 | S2 (float): Phase strength of ϕ2 205 | sigma_LPF (float): std of the low pass filter 206 | thresh_min (float): minimum thershold, we keep features < thresh_min 207 | thresh_max (float): maximum thershold, we keep features > thresh_max 208 | morph_flag (boolean): whether apply morphological operation 209 | 210 | Returns: 211 | torch.Tensor: color-coded directional edge 212 | """ 213 | self.load_img(img_file=img_file) 214 | self.init_kernel(mu_1, mu_2, sigma_1, sigma_2, S1, S2) 215 | self.apply_kernel(sigma_LPF, thresh_min, thresh_max, morph_flag) 216 | self.create_page_edge() 217 | 218 | return self.page_edge 219 | -------------------------------------------------------------------------------- /nodes/outputs/node_video_output.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Union 3 | 4 | import cv2 5 | import dearpygui.dearpygui as dpg 6 | import numpy as np 7 | 8 | from node_editor.connection_objects import NodeAttribute, AttributeType 9 | from node_editor.editor import NodeEditor 10 | from nodes.node import NodeBase 11 | 12 | 13 | class Node(NodeBase): 14 | nodeLabel = "Video Writer" 15 | 16 | _encoderType = {".mp4": "mp4v", ".avi": "DIVX"} 17 | _settings = None 18 | 19 | def __init__(self, 20 | tag: int, 21 | pos: tuple[int, int], 22 | editorHandle: NodeEditor): 23 | super().__init__(tag=tag, editor=editorHandle) 24 | self._width: int = self.settings.nodeWidth 25 | self._folderDialogTag: int = editorHandle.getUniqueTag() 26 | self._outDirPath: Union[Path, None] = None 27 | self._outDirTextInputTag: int = editorHandle.getUniqueTag() 28 | self._outDirBrowseBtnTag: int = editorHandle.getUniqueTag() 29 | self._fileBaseName: str = str() 30 | self._nameChangerInt: int = 1 31 | self._nameChangeIntTextTag: int = editorHandle.getUniqueTag() 32 | self._baseNameTextInputTag: int = editorHandle.getUniqueTag() 33 | self._sizeInputTag: int = editorHandle.getUniqueTag() 34 | self._size: tuple[int, int] = (1280, 720) 35 | self._fpsInputTag: int = editorHandle.getUniqueTag() 36 | self._fpsRange: tuple = (1, 60) 37 | self._fps: int = 24 38 | self._fileFormat: str = list(self._encoderType.keys())[0] 39 | self._saveModes: list[str] = ["record stop", "frame limit"] 40 | self._saveMode: str = self._saveModes[0] 41 | self._frameLimitGroupTag: int = editorHandle.getUniqueTag() 42 | self._frameLimitInputTag: int = editorHandle.getUniqueTag() 43 | self._frameLimitMin: int = 10 44 | self._frameLimit: int = 240 45 | self._currentImage: np.ndarray = np.zeros(shape=(2, 2)) 46 | self._frameCache: list[np.ndarray] = list() 47 | self._isRecording: bool = True 48 | self._overwrite: bool = True 49 | 50 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 51 | parentNodeTag=self._tag, 52 | attrType=AttributeType.Image) 53 | 54 | with dpg.node(tag=self._tag, 55 | parent=editorHandle.tag, 56 | label=self.nodeLabel, 57 | pos=pos): 58 | with dpg.node_attribute(tag=self._attrImageInput.tag, 59 | attribute_type=dpg.mvNode_Attr_Input, 60 | user_data=self._attrImageInput, 61 | shape=dpg.mvNode_PinShape_QuadFilled, 62 | indent=5): 63 | editorHandle.createFolderSelectionDialog(tag=self._folderDialogTag, callback=self.__callbackSetOutDir) 64 | dpg.add_button(label="select out dir", 65 | width=self._width - 10, 66 | callback=lambda: dpg.show_item(item=self._folderDialogTag)) 67 | with dpg.group(horizontal=True): 68 | dpg.add_text(default_value="out dir", indent=15) 69 | dpg.add_input_text(tag=self._outDirTextInputTag, 70 | width=self._width - 90, 71 | readonly=True) 72 | 73 | with dpg.group(horizontal=True): 74 | dpg.add_text(default_value="base name") 75 | dpg.add_input_text(tag=self._baseNameTextInputTag, 76 | width=self._width - 90, 77 | callback=self.__callbackBaseNameChange) 78 | 79 | with dpg.group(horizontal=True): 80 | dpg.add_text(default_value="fps", indent=48) 81 | dpg.add_input_int(tag=self._fpsInputTag, 82 | min_value=self._fpsRange[0], 83 | max_value=self._fpsRange[1], 84 | default_value=self._fps, 85 | width=self._width - 90, 86 | min_clamped=True, 87 | max_clamped=True, 88 | callback=self.__callbackFPSChange) 89 | 90 | with dpg.group(horizontal=True): 91 | dpg.add_text(default_value="size", indent=40) 92 | dpg.add_input_intx(tag=self._sizeInputTag, size=2, default_value=self._size, 93 | width=self._width - 90, callback=self.__callbackSizeChange) 94 | 95 | with dpg.group(horizontal=True): 96 | dpg.add_text(default_value="format", indent=23) 97 | dpg.add_combo(items=list(self._encoderType.keys()), 98 | default_value=self._fileFormat, 99 | width=self._width - 90, 100 | callback=self.__callbackFileFormatChange) 101 | 102 | with dpg.group(horizontal=True): 103 | dpg.add_text(default_value="save mode", indent=0) 104 | dpg.add_combo(items=self._saveModes, 105 | default_value=self._saveMode, 106 | width=self._width - 90, 107 | callback=self.__callbackSaveModeChange) 108 | 109 | with dpg.group(tag=self._frameLimitGroupTag, horizontal=True, show=False): 110 | dpg.add_text(default_value="limit", indent=32) 111 | dpg.add_input_int(tag=self._frameLimitInputTag, 112 | width=self._width - 90, 113 | min_value=self._frameLimitMin, 114 | min_clamped=True, 115 | default_value=self._frameLimit, 116 | callback=self.__callbackLimitChange) 117 | 118 | dpg.add_checkbox(label="record", 119 | default_value=self._isRecording, 120 | callback=self.__callbackRecordStateChange) 121 | 122 | dpg.add_checkbox(label="overwrite existing", 123 | default_value=self._overwrite, 124 | callback=self.__callbackOverWriteStateChange) 125 | 126 | with dpg.group(horizontal=True, horizontal_spacing=5, indent=50): 127 | dpg.add_text(tag=self._nameChangeIntTextTag, default_value="unq int: 1") 128 | dpg.add_button(label="R", width=30, height=30, callback=self.__callbackResetNameChanger) 129 | 130 | def update(self): 131 | if self._outDirPath is None: 132 | return 133 | 134 | if not self._isRecording: 135 | return 136 | 137 | data = self._attrImageInput.data 138 | 139 | if data is None: 140 | return 141 | 142 | if np.array_equal(data, self._currentImage): 143 | return 144 | 145 | self._currentImage = data.copy() 146 | self._frameCache.append(self._currentImage) 147 | print(len(self._frameCache)) 148 | if self._saveMode == "frame limit": 149 | if len(self._frameCache) < self._frameLimit: 150 | return 151 | frames = self._frameCache.copy() 152 | self.__writeVideo(frames=frames) 153 | self._frameCache.clear() 154 | 155 | def __writeVideo(self, frames: list[np.ndarray]): 156 | filePath = self._outDirPath.joinpath(self._fileBaseName + "_" 157 | + str(self._nameChangerInt) 158 | + self._fileFormat) 159 | if filePath.exists() and not self._overwrite: 160 | return 161 | self._nameChangerInt += 1 162 | dpg.set_value(item=self._nameChangeIntTextTag, value=f"unq int: {self._nameChangerInt}") 163 | theFormat = self._encoderType[self._fileFormat] 164 | encoder = cv2.VideoWriter_fourcc(*theFormat) 165 | writer = cv2.VideoWriter(str(filePath.resolve()), 166 | encoder, 167 | self._fps, 168 | self._size, 169 | True) 170 | for frame in frames: 171 | writer.write(frame[:, :, ::-1]) 172 | writer.release() 173 | 174 | def __callbackSetOutDir(self, _, data): 175 | self._outDirPath = Path(data["file_path_name"]) 176 | dpg.set_value(item=self._outDirTextInputTag, value=str(self._outDirPath.resolve())) 177 | self.__callbackResetNameChanger() 178 | 179 | def __callbackBaseNameChange(self, _, data): 180 | self._fileBaseName = data 181 | 182 | def __callbackFPSChange(self, _, data): 183 | self._fps = data 184 | 185 | def __callbackSizeChange(self, _, data): 186 | self._size = data 187 | 188 | def __callbackLimitChange(self, _, data): 189 | self._frameLimit = data 190 | 191 | def __callbackRecordStateChange(self, _, data): 192 | self._isRecording = data 193 | if self._outDirPath is None: 194 | return 195 | if not data: 196 | if self._frameCache: 197 | frames = self._frameCache.copy() 198 | self.__writeVideo(frames=frames) 199 | self._frameCache.clear() 200 | 201 | def __callbackOverWriteStateChange(self, _, data): 202 | self._overwrite = data 203 | 204 | def __callbackFileFormatChange(self, _, data): 205 | self._fileFormat = data 206 | 207 | def __callbackSaveModeChange(self, _, data): 208 | if data == "record stop": 209 | dpg.hide_item(item=self._frameLimitGroupTag) 210 | else: 211 | dpg.show_item(item=self._frameLimitGroupTag) 212 | self._saveMode = data 213 | 214 | def __callbackResetNameChanger(self): 215 | self._nameChangerInt = 1 216 | dpg.set_value(item=self._nameChangeIntTextTag, value="unq int: 1") 217 | -------------------------------------------------------------------------------- /nodes/viewers/node_image_view.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import dearpygui.dearpygui as dpg 3 | import numpy as np 4 | 5 | from node_editor.connection_objects import NodeAttribute, AttributeType 6 | from node_editor.editor import NodeEditor 7 | from nodes.node import NodeBase 8 | 9 | 10 | class Node(NodeBase): 11 | nodeLabel = "Image View" 12 | 13 | _settings = None 14 | 15 | def __init__(self, 16 | tag: int, 17 | pos: tuple[int, int], 18 | editorHandle: NodeEditor): 19 | super().__init__(tag=tag, editor=editorHandle) 20 | self._baseWidth: int = self.settings.nodeWidth 21 | self._baseHeight: int = self.settings.nodeHeight 22 | self._currentWidth: int = self._baseWidth * 2 23 | self._currentHeight: int = self._baseHeight * 2 24 | 25 | self._currentImage: np.ndarray = np.zeros(shape=(2, 2)) 26 | 27 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 28 | parentNodeTag=self._tag, 29 | attrType=AttributeType.Image) 30 | 31 | self.inAttrs.append(self._attrImageInput) 32 | self._previewTextureTag: int = editorHandle.getUniqueTag() 33 | self._previewImageTag: int = editorHandle.getUniqueTag() 34 | self._smallSizeTag: int = editorHandle.getUniqueTag() 35 | self._mediumSizeTag: int = editorHandle.getUniqueTag() 36 | self._largeSizeTag: int = editorHandle.getUniqueTag() 37 | self._customSizeTag: int = editorHandle.getUniqueTag() 38 | self._saveImageTag: int = editorHandle.getUniqueTag() 39 | self._saveImageDialogTag: int = editorHandle.getUniqueTag() 40 | self._customSizeDialogTag: int = editorHandle.getUniqueTag() 41 | self._customWidthInputTag: int = editorHandle.getUniqueTag() 42 | self._customHeightInputTag: int = editorHandle.getUniqueTag() 43 | 44 | background = np.zeros(shape=(self._currentWidth, self._currentHeight, 4), dtype=np.float32) 45 | background[:, :, 3] = 1 46 | background = background.ravel() 47 | with dpg.texture_registry(show=False): 48 | dpg.add_raw_texture(width=self._currentWidth, 49 | height=self._currentHeight, 50 | default_value=background, 51 | tag=self._previewTextureTag, 52 | format=dpg.mvFormat_Float_rgba) 53 | 54 | editorHandle.createSaveImageDialog(tag=self._saveImageDialogTag, 55 | callback=self.__callbackSaveImage) 56 | 57 | with dpg.node(tag=self._tag, 58 | parent=editorHandle.tag, 59 | label=self.nodeLabel, 60 | pos=pos): 61 | with dpg.node_attribute(tag=self._attrImageInput.tag, 62 | attribute_type=dpg.mvNode_Attr_Input, 63 | shape=dpg.mvNode_PinShape_QuadFilled): 64 | dpg.add_image(tag=self._previewImageTag, texture_tag=self._previewTextureTag) 65 | with dpg.popup(parent=self._previewImageTag): 66 | dpg.add_text(default_value="image size", color=(232, 173, 9)) 67 | dpg.add_separator() 68 | dpg.add_selectable(tag=self._smallSizeTag, width=120, label="small", 69 | callback=self.__callbackSmallSize) 70 | dpg.add_selectable(tag=self._mediumSizeTag, width=120, label="medium", default_value=True, 71 | callback=self.__callbackMediumSize) 72 | dpg.add_selectable(tag=self._largeSizeTag, width=120, label="large", 73 | callback=self.__callbackLargeSize) 74 | dpg.add_selectable(tag=self._customSizeTag, width=120, label="custom", 75 | callback=self.__callbackCustomSize) 76 | dpg.add_text(default_value="other", color=(232, 173, 9)) 77 | dpg.add_separator() 78 | dpg.add_selectable(tag=self._saveImageTag, width=120, label="save image", 79 | callback=self.__callbackShowSaveFileDialog) 80 | 81 | with dpg.window(tag=self._customSizeDialogTag, no_title_bar=True, modal=True, no_resize=True, 82 | show=False, no_scrollbar=True): 83 | dpg.add_text(default_value="custom size", color=(232, 173, 9)) 84 | dpg.add_separator() 85 | with dpg.group(horizontal=True): 86 | dpg.add_text(default_value="width", indent=8) 87 | dpg.add_input_int(tag=self._customWidthInputTag, 88 | default_value=self._currentWidth, 89 | width=160) 90 | with dpg.group(horizontal=True): 91 | dpg.add_text(default_value="height") 92 | dpg.add_input_int(tag=self._customHeightInputTag, 93 | default_value=self._currentHeight, 94 | width=160) 95 | dpg.add_button(label="Okay", 96 | indent=85, 97 | callback=self.__callbackCustomSize2) 98 | 99 | def update(self): 100 | data = self._attrImageInput.data 101 | if data is None: 102 | return 103 | if np.array_equal(a1=data, a2=self._currentImage): 104 | return 105 | if self._editor.paused: 106 | return 107 | self._currentImage = data.copy() 108 | previewImg = cv2.resize(src=data, dsize=(self._currentWidth, self._currentHeight)) 109 | dpg.set_value(item=self._previewTextureTag, value=previewImg.ravel()) 110 | 111 | def close(self): 112 | dpg.delete_item(item=self._tag) 113 | del self 114 | 115 | def __callbackSmallSize(self): 116 | self._editor.pause() 117 | dpg.set_value(item=self._smallSizeTag, value=True) 118 | dpg.set_value(item=self._mediumSizeTag, value=False) 119 | dpg.set_value(item=self._largeSizeTag, value=False) 120 | dpg.set_value(item=self._customSizeTag, value=False) 121 | dpg.set_value(item=self._saveImageTag, value=False) 122 | self._currentWidth = self._baseWidth 123 | self._currentHeight = self._baseHeight 124 | self.__updateSize() 125 | 126 | def __callbackMediumSize(self): 127 | self._editor.pause() 128 | dpg.set_value(item=self._smallSizeTag, value=False) 129 | dpg.set_value(item=self._mediumSizeTag, value=True) 130 | dpg.set_value(item=self._largeSizeTag, value=False) 131 | dpg.set_value(item=self._customSizeTag, value=False) 132 | dpg.set_value(item=self._saveImageTag, value=False) 133 | self._currentWidth = self._baseWidth * 2 134 | self._currentHeight = self._baseHeight * 2 135 | self.__updateSize() 136 | 137 | def __callbackLargeSize(self): 138 | self._editor.pause() 139 | dpg.set_value(item=self._smallSizeTag, value=False) 140 | dpg.set_value(item=self._mediumSizeTag, value=False) 141 | dpg.set_value(item=self._largeSizeTag, value=True) 142 | dpg.set_value(item=self._customSizeTag, value=False) 143 | dpg.set_value(item=self._saveImageTag, value=False) 144 | self._currentWidth = self._baseWidth * 3 145 | self._currentHeight = self._baseHeight * 3 146 | self.__updateSize() 147 | 148 | def __callbackCustomSize(self): 149 | self._editor.pause() 150 | dpg.set_value(item=self._smallSizeTag, value=False) 151 | dpg.set_value(item=self._mediumSizeTag, value=False) 152 | dpg.set_value(item=self._largeSizeTag, value=False) 153 | dpg.set_value(item=self._customSizeTag, value=True) 154 | dpg.set_value(item=self._saveImageTag, value=False) 155 | dpg.set_value(item=self._customWidthInputTag, value=self._currentWidth) 156 | dpg.set_value(item=self._customHeightInputTag, value=self._currentHeight) 157 | dpg.show_item(item=self._customSizeDialogTag) 158 | 159 | def __callbackCustomSize2(self): 160 | dpg.hide_item(item=self._customSizeDialogTag) 161 | self._currentWidth = dpg.get_value(item=self._customWidthInputTag) 162 | self._currentHeight = dpg.get_value(item=self._customHeightInputTag) 163 | self.__updateSize() 164 | 165 | def __updateSize(self): 166 | dpg.set_item_width(item=self._previewImageTag, width=self._currentWidth) 167 | dpg.set_item_height(item=self._previewImageTag, height=self._currentHeight) 168 | dpg.delete_item(self._previewTextureTag) 169 | background = np.zeros(shape=(self._currentWidth, self._currentHeight, 4), dtype=np.float32) 170 | background[:, :, 3] = 1 171 | background = background.ravel() 172 | with dpg.texture_registry(show=False): 173 | dpg.add_raw_texture(width=self._currentWidth, 174 | height=self._currentHeight, 175 | default_value=background, 176 | tag=self._previewTextureTag, 177 | format=dpg.mvFormat_Float_rgba) 178 | dpg.configure_item(item=self._previewImageTag, texture_tag=self._previewTextureTag) 179 | if self._attrImageInput.data is None: 180 | self._editor.resume() 181 | return 182 | img = self._currentImage.copy() 183 | previewImg = cv2.resize(src=img, dsize=(self._currentWidth, self._currentHeight)) 184 | dpg.set_value(item=self._previewTextureTag, value=previewImg.ravel()) 185 | self._editor.resume() 186 | 187 | def __callbackShowSaveFileDialog(self): 188 | self._editor.pause() 189 | dpg.set_value(item=self._saveImageTag, value=False) 190 | dpg.show_item(item=self._saveImageDialogTag) 191 | 192 | def __callbackSaveImage(self, _, data): 193 | if self._currentImage is not None: 194 | img = (self._currentImage * 255).astype(np.uint8) 195 | if data["current_filter"] == ".jpg" or img.ndim != 4: 196 | img = cv2.cvtColor(src=img, code=cv2.COLOR_RGBA2BGR) 197 | else: 198 | img = cv2.cvtColor(src=img, code=cv2.COLOR_RGBA2BGRA) 199 | cv2.imwrite(filename=data["file_path_name"], img=img) 200 | dpg.hide_item(item=self._saveImageDialogTag) 201 | self._editor.resume() 202 | -------------------------------------------------------------------------------- /node_editor/editor.py: -------------------------------------------------------------------------------- 1 | import time 2 | from importlib.util import spec_from_file_location, module_from_spec 3 | from pathlib import Path 4 | 5 | import dearpygui.dearpygui as dpg 6 | 7 | from node_editor.connection_objects import TreeNode, Connection, Tree 8 | from settings import AppSettings 9 | 10 | 11 | class NodeEditor(object): 12 | def __init__(self, 13 | settings: AppSettings, 14 | menuDict: dict, 15 | nodeDir: str): 16 | self._tree: Tree = Tree() 17 | self._updateInterval: float = settings.treeUpdateInterval 18 | self._updateT1: float = 0 19 | self._updateT2: float = 0 20 | self._nodeTagToNodeMap: dict = dict() 21 | self._counter: int = 9999 22 | self._terminated: bool = False 23 | self._settings: AppSettings = settings 24 | self._tag: int = self.getUniqueTag() 25 | self._windowTag: int = self.getUniqueTag() 26 | self._lastPos: tuple = (0, 0) 27 | self._paused: bool = False 28 | self._nodesPlannedToBeClosed: list = list() 29 | 30 | self._editorContextMenuTag: int = self.getUniqueTag() 31 | 32 | # creating the window 33 | with dpg.window(tag=self._windowTag, 34 | width=settings.windowWidth, 35 | height=settings.windowHeight, 36 | menubar=True, 37 | no_title_bar=True, 38 | no_close=True, 39 | no_collapse=True): 40 | 41 | # adding menubar 42 | with dpg.menu_bar(label="MenuBar"): 43 | with dpg.menu(label="Options"): 44 | dpg.add_menu_item(label="Toggle Full Screen", callback=dpg.toggle_viewport_fullscreen) 45 | for menuName, itemName in menuDict.items(): 46 | with dpg.menu(label=menuName): 47 | dirPath = Path(nodeDir).joinpath(itemName) 48 | nodePaths = dirPath.glob(pattern="*.py") 49 | for nodePath in nodePaths: 50 | if nodePath.name.startswith('__init__'): 51 | continue 52 | spec = spec_from_file_location(name=nodePath.stem, location=nodePath) 53 | module = module_from_spec(spec=spec) 54 | spec.loader.exec_module(module=module) 55 | 56 | node = module.Node 57 | dpg.add_menu_item(tag=self.getUniqueTag(), 58 | label=node.nodeLabel, 59 | callback=self.__callbackAddNode, 60 | user_data=node) 61 | 62 | # adding the actual node editor 63 | dpg.add_node_editor(tag=self._tag, 64 | callback=self.__callbackAddLink, 65 | delink_callback=self.callbackRemoveLink, 66 | minimap=True, 67 | minimap_location=dpg.mvNodeMiniMap_Location_BottomRight) 68 | 69 | with dpg.handler_registry(): 70 | dpg.add_mouse_click_handler(button=0, callback=self.__callbackLeftMouseClick) 71 | dpg.add_key_press_handler(key=dpg.mvKey_Delete, callback=self.__callbackRemoveNode) 72 | 73 | def __callbackLeftMouseClick(self): 74 | selectedNodesTags = dpg.get_selected_nodes(node_editor=self.tag) 75 | if selectedNodesTags: 76 | lastSelectedNodeTag = selectedNodesTags[0] 77 | self._lastPos = dpg.get_item_pos(item=lastSelectedNodeTag) 78 | 79 | @property 80 | def tag(self): 81 | return self._tag 82 | 83 | @property 84 | def windowTag(self): 85 | return self._windowTag 86 | 87 | @property 88 | def settings(self): 89 | return self._settings 90 | 91 | @property 92 | def terminated(self): 93 | return self._terminated 94 | 95 | @property 96 | def paused(self): 97 | return self._paused 98 | 99 | def pause(self): 100 | self._paused = True 101 | 102 | def resume(self): 103 | self._paused = False 104 | 105 | def terminate(self): 106 | self._paused = True 107 | self._nodesPlannedToBeClosed.extend(list(self._nodeTagToNodeMap.values())) 108 | self._terminated = True 109 | 110 | def __callbackAddNode(self, sender, data, user_data): 111 | # user_data is a node constructor 112 | tag = self.getUniqueTag() 113 | self._lastPos = (self._lastPos[0] + 30, self._lastPos[1] + 30) 114 | nodeobj = user_data(tag=tag, 115 | pos=self._lastPos, 116 | editorHandle=self) 117 | aTreeNode = TreeNode(tag=tag, inAttrs=nodeobj.inAttrs, outAttrs=nodeobj.outAttrs, updateFcn=nodeobj.update) 118 | self._tree.addNode(node=aTreeNode) 119 | self._nodeTagToNodeMap[tag] = nodeobj 120 | 121 | def __callbackRemoveNode(self): 122 | selectedLinksTags = dpg.get_selected_links(node_editor=self.tag) 123 | selectedNodesTags = dpg.get_selected_nodes(node_editor=self.tag) 124 | if selectedNodesTags: 125 | self._nodesPlannedToBeClosed.extend(selectedNodesTags) 126 | elif selectedLinksTags: 127 | for linkTag in selectedLinksTags: 128 | self.callbackRemoveLink(None, linkTag) 129 | 130 | def __removeNodes(self): 131 | for nodeTag in self._nodesPlannedToBeClosed: 132 | node = self._tree.getNodeByTag(tag=nodeTag) 133 | del self._nodeTagToNodeMap[nodeTag] 134 | dpg.delete_item(item=node.tag) 135 | self._tree.removeNodeByObject(node=node) 136 | self._nodesPlannedToBeClosed.clear() 137 | 138 | def __callbackAddLink(self, _, data): 139 | # data is (outAttrTag, inAttrTag) 140 | # remember that the link is directed: from outAttr of one node to inAttr of another 141 | outAttrTag, inAttrTag = data 142 | originAttr = self._tree.getAttrByTag(tag=outAttrTag) 143 | originNode = self._tree.getNodeByTag(tag=originAttr.parentNodeTag) 144 | targetAttr = self._tree.getAttrByTag(tag=inAttrTag) 145 | targetNode = self._tree.getNodeByTag(tag=targetAttr.parentNodeTag) 146 | if targetAttr.blocked and originAttr.attrType != targetAttr.attrType: 147 | return 148 | linkTag = self.getUniqueTag() 149 | aConnection = Connection(tag=linkTag, 150 | originNode=originNode, 151 | originAttr=originAttr, 152 | targetNode=targetNode, 153 | targetAttr=targetAttr) 154 | self._tree.addConnection(connection=aConnection) 155 | dpg.add_node_link(attr_1=outAttrTag, attr_2=inAttrTag, parent=self.tag, tag=linkTag) 156 | 157 | def callbackRemoveLink(self, sender, data): 158 | # data is linkTag 159 | # remember that the link is directed: from outAttr of one node to inAttr of another 160 | self._tree.removeConnectionByTag(tag=data) 161 | dpg.delete_item(item=data) 162 | 163 | def update(self): 164 | while not self._terminated: 165 | if self._paused: 166 | time.sleep(0.3) 167 | continue 168 | if self._nodesPlannedToBeClosed: 169 | self.__removeNodes() 170 | if self._tree.connections: 171 | 172 | self._tree.updateLevels() 173 | if not self._tree.levels: 174 | continue 175 | self._tree.updateConnections() 176 | self._tree.updateNodes() 177 | else: 178 | time.sleep(0.3) 179 | if self._nodesPlannedToBeClosed: 180 | self.__removeNodes() 181 | 182 | def getUniqueTag(self): 183 | tag = self._counter 184 | self._counter += 1 185 | return tag 186 | 187 | def createImageFileSelectionDialog(self, tag, callback): 188 | with dpg.file_dialog(tag=tag, 189 | directory_selector=False, 190 | default_path=self._settings.HomeDir, 191 | show=False, 192 | modal=True, 193 | width=int(self._settings.nodeWidth * 2.5), 194 | height=self._settings.nodeHeight * 3 + 100, 195 | callback=callback): 196 | dpg.add_file_extension(extension="Image (*.bmp *.jpg *.png *.gif){.bmp,.jpg,.png,.gif}") 197 | dpg.add_file_extension(extension="", color=(255, 182, 158)) 198 | 199 | def createVideoFileSelectionDialog(self, tag, callback): 200 | with dpg.file_dialog(tag=tag, 201 | default_path=self._settings.HomeDir, 202 | directory_selector=False, 203 | show=False, 204 | modal=True, 205 | width=int(self._settings.nodeWidth * 2.5), 206 | height=self._settings.nodeHeight * 3 + 100, 207 | callback=callback): 208 | dpg.add_file_extension("Movie (*.mp4 *.avi){.mp4,.avi}") 209 | dpg.add_file_extension("", color=(255, 182, 158)) 210 | 211 | def createFolderSelectionDialog(self, tag, callback): 212 | dpg.add_file_dialog(tag=tag, 213 | default_path=self._settings.HomeDir, 214 | directory_selector=True, 215 | show=False, 216 | modal=True, 217 | width=int(self._settings.nodeWidth * 2.5), 218 | height=self._settings.nodeHeight * 3 + 100, 219 | callback=callback) 220 | 221 | def createSaveImageDialog(self, tag, callback): 222 | with dpg.file_dialog(tag=tag, 223 | directory_selector=False, 224 | default_path=self._settings.HomeDir, 225 | default_filename="output", 226 | show=False, 227 | modal=True, 228 | width=int(self._settings.nodeWidth * 2.5), 229 | height=self._settings.nodeHeight * 3 + 100, 230 | callback=callback): 231 | dpg.add_file_extension(extension=".jpg,.png") 232 | dpg.add_file_extension(extension="", color=(255, 182, 158)) 233 | -------------------------------------------------------------------------------- /nodes/filters/node_convolution.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | import cv2 4 | import dearpygui.dearpygui as dpg 5 | import numpy as np 6 | 7 | from node_editor.connection_objects import NodeAttribute, AttributeType 8 | from node_editor.editor import NodeEditor 9 | from nodes.node import NodeBase 10 | 11 | 12 | class Node(NodeBase): 13 | nodeLabel = "Convolution" 14 | _borderTypes = ["default", "constant", "replicate", "reflect", "reflect101", "transparent", "isolated"] 15 | _borderType2CVEnumMap = {"default": cv2.BORDER_DEFAULT, 16 | "constant": cv2.BORDER_CONSTANT, 17 | "replicate": cv2.BORDER_REPLICATE, 18 | "reflect": cv2.BORDER_REFLECT, 19 | "reflect101": cv2.BORDER_REFLECT101, 20 | "transparent": cv2.BORDER_TRANSPARENT, 21 | "isolated": cv2.BORDER_ISOLATED} 22 | 23 | def __init__(self, 24 | tag: int, 25 | pos: tuple[int, int], 26 | editorHandle: NodeEditor): 27 | super().__init__(tag=tag, editor=editorHandle) 28 | self._width: int = self._settings.nodeWidth 29 | self._currentImage: Union[np.ndarray, None] = None 30 | self._border: str = self._borderTypes[0] 31 | self._kernelSize: tuple[int, int] = (3, 3) 32 | self._kernel: np.ndarray = np.ones(shape=self._kernelSize) 33 | self._kernelRange: tuple[float, float] = (-10, 10) 34 | self._newKernel: np.ndarray = self._kernel 35 | self._anchor: list[int, int] = [self._kernelSize[0] // 2, self._kernelSize[1] // 2] 36 | 37 | self._kernelSizeWindowTag: int = editorHandle.getUniqueTag() 38 | self._kernelWidthInputTag: int = editorHandle.getUniqueTag() 39 | self._kernelHeightInputTag: int = editorHandle.getUniqueTag() 40 | self._kernelValuesWindowTag: int = editorHandle.getUniqueTag() 41 | self._kernelValuesTableTag: int = editorHandle.getUniqueTag() 42 | self._kernelAnchorWindowTag: int = editorHandle.getUniqueTag() 43 | self._kernelAnchorTableTag: int = editorHandle.getUniqueTag() 44 | 45 | self._attrImageInput = NodeAttribute(tag=editorHandle.getUniqueTag(), 46 | parentNodeTag=self._tag, 47 | attrType=AttributeType.Image) 48 | self.inAttrs.append(self._attrImageInput) 49 | 50 | self._attrImageOutput = NodeAttribute(tag=editorHandle.getUniqueTag(), 51 | parentNodeTag=self._tag, 52 | attrType=AttributeType.Image) 53 | self.outAttrs.append(self._attrImageOutput) 54 | 55 | with dpg.node(tag=self._tag, 56 | parent=editorHandle.tag, 57 | label=self.nodeLabel, 58 | pos=pos): 59 | dpg.add_node_attribute(tag=self._attrImageInput.tag, 60 | attribute_type=dpg.mvNode_Attr_Input, 61 | shape=dpg.mvNode_PinShape_QuadFilled) 62 | 63 | with dpg.node_attribute(attribute_type=dpg.mvNode_Attr_Static): 64 | dpg.add_button(label="size", width=150, height=30, indent=45, 65 | callback=self.__callbackShowKernelSizeWindow) 66 | dpg.add_button(label="values", width=150, height=30, indent=45, 67 | callback=self.__callbackShowKernelValuesWindow) 68 | dpg.add_button(label="anchor", width=150, height=30, indent=45, 69 | callback=self.__callbackShowAnchorWindow) 70 | with dpg.group(horizontal=True): 71 | dpg.add_text(default_value="border type") 72 | dpg.add_combo(width=self._width - 95, 73 | items=self._borderTypes, 74 | default_value=self._border, 75 | callback=self.__callbackBorderTypeChange) 76 | 77 | with dpg.window(tag=self._kernelSizeWindowTag, no_title_bar=True, modal=True, no_resize=True, 78 | show=False, no_scrollbar=True): 79 | dpg.add_text(default_value="kernel size", color=(232, 173, 9)) 80 | dpg.add_separator() 81 | with dpg.group(horizontal=True): 82 | dpg.add_text(default_value="rows") 83 | dpg.add_input_int(tag=self._kernelWidthInputTag, 84 | default_value=self._kernelSize[0], 85 | width=160, 86 | min_value=1, 87 | min_clamped=True) 88 | with dpg.group(horizontal=True): 89 | dpg.add_text(default_value="cols") 90 | dpg.add_input_int(tag=self._kernelHeightInputTag, 91 | default_value=self._kernelSize[1], 92 | width=160, 93 | min_value=1, 94 | min_clamped=True) 95 | dpg.add_button(label="Okay", 96 | indent=85, 97 | callback=self.__callbackSetKernelSize) 98 | 99 | dpg.add_node_attribute(tag=self._attrImageOutput.tag, 100 | attribute_type=dpg.mvNode_Attr_Output, 101 | shape=dpg.mvNode_PinShape_Triangle) 102 | 103 | def update(self): 104 | data = self._attrImageInput.data 105 | if data is None: 106 | return 107 | if np.array_equal(data, self._currentImage): 108 | return 109 | self._currentImage = data 110 | self.__applyFilter() 111 | 112 | def close(self): 113 | dpg.delete_item(item=self._tag) 114 | 115 | def __applyFilter(self): 116 | if self._currentImage is None: 117 | return 118 | img = self._currentImage.copy() 119 | img = cv2.filter2D(src=img, 120 | ddepth=cv2.CV_32F, 121 | kernel=self._kernel, 122 | anchor=self._anchor, 123 | borderType=self._borderType2CVEnumMap[self._border]) 124 | self._attrImageOutput.data = img 125 | 126 | def __callbackShowKernelSizeWindow(self): 127 | self._editor.pause() 128 | dpg.set_value(item=self._kernelWidthInputTag, value=self._kernelSize[0]) 129 | dpg.set_value(item=self._kernelHeightInputTag, value=self._kernelSize[1]) 130 | dpg.show_item(item=self._kernelSizeWindowTag) 131 | 132 | def __callbackSetKernelSize(self): 133 | width = dpg.get_value(item=self._kernelWidthInputTag) 134 | height = dpg.get_value(item=self._kernelHeightInputTag) 135 | self._kernelSize = (width, height) 136 | self._kernel = np.ones(shape=self._kernelSize) 137 | self._newKernel = self._kernel 138 | dpg.hide_item(item=self._kernelSizeWindowTag) 139 | self._editor.resume() 140 | self.__applyFilter() 141 | 142 | def __callbackShowKernelValuesWindow(self): 143 | self._editor.pause() 144 | with dpg.window(tag=self._kernelValuesWindowTag, no_title_bar=True, modal=True, no_scrollbar=False, 145 | no_resize=True): 146 | dpg.add_text(default_value="kernel values", color=(232, 173, 9)) 147 | dpg.add_separator() 148 | with dpg.table(tag=self._kernelValuesTableTag, 149 | parent=self._kernelValuesWindowTag, 150 | header_row=False): 151 | for _ in range(self._kernelSize[1]): 152 | dpg.add_table_column() 153 | for i in range(self._kernelSize[0]): 154 | with dpg.table_row(): 155 | for k in range(self._kernelSize[1]): 156 | dpg.add_drag_float(width=55, 157 | speed=0.01, 158 | default_value=self._kernel[i, k], 159 | format="%.3f", 160 | user_data=[i, k], 161 | min_value=self._kernelRange[0], 162 | max_value=self._kernelRange[1], 163 | callback=self.__callbackUpdateKernel) 164 | dpg.add_button(label="Okay", 165 | indent=85, 166 | callback=self.__callbackSetKernelValues) 167 | 168 | def __callbackSetKernelValues(self): 169 | self._kernel = self._newKernel 170 | dpg.delete_item(item=self._kernelValuesWindowTag) 171 | self._editor.resume() 172 | self.__applyFilter() 173 | 174 | def __callbackUpdateKernel(self, _, data, user_data): 175 | self._newKernel[user_data[0], user_data[1]] = data 176 | 177 | def __callbackShowAnchorWindow(self): 178 | self._editor.pause() 179 | with dpg.window(tag=self._kernelAnchorWindowTag, no_title_bar=True, modal=True, no_scrollbar=False, 180 | no_resize=True): 181 | dpg.add_text(default_value="kernel anchor", color=(232, 173, 9)) 182 | dpg.add_separator() 183 | with dpg.table(tag=self._kernelAnchorTableTag, 184 | parent=self._kernelAnchorWindowTag, 185 | header_row=False): 186 | for _ in range(self._kernelSize[1]): 187 | dpg.add_table_column() 188 | for i in range(self._kernelSize[0]): 189 | with dpg.table_row(): 190 | for k in range(self._kernelSize[1]): 191 | dpg.add_selectable(label=f"{[i, k]}", 192 | default_value=False, 193 | callback=self.__callbackSetKernelAnchor, 194 | user_data=[i, k]) 195 | 196 | def __callbackSetKernelAnchor(self, _, data, user_data): 197 | dpg.delete_item(item=self._kernelAnchorWindowTag) 198 | self._anchor = user_data 199 | self._editor.resume() 200 | self.__applyFilter() 201 | 202 | def __callbackBorderTypeChange(self, _, data): 203 | self._border = data 204 | self.__applyFilter() 205 | -------------------------------------------------------------------------------- /nodes/viewers/objects/composer_objects.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import random 3 | from typing import Union 4 | 5 | import cv2 6 | import numpy as np 7 | 8 | 9 | class BlendModes(enum.Enum): 10 | NORMAL = 0 11 | DISSOLVE = 1 12 | DANCING_DISSOLVE = 2 13 | MULTIPLY = 3 14 | SCREEN = 4 15 | OVERLAY = 5 16 | SUBTRACT = 6 17 | DIFFERENCE = 7 18 | 19 | 20 | class Canvas: 21 | defaultWidth: int = 1920 22 | defaultHeight: int = 1080 23 | defaultColor: tuple[int, int, int, int] = (1, 1, 1, 1) 24 | 25 | def __init__(self): 26 | self._width: int = self.defaultWidth 27 | self._height: int = self.defaultHeight 28 | self._color = self.defaultColor 29 | self._img: np.ndarray = np.nan 30 | self._layers: list[CanvasImage] = list() 31 | self.createBackground() 32 | 33 | @property 34 | def width(self): 35 | return self._width 36 | 37 | @width.setter 38 | def width(self, value: int): 39 | self._width = value 40 | self.createBackground() 41 | 42 | @property 43 | def height(self): 44 | return self._height 45 | 46 | @height.setter 47 | def height(self, value: int): 48 | self._height = value 49 | self.createBackground() 50 | 51 | @property 52 | def color(self): 53 | return self._color 54 | 55 | @color.setter 56 | def color(self, value: tuple[int, int, int, int]): 57 | self._color = value 58 | self.createBackground() 59 | 60 | def createBackground(self): 61 | reds: np.ndarray = np.ones(shape=(self._height, self._width), dtype=np.float32) * self._color[0] 62 | greens: np.ndarray = np.ones(shape=(self._height, self._width), dtype=np.float32) * self._color[1] 63 | blues: np.ndarray = np.ones(shape=(self._height, self._width), dtype=np.float32) * self._color[2] 64 | alphas: np.ndarray = np.ones(shape=(self._height, self._width), dtype=np.float32) * self._color[3] 65 | self._img = (np.dstack(tup=(reds, greens, blues, alphas))).astype(np.float32) 66 | 67 | @property 68 | def layers(self): 69 | return self._layers 70 | 71 | def render(self) -> np.ndarray: 72 | img = self._img.copy() 73 | canvasHeight, canvasWidth = img.shape[:2] 74 | layers = self._layers.copy() 75 | layers.reverse() 76 | 77 | for layer in layers: 78 | if layer.currentImage is None: 79 | continue 80 | layerImage = layer.currentImage.copy() 81 | layerWidth = layer.currentWidth 82 | layerHeight = layer.currentHeight 83 | layerTop = layer.top 84 | layerLeft = layer.left 85 | layerAlpha = layer.opacity / 100 86 | layerBlendMode = layer.blendMode 87 | staticDissolveSeed = layer.staticDissolveSeed 88 | 89 | if layer.currentImage is None: 90 | continue 91 | if layerTop > canvasHeight \ 92 | or layerTop + canvasHeight < 0 \ 93 | or layerLeft > canvasWidth \ 94 | or layerLeft + canvasWidth < 0: 95 | continue 96 | 97 | # canvas start and end indices 98 | canvasRowStart = layerTop if layerTop > 0 else 0 99 | canvasRowEnd = layerTop + layerHeight 100 | if canvasRowEnd > canvasHeight: 101 | canvasRowEnd = canvasHeight 102 | 103 | canvasColStart = layerLeft if layerLeft > 0 else 0 104 | canvasColEnd = layerLeft + layerWidth 105 | if canvasColEnd > canvasWidth: 106 | canvasColEnd = canvasWidth 107 | 108 | # layer start and end indices 109 | layerRowStart = 0 if layerTop > 0 else abs(layerTop) 110 | if layerTop + layerHeight < canvasHeight: 111 | layerRowEnd = layerHeight 112 | else: 113 | layerRowEnd = canvasHeight - layerTop 114 | 115 | layerColStart = 0 if layerLeft > 0 else abs(layerLeft) 116 | if layerLeft + layerWidth < canvasWidth: 117 | layerColEnd = layerWidth 118 | else: 119 | layerColEnd = canvasWidth - layerLeft 120 | 121 | layerImage = layerImage[layerRowStart:layerRowEnd, layerColStart:layerColEnd] 122 | canvasRegion = img[canvasRowStart:canvasRowEnd, canvasColStart:canvasColEnd] 123 | indices2Fade = layerImage[:, :, 3] == 0 124 | pixels2replace = canvasRegion[indices2Fade].copy() 125 | 126 | # adding layers and applying blend mode 127 | if layerBlendMode == BlendModes.NORMAL: 128 | canvasRegion *= (1 - layerAlpha) 129 | canvasRegion += layerImage * layerAlpha 130 | 131 | elif layerBlendMode == BlendModes.DISSOLVE or layerBlendMode == BlendModes.DANCING_DISSOLVE: 132 | if layerBlendMode == BlendModes.DISSOLVE: 133 | np.random.seed(staticDissolveSeed) 134 | indices = np.random.choice(a=[True, False], 135 | size=(layerRowEnd - layerRowStart, layerColEnd - layerColStart), 136 | p=(layerAlpha, 1 - layerAlpha)) 137 | canvasRegion[indices] = 0 138 | layerImage[np.logical_not(indices)] = 0 139 | canvasRegion += layerImage 140 | 141 | elif layer.blendMode == BlendModes.MULTIPLY: 142 | layerImage *= canvasRegion 143 | canvasRegion *= (1 - layerAlpha) 144 | canvasRegion += layerImage * layerAlpha 145 | 146 | elif layer.blendMode == BlendModes.SCREEN: 147 | layerImage = 1 - (1 - layerImage) * (1 - canvasRegion) 148 | canvasRegion *= (1 - layerAlpha) 149 | canvasRegion += layerImage * layerAlpha 150 | 151 | elif layer.blendMode == BlendModes.OVERLAY: 152 | pass 153 | piece1 = layerImage * canvasRegion * 2 154 | piece2 = 1 - 2 * (1 - layerImage) * (1 - canvasRegion) 155 | condition = canvasRegion < 0.5 156 | layerImage = np.where(condition, piece1, piece2) 157 | canvasRegion *= (1 - layerAlpha) 158 | canvasRegion += layerImage * layerAlpha 159 | 160 | elif layer.blendMode == BlendModes.SUBTRACT: 161 | layerImage[:, :, :3] = canvasRegion[:, :, :3] - layerImage[:, :, :3] 162 | layerImage[layerImage < 0] = 0 163 | canvasRegion *= (1 - layerAlpha) 164 | canvasRegion += layerImage * layerAlpha 165 | 166 | elif layer.blendMode == BlendModes.DIFFERENCE: 167 | pass 168 | 169 | canvasRegion[indices2Fade] = pixels2replace 170 | return img 171 | 172 | 173 | class CanvasImage: 174 | def __init__(self): 175 | self._src: Union[np.ndarray, None] = None 176 | self._width: int = 0 177 | self._height: int = 0 178 | self._currentImage: Union[np.ndarray, None] = None 179 | self._currentWidth: int = 0 180 | self._currentHeight: int = 0 181 | self._scale: int = 1 182 | self._left: int = 0 183 | self._top: int = 0 184 | self._rot: float = 0 185 | self._blendMode: BlendModes = BlendModes.NORMAL 186 | self._opacity: int = 100 187 | self._staticDissolveSeed = random.randint(a=1, b=1000) 188 | 189 | @property 190 | def src(self): 191 | return self._src 192 | 193 | @src.setter 194 | def src(self, value: Union[np.ndarray, None]): 195 | self._src = value 196 | if value is None: 197 | self._currentImage = None 198 | return 199 | self._width = self._src.shape[1] 200 | self._height = self._src.shape[0] 201 | self.__update() 202 | 203 | @property 204 | def currentWidth(self): 205 | return self._currentWidth 206 | 207 | @property 208 | def currentHeight(self): 209 | return self._currentHeight 210 | 211 | @property 212 | def scale(self): 213 | return self._scale 214 | 215 | @scale.setter 216 | def scale(self, value: float): 217 | self._scale = value 218 | self.__update() 219 | 220 | @property 221 | def left(self): 222 | return self._left 223 | 224 | @left.setter 225 | def left(self, value: int): 226 | self._left = value 227 | 228 | @property 229 | def top(self): 230 | return self._top 231 | 232 | @top.setter 233 | def top(self, value: int): 234 | self._top = value 235 | 236 | @property 237 | def rot(self): 238 | return self._rot 239 | 240 | @rot.setter 241 | def rot(self, value: float): 242 | self._rot = value 243 | self.__update() 244 | 245 | @property 246 | def blendMode(self): 247 | return self._blendMode 248 | 249 | @blendMode.setter 250 | def blendMode(self, value: BlendModes): 251 | self._blendMode = value 252 | 253 | @property 254 | def opacity(self): 255 | return self._opacity 256 | 257 | @opacity.setter 258 | def opacity(self, value: int): 259 | self._opacity = value 260 | 261 | @property 262 | def staticDissolveSeed(self): 263 | return self._staticDissolveSeed 264 | 265 | @property 266 | def currentImage(self): 267 | return self._currentImage 268 | 269 | def __update(self): 270 | self._currentImage = self.rotate_image(mat=self._src, angle=self._rot) 271 | self._currentWidth = int(self._currentImage.shape[1] * self._scale) 272 | self._currentHeight = int(self._currentImage.shape[0] * self._scale) 273 | self._currentImage = cv2.resize(src=self._currentImage, dsize=(self._currentWidth, self._currentHeight)) 274 | 275 | def rotate_image(self, mat, angle): 276 | """ 277 | Rotates an image (angle in degrees) and expands image to avoid cropping 278 | """ 279 | 280 | height, width = mat.shape[:2] 281 | image_center = (width / 2, 282 | height / 2) 283 | 284 | rotation_mat = cv2.getRotationMatrix2D(center=image_center, angle=angle, scale=1) 285 | 286 | # rotation calculates the cos and sin, taking absolutes of those. 287 | abs_cos = abs(rotation_mat[0, 0]) 288 | abs_sin = abs(rotation_mat[0, 1]) 289 | 290 | # find the new width and height bounds 291 | bound_w = int(height * abs_sin + width * abs_cos) 292 | bound_h = int(height * abs_cos + width * abs_sin) 293 | 294 | # subtract old image center (bringing image back to origo) and adding the new image center coordinates 295 | rotation_mat[0, 2] += bound_w / 2 - image_center[0] 296 | rotation_mat[1, 2] += bound_h / 2 - image_center[1] 297 | 298 | # rotate image with the new bounds and translated rotation matrix 299 | rotated_mat = cv2.warpAffine(src=mat, M=rotation_mat, dsize=(bound_w, bound_h), borderValue=0) 300 | return rotated_mat 301 | --------------------------------------------------------------------------------