pynode-flow 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,102 @@
1
+ """
2
+ OpenCV FFT Node - performs Fast Fourier Transform on an image.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Dict
8
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
9
+
10
+ _info = Info()
11
+ _info.add_text("Performs Fast Fourier Transform (FFT) on an input image to extract frequency components.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullet("Input 0:", "Input image (converted to grayscale automatically)")
14
+ _info.add_header("Outputs")
15
+ _info.add_bullet("Output 0:", "Magnitude Spectrum (visualizable) + FFT Complex Data (in metadata)")
16
+ _info.add_header("Notes")
17
+ _info.add_text("The complex frequency data is stored in the message metadata ('_fft_data') for downstream processing by the Frequency Filter node.")
18
+ _info.add_text("The visible output is the Magnitude Spectrum (log-scaled).")
19
+
20
+
21
+ class FFTNode(BaseNode):
22
+ """
23
+ FFT Node - converts image to frequency domain.
24
+ """
25
+ info = str(_info)
26
+ display_name = 'FFT'
27
+ icon = '∿'
28
+ category = 'opencv'
29
+ color = '#8E44AD' # Purple-ish
30
+ border_color = '#6C3483'
31
+ text_color = '#FFFFFF'
32
+ input_count = 1
33
+ output_count = 1
34
+
35
+ DEFAULT_CONFIG = {
36
+ 'output_view': 'magnitude'
37
+ }
38
+
39
+ properties = [
40
+ {
41
+ 'name': 'output_view',
42
+ 'label': 'Output View',
43
+ 'type': 'select',
44
+ 'options': [
45
+ {'value': 'magnitude', 'label': 'Magnitude Spectrum'}
46
+ ],
47
+ 'default': DEFAULT_CONFIG['output_view'],
48
+ 'help': 'What to display in the image payload'
49
+ }
50
+ ]
51
+
52
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
53
+ """Process incoming image and compute FFT."""
54
+ if MessageKeys.PAYLOAD not in msg:
55
+ self.send(msg)
56
+ return
57
+
58
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
59
+ if img is None:
60
+ self.send(msg)
61
+ return
62
+
63
+ if format_type is None:
64
+ format_type = 'numpy_array'
65
+
66
+ # Convert to grayscale if necessary
67
+ if len(img.shape) == 3:
68
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
69
+ else:
70
+ gray = img
71
+
72
+ # Compute DFT
73
+ # cv2.dft expects float32
74
+ dft = cv2.dft(np.float32(gray), flags=cv2.DFT_COMPLEX_OUTPUT)
75
+ dft_shift = np.fft.fftshift(dft)
76
+
77
+ # Compute Magnitude Spectrum for visualization
78
+ # magnitude = sqrt(Re^2 + Im^2)
79
+ magnitude = cv2.magnitude(dft_shift[:,:,0], dft_shift[:,:,1])
80
+
81
+ # Log scale: 20 * log(1 + mag)
82
+ magnitude_spectrum = 20 * np.log(magnitude + 1)
83
+
84
+ # Normalize to 0-255
85
+ cv2.normalize(magnitude_spectrum, magnitude_spectrum, 0, 255, cv2.NORM_MINMAX)
86
+ result_img = np.uint8(magnitude_spectrum)
87
+
88
+ # Create new message with result
89
+ new_msg = msg.copy()
90
+
91
+ # Encode result image
92
+ if not isinstance(new_msg.get(MessageKeys.PAYLOAD), dict):
93
+ new_msg[MessageKeys.PAYLOAD] = {}
94
+
95
+ new_msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result_img, format_type)
96
+
97
+ # Store complex data for downstream nodes
98
+ # Use a hidden key to pass the raw complex numpy array
99
+ new_msg['_fft_data'] = dft_shift
100
+ new_msg['_fft_shape'] = gray.shape # Store original shape
101
+
102
+ self.send(new_msg)
@@ -0,0 +1,189 @@
1
+ """
2
+ OpenCV Focus Estimation Node - measures image sharpness using various focus operators.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Dict
8
+ from pynode.nodes.base_node import BaseNode, process_image, Info, MessageKeys
9
+
10
+ _info = Info()
11
+ _info.add_text("Estimates image focus/sharpness using various computational methods. Outputs a focus score indicating how sharp the image is.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Source image"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(("Output 0:", "Message with focus score (and optionally the image)"))
16
+ _info.add_header("Focus Methods")
17
+ _info.add_bullets(
18
+ ("Laplacian:", "Variance of Laplacian operator - best for general use"),
19
+ ("Tenengrad:", "Sobel gradient magnitude - excellent edge detection"),
20
+ ("Sobel + Variance:", "Combined gradient and variance - robust method"),
21
+ ("Brenner Gradient:", "Pixel difference method - fast and simple"),
22
+ ("Local Variance:", "Local intensity variations - works for high contrast"),
23
+ ("Entropy:", "Shannon entropy - good for texture-rich images")
24
+ )
25
+ _info.add_header("Output")
26
+ _info.add_bullets(
27
+ ("focus_score:", "Numerical sharpness value (higher = sharper)"),
28
+ ("method:", "The focus estimation method used"),
29
+ ("image:", "Original image (if 'Include Image' is enabled)")
30
+ )
31
+
32
+ class FocusEstimationNode(BaseNode):
33
+ """
34
+ Focus Estimation node - measures image sharpness using various focus operators.
35
+ """
36
+ info = str(_info)
37
+ display_name = 'Focus Estimation'
38
+ icon = '⌖'
39
+ category = 'opencv'
40
+ color = '#4A90D9'
41
+ border_color = '#2E6BB0'
42
+ text_color = '#FFFFFF'
43
+ input_count = 1
44
+ output_count = 1
45
+
46
+ DEFAULT_CONFIG = {
47
+ 'method': 'laplacian',
48
+ 'include_image': False
49
+ }
50
+
51
+ properties = [
52
+ {
53
+ 'name': 'method',
54
+ 'label': 'Focus Method',
55
+ 'type': 'select',
56
+ 'options': [
57
+ {'value': 'laplacian', 'label': 'Laplacian (Recommended)'},
58
+ {'value': 'tenengrad', 'label': 'Tenengrad (Sobel)'},
59
+ {'value': 'sobel_variance', 'label': 'Sobel + Variance'},
60
+ {'value': 'brenner', 'label': 'Brenner Gradient'},
61
+ {'value': 'variance', 'label': 'Local Variance'},
62
+ {'value': 'entropy', 'label': 'Entropy'}
63
+ ],
64
+ 'default': DEFAULT_CONFIG['method'],
65
+ 'help': 'Method to compute focus score'
66
+ },
67
+ {
68
+ 'name': 'include_image',
69
+ 'label': 'Include Image',
70
+ 'type': 'checkbox',
71
+ 'default': DEFAULT_CONFIG['include_image'],
72
+ 'help': 'Include image in output message'
73
+ }
74
+ ]
75
+
76
+ def __init__(self, node_id=None, name="focus_estimation"):
77
+ super().__init__(node_id, name)
78
+
79
+ def _compute_laplacian(self, gray: np.ndarray) -> float:
80
+ """
81
+ Laplacian-based focus measure.
82
+ Returns variance of Laplacian - sharp images have higher variance.
83
+ """
84
+ laplacian = cv2.Laplacian(gray, cv2.CV_64F)
85
+ return float(np.var(laplacian))
86
+
87
+ def _compute_tenengrad(self, gray: np.ndarray) -> float:
88
+ """
89
+ Tenengrad (Sobel gradient) focus measure.
90
+ Returns mean gradient magnitude - sharp images have stronger edges.
91
+ """
92
+ sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
93
+ sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
94
+ tenengrad = np.sqrt(sobel_x**2 + sobel_y**2)
95
+ return float(np.mean(tenengrad))
96
+
97
+ def _compute_sobel_variance(self, gray: np.ndarray) -> float:
98
+ """
99
+ Combined Sobel + Variance focus measure.
100
+ Returns sum of mean gradient magnitude and variance.
101
+ """
102
+ sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
103
+ sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
104
+ sobel_magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
105
+ variance = np.var(gray)
106
+ return float(np.mean(sobel_magnitude) + variance)
107
+
108
+ def _compute_brenner_gradient(self, gray: np.ndarray) -> float:
109
+ """
110
+ Brenner gradient focus measure.
111
+ Returns sum of squared differences between pixels 2 positions apart.
112
+ """
113
+ shifted = np.roll(gray, -2, axis=1)
114
+ diff = (gray.astype(np.float64) - shifted.astype(np.float64)) ** 2
115
+ return float(np.sum(diff))
116
+
117
+ def _compute_local_variance(self, gray: np.ndarray, ksize: int = 5) -> float:
118
+ """
119
+ Local variance focus measure.
120
+ Returns mean of local variance - sharp images have higher local variations.
121
+ """
122
+ gray_float = gray.astype(np.float64)
123
+ mean = cv2.blur(gray_float, (ksize, ksize))
124
+ squared_mean = cv2.blur(gray_float**2, (ksize, ksize))
125
+ variance = squared_mean - (mean**2)
126
+ return float(np.mean(variance))
127
+
128
+ def _compute_entropy(self, gray: np.ndarray) -> float:
129
+ """
130
+ Entropy-based focus measure using Shannon entropy.
131
+ Returns entropy value - sharp images have higher entropy.
132
+ """
133
+ # Compute histogram
134
+ hist = cv2.calcHist([gray], [0], None, [256], [0, 256])
135
+ hist = hist.flatten()
136
+
137
+ # Normalize histogram to get probability distribution
138
+ hist = hist / hist.sum()
139
+
140
+ # Remove zero entries
141
+ hist = hist[hist > 0]
142
+
143
+ # Compute Shannon entropy
144
+ entropy = -np.sum(hist * np.log2(hist))
145
+ return float(entropy)
146
+
147
+ @process_image()
148
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
149
+ """Compute focus score for the input image."""
150
+ method = self.config.get('method', 'laplacian')
151
+ include_image = self.get_config_bool('include_image', False)
152
+
153
+ # Convert to grayscale for focus computation
154
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
155
+
156
+ # Compute focus score based on selected method
157
+ if method == 'laplacian':
158
+ focus_score = self._compute_laplacian(gray)
159
+ elif method == 'tenengrad':
160
+ focus_score = self._compute_tenengrad(gray)
161
+ elif method == 'sobel_variance':
162
+ focus_score = self._compute_sobel_variance(gray)
163
+ elif method == 'brenner':
164
+ focus_score = self._compute_brenner_gradient(gray)
165
+ elif method == 'variance':
166
+ focus_score = self._compute_local_variance(gray)
167
+ elif method == 'entropy':
168
+ focus_score = self._compute_entropy(gray)
169
+ else:
170
+ focus_score = self._compute_laplacian(gray)
171
+
172
+ # Create output message with focus score
173
+ output_msg = {MessageKeys.PAYLOAD: {}}
174
+
175
+ # Store the focus estimation results in msg.payload
176
+ output_msg[MessageKeys.PAYLOAD] = {
177
+ 'focus_score': focus_score,
178
+ 'method': method
179
+ }
180
+
181
+ # Include image if requested
182
+ if include_image:
183
+ output_msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = image
184
+
185
+ # Send the message with focus information
186
+ self.send(output_msg, 0)
187
+
188
+ # Return None since we're sending a custom message
189
+ return None
@@ -0,0 +1,241 @@
1
+ """
2
+ OpenCV Frequency Filter Node - filters frequencies and performs inverse FFT.
3
+ """
4
+
5
+ import logging
6
+
7
+ import cv2
8
+ import numpy as np
9
+ from typing import Any, Dict
10
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _info = Info()
15
+ _info.add_text("Modifies frequency components (filtering) and reconstructs the image via Inverse FFT.")
16
+ _info.add_header("Inputs")
17
+ _info.add_bullet("Input 0:", "FFT Node output (must contain frequency data)")
18
+ _info.add_header("Outputs")
19
+ _info.add_bullet("Output 0:", "Filtered Image (spatial domain) or Filtered Spectrum")
20
+ _info.add_header("Operations")
21
+ _info.add_bullets(
22
+ ("Low Pass:", "Keeps low frequencies (center), suppresses details/noise. Blurs the image."),
23
+ ("High Pass:", "Keeps high frequencies (edges), suppresses smooth areas. Edge detection."),
24
+ ("Notch Filter:", "Manually blocks specific frequency spots (harmonics/noise) using coordinates.")
25
+ )
26
+ _info.add_header("Notch Configuration")
27
+ _info.add_text("Enter coordinates to block in the 'Notch Filters' field. Format: x,y,radius; x2,y2,radius2.")
28
+ _info.add_text("Example: '100,200,5; 340,150,10' blocks two circular regions.")
29
+
30
+
31
+ class FrequencyFilterNode(BaseNode):
32
+ """
33
+ Frequency Filter Node - applies masks to frequency domain data.
34
+ """
35
+ info = str(_info)
36
+ display_name = 'Freq Filter'
37
+ icon = '⌖'
38
+ category = 'opencv'
39
+ color = '#8E44AD' # Same purple scheme
40
+ border_color = '#6C3483'
41
+ text_color = '#FFFFFF'
42
+ input_count = 1
43
+ output_count = 1
44
+
45
+ DEFAULT_CONFIG = {
46
+ 'filter_type': 'low_pass',
47
+ 'radius': 30,
48
+ 'notch_filters': '',
49
+ 'symmetric_notches': 'true',
50
+ 'output_mode': 'reconstructed'
51
+ }
52
+
53
+ properties = [
54
+ {
55
+ 'name': 'filter_type',
56
+ 'label': 'Filter Type',
57
+ 'type': 'select',
58
+ 'options': [
59
+ {'value': 'low_pass', 'label': 'Low Pass (Blur)'},
60
+ {'value': 'high_pass', 'label': 'High Pass (Edges)'},
61
+ {'value': 'none', 'label': 'None (Pass Through / Just Notches)'}
62
+ ],
63
+ 'default': DEFAULT_CONFIG['filter_type']
64
+ },
65
+ {
66
+ 'name': 'radius',
67
+ 'label': 'Cutoff Radius',
68
+ 'type': 'number',
69
+ 'default': DEFAULT_CONFIG['radius'],
70
+ 'min': 1,
71
+ 'max': 500
72
+ },
73
+ {
74
+ 'name': 'notch_filters',
75
+ 'label': 'Notch Filters (x,y,r; ...)',
76
+ 'type': 'text',
77
+ 'default': DEFAULT_CONFIG['notch_filters'],
78
+ 'help': 'Block specific frequencies. Format: x,y,radius (semicolon separated). E.g. 120,45,5'
79
+ },
80
+ {
81
+ 'name': 'symmetric_notches',
82
+ 'label': 'Apply Symmetrically',
83
+ 'type': 'checkbox',
84
+ 'default': DEFAULT_CONFIG['symmetric_notches'],
85
+ 'help': 'Automatically block the symmetric point across the center (required for real signals)'
86
+ },
87
+ {
88
+ 'name': 'output_mode',
89
+ 'label': 'Output Mode',
90
+ 'type': 'select',
91
+ 'options': [
92
+ {'value': 'reconstructed', 'label': 'Reconstructed Image'},
93
+ {'value': 'spectrum', 'label': 'Filtered Spectrum (View)'},
94
+ {'value': 'both', 'label': 'Both (Reconstructed + Spectrum)'}
95
+ ],
96
+ 'default': DEFAULT_CONFIG['output_mode'],
97
+ 'help': 'View the resulting image or the modified frequency spectrum'
98
+ }
99
+ ]
100
+
101
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
102
+ """Process incoming frequency data."""
103
+ # Check for FFT data
104
+ dft_shift = msg.get('_fft_data')
105
+
106
+ if dft_shift is None:
107
+ # If no FFT data, try to look for image in payload to auto-convert (optional convenience)
108
+ # But strictly, we should expect FFT data.
109
+ # Let's pass through if we can't process
110
+ self.send(msg)
111
+ return
112
+
113
+ rows, cols = dft_shift.shape[:2]
114
+ crow, ccol = rows//2, cols//2 # Center
115
+
116
+ # Create a mask, first separate channels (2 channels: Real, Imag)
117
+ # We need a mask that applies to both channels
118
+ mask = np.zeros((rows, cols, 2), np.uint8)
119
+
120
+ try:
121
+ radius = int(self.config.get('radius', 30))
122
+ except ValueError:
123
+ radius = 30
124
+
125
+ filter_type = self.config.get('filter_type', 'low_pass')
126
+
127
+ # Create circular mask area
128
+ # Using grid indices
129
+ y, x = np.ogrid[:rows, :cols]
130
+ mask_area = (x - ccol)**2 + (y - crow)**2 <= radius*radius
131
+
132
+ if filter_type == 'low_pass':
133
+ # 1 inside circle, 0 outside
134
+ mask[mask_area] = 1
135
+ elif filter_type == 'high_pass':
136
+ # 0 inside circle, 1 outside
137
+ mask[~mask_area] = 1
138
+ else: # none / pass-through
139
+ mask.fill(1)
140
+
141
+ # Apply Notch Filters (Manual Blocking)
142
+ notch_str = self.config.get('notch_filters', '')
143
+ symmetric = self.config.get('symmetric_notches', 'true') == 'true'
144
+
145
+ if notch_str:
146
+ try:
147
+ for notch_def in notch_str.split(';'):
148
+ if not notch_def.strip():
149
+ continue
150
+
151
+ parts = [float(p.strip()) for p in notch_def.split(',') if p.strip()]
152
+ if len(parts) >= 2:
153
+ nx, ny = int(parts[0]), int(parts[1])
154
+ # Default radius 5 if not specified
155
+ nr = int(parts[2]) if len(parts) > 2 else 5
156
+
157
+ # Apply notch at (nx, ny)
158
+ # Remember x matches cols index, y matches rows index
159
+ notch_area = (x - nx)**2 + (y - ny)**2 <= nr*nr
160
+ mask[notch_area] = 0
161
+
162
+ if symmetric:
163
+ # Calculate symmetric point relative to center (ccol, crow)
164
+ # Actually relative to image dimensions: (cols - x, rows - y)
165
+ # Center is at axis of rotation
166
+ # Point (x, y) reflected across (cx, cy) is (2cx - x, 2cy - y)
167
+ # Since cx=cols/2, 2cx = cols. So (cols - x, rows - y) approximately
168
+ # Let's use exact center calculation
169
+
170
+ # Center of image is (cols/2, rows/2).
171
+ # Because indices are 0-based, exact center is (cols-1)/2 ??
172
+ # FFT shift puts DC at rows//2, cols//2.
173
+
174
+ sym_x = 2 * ccol - nx
175
+ sym_y = 2 * crow - ny
176
+
177
+ # Ensure within bounds
178
+ # if 0 <= sym_x < cols and 0 <= sym_y < rows:
179
+ # Actually, simpler logic:
180
+ # The mask uses coordinates. Creating a circular mask at sym coords works.
181
+
182
+ notch_area_sym = (x - sym_x)**2 + (y - sym_y)**2 <= nr*nr
183
+ mask[notch_area_sym] = 0
184
+
185
+ except Exception as e:
186
+ # Log error but continue with what we have
187
+ logger.error(f"Error parsing notch filters: {e}")
188
+
189
+ # Apply mask
190
+ fshift = dft_shift * mask
191
+
192
+ result_img = None
193
+ spectrum_img = None
194
+ output_mode = self.config.get('output_mode', 'reconstructed')
195
+
196
+ def compute_spectrum(f_data):
197
+ mag = cv2.magnitude(f_data[:,:,0], f_data[:,:,1])
198
+ mag_spec = 20 * np.log(mag + 1)
199
+ cv2.normalize(mag_spec, mag_spec, 0, 255, cv2.NORM_MINMAX)
200
+ return np.uint8(mag_spec)
201
+
202
+ def compute_reconstruction(f_data):
203
+ f_ishift = np.fft.ifftshift(f_data)
204
+ img_b = cv2.idft(f_ishift)
205
+ img_b = cv2.magnitude(img_b[:,:,0], img_b[:,:,1])
206
+ cv2.normalize(img_b, img_b, 0, 255, cv2.NORM_MINMAX)
207
+ return np.uint8(img_b)
208
+
209
+ if output_mode == 'spectrum':
210
+ result_img = compute_spectrum(fshift)
211
+ elif output_mode == 'both':
212
+ result_img = compute_reconstruction(fshift)
213
+ spectrum_img = compute_spectrum(fshift)
214
+ else:
215
+ result_img = compute_reconstruction(fshift)
216
+
217
+ # Create response
218
+ new_msg = msg.copy()
219
+
220
+ # Update payload
221
+ # Use existing format type or default to jpg
222
+ # We can extract format from payload path if present, or just assume
223
+ # The encode_image method usually handles basic types.
224
+
225
+ # We need to construct a proper payload structure if it doesn't exist
226
+ if MessageKeys.PAYLOAD not in new_msg or not isinstance(new_msg[MessageKeys.PAYLOAD], dict):
227
+ new_msg[MessageKeys.PAYLOAD] = {}
228
+
229
+ # Encode
230
+ # Use bgr_numpy_dict to provide a consistent object structure (data, width, height, etc)
231
+ # while keeping the data as numpy array for efficiency.
232
+ format_type = 'bgr_numpy_dict'
233
+ new_msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result_img, format_type)
234
+
235
+ if spectrum_img is not None:
236
+ new_msg[MessageKeys.PAYLOAD]['spectrum'] = self.encode_image(spectrum_img, format_type)
237
+
238
+ # Update metadata for chaining (optional)
239
+ new_msg['_fft_data'] = fshift
240
+
241
+ self.send(new_msg)
@@ -0,0 +1,160 @@
1
+ """
2
+ OpenCV Histogram Node - computes and optionally equalizes image histograms.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Dict
8
+ from pynode.nodes.base_node import BaseNode, process_image, Info
9
+
10
+ _info = Info()
11
+ _info.add_text("Computes histograms and applies histogram operations for contrast enhancement. For color images, operates on the L channel in LAB color space.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Image (color or grayscale)"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(
16
+ ("Output 0:", "Processed image"),
17
+ ("msg.histogram:", "Array of 256 values representing pixel distribution")
18
+ )
19
+ _info.add_header("Operations")
20
+ _info.add_bullets(
21
+ ("Compute only:", "Calculate histogram without modifying image"),
22
+ ("Equalize:", "Standard histogram equalization for global contrast"),
23
+ ("CLAHE:", "Adaptive equalization that prevents over-amplification"),
24
+ ("Normalize:", "Stretch pixel values to specified range")
25
+ )
26
+ _info.add_header("Tips")
27
+ _info.add_bullets(
28
+ ("CLAHE:", "Best for images with varying lighting conditions"),
29
+ ("Clip Limit:", "Higher values = more contrast, may increase noise")
30
+ )
31
+
32
+
33
+ class HistogramNode(BaseNode):
34
+ info = str(_info)
35
+ """
36
+ Histogram node - computes histograms and applies histogram equalization.
37
+ """
38
+ display_name = 'Histogram'
39
+ icon = '📊'
40
+ category = 'opencv'
41
+ color = '#4A90D9'
42
+ border_color = '#2E6BB0'
43
+ text_color = '#FFFFFF'
44
+ input_count = 1
45
+ output_count = 1
46
+
47
+ DEFAULT_CONFIG = {
48
+ 'operation': 'equalize',
49
+ 'clip_limit': 2.0,
50
+ 'tile_size': 8,
51
+ 'normalize_alpha': 0,
52
+ 'normalize_beta': 255
53
+ }
54
+
55
+ properties = [
56
+ {
57
+ 'name': 'operation',
58
+ 'label': 'Operation',
59
+ 'type': 'select',
60
+ 'options': [
61
+ {'value': 'compute', 'label': 'Compute histogram only'},
62
+ {'value': 'equalize', 'label': 'Histogram equalization'},
63
+ {'value': 'clahe', 'label': 'CLAHE (adaptive)'},
64
+ {'value': 'normalize', 'label': 'Normalize'}
65
+ ],
66
+ 'default': DEFAULT_CONFIG['operation'],
67
+ 'help': 'Histogram operation to perform'
68
+ },
69
+ {
70
+ 'name': 'clip_limit',
71
+ 'label': 'CLAHE Clip Limit',
72
+ 'type': 'number',
73
+ 'default': DEFAULT_CONFIG['clip_limit'],
74
+ 'min': 0.1,
75
+ 'max': 10,
76
+ 'step': 0.1,
77
+ 'help': 'Threshold for contrast limiting (CLAHE)',
78
+ 'showIf': {'operation': 'clahe'}
79
+ },
80
+ {
81
+ 'name': 'tile_size',
82
+ 'label': 'CLAHE Tile Size',
83
+ 'type': 'number',
84
+ 'default': DEFAULT_CONFIG['tile_size'],
85
+ 'min': 2,
86
+ 'max': 32,
87
+ 'help': 'Size of grid for CLAHE',
88
+ 'showIf': {'operation': 'clahe'}
89
+ },
90
+ {
91
+ 'name': 'normalize_alpha',
92
+ 'label': 'Normalize Alpha',
93
+ 'type': 'number',
94
+ 'default': DEFAULT_CONFIG['normalize_alpha'],
95
+ 'min': 0,
96
+ 'max': 255,
97
+ 'help': 'Lower bound for normalization',
98
+ 'showIf': {'operation': 'normalize'}
99
+ },
100
+ {
101
+ 'name': 'normalize_beta',
102
+ 'label': 'Normalize Beta',
103
+ 'type': 'number',
104
+ 'default': DEFAULT_CONFIG['normalize_beta'],
105
+ 'min': 0,
106
+ 'max': 255,
107
+ 'help': 'Upper bound for normalization',
108
+ 'showIf': {'operation': 'normalize'}
109
+ }
110
+ ]
111
+
112
+ def __init__(self, node_id=None, name="histogram"):
113
+ super().__init__(node_id, name)
114
+
115
+ @process_image()
116
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
117
+ """Process image histogram."""
118
+ operation = self.config.get('operation', 'equalize')
119
+ clip_limit = self.get_config_float('clip_limit', 2.0)
120
+ tile_size = self.get_config_int('tile_size', 8)
121
+ alpha = self.get_config_int('normalize_alpha', 0)
122
+ beta = self.get_config_int('normalize_beta', 255)
123
+
124
+ # Convert to grayscale for processing if color
125
+ if len(image.shape) == 3:
126
+ is_color = True
127
+ # Convert to LAB for better color handling
128
+ lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
129
+ l_channel = lab[:, :, 0]
130
+ else:
131
+ is_color = False
132
+ l_channel = image
133
+
134
+ # Compute histogram
135
+ hist = cv2.calcHist([l_channel], [0], None, [256], [0, 256])
136
+ extra_fields = {'histogram': hist.flatten().tolist()}
137
+
138
+ if operation == 'compute':
139
+ # Just compute, don't modify image
140
+ return image, extra_fields
141
+
142
+ if operation == 'equalize':
143
+ result = cv2.equalizeHist(l_channel)
144
+ elif operation == 'clahe':
145
+ clahe = cv2.createCLAHE(clipLimit=clip_limit,
146
+ tileGridSize=(tile_size, tile_size))
147
+ result = clahe.apply(l_channel)
148
+ elif operation == 'normalize':
149
+ result = cv2.normalize(l_channel, None, alpha, beta, cv2.NORM_MINMAX)
150
+ else:
151
+ result = l_channel
152
+
153
+ # Reconstruct color image if needed
154
+ if is_color:
155
+ lab[:, :, 0] = result
156
+ output = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR)
157
+ else:
158
+ output = result
159
+
160
+ return output, extra_fields