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.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- 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 @@
|
|
|
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
|