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,175 @@
1
+ """
2
+ OpenCV Resize Node - resizes 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, process_image, Info
9
+
10
+ _info = Info()
11
+ _info.add_text("Resizes images using various modes and interpolation methods. Supports absolute dimensions, scale factors, and aspect-ratio preserving modes.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Source image"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(("Output 0:", "Resized image"))
16
+ _info.add_header("Modes")
17
+ _info.add_bullets(
18
+ ("Absolute:", "Resize to exact width and height"),
19
+ ("Scale:", "Multiply dimensions by scale factor"),
20
+ ("Fit:", "Fit within bounds, preserve aspect ratio"),
21
+ ("Fill:", "Fill bounds with cropping, preserve aspect ratio")
22
+ )
23
+ _info.add_header("Interpolation")
24
+ _info.add_bullets(
25
+ ("Nearest:", "Fast, pixelated (good for pixel art)"),
26
+ ("Linear:", "Balanced quality and speed"),
27
+ ("Area:", "Best for shrinking images"),
28
+ ("Cubic:", "High quality for enlarging"),
29
+ ("Lanczos:", "Highest quality, slowest")
30
+ )
31
+
32
+ class ResizeNode(BaseNode):
33
+ """
34
+ Resize node - resizes images to specified dimensions or scale.
35
+ """
36
+ info = str(_info)
37
+ display_name = 'Resize'
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
+ 'mode': 'scale',
48
+ 'width': 640,
49
+ 'height': 480,
50
+ 'scale': 0.5,
51
+ 'interpolation': 'linear'
52
+ }
53
+
54
+ properties = [
55
+ {
56
+ 'name': 'mode',
57
+ 'label': 'Mode',
58
+ 'type': 'select',
59
+ 'options': [
60
+ {'value': 'absolute', 'label': 'Absolute size'},
61
+ {'value': 'scale', 'label': 'Scale factor'},
62
+ {'value': 'fit', 'label': 'Fit within bounds'},
63
+ {'value': 'fill', 'label': 'Fill bounds (crop)'}
64
+ ],
65
+ 'default': DEFAULT_CONFIG['mode'],
66
+ 'help': 'How to determine new size'
67
+ },
68
+ {
69
+ 'name': 'width',
70
+ 'label': 'Width',
71
+ 'type': 'number',
72
+ 'default': DEFAULT_CONFIG['width'],
73
+ 'min': 1,
74
+ 'help': 'Target width in pixels',
75
+ 'showIf': {'mode': ['absolute', 'fit', 'fill']}
76
+ },
77
+ {
78
+ 'name': 'height',
79
+ 'label': 'Height',
80
+ 'type': 'number',
81
+ 'default': DEFAULT_CONFIG['height'],
82
+ 'min': 1,
83
+ 'help': 'Target height in pixels',
84
+ 'showIf': {'mode': ['absolute', 'fit', 'fill']}
85
+ },
86
+ {
87
+ 'name': 'scale',
88
+ 'label': 'Scale Factor',
89
+ 'type': 'number',
90
+ 'default': DEFAULT_CONFIG['scale'],
91
+ 'min': 0.01,
92
+ 'max': 10,
93
+ 'step': 0.1,
94
+ 'help': 'Scale multiplier (1.0 = original size)',
95
+ 'showIf': {'mode': 'scale'}
96
+ },
97
+ {
98
+ 'name': 'interpolation',
99
+ 'label': 'Interpolation',
100
+ 'type': 'select',
101
+ 'options': [
102
+ {'value': 'nearest', 'label': 'Nearest neighbor'},
103
+ {'value': 'linear', 'label': 'Bilinear'},
104
+ {'value': 'area', 'label': 'Area (best for shrinking)'},
105
+ {'value': 'cubic', 'label': 'Bicubic'},
106
+ {'value': 'lanczos', 'label': 'Lanczos'}
107
+ ],
108
+ 'default': DEFAULT_CONFIG['interpolation'],
109
+ 'help': 'Interpolation method'
110
+ }
111
+ ]
112
+
113
+ def __init__(self, node_id=None, name="resize"):
114
+ super().__init__(node_id, name)
115
+
116
+ @process_image()
117
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
118
+ """Resize the input image."""
119
+ mode = self.config.get('mode', 'scale')
120
+ target_width = self.get_config_int('width', 640)
121
+ target_height = self.get_config_int('height', 480)
122
+ scale = self.get_config_float('scale', 0.5)
123
+ interp_str = self.config.get('interpolation', 'linear')
124
+
125
+ # Map interpolation string to OpenCV constant
126
+ interp_map = {
127
+ 'nearest': cv2.INTER_NEAREST,
128
+ 'linear': cv2.INTER_LINEAR,
129
+ 'area': cv2.INTER_AREA,
130
+ 'cubic': cv2.INTER_CUBIC,
131
+ 'lanczos': cv2.INTER_LANCZOS4
132
+ }
133
+ interpolation = interp_map.get(interp_str, cv2.INTER_LINEAR)
134
+
135
+ h, w = image.shape[:2]
136
+
137
+ if mode == 'absolute':
138
+ new_width = target_width
139
+ new_height = target_height
140
+ elif mode == 'scale':
141
+ new_width = int(w * scale)
142
+ new_height = int(h * scale)
143
+ elif mode == 'fit':
144
+ # Fit within bounds while maintaining aspect ratio
145
+ ratio = min(target_width / w, target_height / h)
146
+ new_width = int(w * ratio)
147
+ new_height = int(h * ratio)
148
+ elif mode == 'fill':
149
+ # Fill bounds while maintaining aspect ratio (may crop)
150
+ ratio = max(target_width / w, target_height / h)
151
+ new_width = int(w * ratio)
152
+ new_height = int(h * ratio)
153
+ else:
154
+ new_width = w
155
+ new_height = h
156
+
157
+ # Ensure minimum size
158
+ new_width = max(1, new_width)
159
+ new_height = max(1, new_height)
160
+
161
+ result = cv2.resize(image, (new_width, new_height), interpolation=interpolation)
162
+
163
+ # For fill mode, crop to target size
164
+ if mode == 'fill':
165
+ y_offset = (new_height - target_height) // 2
166
+ x_offset = (new_width - target_width) // 2
167
+ result = result[y_offset:y_offset + target_height,
168
+ x_offset:x_offset + target_width]
169
+
170
+ extra_fields = {
171
+ 'original_size': {'width': w, 'height': h},
172
+ 'new_size': {'width': result.shape[1], 'height': result.shape[0]}
173
+ }
174
+
175
+ return result, extra_fields
@@ -0,0 +1,159 @@
1
+ """
2
+ OpenCV Rotate Node - rotates 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, process_image, Info
9
+
10
+ _info = Info()
11
+ _info.add_text("Rotates and flips images. Supports preset rotations (90°, 180°), custom angles, and flip operations.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Source image"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(("Output 0:", "Rotated/flipped image"))
16
+ _info.add_header("Modes")
17
+ _info.add_bullets(
18
+ ("90° CW/CCW:", "Fast 90-degree rotations"),
19
+ ("180°:", "Rotate upside down"),
20
+ ("Custom Angle:", "Rotate by any angle in degrees"),
21
+ ("Flip H/V/Both:", "Mirror image horizontally, vertically, or both")
22
+ )
23
+ _info.add_header("Custom Angle Options")
24
+ _info.add_bullets(
25
+ ("Expand Canvas:", "Grow canvas to fit rotated image"),
26
+ ("Fill Color:", "Color for empty areas (B,G,R format)")
27
+ )
28
+
29
+ class RotateNode(BaseNode):
30
+ """
31
+ Rotate node - rotates images by specified angle.
32
+ """
33
+ info = str(_info)
34
+ display_name = 'Rotate'
35
+ icon = '↻'
36
+ category = 'opencv'
37
+ color = '#4A90D9'
38
+ border_color = '#2E6BB0'
39
+ text_color = '#FFFFFF'
40
+ input_count = 1
41
+ output_count = 1
42
+
43
+ DEFAULT_CONFIG = {
44
+ 'mode': '90cw',
45
+ 'angle': 45,
46
+ 'expand': 'yes',
47
+ 'fill_color': '0,0,0'
48
+ }
49
+
50
+ properties = [
51
+ {
52
+ 'name': 'mode',
53
+ 'label': 'Mode',
54
+ 'type': 'select',
55
+ 'options': [
56
+ {'value': 'angle', 'label': 'Custom angle'},
57
+ {'value': '90cw', 'label': '90° clockwise'},
58
+ {'value': '90ccw', 'label': '90° counter-clockwise'},
59
+ {'value': '180', 'label': '180°'},
60
+ {'value': 'flip_h', 'label': 'Flip horizontal'},
61
+ {'value': 'flip_v', 'label': 'Flip vertical'},
62
+ {'value': 'flip_both', 'label': 'Flip both'}
63
+ ],
64
+ 'default': DEFAULT_CONFIG['mode'],
65
+ 'help': 'Rotation mode'
66
+ },
67
+ {
68
+ 'name': 'angle',
69
+ 'label': 'Angle (degrees)',
70
+ 'type': 'number',
71
+ 'default': DEFAULT_CONFIG['angle'],
72
+ 'min': -360,
73
+ 'max': 360,
74
+ 'help': 'Rotation angle in degrees (positive = counter-clockwise)',
75
+ 'showIf': {'mode': 'angle'}
76
+ },
77
+ {
78
+ 'name': 'expand',
79
+ 'label': 'Expand Canvas',
80
+ 'type': 'select',
81
+ 'options': [
82
+ {'value': 'yes', 'label': 'Yes'},
83
+ {'value': 'no', 'label': 'No'}
84
+ ],
85
+ 'default': DEFAULT_CONFIG['expand'],
86
+ 'help': 'Expand canvas to fit rotated image',
87
+ 'showIf': {'mode': 'angle'}
88
+ },
89
+ {
90
+ 'name': 'fill_color',
91
+ 'label': 'Fill Color (B,G,R)',
92
+ 'type': 'text',
93
+ 'default': DEFAULT_CONFIG['fill_color'],
94
+ 'help': 'Color to fill empty areas',
95
+ 'showIf': {'mode': 'angle'}
96
+ }
97
+ ]
98
+
99
+ def __init__(self, node_id=None, name="rotate"):
100
+ super().__init__(node_id, name)
101
+
102
+ def _parse_color(self, color_str):
103
+ """Parse color string to BGR tuple."""
104
+ try:
105
+ parts = [int(x.strip()) for x in str(color_str).split(',')]
106
+ if len(parts) >= 3:
107
+ return (parts[0], parts[1], parts[2])
108
+ return (0, 0, 0)
109
+ except Exception:
110
+ return (0, 0, 0)
111
+
112
+ @process_image()
113
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
114
+ """Rotate the input image."""
115
+ mode = self.config.get('mode', '90cw')
116
+
117
+ if mode == '90cw':
118
+ result = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
119
+ elif mode == '90ccw':
120
+ result = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
121
+ elif mode == '180':
122
+ result = cv2.rotate(image, cv2.ROTATE_180)
123
+ elif mode == 'flip_h':
124
+ result = cv2.flip(image, 1)
125
+ elif mode == 'flip_v':
126
+ result = cv2.flip(image, 0)
127
+ elif mode == 'flip_both':
128
+ result = cv2.flip(image, -1)
129
+ elif mode == 'angle':
130
+ angle = self.get_config_float('angle', 45)
131
+ expand = self.get_config_bool('expand', True)
132
+ fill_color = self._parse_color(self.config.get('fill_color', '0,0,0'))
133
+
134
+ h, w = image.shape[:2]
135
+ center = (w // 2, h // 2)
136
+
137
+ # Get rotation matrix
138
+ M = cv2.getRotationMatrix2D(center, angle, 1.0)
139
+
140
+ if expand:
141
+ # Calculate new bounding box size
142
+ cos = np.abs(M[0, 0])
143
+ sin = np.abs(M[0, 1])
144
+ new_w = int(h * sin + w * cos)
145
+ new_h = int(h * cos + w * sin)
146
+
147
+ # Adjust the rotation matrix
148
+ M[0, 2] += (new_w - w) / 2
149
+ M[1, 2] += (new_h - h) / 2
150
+
151
+ result = cv2.warpAffine(image, M, (new_w, new_h),
152
+ borderValue=fill_color)
153
+ else:
154
+ result = cv2.warpAffine(image, M, (w, h),
155
+ borderValue=fill_color)
156
+ else:
157
+ result = image
158
+
159
+ return result
@@ -0,0 +1,227 @@
1
+ """
2
+ OpenCV Template Matching Node - finds template in 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("Finds occurrences of a template image within a larger image. Useful for object detection, pattern recognition, and image search.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(
14
+ ("Input 0:", "Source image to search in"),
15
+ ("Input 1:", "Template image to find")
16
+ )
17
+ _info.add_header("Outputs")
18
+ _info.add_bullets(("Output 0:", "Image with matches drawn (if enabled)"))
19
+ _info.add_header("Properties")
20
+ _info.add_bullets(
21
+ ("Method:", "Matching algorithm (normalized methods recommended)"),
22
+ ("Threshold:", "Match confidence threshold (0-1)"),
23
+ ("Multi Match:", "Find all matches or best only"),
24
+ ("Draw Matches:", "Draw rectangles around found matches")
25
+ )
26
+ _info.add_header("Message Fields")
27
+ _info.add_bullets(
28
+ ("matches:", "List of match locations and scores"),
29
+ ("match_count:", "Number of matches found")
30
+ )
31
+
32
+ class TemplateMatchNode(BaseNode):
33
+ """
34
+ Template Match node - finds occurrences of a template image in the input.
35
+ """
36
+ info = str(_info)
37
+ display_name = 'Template Match'
38
+ icon = '🔍'
39
+ category = 'opencv'
40
+ color = '#4A90D9'
41
+ border_color = '#2E6BB0'
42
+ text_color = '#FFFFFF'
43
+ input_count = 2 # Input 0: image, Input 1: template
44
+ output_count = 1
45
+
46
+ DEFAULT_CONFIG = {
47
+ 'method': 'ccoeff_normed',
48
+ 'threshold': 0.8,
49
+ 'multi_match': 'no',
50
+ 'draw_matches': 'yes',
51
+ 'match_color': '0,255,0'
52
+ }
53
+
54
+ properties = [
55
+ {
56
+ 'name': 'method',
57
+ 'label': 'Method',
58
+ 'type': 'select',
59
+ 'options': [
60
+ {'value': 'ccoeff_normed', 'label': 'Correlation Coefficient (normalized)'},
61
+ {'value': 'ccorr_normed', 'label': 'Cross Correlation (normalized)'},
62
+ {'value': 'sqdiff_normed', 'label': 'Squared Difference (normalized)'},
63
+ {'value': 'ccoeff', 'label': 'Correlation Coefficient'},
64
+ {'value': 'ccorr', 'label': 'Cross Correlation'},
65
+ {'value': 'sqdiff', 'label': 'Squared Difference'}
66
+ ],
67
+ 'default': DEFAULT_CONFIG['method'],
68
+ 'help': 'Template matching method'
69
+ },
70
+ {
71
+ 'name': 'threshold',
72
+ 'label': 'Threshold',
73
+ 'type': 'number',
74
+ 'default': DEFAULT_CONFIG['threshold'],
75
+ 'min': 0,
76
+ 'max': 1,
77
+ 'step': 0.05,
78
+ 'help': 'Match threshold (0-1)'
79
+ },
80
+ {
81
+ 'name': 'multi_match',
82
+ 'label': 'Multi Match',
83
+ 'type': 'select',
84
+ 'options': [
85
+ {'value': 'yes', 'label': 'Yes (find all)'},
86
+ {'value': 'no', 'label': 'No (best only)'}
87
+ ],
88
+ 'default': DEFAULT_CONFIG['multi_match'],
89
+ 'help': 'Find multiple matches above threshold'
90
+ },
91
+ {
92
+ 'name': 'draw_matches',
93
+ 'label': 'Draw Matches',
94
+ 'type': 'select',
95
+ 'options': [
96
+ {'value': 'yes', 'label': 'Yes'},
97
+ {'value': 'no', 'label': 'No'}
98
+ ],
99
+ 'default': DEFAULT_CONFIG['draw_matches'],
100
+ 'help': 'Draw rectangles around matches'
101
+ },
102
+ {
103
+ 'name': 'match_color',
104
+ 'label': 'Match Color (B,G,R)',
105
+ 'type': 'text',
106
+ 'default': DEFAULT_CONFIG['match_color'],
107
+ 'help': 'Color for match rectangles'
108
+ }
109
+ ]
110
+
111
+ def __init__(self, node_id=None, name="template match"):
112
+ super().__init__(node_id, name)
113
+ self._template = None
114
+
115
+ def _parse_color(self, color_str):
116
+ """Parse color string to BGR tuple."""
117
+ try:
118
+ parts = [int(x.strip()) for x in str(color_str).split(',')]
119
+ if len(parts) >= 3:
120
+ return (parts[0], parts[1], parts[2])
121
+ return (0, 255, 0)
122
+ except Exception:
123
+ return (0, 255, 0)
124
+
125
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
126
+ """Process template matching."""
127
+ if MessageKeys.PAYLOAD not in msg:
128
+ self.send(msg)
129
+ return
130
+
131
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
132
+ if img is None:
133
+ self.send(msg)
134
+ return
135
+
136
+ # Input 1 is the template
137
+ if input_index == 1:
138
+ self._template = img
139
+ return
140
+
141
+ # Input 0 is the image to search
142
+ if self._template is None:
143
+ msg['matches'] = []
144
+ msg['match_count'] = 0
145
+ self.send(msg)
146
+ return
147
+
148
+ method_str = self.config.get('method', 'ccoeff_normed')
149
+ threshold = self.get_config_float('threshold', 0.8)
150
+ multi_match = self.get_config_bool('multi_match', False)
151
+ draw = self.get_config_bool('draw_matches', True)
152
+ match_color = self._parse_color(self.config.get('match_color', '0,255,0'))
153
+
154
+ # Map method string to OpenCV constant
155
+ method_map = {
156
+ 'ccoeff_normed': cv2.TM_CCOEFF_NORMED,
157
+ 'ccorr_normed': cv2.TM_CCORR_NORMED,
158
+ 'sqdiff_normed': cv2.TM_SQDIFF_NORMED,
159
+ 'ccoeff': cv2.TM_CCOEFF,
160
+ 'ccorr': cv2.TM_CCORR,
161
+ 'sqdiff': cv2.TM_SQDIFF
162
+ }
163
+ method = method_map.get(method_str, cv2.TM_CCOEFF_NORMED)
164
+
165
+ # Perform template matching
166
+ result = cv2.matchTemplate(img, self._template, method)
167
+
168
+ h, w = self._template.shape[:2]
169
+ matches = []
170
+
171
+ # For SQDIFF methods, lower is better
172
+ is_sqdiff = method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]
173
+
174
+ if multi_match:
175
+ if is_sqdiff:
176
+ locations = np.where(result <= (1 - threshold))
177
+ else:
178
+ locations = np.where(result >= threshold)
179
+
180
+ for pt in zip(*locations[::-1]):
181
+ score = float(result[pt[1], pt[0]])
182
+ matches.append({
183
+ 'x': int(pt[0]),
184
+ 'y': int(pt[1]),
185
+ 'width': w,
186
+ 'height': h,
187
+ 'score': score if not is_sqdiff else 1 - score
188
+ })
189
+ else:
190
+ min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
191
+
192
+ if is_sqdiff:
193
+ best_loc = min_loc
194
+ best_score = 1 - min_val
195
+ else:
196
+ best_loc = max_loc
197
+ best_score = max_val
198
+
199
+ if (is_sqdiff and min_val <= (1 - threshold)) or \
200
+ (not is_sqdiff and max_val >= threshold):
201
+ matches.append({
202
+ 'x': int(best_loc[0]),
203
+ 'y': int(best_loc[1]),
204
+ 'width': w,
205
+ 'height': h,
206
+ 'score': float(best_score)
207
+ })
208
+
209
+ # Draw matches if requested
210
+ if draw and matches:
211
+ if len(img.shape) == 2:
212
+ output = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
213
+ else:
214
+ output = img.copy()
215
+
216
+ for match in matches:
217
+ pt1 = (match['x'], match['y'])
218
+ pt2 = (match['x'] + w, match['y'] + h)
219
+ cv2.rectangle(output, pt1, pt2, match_color, 2)
220
+
221
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
222
+ msg[MessageKeys.PAYLOAD] = {}
223
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(output, format_type)
224
+
225
+ msg['matches'] = matches
226
+ msg['match_count'] = len(matches)
227
+ self.send(msg)