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,376 @@
1
+ """
2
+ OpenCV Arithmetic Operations Node - performs pixel-wise arithmetic operations on images.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Dict, Optional
8
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
9
+
10
+ _info = Info()
11
+ _info.add_text("Performs pixel-wise arithmetic operations between two images or with a constant value.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(
14
+ ("Input 0:", "First image (primary)"),
15
+ ("Input 1:", "Second image (optional, for image-to-image operations)")
16
+ )
17
+ _info.add_header("Operations")
18
+ _info.add_bullets(
19
+ ("Add:", "Adds pixel values (saturated to max value)"),
20
+ ("Subtract:", "Subtracts pixel values (saturated to 0)"),
21
+ ("Multiply:", "Multiplies pixel values"),
22
+ ("Divide:", "Divides pixel values"),
23
+ ("Weighted Add:", "Blends two images with alpha weights"),
24
+ ("Absolute Difference:", "Absolute difference between images"),
25
+ ("Min:", "Minimum of two images pixel-wise"),
26
+ ("Max:", "Maximum of two images pixel-wise")
27
+ )
28
+ _info.add_header("Modes")
29
+ _info.add_bullets(
30
+ ("Image + Image:", "Operate between two input images"),
31
+ ("Image + Constant:", "Operate with a constant scalar value")
32
+ )
33
+ _info.add_header("Output")
34
+ _info.add_text("Outputs the resulting image from the arithmetic operation.")
35
+
36
+
37
+ class ArithmeticNode(BaseNode):
38
+ """
39
+ Arithmetic node - performs pixel-wise arithmetic operations on images.
40
+ Supports operations between two images or with a constant value.
41
+ """
42
+ info = str(_info)
43
+ display_name = 'Arithmetic'
44
+ icon = '±'
45
+ category = 'opencv'
46
+ color = '#4A90D9'
47
+ border_color = '#2E6BB0'
48
+ text_color = '#FFFFFF'
49
+ input_count = 2 # Input 0: image1, Input 1: image2 (optional)
50
+ output_count = 1
51
+
52
+ DEFAULT_CONFIG = {
53
+ 'operation': 'add',
54
+ 'mode': 'image',
55
+ 'constant': '50',
56
+ 'alpha': '0.5',
57
+ 'beta': '0.5',
58
+ 'gamma': '0',
59
+ 'auto_resize': 'true',
60
+ 'resize_method': 'resize_second',
61
+ 'clip_output': 'true'
62
+ }
63
+
64
+ properties = [
65
+ {
66
+ 'name': 'operation',
67
+ 'label': 'Operation',
68
+ 'type': 'select',
69
+ 'options': [
70
+ {'value': 'add', 'label': 'Add'},
71
+ {'value': 'subtract', 'label': 'Subtract'},
72
+ {'value': 'multiply', 'label': 'Multiply'},
73
+ {'value': 'divide', 'label': 'Divide'},
74
+ {'value': 'weighted_add', 'label': 'Weighted Add (Blend)'},
75
+ {'value': 'absdiff', 'label': 'Absolute Difference'},
76
+ {'value': 'min', 'label': 'Minimum'},
77
+ {'value': 'max', 'label': 'Maximum'},
78
+ {'value': 'power', 'label': 'Power'},
79
+ {'value': 'sqrt', 'label': 'Square Root (single image)'}
80
+ ],
81
+ 'default': DEFAULT_CONFIG['operation'],
82
+ 'help': 'Arithmetic operation to perform'
83
+ },
84
+ {
85
+ 'name': 'mode',
86
+ 'label': 'Mode',
87
+ 'type': 'select',
88
+ 'options': [
89
+ {'value': 'image', 'label': 'Image + Image'},
90
+ {'value': 'constant', 'label': 'Image + Constant'}
91
+ ],
92
+ 'default': DEFAULT_CONFIG['mode'],
93
+ 'help': 'Operate between images or with a constant value'
94
+ },
95
+ {
96
+ 'name': 'constant',
97
+ 'label': 'Constant Value',
98
+ 'type': 'text',
99
+ 'default': DEFAULT_CONFIG['constant'],
100
+ 'help': 'Scalar value for constant mode (can be negative for subtract)'
101
+ },
102
+ {
103
+ 'name': 'alpha',
104
+ 'label': 'Alpha (Weight 1)',
105
+ 'type': 'text',
106
+ 'default': DEFAULT_CONFIG['alpha'],
107
+ 'help': 'Weight for first image in weighted add (0.0-1.0)'
108
+ },
109
+ {
110
+ 'name': 'beta',
111
+ 'label': 'Beta (Weight 2)',
112
+ 'type': 'text',
113
+ 'default': DEFAULT_CONFIG['beta'],
114
+ 'help': 'Weight for second image in weighted add (0.0-1.0)'
115
+ },
116
+ {
117
+ 'name': 'gamma',
118
+ 'label': 'Gamma (Offset)',
119
+ 'type': 'text',
120
+ 'default': DEFAULT_CONFIG['gamma'],
121
+ 'help': 'Scalar added to weighted sum'
122
+ },
123
+ {
124
+ 'name': 'auto_resize',
125
+ 'label': 'Auto Resize',
126
+ 'type': 'select',
127
+ 'options': [
128
+ {'value': 'true', 'label': 'Yes'},
129
+ {'value': 'false', 'label': 'No'}
130
+ ],
131
+ 'default': DEFAULT_CONFIG['auto_resize'],
132
+ 'help': 'Automatically resize images to match dimensions'
133
+ },
134
+ {
135
+ 'name': 'resize_method',
136
+ 'label': 'Resize Method',
137
+ 'type': 'select',
138
+ 'options': [
139
+ {'value': 'resize_second', 'label': 'Resize second to match first'},
140
+ {'value': 'resize_first', 'label': 'Resize first to match second'},
141
+ {'value': 'resize_larger', 'label': 'Resize to larger dimensions'},
142
+ {'value': 'resize_smaller', 'label': 'Resize to smaller dimensions'}
143
+ ],
144
+ 'default': DEFAULT_CONFIG['resize_method'],
145
+ 'help': 'How to handle size mismatch between images'
146
+ },
147
+ {
148
+ 'name': 'clip_output',
149
+ 'label': 'Clip Output',
150
+ 'type': 'select',
151
+ 'options': [
152
+ {'value': 'true', 'label': 'Yes (0-255)'},
153
+ {'value': 'false', 'label': 'No'}
154
+ ],
155
+ 'default': DEFAULT_CONFIG['clip_output'],
156
+ 'help': 'Clip output values to valid range (0-255)'
157
+ }
158
+ ]
159
+
160
+ def __init__(self, node_id=None, name="arithmetic"):
161
+ super().__init__(node_id, name)
162
+ self._image1: Optional[np.ndarray] = None
163
+ self._image2: Optional[np.ndarray] = None
164
+ self._format_type: Optional[str] = None
165
+ self._last_msg: Optional[Dict] = None
166
+
167
+ def _resize_images(self, img1: np.ndarray, img2: np.ndarray) -> tuple:
168
+ """Resize images based on configuration."""
169
+ if img1.shape[:2] == img2.shape[:2]:
170
+ return img1, img2
171
+
172
+ auto_resize = self.config.get('auto_resize', 'true') == 'true'
173
+ if not auto_resize:
174
+ # Just resize second to first as fallback
175
+ img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
176
+ return img1, img2
177
+
178
+ resize_method = self.config.get('resize_method', 'resize_second')
179
+
180
+ h1, w1 = img1.shape[:2]
181
+ h2, w2 = img2.shape[:2]
182
+
183
+ if resize_method == 'resize_second':
184
+ img2 = cv2.resize(img2, (w1, h1))
185
+ elif resize_method == 'resize_first':
186
+ img1 = cv2.resize(img1, (w2, h2))
187
+ elif resize_method == 'resize_larger':
188
+ target_w = max(w1, w2)
189
+ target_h = max(h1, h2)
190
+ if (h1, w1) != (target_h, target_w):
191
+ img1 = cv2.resize(img1, (target_w, target_h))
192
+ if (h2, w2) != (target_h, target_w):
193
+ img2 = cv2.resize(img2, (target_w, target_h))
194
+ elif resize_method == 'resize_smaller':
195
+ target_w = min(w1, w2)
196
+ target_h = min(h1, h2)
197
+ if (h1, w1) != (target_h, target_w):
198
+ img1 = cv2.resize(img1, (target_w, target_h))
199
+ if (h2, w2) != (target_h, target_w):
200
+ img2 = cv2.resize(img2, (target_w, target_h))
201
+
202
+ return img1, img2
203
+
204
+ def _match_channels(self, img1: np.ndarray, img2: np.ndarray) -> tuple:
205
+ """Ensure both images have the same number of channels."""
206
+ c1 = img1.shape[2] if len(img1.shape) == 3 else 1
207
+ c2 = img2.shape[2] if len(img2.shape) == 3 else 1
208
+
209
+ if c1 == c2:
210
+ return img1, img2
211
+
212
+ # Convert grayscale to BGR if needed
213
+ if c1 == 1 and c2 == 3:
214
+ img1 = cv2.cvtColor(img1, cv2.COLOR_GRAY2BGR)
215
+ elif c1 == 3 and c2 == 1:
216
+ img2 = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)
217
+
218
+ return img1, img2
219
+
220
+ def _create_constant_image(self, img: np.ndarray, value: float) -> np.ndarray:
221
+ """Create a constant image matching the input dimensions."""
222
+ const_img = np.full_like(img, abs(value), dtype=np.float32)
223
+ return const_img
224
+
225
+ def _perform_operation(self, img1: np.ndarray, img2: Optional[np.ndarray]) -> np.ndarray:
226
+ """Perform the configured arithmetic operation."""
227
+ operation = self.config.get('operation', 'add')
228
+ mode = self.config.get('mode', 'image')
229
+ clip_output = self.config.get('clip_output', 'true') == 'true'
230
+
231
+ # Convert to float for precision
232
+ img1_f = img1.astype(np.float32)
233
+
234
+ # Handle single-image operations
235
+ if operation == 'sqrt':
236
+ result = np.sqrt(img1_f)
237
+ if clip_output:
238
+ result = np.clip(result, 0, 255)
239
+ return result.astype(np.uint8)
240
+
241
+ # Get second operand (image or constant)
242
+ if mode == 'constant':
243
+ try:
244
+ const_value = float(self.config.get('constant', '50'))
245
+ except ValueError:
246
+ const_value = 50.0
247
+ img2_f = self._create_constant_image(img1, const_value)
248
+ else:
249
+ if img2 is None:
250
+ return img1
251
+ img1, img2 = self._resize_images(img1, img2)
252
+ img1, img2 = self._match_channels(img1, img2)
253
+ img1_f = img1.astype(np.float32)
254
+ img2_f = img2.astype(np.float32)
255
+
256
+ # Perform operation
257
+ if operation == 'add':
258
+ result = cv2.add(img1, img2 if mode == 'image' else img2_f.astype(np.uint8))
259
+ elif operation == 'subtract':
260
+ if mode == 'constant':
261
+ const_value = float(self.config.get('constant', '50'))
262
+ if const_value >= 0:
263
+ result = cv2.subtract(img1, img2_f.astype(np.uint8))
264
+ else:
265
+ # Negative constant means add
266
+ result = cv2.add(img1, np.full_like(img1, abs(const_value), dtype=np.uint8))
267
+ else:
268
+ result = cv2.subtract(img1, img2)
269
+ elif operation == 'multiply':
270
+ # Scale factor for multiply
271
+ result = img1_f * (img2_f / 255.0 if mode == 'image' else img2_f / 255.0)
272
+ if clip_output:
273
+ result = np.clip(result, 0, 255)
274
+ result = result.astype(np.uint8)
275
+ elif operation == 'divide':
276
+ # Avoid division by zero
277
+ img2_safe = np.where(img2_f == 0, 1, img2_f)
278
+ if mode == 'constant':
279
+ result = img1_f / (img2_safe / 255.0) if img2_f.mean() != 0 else img1_f
280
+ else:
281
+ result = (img1_f / img2_safe) * 255.0
282
+ if clip_output:
283
+ result = np.clip(result, 0, 255)
284
+ result = result.astype(np.uint8)
285
+ elif operation == 'weighted_add':
286
+ try:
287
+ alpha = float(self.config.get('alpha', '0.5'))
288
+ beta = float(self.config.get('beta', '0.5'))
289
+ gamma = float(self.config.get('gamma', '0'))
290
+ except ValueError:
291
+ alpha, beta, gamma = 0.5, 0.5, 0.0
292
+
293
+ if mode == 'constant':
294
+ result = cv2.addWeighted(img1, alpha, img2_f.astype(np.uint8), beta, gamma)
295
+ else:
296
+ result = cv2.addWeighted(img1, alpha, img2, beta, gamma)
297
+ elif operation == 'absdiff':
298
+ if mode == 'constant':
299
+ result = cv2.absdiff(img1, img2_f.astype(np.uint8))
300
+ else:
301
+ result = cv2.absdiff(img1, img2)
302
+ elif operation == 'min':
303
+ if mode == 'constant':
304
+ result = np.minimum(img1_f, img2_f)
305
+ else:
306
+ result = np.minimum(img1_f, img2_f)
307
+ result = result.astype(np.uint8)
308
+ elif operation == 'max':
309
+ if mode == 'constant':
310
+ result = np.maximum(img1_f, img2_f)
311
+ else:
312
+ result = np.maximum(img1_f, img2_f)
313
+ result = result.astype(np.uint8)
314
+ elif operation == 'power':
315
+ try:
316
+ const_value = float(self.config.get('constant', '2'))
317
+ except ValueError:
318
+ const_value = 2.0
319
+ # Normalize, apply power, denormalize
320
+ result = np.power(img1_f / 255.0, const_value) * 255.0
321
+ if clip_output:
322
+ result = np.clip(result, 0, 255)
323
+ result = result.astype(np.uint8)
324
+ else:
325
+ result = img1
326
+
327
+ return result
328
+
329
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
330
+ """Process incoming image and perform arithmetic operation."""
331
+ if MessageKeys.PAYLOAD not in msg:
332
+ self.send(msg)
333
+ return
334
+
335
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
336
+ if img is None:
337
+ self.send(msg)
338
+ return
339
+
340
+ # Store image based on input index
341
+ if input_index == 0:
342
+ self._image1 = img
343
+ self._format_type = format_type
344
+ self._last_msg = msg.copy()
345
+ else:
346
+ self._image2 = img
347
+
348
+ operation = self.config.get('operation', 'add')
349
+ mode = self.config.get('mode', 'image')
350
+
351
+ # Single-image operations (sqrt) only need image1
352
+ if operation == 'sqrt' and self._image1 is not None:
353
+ result = self._perform_operation(self._image1, None)
354
+ self._send_result(result, msg)
355
+ return
356
+
357
+ # Constant mode only needs image1
358
+ if mode == 'constant' and self._image1 is not None:
359
+ result = self._perform_operation(self._image1, None)
360
+ self._send_result(result, self._last_msg or msg)
361
+ return
362
+
363
+ # Image mode needs both images
364
+ if mode == 'image' and self._image1 is not None and self._image2 is not None:
365
+ result = self._perform_operation(self._image1, self._image2)
366
+ self._send_result(result, self._last_msg or msg)
367
+ # Reset for next pair
368
+ self._image1 = None
369
+ self._image2 = None
370
+
371
+ def _send_result(self, result: np.ndarray, msg: Dict[str, Any]):
372
+ """Send the result image."""
373
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
374
+ msg[MessageKeys.PAYLOAD] = {}
375
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result, self._format_type)
376
+ self.send(msg)
@@ -0,0 +1,127 @@
1
+ """
2
+ OpenCV Bitwise Operations Node - performs bitwise operations on images.
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 bitwise operations (AND, OR, XOR, NOT) between two images or applies a mask.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(
14
+ ("Input 0:", "First image (or only image for NOT operation)"),
15
+ ("Input 1:", "Second image or mask")
16
+ )
17
+ _info.add_header("Operations")
18
+ _info.add_bullets(
19
+ ("AND:", "Combines pixels where both images have values"),
20
+ ("OR:", "Combines pixels where either image has values"),
21
+ ("XOR:", "Combines pixels where only one image has values"),
22
+ ("NOT:", "Inverts the first image (ignores second input)"),
23
+ ("Mask:", "Applies second image as a mask to the first")
24
+ )
25
+ _info.add_header("Output")
26
+ _info.add_text("Outputs the resulting image from the bitwise operation.")
27
+
28
+
29
+ class BitwiseNode(BaseNode):
30
+ """
31
+ Bitwise node - performs bitwise operations between images or with masks.
32
+ """
33
+ info = str(_info)
34
+ display_name = 'Bitwise'
35
+ icon = '&'
36
+ category = 'opencv'
37
+ color = '#4A90D9'
38
+ border_color = '#2E6BB0'
39
+ text_color = '#FFFFFF'
40
+ input_count = 2 # Input 0: image1, Input 1: image2 or mask
41
+ output_count = 1
42
+
43
+ DEFAULT_CONFIG = {
44
+ 'operation': 'and'
45
+ }
46
+
47
+ properties = [
48
+ {
49
+ 'name': 'operation',
50
+ 'label': 'Operation',
51
+ 'type': 'select',
52
+ 'options': [
53
+ {'value': 'and', 'label': 'AND'},
54
+ {'value': 'or', 'label': 'OR'},
55
+ {'value': 'xor', 'label': 'XOR'},
56
+ {'value': 'not', 'label': 'NOT (invert)'},
57
+ {'value': 'mask', 'label': 'Apply mask'}
58
+ ],
59
+ 'default': DEFAULT_CONFIG['operation'],
60
+ 'help': 'Bitwise operation to perform'
61
+ }
62
+ ]
63
+
64
+ def __init__(self, node_id=None, name="bitwise"):
65
+ super().__init__(node_id, name)
66
+ self._image1 = None
67
+ self._image2 = None
68
+ self._format_type = None
69
+
70
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
71
+ """Perform bitwise operation."""
72
+ if MessageKeys.PAYLOAD not in msg:
73
+ self.send(msg)
74
+ return
75
+
76
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
77
+ if img is None:
78
+ self.send(msg)
79
+ return
80
+
81
+ if input_index == 0:
82
+ self._image1 = img
83
+ self._format_type = format_type
84
+ else:
85
+ self._image2 = img
86
+
87
+ operation = self.config.get('operation', 'and')
88
+
89
+ # NOT operation only needs one image
90
+ if operation == 'not' and self._image1 is not None:
91
+ result = cv2.bitwise_not(self._image1)
92
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
93
+ msg[MessageKeys.PAYLOAD] = {}
94
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result, self._format_type)
95
+ self.send(msg)
96
+ return
97
+
98
+ # Other operations need both images
99
+ if self._image1 is None or self._image2 is None:
100
+ return
101
+
102
+ # Ensure images are same size
103
+ if self._image1.shape[:2] != self._image2.shape[:2]:
104
+ # Resize image2 to match image1
105
+ self._image2 = cv2.resize(self._image2,
106
+ (self._image1.shape[1], self._image1.shape[0]))
107
+
108
+ if operation == 'and':
109
+ result = cv2.bitwise_and(self._image1, self._image2)
110
+ elif operation == 'or':
111
+ result = cv2.bitwise_or(self._image1, self._image2)
112
+ elif operation == 'xor':
113
+ result = cv2.bitwise_xor(self._image1, self._image2)
114
+ elif operation == 'mask':
115
+ # Use image2 as mask
116
+ if len(self._image2.shape) == 3:
117
+ mask = cv2.cvtColor(self._image2, cv2.COLOR_BGR2GRAY)
118
+ else:
119
+ mask = self._image2
120
+ result = cv2.bitwise_and(self._image1, self._image1, mask=mask)
121
+ else:
122
+ result = self._image1
123
+
124
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
125
+ msg[MessageKeys.PAYLOAD] = {}
126
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result, self._format_type)
127
+ self.send(msg)
@@ -0,0 +1,127 @@
1
+ """
2
+ OpenCV Blend Node - blends two images together.
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("Blends two images together using weighted addition: result = alpha×image1 + beta×image2 + gamma.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(
14
+ ("Input 0:", "First image"),
15
+ ("Input 1:", "Second image (will be resized to match first if different size)")
16
+ )
17
+ _info.add_header("Parameters")
18
+ _info.add_bullets(
19
+ ("Alpha:", "Weight for first image (0-1)"),
20
+ ("Beta:", "Weight for second image (0-1)"),
21
+ ("Gamma:", "Brightness adjustment added to result")
22
+ )
23
+ _info.add_header("Output")
24
+ _info.add_text("Outputs the blended image.")
25
+
26
+
27
+ class BlendNode(BaseNode):
28
+ """
29
+ Blend node - blends two images together using weighted addition.
30
+ """
31
+ info = str(_info)
32
+ display_name = 'Blend'
33
+ icon = '⧉'
34
+ category = 'opencv'
35
+ color = '#4A90D9'
36
+ border_color = '#2E6BB0'
37
+ text_color = '#FFFFFF'
38
+ input_count = 2 # Input 0: image1, Input 1: image2
39
+ output_count = 1
40
+
41
+ DEFAULT_CONFIG = {
42
+ 'alpha': 0.5,
43
+ 'beta': 0.5,
44
+ 'gamma': 0
45
+ }
46
+
47
+ properties = [
48
+ {
49
+ 'name': 'alpha',
50
+ 'label': 'Alpha (image 1 weight)',
51
+ 'type': 'number',
52
+ 'default': DEFAULT_CONFIG['alpha'],
53
+ 'min': 0,
54
+ 'max': 1,
55
+ 'step': 0.1,
56
+ 'help': 'Weight for first image (0-1)'
57
+ },
58
+ {
59
+ 'name': 'beta',
60
+ 'label': 'Beta (image 2 weight)',
61
+ 'type': 'number',
62
+ 'default': DEFAULT_CONFIG['beta'],
63
+ 'min': 0,
64
+ 'max': 1,
65
+ 'step': 0.1,
66
+ 'help': 'Weight for second image (0-1)'
67
+ },
68
+ {
69
+ 'name': 'gamma',
70
+ 'label': 'Gamma (brightness)',
71
+ 'type': 'number',
72
+ 'default': DEFAULT_CONFIG['gamma'],
73
+ 'min': -100,
74
+ 'max': 100,
75
+ 'help': 'Added to final result'
76
+ }
77
+ ]
78
+
79
+ def __init__(self, node_id=None, name="blend"):
80
+ super().__init__(node_id, name)
81
+ self._image1 = None
82
+ self._image2 = None
83
+ self._format_type = None
84
+
85
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
86
+ """Blend two images."""
87
+ if MessageKeys.PAYLOAD not in msg:
88
+ self.send(msg)
89
+ return
90
+
91
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
92
+ if img is None:
93
+ self.send(msg)
94
+ return
95
+
96
+ if input_index == 0:
97
+ self._image1 = img
98
+ self._format_type = format_type
99
+ else:
100
+ self._image2 = img
101
+
102
+ # Need both images
103
+ if self._image1 is None or self._image2 is None:
104
+ return
105
+
106
+ alpha = self.get_config_float('alpha', 0.5)
107
+ beta = self.get_config_float('beta', 0.5)
108
+ gamma = self.get_config_float('gamma', 0)
109
+
110
+ # Ensure images are same size
111
+ if self._image1.shape[:2] != self._image2.shape[:2]:
112
+ self._image2 = cv2.resize(self._image2,
113
+ (self._image1.shape[1], self._image1.shape[0]))
114
+
115
+ # Ensure same number of channels
116
+ if len(self._image1.shape) != len(self._image2.shape):
117
+ if len(self._image1.shape) == 2:
118
+ self._image1 = cv2.cvtColor(self._image1, cv2.COLOR_GRAY2BGR)
119
+ if len(self._image2.shape) == 2:
120
+ self._image2 = cv2.cvtColor(self._image2, cv2.COLOR_GRAY2BGR)
121
+
122
+ result = cv2.addWeighted(self._image1, alpha, self._image2, beta, gamma)
123
+
124
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
125
+ msg[MessageKeys.PAYLOAD] = {}
126
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(result, self._format_type)
127
+ self.send(msg)