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,183 @@
1
+ """
2
+ Rate Probe Node - monitors message throughput rate.
3
+ Passes messages through while tracking and displaying the rate.
4
+ """
5
+
6
+ import time
7
+ from typing import Any, Dict
8
+ from collections import deque
9
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
10
+
11
+ _info = Info()
12
+ _info.add_text("Monitors message throughput rate by counting messages within a time window. Displays the rate on the node and outputs rate information.")
13
+ _info.add_header("Inputs")
14
+ _info.add_bullets(
15
+ ("Input 0:", "Any message to count")
16
+ )
17
+ _info.add_header("Outputs")
18
+ _info.add_bullets(
19
+ ("Output 0:", f"Message with rate statistics in {MessageKeys.PAYLOAD}")
20
+ )
21
+ _info.add_header("Configuration")
22
+ _info.add_bullets(
23
+ ("Window Size:", "Time window in seconds for calculating the rate (default: 1 second)")
24
+ )
25
+ _info.add_header("Output Message")
26
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.rate').text(" - Messages per second as a number").end()
27
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.display').text(" - Human-readable rate string (e.g., '30/s', '2.5k/s')").end()
28
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.message_count').text(" - Number of messages in the current window").end()
29
+
30
+
31
+ class RateProbeNode(BaseNode):
32
+ """
33
+ Rate Probe node - monitors and displays message rate.
34
+ Passes all messages through unchanged while tracking throughput.
35
+ """
36
+ info = str(_info)
37
+ display_name = 'Rate Probe'
38
+ icon = '📊'
39
+ category = 'node probes'
40
+ color = '#E2D96E'
41
+ border_color = '#B8AF4A'
42
+ text_color = '#000000'
43
+ input_count = 1
44
+ output_count = 1
45
+ ui_component = 'rate-display'
46
+ ui_component_config = {
47
+ 'format': '{value}/s',
48
+ 'precision': 1
49
+ }
50
+
51
+ api_routes = [
52
+ {'route': 'rate', 'methods': ['GET'], 'handler': 'get_rate_info'},
53
+ ]
54
+
55
+ sse_handlers = [
56
+ {'type': 'rate', 'handler': 'get_rate_sse', 'throttle': 0.5},
57
+ ]
58
+
59
+ DEFAULT_CONFIG = {
60
+ 'window_size': 1.0, # in seconds
61
+ MessageKeys.DROP_MESSAGES: 'false'
62
+ }
63
+
64
+ properties = [
65
+ {
66
+ 'name': MessageKeys.DROP_MESSAGES,
67
+ 'label': 'Drop Messages When Busy',
68
+ 'type': 'checkbox',
69
+ 'default': False
70
+ },
71
+ {
72
+ 'name': 'window_size',
73
+ 'label': 'Window Size (seconds)',
74
+ 'type': 'number',
75
+ 'default': DEFAULT_CONFIG['window_size'],
76
+ 'help': 'Time window for calculating rate'
77
+ }
78
+ ]
79
+
80
+ def __init__(self, node_id=None, name="rate probe"):
81
+ super().__init__(node_id, name)
82
+ # Store timestamps of recent messages
83
+ self._timestamps = deque()
84
+ self._current_rate = 0.0
85
+ self._last_update = time.time()
86
+
87
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
88
+ """
89
+ Pass through the message and update rate calculation.
90
+ Sends rate information as the payload.
91
+ """
92
+ current_time = time.time()
93
+ window_size = self.get_config_float('window_size', 1.0)
94
+
95
+ # Add current timestamp
96
+ self._timestamps.append(current_time)
97
+
98
+ # Remove timestamps outside the window
99
+ cutoff = current_time - window_size
100
+ while self._timestamps and self._timestamps[0] < cutoff:
101
+ self._timestamps.popleft()
102
+
103
+ # Calculate rate (messages per second)
104
+ if window_size > 0:
105
+ self._current_rate = len(self._timestamps) / window_size
106
+ else:
107
+ self._current_rate = 0.0
108
+
109
+ self._last_update = current_time
110
+
111
+ # Preserve original message properties (like frame_count) and add rate info
112
+ # Note: send() handles deep copying, so we modify msg directly
113
+ msg[MessageKeys.PAYLOAD] = {
114
+ 'rate': self._current_rate,
115
+ 'display': self.get_rate_display(),
116
+ 'window_size': window_size,
117
+ 'message_count': len(self._timestamps)
118
+ }
119
+ msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'rate')
120
+ self.send(msg)
121
+
122
+ def get_rate(self) -> float:
123
+ """Get the current message rate."""
124
+ current_time = time.time()
125
+ window_size = self.get_config_float('window_size', 1.0)
126
+
127
+ # Clean up old timestamps
128
+ cutoff = current_time - window_size
129
+ while self._timestamps and self._timestamps[0] < cutoff:
130
+ self._timestamps.popleft()
131
+
132
+ # Recalculate rate
133
+ if window_size > 0:
134
+ self._current_rate = len(self._timestamps) / window_size
135
+
136
+ return self._current_rate
137
+
138
+ def get_rate_display(self) -> str:
139
+ """Get formatted rate string for display."""
140
+ rate = self.get_rate()
141
+ if rate >= 1000:
142
+ # Format as k/s, hide .0 precision
143
+ formatted = f"{rate/1000:.1f}".rstrip('0').rstrip('.')
144
+ return f"{formatted}k/s"
145
+ elif rate >= 1:
146
+ # Format as /s, hide .0 precision
147
+ formatted = f"{rate:.1f}".rstrip('0').rstrip('.')
148
+ return f"{formatted}/s"
149
+ elif rate > 0:
150
+ # Below 1/s, show seconds per message
151
+ interval = 1 / rate
152
+ if interval >= 60:
153
+ # Show in minutes
154
+ minutes = interval / 60
155
+ formatted = f"{minutes:.1f}".rstrip('0').rstrip('.')
156
+ return f"{formatted}m/msg"
157
+ else:
158
+ # Show in seconds
159
+ formatted = f"{interval:.1f}".rstrip('0').rstrip('.')
160
+ return f"{formatted}s/msg"
161
+ else:
162
+ return "0/s"
163
+
164
+ def on_stop(self):
165
+ """Clear timestamps when stopped."""
166
+ self._timestamps.clear()
167
+ self._current_rate = 0.0
168
+ super().on_stop()
169
+
170
+ def get_rate_info(self):
171
+ """API route handler: return rate info as dict."""
172
+ rate = self.get_rate()
173
+ return {
174
+ 'rate': rate,
175
+ 'display': self.get_rate_display(),
176
+ }
177
+
178
+ def get_rate_sse(self):
179
+ """SSE handler: return rate data for broadcast."""
180
+ return {
181
+ 'display': self.get_rate_display(),
182
+ 'rate': self.get_rate(),
183
+ }
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,3 @@
1
+ from .roboflow_upload_node import RoboflowUploadNode
2
+
3
+ __all__ = ['RoboflowUploadNode']
@@ -0,0 +1 @@
1
+ roboflow>=1.0.0
@@ -0,0 +1,229 @@
1
+ """
2
+ Roboflow Upload Node - uploads images to a Roboflow project for annotation.
3
+ """
4
+
5
+ import base64
6
+ import logging
7
+ import os
8
+ import tempfile
9
+ import threading
10
+ from datetime import datetime
11
+ from typing import Any, Dict
12
+ import numpy as np
13
+ import cv2
14
+
15
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _info = Info()
20
+ _info.add_text("Uploads images to a Roboflow project for annotation and training.")
21
+ _info.add_header("Inputs")
22
+ _info.add_bullets(
23
+ ("Input 0:", "Message containing an image in any supported format (numpy array, base64 JPEG, camera dict).")
24
+ )
25
+ _info.add_header("Outputs")
26
+ _info.add_bullets(
27
+ ("Output 0:", "The original message is passed through after upload.")
28
+ )
29
+ _info.add_header("Properties")
30
+ _info.add_bullets(
31
+ ("API Key:", "Roboflow API key from workspace settings."),
32
+ ("Workspace ID:", "Roboflow workspace identifier (e.g. my-workspace)."),
33
+ ("Project ID:", "Roboflow project identifier (e.g. my-project)."),
34
+ ("Split:", "Which dataset split to upload to (train, valid, or test)."),
35
+ ("Batch Name:", "Name for the upload batch (optional)."),
36
+ ("Rate Limit:", "Minimum seconds between uploads (0 = no limit).")
37
+ )
38
+ _info.add_header("Notes")
39
+ _info.add_text("Requires the roboflow package. The node connects to Roboflow when the workflow starts and disconnects when it stops.")
40
+
41
+
42
+ class RoboflowUploadNode(BaseNode):
43
+ """Uploads images to Roboflow for annotation."""
44
+
45
+ display_name = 'Roboflow Upload'
46
+ icon = '\u2B06'
47
+ category = 'output'
48
+ color = '#6706CE'
49
+ border_color = '#4A049A'
50
+ text_color = '#FFFFFF'
51
+ input_count = 1
52
+ output_count = 1
53
+ info = str(_info)
54
+
55
+ DEFAULT_CONFIG = {
56
+ 'api_key': '',
57
+ 'workspace_id': '',
58
+ 'project_id': '',
59
+ 'split': 'train',
60
+ 'batch_name': 'pynode',
61
+ 'rate_limit': 0,
62
+ MessageKeys.DROP_MESSAGES: True
63
+ }
64
+
65
+ properties = [
66
+ {
67
+ 'name': 'api_key',
68
+ 'label': 'API Key',
69
+ 'type': 'text',
70
+ 'default': '',
71
+ 'placeholder': 'Your Roboflow API key',
72
+ 'help': 'API key from Roboflow workspace settings'
73
+ },
74
+ {
75
+ 'name': 'workspace_id',
76
+ 'label': 'Workspace ID',
77
+ 'type': 'text',
78
+ 'default': '',
79
+ 'placeholder': 'e.g. my-workspace',
80
+ 'help': 'Roboflow workspace identifier'
81
+ },
82
+ {
83
+ 'name': 'project_id',
84
+ 'label': 'Project ID',
85
+ 'type': 'text',
86
+ 'default': '',
87
+ 'placeholder': 'e.g. my-project',
88
+ 'help': 'Roboflow project identifier'
89
+ },
90
+ {
91
+ 'name': 'split',
92
+ 'label': 'Dataset Split',
93
+ 'type': 'select',
94
+ 'options': [
95
+ {'value': 'train', 'label': 'Train'},
96
+ {'value': 'valid', 'label': 'Validation'},
97
+ {'value': 'test', 'label': 'Test'}
98
+ ],
99
+ 'default': 'train',
100
+ 'help': 'Which dataset split to upload images to'
101
+ },
102
+ {
103
+ 'name': 'batch_name',
104
+ 'label': 'Upload Batch Name',
105
+ 'type': 'text',
106
+ 'default': 'pynode',
107
+ 'placeholder': 'e.g. my-batch',
108
+ 'help': 'Name for the upload batch (optional)'
109
+ },
110
+ {
111
+ 'name': 'rate_limit',
112
+ 'label': 'Rate Limit (seconds)',
113
+ 'type': 'number',
114
+ 'default': 0,
115
+ 'min': 0,
116
+ 'help': 'Minimum seconds between uploads (0 = no limit)'
117
+ },
118
+ {
119
+ 'name': MessageKeys.DROP_MESSAGES,
120
+ 'label': 'Drop Messages When Busy',
121
+ 'type': 'checkbox',
122
+ 'default': True
123
+ }
124
+ ]
125
+
126
+ def __init__(self, node_id=None, name="roboflow upload"):
127
+ super().__init__(node_id, name)
128
+ self._rf_project = None
129
+ self._last_upload_time = 0.0
130
+ self._lock = threading.Lock()
131
+
132
+ def on_start(self):
133
+ """Connect to Roboflow when the workflow starts."""
134
+ super().on_start()
135
+ self._connect()
136
+
137
+ def on_stop(self):
138
+ """Disconnect from Roboflow when the workflow stops."""
139
+ self._rf_project = None
140
+ super().on_stop()
141
+
142
+ def configure(self, config: Dict[str, Any]):
143
+ """Apply configuration."""
144
+ super().configure(config)
145
+
146
+ def _connect(self):
147
+ """Establish connection to Roboflow."""
148
+ api_key = self.config.get('api_key', '').strip()
149
+ workspace_id = self.config.get('workspace_id', '').strip()
150
+ project_id = self.config.get('project_id', '').strip()
151
+
152
+ if not api_key or not workspace_id or not project_id:
153
+ self.report_error("Roboflow API Key, Workspace ID, and Project ID are all required")
154
+ return
155
+
156
+ try:
157
+ from roboflow import Roboflow
158
+ except ImportError:
159
+ self.report_error("roboflow package not installed. Install with: pip install roboflow")
160
+ return
161
+
162
+ try:
163
+ rf = Roboflow(api_key=api_key)
164
+ workspace = rf.workspace(workspace_id)
165
+ self._rf_project = workspace.project(project_id)
166
+ logger.info(f"[RoboflowUploadNode] Connected to {workspace_id}/{project_id}")
167
+ except Exception as e:
168
+ self.report_error(f"Roboflow connection failed: {e}")
169
+ self._rf_project = None
170
+
171
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
172
+ """Decode image from message and upload to Roboflow."""
173
+ if not self._rf_project:
174
+ self.report_error("Not connected to Roboflow (check configuration)")
175
+ self.send(msg)
176
+ return
177
+
178
+ # Rate limiting
179
+ rate_limit = self.get_config_float('rate_limit', 0)
180
+ if rate_limit > 0:
181
+ import time
182
+ now = time.time()
183
+ with self._lock:
184
+ if now - self._last_upload_time < rate_limit:
185
+ self.send(msg)
186
+ return
187
+ self._last_upload_time = now
188
+
189
+ # Extract and decode image
190
+ payload = msg.get(MessageKeys.PAYLOAD)
191
+ if payload is None:
192
+ self.report_error("No payload in message")
193
+ self.send(msg)
194
+ return
195
+
196
+ image, _ = self.decode_image(payload)
197
+ if image is None:
198
+ self.report_error("Could not decode image from message payload")
199
+ self.send(msg)
200
+ return
201
+
202
+ # Encode to JPEG and write to a temp file (Roboflow SDK requires a file path)
203
+ timestamp = datetime.utcnow().strftime('%Y%m%d_%H%M%S_%f')[:-3]
204
+ filename = f"pynode_{timestamp}.jpg"
205
+ temp_path = os.path.join(tempfile.gettempdir(), filename)
206
+
207
+ try:
208
+ success, buffer = cv2.imencode('.jpg', image, [cv2.IMWRITE_JPEG_QUALITY, 95])
209
+ if not success:
210
+ self.report_error("Failed to encode image as JPEG")
211
+ self.send(msg)
212
+ return
213
+
214
+ with open(temp_path, 'wb') as f:
215
+ f.write(buffer.tobytes())
216
+
217
+ batch_name = self.config.get('batch_name', '').strip() or None
218
+ self._rf_project.upload(temp_path, batch_name=batch_name)
219
+
220
+ except Exception as e:
221
+ self.report_error(f"Roboflow upload failed: {e}")
222
+ finally:
223
+ try:
224
+ if os.path.exists(temp_path):
225
+ os.remove(temp_path)
226
+ except Exception:
227
+ pass
228
+
229
+ self.send(msg)
@@ -0,0 +1,149 @@
1
+ # Slice Node - SAHI-style Image Tiling
2
+
3
+ This module provides nodes for slicing large images into tiles for improved small object detection, based on the SAHI (Slicing Aided Hyper Inference) methodology.
4
+
5
+ ## Nodes
6
+
7
+ ### SliceImageNode
8
+
9
+ Divides images into overlapping tiles for better detection of small objects in large images.
10
+
11
+ **Properties:**
12
+ - **Output Mode**:
13
+ - `Array`: All slices in one message (use with Split node)
14
+ - `Split`: Separate message per slice (direct to detector)
15
+ - **Auto Slice Resolution**: Automatically calculate slice parameters based on image size
16
+ - **Slice Width/Height**: Size of each tile in pixels (default: 640x640)
17
+ - **Overlap Width/Height Ratio**: Fractional overlap between tiles (default: 0.2 = 20%)
18
+ - **Include Full Image**: Whether to include the original image at full scale in output (for dual-scale detection)
19
+
20
+ **Output Format (Array mode):**
21
+ ```json
22
+ {
23
+ "payload": {
24
+ "slices": [
25
+ {
26
+ "image": "<encoded_image>",
27
+ "offset": [x, y],
28
+ "bbox": [x1, y1, x2, y2],
29
+ "slice_index": 0,
30
+ "is_full_image": true/false,
31
+ "original_width": 1920,
32
+ "original_height": 1080
33
+ }
34
+ ],
35
+ "slice_count": 5,
36
+ "original_width": 1920,
37
+ "original_height": 1080
38
+ }
39
+ }
40
+ ```
41
+
42
+ **Output Format (Split mode):**
43
+ Each slice sent as separate message with `parts` metadata for collection:
44
+ ```json
45
+ {
46
+ "payload": {
47
+ "image": "<encoded_image>",
48
+ "offset": [x, y],
49
+ ...
50
+ },
51
+ "parts": {
52
+ "index": 0,
53
+ "count": 5,
54
+ "id": "<parent_msg_id>"
55
+ },
56
+ "slice_offset": [x, y],
57
+ "is_full_image": false
58
+ }
59
+ ```
60
+
61
+ ### SliceCollectorNode
62
+
63
+ Collects detection predictions from multiple image slices and merges them into a single unified result.
64
+
65
+ **Properties:**
66
+ - **Collection Timeout**: Maximum time to wait for all slices (default: 5 seconds)
67
+ - **NMS IoU Threshold**: IoU threshold for Non-Maximum Suppression (default: 0.5)
68
+ - **Match Metric**: `IoU` or `IoS` (Intersection over Smaller)
69
+ - **Class Agnostic NMS**: Whether to apply NMS across all classes or per-class
70
+
71
+ **Features:**
72
+ - Transforms slice-local coordinates to original image coordinates
73
+ - Applies NMS to remove duplicate detections from overlapping regions
74
+ - Handles full image detections separately and merges with slice detections
75
+ - Tracks collections by message ID for concurrent processing
76
+
77
+ ### MergeSlicePredictionsNode
78
+
79
+ Alternative merge node that expects predictions already collected in a single message.
80
+
81
+ ## Workflow Examples
82
+
83
+ ### Workflow 1: Split Mode (Recommended)
84
+
85
+ ```
86
+ [Camera] → [SliceImage (split)] → [YOLO] → [SliceCollector] → [Output]
87
+ ```
88
+
89
+ 1. SliceImageNode in "Split" mode sends each tile as a separate message
90
+ 2. YOLO processes each tile
91
+ 3. SliceCollectorNode collects all predictions and merges them
92
+
93
+ ### Workflow 2: Array Mode
94
+
95
+ ```
96
+ [Camera] → [SliceImage (array)] → [Split] → [YOLO] → [SliceCollector] → [Output]
97
+ ```
98
+
99
+ 1. SliceImageNode outputs all tiles in an array
100
+ 2. Split node separates them into individual messages
101
+ 3. YOLO processes each tile
102
+ 4. SliceCollectorNode collects and merges
103
+
104
+ ### Workflow 3: Batch Processing
105
+
106
+ ```
107
+ [Camera] → [SliceImage (array)] → [Custom Processing]
108
+ ```
109
+
110
+ Process all slices together in a custom node that can handle arrays.
111
+
112
+ ## SAHI Methodology
113
+
114
+ The Slicing Aided Hyper Inference approach:
115
+
116
+ 1. **Slice**: Divide large image into smaller overlapping tiles
117
+ 2. **Detect**: Run object detection on each tile
118
+ 3. **Transform**: Convert tile-local coordinates to original image coordinates
119
+ 4. **Merge**: Apply NMS to remove duplicates from overlapping regions
120
+
121
+ ### Why Include Full Image?
122
+
123
+ Running detection on both tiles AND the full image provides:
124
+ - Tiles: Better detection of small objects
125
+ - Full image: Better detection of large objects that may be split across tiles
126
+
127
+ The merge step combines both for comprehensive detection.
128
+
129
+ ## Configuration Tips
130
+
131
+ ### Tile Size
132
+ - Match your detector's training resolution (e.g., 640x640 for YOLO)
133
+ - Smaller tiles = more tiles but better small object detection
134
+ - Larger tiles = fewer tiles but may miss small objects
135
+
136
+ ### Overlap Ratio
137
+ - 0.2 (20%) is a good default
138
+ - Increase for dense scenes with many small objects
139
+ - Decrease for faster processing with fewer duplicates
140
+
141
+ ### NMS Threshold
142
+ - 0.5 is a good default
143
+ - Lower = more aggressive duplicate removal
144
+ - Higher = keep more detections (may have duplicates)
145
+
146
+ ## Dependencies
147
+
148
+ - OpenCV (cv2)
149
+ - NumPy
@@ -0,0 +1,5 @@
1
+ from .slice_image_node import SliceImageNode
2
+ from .merge_predictions_node import MergeSlicePredictionsNode
3
+ from .slice_collector_node import SliceCollectorNode
4
+
5
+ __all__ = ['SliceImageNode', 'MergeSlicePredictionsNode', 'SliceCollectorNode']