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,250 @@
1
+ """
2
+ BBoxMetricsNode - Calculate metrics from bounding boxes
3
+
4
+ This node calculates various metrics from bounding box predictions including:
5
+ - Width and height
6
+ - Area
7
+ - Aspect ratio
8
+ - Center point coordinates
9
+ - Diagonal length
10
+
11
+ Input format:
12
+ msg['detections'] - List of detections with bounding boxes
13
+ OR
14
+ msg['boxes'] - Array of bounding boxes [x1, y1, x2, y2]
15
+ OR
16
+ msg['bbox'] - Single bounding box [x1, y1, x2, y2]
17
+
18
+ Output format:
19
+ msg['metrics'] - Dictionary containing calculated metrics
20
+ msg['detections'] - Original detections with added 'metrics' field
21
+ """
22
+
23
+ import sys
24
+ import os
25
+ from typing import Any, Dict
26
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
27
+
28
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
29
+ import numpy as np
30
+
31
+ _info = Info()
32
+ _info.add_text("Calculates geometric metrics from bounding boxes including dimensions, area, aspect ratio, center point, and diagonal length.")
33
+ _info.add_header("Input")
34
+ _info.add_bullets(
35
+ ("detections:", "List of detection objects containing 'bbox' field."),
36
+ ("boxes:", "Array of bounding boxes [x1, y1, x2, y2]."),
37
+ ("bbox:", "Single bounding box [x1, y1, x2, y2]."),
38
+ )
39
+ _info.add_header("Output")
40
+ _info.add_bullets(
41
+ ("metrics:", "Calculated metrics (width, height, area, aspect_ratio, center, diagonal)."),
42
+ ("detections:", "Original detections with 'metrics' field added to each."),
43
+ )
44
+ _info.add_header("Properties")
45
+ _info.add_bullets(
46
+ ("Metrics to Calculate:", "Select which metrics to compute."),
47
+ ("Normalize:", "Normalize values by image dimensions."),
48
+ ("Image Dimensions:", "Reference size for normalization."),
49
+ )
50
+
51
+
52
+ class BBoxMetricsNode(BaseNode):
53
+ """Calculate metrics from bounding boxes"""
54
+ info = str(_info)
55
+ display_name = 'BBox Metrics'
56
+ icon = '📏'
57
+ category = 'analysis'
58
+ color = '#FFE5B4'
59
+ border_color = '#D4AF37'
60
+ text_color = '#000000'
61
+
62
+ DEFAULT_CONFIG = {
63
+ 'metrics': ['width', 'height', 'area', 'aspect_ratio', 'center', 'diagonal'],
64
+ 'normalize': False,
65
+ 'use_image_dimensions': False,
66
+ 'image_width': 1920,
67
+ 'image_height': 1080
68
+ }
69
+
70
+ properties = [
71
+ {
72
+ 'name': 'metrics',
73
+ 'label': 'Metrics to Calculate',
74
+ 'type': 'multiselect',
75
+ 'options': ['width', 'height', 'area', 'aspect_ratio', 'center', 'diagonal'],
76
+ 'default': DEFAULT_CONFIG['metrics']
77
+ },
78
+ {
79
+ 'name': 'normalize',
80
+ 'label': 'Normalize by Image Size',
81
+ 'type': 'checkbox',
82
+ 'default': DEFAULT_CONFIG['normalize']
83
+ },
84
+ {
85
+ 'name': 'use_image_dimensions',
86
+ 'label': 'Use Image Dimensions from Message',
87
+ 'type': 'checkbox',
88
+ 'default': DEFAULT_CONFIG['use_image_dimensions']
89
+ },
90
+ {
91
+ 'name': 'image_width',
92
+ 'label': 'Reference Image Width',
93
+ 'type': 'number',
94
+ 'default': DEFAULT_CONFIG['image_width'],
95
+ 'showIf': {'use_image_dimensions': False}
96
+ },
97
+ {
98
+ 'name': 'image_height',
99
+ 'label': 'Reference Image Height',
100
+ 'type': 'number',
101
+ 'default': DEFAULT_CONFIG['image_height'],
102
+ 'showIf': {'use_image_dimensions': False}
103
+ }
104
+ ]
105
+
106
+ def __init__(self, node_id=None, name="BBox Metrics"):
107
+ super().__init__(node_id, name)
108
+
109
+ def _calculate_bbox_metrics(self, bbox, normalize=False, img_w=1920, img_h=1080, bbox_format='xyxy'):
110
+ """
111
+ Calculate metrics for a single bounding box
112
+
113
+ Args:
114
+ bbox: Bounding box in specified format
115
+ normalize: Whether to normalize metrics by image dimensions
116
+ img_w: Image width for normalization
117
+ img_h: Image height for normalization
118
+ bbox_format: Format of bbox - 'xyxy' [x1,y1,x2,y2], 'xywh' [x,y,w,h], 'cxcywh' [cx,cy,w,h]
119
+
120
+ Returns:
121
+ Dictionary of metrics
122
+ """
123
+ # Convert bbox to xyxy format for consistent processing
124
+ if bbox_format == 'xyxy':
125
+ x1, y1, x2, y2 = bbox[:4]
126
+ elif bbox_format == 'xywh':
127
+ x, y, w, h = bbox[:4]
128
+ x1, y1, x2, y2 = x, y, x + w, y + h
129
+ elif bbox_format == 'cxcywh':
130
+ cx, cy, w, h = bbox[:4]
131
+ x1 = cx - w / 2
132
+ y1 = cy - h / 2
133
+ x2 = cx + w / 2
134
+ y2 = cy + h / 2
135
+ else:
136
+ # Default to xyxy if unknown format
137
+ x1, y1, x2, y2 = bbox[:4]
138
+
139
+ # Basic dimensions
140
+ width = abs(x2 - x1)
141
+ height = abs(y2 - y1)
142
+ area = width * height
143
+
144
+ # Center point
145
+ center_x = (x1 + x2) / 2
146
+ center_y = (y1 + y2) / 2
147
+
148
+ # Aspect ratio (width/height)
149
+ aspect_ratio = width / height if height > 0 else 0
150
+
151
+ # Diagonal length
152
+ diagonal = np.sqrt(width**2 + height**2)
153
+
154
+ metrics = {
155
+ 'width': width,
156
+ 'height': height,
157
+ 'area': area,
158
+ 'aspect_ratio': aspect_ratio,
159
+ 'center': {'x': center_x, 'y': center_y},
160
+ 'diagonal': diagonal,
161
+ 'bbox': [x1, y1, x2, y2],
162
+ 'bbox_format': 'xyxy' # Output is always normalized to xyxy
163
+ }
164
+
165
+ # Normalize if requested
166
+ if normalize:
167
+ metrics['width_normalized'] = width / img_w
168
+ metrics['height_normalized'] = height / img_h
169
+ metrics['area_normalized'] = area / (img_w * img_h)
170
+ metrics['center_normalized'] = [center_x / img_w, center_y / img_h]
171
+ metrics['diagonal_normalized'] = diagonal / np.sqrt(img_w**2 + img_h**2)
172
+
173
+ # Filter to only requested metrics
174
+ selected_metrics = self.config.get('metrics', [])
175
+ if selected_metrics:
176
+ filtered = {k: v for k, v in metrics.items() if k in selected_metrics or k == 'bbox'}
177
+ # Always include normalized versions if normalize is enabled
178
+ if normalize:
179
+ for key in list(filtered.keys()):
180
+ norm_key = f"{key}_normalized"
181
+ if norm_key in metrics:
182
+ filtered[norm_key] = metrics[norm_key]
183
+ return filtered
184
+
185
+ return metrics
186
+
187
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
188
+ """Calculate bounding box metrics"""
189
+ try:
190
+ normalize = self.config.get('normalize', False)
191
+ use_image_dimensions = self.config.get('use_image_dimensions', True)
192
+ img_w = self.get_config_int('image_width', 1920)
193
+ img_h = self.get_config_int('image_height', 1080)
194
+
195
+ # Check both top level and payload for data
196
+ data_source = msg.get(MessageKeys.PAYLOAD, msg)
197
+
198
+ # Get bbox format from message (default to xyxy)
199
+ bbox_format = data_source.get('bbox_format', 'xyxy')
200
+
201
+ # Try to get image dimensions from message if enabled
202
+ if use_image_dimensions:
203
+ if MessageKeys.IMAGE.PATH in data_source and hasattr(data_source[MessageKeys.IMAGE.PATH], 'shape'):
204
+ img_h, img_w = data_source[MessageKeys.IMAGE.PATH].shape[:2]
205
+ elif MessageKeys.IMAGE.PATH in data_source and isinstance(data_source[MessageKeys.IMAGE.PATH], dict):
206
+ # Handle nested image structure
207
+ img_w = data_source[MessageKeys.IMAGE.PATH].get(MessageKeys.IMAGE.WIDTH, img_w)
208
+ img_h = data_source[MessageKeys.IMAGE.PATH].get(MessageKeys.IMAGE.HEIGHT, img_h)
209
+ elif 'image_width' in data_source:
210
+ img_w = data_source['image_width']
211
+ img_h = data_source.get('image_height', img_h)
212
+
213
+ # Process detections with bounding boxes
214
+ if 'detections' in data_source:
215
+ detections = data_source['detections']
216
+ if isinstance(detections, list):
217
+ for det in detections:
218
+ if 'bbox' in det or 'box' in det:
219
+ bbox = det.get('bbox', det.get('box'))
220
+ # Check for detection-specific bbox_format, fallback to message-level
221
+ det_bbox_format = det.get('bbox_format', bbox_format)
222
+ det['metrics'] = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, det_bbox_format)
223
+ data_source['detections'] = detections
224
+ self.send(msg)
225
+
226
+ # Process array of boxes
227
+ elif 'boxes' in data_source:
228
+ boxes = data_source['boxes']
229
+ if isinstance(boxes, (list, np.ndarray)):
230
+ metrics_list = []
231
+ for bbox in boxes:
232
+ metrics = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, bbox_format)
233
+ metrics_list.append(metrics)
234
+ data_source['metrics'] = metrics_list
235
+ self.send(msg)
236
+
237
+ # Process single bbox
238
+ elif 'bbox' in data_source:
239
+ bbox = data_source['bbox']
240
+ metrics = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, bbox_format)
241
+ data_source['metrics'] = metrics
242
+ self.send(msg)
243
+
244
+ else:
245
+ self.report_error(f"No bounding boxes found in message. Expected 'detections', 'boxes', or 'bbox' field.")
246
+
247
+ except Exception as e:
248
+ self.report_error(f"Error calculating bbox metrics: {str(e)}")
249
+ import traceback
250
+ self.report_error(traceback.format_exc())
@@ -0,0 +1 @@
1
+ numpy
@@ -0,0 +1,3 @@
1
+ from .batch_node import BatchNode
2
+
3
+ __all__ = ['BatchNode']
@@ -0,0 +1,86 @@
1
+ """
2
+ Batch Node - groups messages into batches.
3
+ """
4
+
5
+ from typing import Any, Dict, List
6
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
7
+
8
+ _info = Info()
9
+ _info.add_text("Collects incoming messages into batches and sends them together as an array.")
10
+ _info.add_header("Input")
11
+ _info.add_bullets(
12
+ ("payload:", "Any message payload to be collected into a batch."),
13
+ )
14
+ _info.add_header("Output")
15
+ _info.add_bullets(
16
+ ("payload:", "Array of payloads from the batched messages."),
17
+ ("batch_size:", "Number of messages in the batch."),
18
+ )
19
+ _info.add_header("Properties")
20
+ _info.add_bullets(
21
+ ("Batch Size:", "Number of messages to collect before sending."),
22
+ ("Overlap:", "Number of messages to retain for the next batch."),
23
+ )
24
+
25
+
26
+ class BatchNode(BaseNode):
27
+ """
28
+ Batch Node - collects messages into batches and sends them together.
29
+ """
30
+ info = str(_info)
31
+ display_name = 'Batch'
32
+ icon = '📦'
33
+ category = 'function'
34
+ color = '#DDA0DD'
35
+ border_color = '#BA55D3'
36
+ text_color = '#000000'
37
+ input_count = 1
38
+ output_count = 1
39
+
40
+ DEFAULT_CONFIG = {
41
+ 'batch_size': 10,
42
+ 'overlap': 0,
43
+ MessageKeys.DROP_MESSAGES: False
44
+ }
45
+
46
+ properties = [
47
+ {
48
+ 'name': 'batch_size',
49
+ 'label': 'Batch Size',
50
+ 'type': 'number',
51
+ 'default': DEFAULT_CONFIG['batch_size']
52
+ },
53
+ {
54
+ 'name': 'overlap',
55
+ 'label': 'Overlap (messages)',
56
+ 'type': 'number',
57
+ 'default': DEFAULT_CONFIG['overlap'],
58
+ }
59
+ ]
60
+
61
+ def __init__(self, node_id=None, name="batch"):
62
+ super().__init__(node_id, name)
63
+ self.buffer = []
64
+
65
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
66
+ """Collect messages into batches."""
67
+ self.buffer.append(msg.copy())
68
+
69
+ batch_size = self.get_config_int('batch_size', 10)
70
+
71
+ if len(self.buffer) >= batch_size:
72
+ # Send batch
73
+ batch = self.buffer[:batch_size]
74
+ out_msg = self.create_message(
75
+ payload=[m.get(MessageKeys.PAYLOAD) for m in batch],
76
+ topic=batch[0].get(MessageKeys.TOPIC, '') if batch else '',
77
+ batch_size=len(batch)
78
+ )
79
+ self.send(out_msg)
80
+
81
+ # Handle overlap
82
+ overlap = self.get_config_int('overlap', 0)
83
+ if overlap > 0 and overlap < batch_size:
84
+ self.buffer = self.buffer[batch_size - overlap:]
85
+ else:
86
+ self.buffer = []
@@ -0,0 +1 @@
1
+ # No external dependencies - uses only Python standard library
@@ -0,0 +1,4 @@
1
+ from .camera_node import CameraNode
2
+
3
+ __all__ = ['CameraNode']
4
+
@@ -0,0 +1,204 @@
1
+ """
2
+ Camera node - captures frames from a webcam using OpenCV.
3
+ """
4
+
5
+ import cv2
6
+ import base64
7
+ import threading
8
+ import time
9
+ from typing import Any, Dict
10
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
11
+
12
+ _info = Info()
13
+ _info.add_text("Captures video frames from a webcam or video device using OpenCV and outputs them as messages.")
14
+ _info.add_header("Inputs")
15
+ _info.add_text("This node has no inputs. It generates frames automatically when the workflow starts.")
16
+ _info.add_header("Output")
17
+ _info.add_bullets(
18
+ ("payload.image:", "Image data (JPEG base64 or numpy array depending on config)."),
19
+ ("payload.image.width:", "Frame width in pixels."),
20
+ ("payload.image.height:", "Frame height in pixels."),
21
+ )
22
+ _info.add_header("Properties")
23
+ _info.add_bullets(
24
+ ("Camera Index:", "Device index (0 for default camera)."),
25
+ ("Frame Rate:", "Target FPS for capture."),
26
+ ("Width/Height:", "Resolution settings."),
27
+ ("Encode as JPEG:", "Output as base64 JPEG or raw numpy array."),
28
+ )
29
+
30
+
31
+ class CameraNode(BaseNode):
32
+ """
33
+ Camera node - captures frames from a webcam and outputs them as messages.
34
+ """
35
+ info = str(_info)
36
+ display_name = 'Camera'
37
+ icon = '📷'
38
+ category = 'input'
39
+ color = '#C0DEED'
40
+ border_color = '#7FA7C9'
41
+ text_color = '#000000'
42
+ input_count = 0 # No input
43
+ output_count = 1
44
+
45
+ DEFAULT_CONFIG = {
46
+ MessageKeys.CAMERA.DEVICE_INDEX: 0,
47
+ MessageKeys.CAMERA.FPS: 30,
48
+ MessageKeys.CAMERA.WIDTH: 640,
49
+ MessageKeys.CAMERA.HEIGHT: 480,
50
+ MessageKeys.CAMERA.ENCODE_JPEG: False,
51
+ MessageKeys.CAMERA.JPEG_QUALITY: 75
52
+ }
53
+
54
+ properties = [
55
+ {
56
+ 'name': MessageKeys.CAMERA.DEVICE_INDEX,
57
+ 'label': 'Camera Index',
58
+ 'type': 'number',
59
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.DEVICE_INDEX]
60
+ },
61
+ {
62
+ 'name': MessageKeys.CAMERA.FPS,
63
+ 'label': 'Frame Rate (FPS)',
64
+ 'type': 'number',
65
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.FPS]
66
+ },
67
+ {
68
+ 'name': MessageKeys.CAMERA.WIDTH,
69
+ 'label': 'Width',
70
+ 'type': 'number',
71
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.WIDTH]
72
+ },
73
+ {
74
+ 'name': MessageKeys.CAMERA.HEIGHT,
75
+ 'label': 'Height',
76
+ 'type': 'number',
77
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.HEIGHT]
78
+ },
79
+ {
80
+ 'name': MessageKeys.CAMERA.ENCODE_JPEG,
81
+ 'label': 'Encode as JPEG',
82
+ 'type': 'checkbox',
83
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.ENCODE_JPEG]
84
+ },
85
+ {
86
+ 'name': MessageKeys.CAMERA.JPEG_QUALITY,
87
+ 'label': 'JPEG Quality (1-100)',
88
+ 'type': 'number',
89
+ 'default': DEFAULT_CONFIG[MessageKeys.CAMERA.JPEG_QUALITY]
90
+ }
91
+ ]
92
+
93
+ def __init__(self, node_id=None, name="camera"):
94
+ super().__init__(node_id, name)
95
+ self.camera = None
96
+ self.capture_thread = None
97
+ self.running = False
98
+
99
+ def on_start(self):
100
+ """Start the camera capture when workflow starts."""
101
+ super().on_start() # Start base node worker thread
102
+
103
+ camera_index = self.get_config_int(MessageKeys.CAMERA.DEVICE_INDEX, 0)
104
+ fps = self.get_config_int(MessageKeys.CAMERA.FPS, 10)
105
+ width = self.get_config_int(MessageKeys.CAMERA.WIDTH, 640)
106
+ height = self.get_config_int(MessageKeys.CAMERA.HEIGHT, 480)
107
+
108
+ try:
109
+ # Open the camera
110
+ self.camera = cv2.VideoCapture(camera_index)
111
+
112
+ if not self.camera.isOpened():
113
+ self.report_error(f"Failed to open camera {camera_index}")
114
+ return
115
+
116
+ # Set resolution
117
+ self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, width)
118
+ self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
119
+
120
+ # Start capture thread
121
+ self.running = True
122
+ self.capture_thread = threading.Thread(target=self._capture_loop, args=(fps,), daemon=True)
123
+ self.capture_thread.start()
124
+
125
+ except Exception as e:
126
+ self.report_error(f"Error starting camera: {e}")
127
+
128
+ def on_stop(self):
129
+ """Stop the camera capture when workflow stops."""
130
+ super().on_stop() # Stop base node worker thread
131
+
132
+ self.running = False
133
+
134
+ if self.capture_thread:
135
+ self.capture_thread.join(timeout=2.0)
136
+ self.capture_thread = None
137
+
138
+ if self.camera:
139
+ self.camera.release()
140
+ self.camera = None
141
+
142
+ def on_close(self):
143
+ """Cleanup when node is deleted."""
144
+ self.on_stop()
145
+
146
+ def _capture_loop(self, fps):
147
+ """Capture frames in a loop and send them as messages."""
148
+ frame_interval = 1.0 / fps
149
+ encode_jpeg = self.config.get(MessageKeys.CAMERA.ENCODE_JPEG, True)
150
+
151
+ while self.running and self.camera and self.camera.isOpened():
152
+ start_time = time.time()
153
+
154
+ try:
155
+ ret, frame = self.camera.read()
156
+
157
+ if not ret or frame is None:
158
+ self.report_error("Failed to capture frame")
159
+ time.sleep(frame_interval)
160
+ continue
161
+
162
+ # Prepare the payload
163
+ if encode_jpeg:
164
+ # Encode frame as JPEG with quality setting
165
+ jpeg_quality = self.get_config_int(MessageKeys.CAMERA.JPEG_QUALITY, 75)
166
+ encode_params = (cv2.IMWRITE_JPEG_QUALITY, jpeg_quality)
167
+ ret, buffer = cv2.imencode('.jpg', frame, encode_params)
168
+ if ret:
169
+ # Convert to bytes (more efficient than base64)
170
+ jpeg_bytes = buffer.tobytes()
171
+ # Convert to base64 for JSON transmission
172
+ jpeg_base64 = base64.b64encode(jpeg_bytes).decode('utf-8')
173
+ payload = {
174
+ MessageKeys.IMAGE.FORMAT: 'jpeg',
175
+ MessageKeys.IMAGE.ENCODING: 'base64',
176
+ MessageKeys.IMAGE.DATA: jpeg_base64,
177
+ MessageKeys.IMAGE.WIDTH: frame.shape[1],
178
+ MessageKeys.IMAGE.HEIGHT: frame.shape[0]
179
+ }
180
+ else:
181
+ self.report_error("Failed to encode JPEG")
182
+ continue
183
+ else:
184
+ # Send raw frame as numpy array
185
+ payload = {
186
+ MessageKeys.IMAGE.FORMAT: 'bgr',
187
+ MessageKeys.IMAGE.ENCODING: 'numpy',
188
+ MessageKeys.IMAGE.DATA: frame,
189
+ MessageKeys.IMAGE.WIDTH: frame.shape[1],
190
+ MessageKeys.IMAGE.HEIGHT: frame.shape[0]
191
+ }
192
+
193
+ # Create and send message with image wrapped in payload.image
194
+ msg = self.create_message(payload={MessageKeys.IMAGE.PATH: payload}, topic='camera/frame')
195
+ self.send(msg)
196
+
197
+ except Exception as e:
198
+ self.report_error(f"Error capturing frame: {e}")
199
+
200
+ # Maintain frame rate
201
+ elapsed = time.time() - start_time
202
+ sleep_time = max(0, frame_interval - elapsed)
203
+ if sleep_time > 0:
204
+ time.sleep(sleep_time)
@@ -0,0 +1,2 @@
1
+ opencv-python>=4.5.0
2
+ numpy>=1.19.0
@@ -0,0 +1 @@
1
+ from .change_node import ChangeNode