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,331 @@
1
+ """
2
+ PolygonMetricsNode - Calculate metrics from polygons
3
+
4
+ This node calculates various metrics from polygon predictions including:
5
+ - Polygon area
6
+ - Bounding box (width, height, area)
7
+ - Maximum internal rectangle
8
+ - Minimum external rectangle
9
+ - Perimeter
10
+ - Centroid
11
+
12
+ Input format:
13
+ msg['detections'] - List of detections with polygons
14
+ OR
15
+ msg['polygons'] - List of polygons (array of points)
16
+ OR
17
+ msg['polygon'] - Single polygon (array of points [[x1,y1], [x2,y2], ...])
18
+
19
+ Output format:
20
+ msg['metrics'] - Dictionary containing calculated metrics
21
+ msg['detections'] - Original detections with added 'metrics' field
22
+ """
23
+
24
+ import sys
25
+ import os
26
+ from typing import Any, Dict
27
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
28
+
29
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
30
+ import numpy as np
31
+ import cv2
32
+
33
+ _info = Info()
34
+ _info.add_text("Calculate metrics from polygons including area, perimeter, bounding box, centroid, and rectangles.")
35
+ _info.add_header("Inputs")
36
+ _info.add_bullets(
37
+ ("detections:", "List of detections with polygons"),
38
+ ("polygons:", "List of polygons"),
39
+ ("polygon:", "Single polygon [[x1,y1], [x2,y2], ...]")
40
+ )
41
+ _info.add_header("Outputs")
42
+ _info.add_bullets(
43
+ ("metrics:", "Dictionary with calculated metrics"),
44
+ ("detections:", "Original detections with added 'metrics' field")
45
+ )
46
+
47
+
48
+ class PolygonMetricsNode(BaseNode):
49
+ """Calculate metrics from polygons"""
50
+
51
+ info = str(_info)
52
+ display_name = 'Polygon Metrics'
53
+ icon = '⬡'
54
+ category = 'analysis'
55
+ color = '#E6E6FA'
56
+ border_color = '#9370DB'
57
+ text_color = '#000000'
58
+
59
+ DEFAULT_CONFIG = {
60
+ 'metrics': ['area', 'perimeter', 'bbox', 'centroid', 'max_internal_rect', 'min_external_rect'],
61
+ 'normalize': False,
62
+ 'image_width': 1920,
63
+ 'image_height': 1080
64
+ }
65
+
66
+ properties = [
67
+ {
68
+ 'name': 'metrics',
69
+ 'label': 'Metrics to Calculate',
70
+ 'type': 'multiselect',
71
+ 'options': ['area', 'perimeter', 'bbox', 'centroid', 'max_internal_rect', 'min_external_rect'],
72
+ 'default': DEFAULT_CONFIG['metrics']
73
+ },
74
+ {
75
+ 'name': 'normalize',
76
+ 'label': 'Normalize by Image Size',
77
+ 'type': 'checkbox',
78
+ 'default': DEFAULT_CONFIG['normalize']
79
+ },
80
+ {
81
+ 'name': 'image_width',
82
+ 'label': 'Image Width (for normalization)',
83
+ 'type': 'number',
84
+ 'default': DEFAULT_CONFIG['image_width']
85
+ },
86
+ {
87
+ 'name': 'image_height',
88
+ 'label': 'Image Height (for normalization)',
89
+ 'type': 'number',
90
+ 'default': DEFAULT_CONFIG['image_height']
91
+ }
92
+ ]
93
+
94
+ def __init__(self, node_id=None, name="Polygon Metrics"):
95
+ super().__init__(node_id, name)
96
+
97
+ def _polygon_area(self, points):
98
+ """Calculate polygon area using shoelace formula"""
99
+ if len(points) < 3:
100
+ return 0.0
101
+
102
+ points = np.array(points)
103
+ x = points[:, 0]
104
+ y = points[:, 1]
105
+ return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
106
+
107
+ def _polygon_perimeter(self, points):
108
+ """Calculate polygon perimeter"""
109
+ if len(points) < 2:
110
+ return 0.0
111
+
112
+ points = np.array(points)
113
+ # Calculate distance between consecutive points
114
+ diffs = np.diff(points, axis=0, append=[points[0]])
115
+ distances = np.sqrt(np.sum(diffs**2, axis=1))
116
+ return np.sum(distances)
117
+
118
+ def _polygon_centroid(self, points):
119
+ """Calculate polygon centroid"""
120
+ if len(points) < 3:
121
+ return [0.0, 0.0]
122
+
123
+ points = np.array(points)
124
+ M = cv2.moments(points.astype(np.float32))
125
+
126
+ if M['m00'] != 0:
127
+ cx = M['m10'] / M['m00']
128
+ cy = M['m01'] / M['m00']
129
+ return [float(cx), float(cy)]
130
+ else:
131
+ # Fallback to simple average
132
+ return [float(np.mean(points[:, 0])), float(np.mean(points[:, 1]))]
133
+
134
+ def _polygon_bounding_box(self, points):
135
+ """Get bounding box of polygon"""
136
+ if len(points) < 1:
137
+ return [0, 0, 0, 0]
138
+
139
+ points = np.array(points)
140
+ x_min = np.min(points[:, 0])
141
+ y_min = np.min(points[:, 1])
142
+ x_max = np.max(points[:, 0])
143
+ y_max = np.max(points[:, 1])
144
+
145
+ return [float(x_min), float(y_min), float(x_max), float(y_max)]
146
+
147
+ def _minimum_external_rect(self, points):
148
+ """Calculate minimum area rectangle that contains the polygon"""
149
+ if len(points) < 3:
150
+ return None
151
+
152
+ points = np.array(points, dtype=np.float32)
153
+ rect = cv2.minAreaRect(points)
154
+ box = cv2.boxPoints(rect)
155
+
156
+ # rect is ((center_x, center_y), (width, height), angle)
157
+ center, size, angle = rect
158
+ width, height = size
159
+
160
+ return {
161
+ 'center': [float(center[0]), float(center[1])],
162
+ 'width': float(width),
163
+ 'height': float(height),
164
+ 'area': float(width * height),
165
+ 'angle': float(angle),
166
+ 'corners': box.tolist()
167
+ }
168
+
169
+ def _maximum_internal_rect(self, points, image_width=1920, image_height=1080):
170
+ """
171
+ Estimate maximum inscribed rectangle.
172
+ This is a simplified approximation using the bounding box approach.
173
+ For more accurate results, more complex algorithms would be needed.
174
+ """
175
+ if len(points) < 3:
176
+ return None
177
+
178
+ # Get bounding box as approximation
179
+ bbox = self._polygon_bounding_box(points)
180
+ x1, y1, x2, y2 = bbox
181
+ width = x2 - x1
182
+ height = y2 - y1
183
+
184
+ # For convex polygons, we could do better, but this is a reasonable approximation
185
+ # Scale down by a factor to ensure it fits inside
186
+ scale_factor = 0.9
187
+ scaled_width = width * scale_factor
188
+ scaled_height = height * scale_factor
189
+
190
+ center = [(x1 + x2) / 2, (y1 + y2) / 2]
191
+
192
+ return {
193
+ 'center': center,
194
+ 'width': float(scaled_width),
195
+ 'height': float(scaled_height),
196
+ 'area': float(scaled_width * scaled_height),
197
+ 'bbox': [
198
+ center[0] - scaled_width/2,
199
+ center[1] - scaled_height/2,
200
+ center[0] + scaled_width/2,
201
+ center[1] + scaled_height/2
202
+ ]
203
+ }
204
+
205
+ def _calculate_polygon_metrics(self, polygon, normalize=False, img_w=1920, img_h=1080):
206
+ """
207
+ Calculate metrics for a single polygon
208
+
209
+ Args:
210
+ polygon: List of points [[x1, y1], [x2, y2], ...]
211
+ normalize: Whether to normalize metrics by image dimensions
212
+ img_w: Image width for normalization
213
+ img_h: Image height for normalization
214
+
215
+ Returns:
216
+ Dictionary of metrics
217
+ """
218
+ if isinstance(polygon, np.ndarray):
219
+ if polygon.ndim == 1:
220
+ # Reshape flat array to points
221
+ polygon = polygon.reshape(-1, 2)
222
+ polygon = polygon.tolist()
223
+
224
+ # Calculate all metrics
225
+ area = self._polygon_area(polygon)
226
+ perimeter = self._polygon_perimeter(polygon)
227
+ centroid = self._polygon_centroid(polygon)
228
+ bbox = self._polygon_bounding_box(polygon)
229
+ min_rect = self._minimum_external_rect(polygon)
230
+ max_rect = self._maximum_internal_rect(polygon, img_w, img_h)
231
+
232
+ bbox_width = bbox[2] - bbox[0]
233
+ bbox_height = bbox[3] - bbox[1]
234
+ bbox_area = bbox_width * bbox_height
235
+
236
+ metrics = {
237
+ 'area': float(area),
238
+ 'perimeter': float(perimeter),
239
+ 'centroid': centroid,
240
+ 'bbox': bbox,
241
+ 'bbox_width': float(bbox_width),
242
+ 'bbox_height': float(bbox_height),
243
+ 'bbox_area': float(bbox_area),
244
+ 'min_external_rect': min_rect,
245
+ 'max_internal_rect': max_rect,
246
+ 'num_points': len(polygon)
247
+ }
248
+
249
+ # Normalize if requested
250
+ if normalize:
251
+ metrics['area_normalized'] = area / (img_w * img_h)
252
+ metrics['perimeter_normalized'] = perimeter / (img_w + img_h)
253
+ metrics['centroid_normalized'] = [centroid[0] / img_w, centroid[1] / img_h]
254
+ metrics['bbox_width_normalized'] = bbox_width / img_w
255
+ metrics['bbox_height_normalized'] = bbox_height / img_h
256
+ metrics['bbox_area_normalized'] = bbox_area / (img_w * img_h)
257
+
258
+ # Filter to only requested metrics
259
+ selected_metrics = self.config.get('metrics', [])
260
+ if selected_metrics and isinstance(selected_metrics, list):
261
+ filtered = {k: v for k, v in metrics.items()
262
+ if any(sel in k for sel in selected_metrics) or k == 'num_points'}
263
+ return filtered
264
+
265
+ return metrics
266
+
267
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
268
+ """Calculate polygon metrics"""
269
+ try:
270
+ normalize = self.config.get('normalize', False)
271
+ img_w = self.get_config_int('image_width', 1920)
272
+ img_h = self.get_config_int('image_height', 1080)
273
+
274
+ # Check both top level and payload for data
275
+ data_source = msg.get(MessageKeys.PAYLOAD, msg)
276
+
277
+ # Try to get image dimensions from message
278
+ if MessageKeys.IMAGE.PATH in data_source and hasattr(data_source[MessageKeys.IMAGE.PATH], 'shape'):
279
+ img_h, img_w = data_source[MessageKeys.IMAGE.PATH].shape[:2]
280
+ elif MessageKeys.IMAGE.PATH in data_source and isinstance(data_source[MessageKeys.IMAGE.PATH], dict):
281
+ img_w = data_source[MessageKeys.IMAGE.PATH].get('width', img_w)
282
+ img_h = data_source[MessageKeys.IMAGE.PATH].get('height', img_h)
283
+ elif 'image_width' in data_source:
284
+ img_w = data_source['image_width']
285
+ img_h = data_source.get('image_height', img_h)
286
+
287
+ # Process detections with polygons
288
+ if 'detections' in data_source:
289
+ detections = data_source['detections']
290
+ if isinstance(detections, list):
291
+ for det in detections:
292
+ # Look for polygon data in various formats
293
+ polygon = None
294
+ if 'polygon' in det:
295
+ polygon = det['polygon']
296
+ elif 'segmentation' in det:
297
+ polygon = det['segmentation']
298
+ elif 'mask' in det and isinstance(det['mask'], list):
299
+ polygon = det['mask']
300
+
301
+ if polygon is not None:
302
+ det['metrics'] = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
303
+
304
+ data_source['detections'] = detections
305
+ self.send(msg)
306
+
307
+ # Process array of polygons
308
+ elif 'polygons' in data_source:
309
+ polygons = data_source['polygons']
310
+ if isinstance(polygons, list):
311
+ metrics_list = []
312
+ for polygon in polygons:
313
+ metrics = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
314
+ metrics_list.append(metrics)
315
+ data_source['metrics'] = metrics_list
316
+ self.send(msg)
317
+
318
+ # Process single polygon
319
+ elif 'polygon' in data_source:
320
+ polygon = data_source['polygon']
321
+ metrics = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
322
+ data_source['metrics'] = metrics
323
+ self.send(msg)
324
+
325
+ else:
326
+ self.report_error(f"No polygons found in message. Expected 'detections', 'polygons', or 'polygon' field.")
327
+
328
+ except Exception as e:
329
+ self.report_error(f"Error calculating polygon metrics: {str(e)}")
330
+ import traceback
331
+ self.report_error(traceback.format_exc())
@@ -0,0 +1,2 @@
1
+ numpy
2
+ opencv-python
@@ -0,0 +1,3 @@
1
+ from .queue_length_probe_node import QueueLengthProbeNode
2
+
3
+ __all__ = ['QueueLengthProbeNode']
@@ -0,0 +1,116 @@
1
+ """
2
+ Queue Length Probe Node - monitors message queue length.
3
+ Passes messages through while tracking and displaying the queue length.
4
+ """
5
+
6
+ from typing import Any, Dict, Optional
7
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
8
+
9
+ _info = Info()
10
+ _info.add_text("Monitors message queue length by displaying the _queue_length value from incoming messages. Displays the queue length on the node and outputs queue length information.")
11
+ _info.add_header("Inputs")
12
+ _info.add_bullets(
13
+ ("Input 0:", "Any message to monitor (should contain _queue_length)")
14
+ )
15
+ _info.add_header("Outputs")
16
+ _info.add_bullets(
17
+ ("Output 0:", f"Original message with queue length statistics in {MessageKeys.PAYLOAD}")
18
+ )
19
+ _info.add_header("Configuration")
20
+ _info.add_bullets(
21
+ ("None:", "No configuration options")
22
+ )
23
+ _info.add_header("Output Message")
24
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.queue_length').text(" - Current queue length as a number").end()
25
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.display').text(" - Human-readable queue length string (e.g., '5 queued', '0 queued')").end()
26
+ _info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.source_node').text(" - ID of the node where the queue length was measured").end()
27
+
28
+
29
+ class QueueLengthProbeNode(BaseNode):
30
+ """
31
+ Queue Length Probe node - monitors and displays message queue length.
32
+ Passes all messages through unchanged while displaying queue length.
33
+ """
34
+ info = str(_info)
35
+ display_name = 'Queue Length Probe'
36
+ icon = '📊'
37
+ category = 'node probes'
38
+ color = '#E2D96E'
39
+ border_color = '#B8AF4A'
40
+ text_color = '#000000'
41
+ input_count = 1
42
+ output_count = 1
43
+ ui_component = 'queue-length-display'
44
+ ui_component_config = {
45
+ 'format': '{value} queued',
46
+ 'precision': 0
47
+ }
48
+
49
+ sse_handlers = [
50
+ {'type': 'queue_length', 'handler': 'get_queue_length_sse', 'throttle': 0.5},
51
+ ]
52
+
53
+ DEFAULT_CONFIG = {
54
+ MessageKeys.DROP_MESSAGES: 'false'
55
+ }
56
+
57
+ properties = [
58
+ {
59
+ 'name': MessageKeys.DROP_MESSAGES,
60
+ 'label': 'Drop Messages When Busy',
61
+ 'type': 'checkbox',
62
+ 'default': False
63
+ }
64
+ ]
65
+
66
+ def __init__(self, node_id=None, name="queue length probe"):
67
+ super().__init__(node_id, name)
68
+ self._current_queue_length = 0
69
+
70
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
71
+ """
72
+ Pass through the message and extract queue length for display.
73
+ Sends queue length information as the payload.
74
+ """
75
+ # Extract queue length from the message
76
+ queue_length = msg.get(MessageKeys.QUEUE_LENGTH, 0)
77
+ self._current_queue_length = queue_length
78
+
79
+ # Replace payload with queue length statistics (like RateProbeNode)
80
+ msg[MessageKeys.PAYLOAD] = {
81
+ 'queue_length': queue_length,
82
+ 'display': self.get_queue_length_display(queue_length),
83
+ 'source_node': msg.get('source_node_id', 'unknown')
84
+ }
85
+ msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'queue_length')
86
+
87
+ # Send the message through
88
+ self.send(msg)
89
+
90
+ def get_queue_length(self) -> int:
91
+ """Get the current queue length."""
92
+ return self._current_queue_length
93
+
94
+ def get_queue_length_display(self, queue_length: Optional[int] = None) -> str:
95
+ """Get formatted queue length string for display."""
96
+ if queue_length is None:
97
+ queue_length = self._current_queue_length
98
+
99
+ if queue_length == 0:
100
+ return "No items queued"
101
+ elif queue_length == 1:
102
+ return "1 item queued"
103
+ else:
104
+ return f"{queue_length} items queued"
105
+
106
+ def on_stop(self):
107
+ """Reset queue length when stopped."""
108
+ self._current_queue_length = 0
109
+ super().on_stop()
110
+
111
+ def get_queue_length_sse(self):
112
+ """SSE handler: return queue length data for broadcast."""
113
+ return {
114
+ 'display': self.get_queue_length_display(),
115
+ 'queue_length': self.get_queue_length(),
116
+ }
@@ -0,0 +1,3 @@
1
+ from .qwen3_vlm_node import Qwen3VLMNode
2
+
3
+ __all__ = ['Qwen3VLMNode']