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,317 @@
1
+ """
2
+ Crop Node - crops bounding boxes from images.
3
+ """
4
+
5
+ import base64
6
+ import cv2
7
+ import numpy as np
8
+ from typing import Any, Dict
9
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
10
+
11
+ _info = Info()
12
+ _info.add_text("Extracts cropped regions from images based on bounding box coordinates from detections or manual input.")
13
+ _info.add_header("Input")
14
+ _info.add_bullets(
15
+ ("payload.image:", "Source image to crop from."),
16
+ ("payload.detections:", "List of detections with 'bbox' field (when using detection mode)."),
17
+ )
18
+ _info.add_header("Output")
19
+ _info.add_bullets(
20
+ ("payload.image:", "Cropped image data."),
21
+ ("payload.bbox:", "Bounding box coordinates used for the crop (pixels)."),
22
+ ("payload.bbox_normalized:", "Bounding box in normalized coordinates (0.0-1.0)."),
23
+ ("payload.detection:", "Original detection info (if from detections)."),
24
+ ("parts:", "Sequence info when outputting separate messages."),
25
+ )
26
+ _info.add_header("Coordinate Handling")
27
+ _info.add_text("Automatically detects normalized (0.0-1.0) vs pixel coordinates:")
28
+ _info.add_bullets(
29
+ ("Normalized:", "All bbox values are between 0.0 and 1.0"),
30
+ ("Pixels:", "Any bbox value > 1.0 indicates pixel coordinates"),
31
+ )
32
+ _info.add_header("Manual Coordinates")
33
+ _info.add_text("When using manual mode, coordinates are normalized (0.0-1.0):")
34
+ _info.add_bullets(
35
+ ("0.0:", "Left/Top edge"),
36
+ ("0.5:", "Center"),
37
+ ("1.0:", "Right/Bottom edge"),
38
+ )
39
+ _info.add_header("Properties")
40
+ _info.add_bullets(
41
+ ("Bounding Box Source:", "Use detections from message or manual coordinates."),
42
+ ("Manual Coordinates:", "Normalized X1, Y1, X2, Y2 values (0.0-1.0)."),
43
+ ("Output Mode:", "Send all crops in one message or separate messages."),
44
+ )
45
+
46
+
47
+ class CropNode(BaseNode):
48
+ """
49
+ Crop Node - extracts bounding box regions from images.
50
+ Receives image data and bbox coordinates, outputs cropped images.
51
+ """
52
+ info = str(_info)
53
+ display_name = 'Crop'
54
+ icon = '✂️'
55
+ category = 'vision'
56
+ color = '#98D8C8'
57
+ border_color = '#6DB6A3'
58
+ text_color = '#000000'
59
+ input_count = 1
60
+ output_count = 1
61
+
62
+ DEFAULT_CONFIG = {
63
+ 'bbox_source': 'detections',
64
+ 'x1': 0.0,
65
+ 'y1': 0.0,
66
+ 'x2': 0.5,
67
+ 'y2': 0.5,
68
+ 'output_mode': 'separate',
69
+ 'drop_messages': False
70
+ }
71
+
72
+ properties = [
73
+ {
74
+ 'name': 'bbox_source',
75
+ 'label': 'Bounding Box Source',
76
+ 'type': 'select',
77
+ 'options': [
78
+ {'value': 'detections', 'label': 'From msg.payload.detections'},
79
+ {'value': 'manual', 'label': 'Manual coordinates'}
80
+ ],
81
+ 'default': DEFAULT_CONFIG['bbox_source']
82
+ },
83
+ {
84
+ 'name': 'x1',
85
+ 'label': 'X1 (left)',
86
+ 'type': 'number',
87
+ 'default': DEFAULT_CONFIG['x1'],
88
+ 'min': 0.0,
89
+ 'max': 1.0,
90
+ 'step': 0.01,
91
+ 'help': 'Normalized X coordinate (0.0-1.0)',
92
+ 'showIf': {'bbox_source': 'manual'}
93
+ },
94
+ {
95
+ 'name': 'y1',
96
+ 'label': 'Y1 (top)',
97
+ 'type': 'number',
98
+ 'default': DEFAULT_CONFIG['y1'],
99
+ 'min': 0.0,
100
+ 'max': 1.0,
101
+ 'step': 0.01,
102
+ 'help': 'Normalized Y coordinate (0.0-1.0)',
103
+ 'showIf': {'bbox_source': 'manual'}
104
+ },
105
+ {
106
+ 'name': 'x2',
107
+ 'label': 'X2 (right)',
108
+ 'type': 'number',
109
+ 'default': DEFAULT_CONFIG['x2'],
110
+ 'min': 0.0,
111
+ 'max': 1.0,
112
+ 'step': 0.01,
113
+ 'help': 'Normalized X coordinate (0.0-1.0)',
114
+ 'showIf': {'bbox_source': 'manual'}
115
+ },
116
+ {
117
+ 'name': 'y2',
118
+ 'label': 'Y2 (bottom)',
119
+ 'type': 'number',
120
+ 'default': DEFAULT_CONFIG['y2'],
121
+ 'min': 0.0,
122
+ 'max': 1.0,
123
+ 'step': 0.01,
124
+ 'help': 'Normalized Y coordinate (0.0-1.0)',
125
+ 'showIf': {'bbox_source': 'manual'}
126
+ },
127
+ {
128
+ 'name': 'output_mode',
129
+ 'label': 'Output Mode',
130
+ 'type': 'select',
131
+ 'options': [
132
+ {'value': 'all', 'label': 'All crops in one message'},
133
+ {'value': 'separate', 'label': 'Separate messages per crop'}
134
+ ],
135
+ 'default': DEFAULT_CONFIG['output_mode']
136
+ }
137
+ ]
138
+
139
+ def __init__(self, node_id=None, name="crop"):
140
+ super().__init__(node_id, name)
141
+
142
+ def _is_normalized_bbox(self, bbox) -> bool:
143
+ """Check if bbox appears to be normalized (all values 0.0-1.0)."""
144
+ if not bbox or len(bbox) < 4:
145
+ return False
146
+ # If all coordinate values are <= 1.0, assume normalized
147
+ return all(0.0 <= float(v) <= 1.0 for v in bbox[:4])
148
+
149
+ def _convert_bbox_to_pixels(self, bbox, img_w: int, img_h: int) -> tuple:
150
+ """Convert bbox to pixel coordinates, handling both normalized and pixel inputs."""
151
+ if not bbox or len(bbox) < 4:
152
+ return 0, 0, 0, 0
153
+
154
+ x1, y1, x2, y2 = float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])
155
+
156
+ # Auto-detect normalized vs pixel coordinates
157
+ if self._is_normalized_bbox(bbox):
158
+ # Convert normalized to pixels
159
+ x1 = int(x1 * img_w)
160
+ y1 = int(y1 * img_h)
161
+ x2 = int(x2 * img_w)
162
+ y2 = int(y2 * img_h)
163
+ else:
164
+ # Already pixels, just convert to int
165
+ x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
166
+
167
+ return x1, y1, x2, y2
168
+
169
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
170
+ """Process incoming image and crop based on bounding boxes."""
171
+ payload = msg.get(MessageKeys.PAYLOAD)
172
+ if not payload or not isinstance(payload, dict):
173
+ self.report_error("No image payload found")
174
+ return
175
+
176
+ # Decode image and track input format
177
+ image, input_format = self._decode_image(payload)
178
+ if image is None:
179
+ return
180
+
181
+ bbox_source = self.config.get('bbox_source', 'detections')
182
+ crops = []
183
+
184
+ # Get image dimensions for coordinate conversion
185
+ img_h, img_w = image.shape[:2]
186
+
187
+ if bbox_source == 'detections':
188
+ # Extract from msg.payload.detections
189
+ detections = payload.get('detections', [])
190
+ if not detections:
191
+ self.report_error("No detections found in payload")
192
+ return
193
+
194
+ for i, detection in enumerate(detections):
195
+ bbox = detection.get('bbox')
196
+ if bbox and len(bbox) >= 4:
197
+ # Auto-convert normalized coords to pixels
198
+ x1, y1, x2, y2 = self._convert_bbox_to_pixels(bbox, img_w, img_h)
199
+ crop = self._crop_image(image, x1, y1, x2, y2)
200
+ if crop is not None:
201
+ # Store both pixel and normalized bbox
202
+ bbox_normalized = [x1 / img_w, y1 / img_h, x2 / img_w, y2 / img_h]
203
+ crops.append({
204
+ MessageKeys.IMAGE.PATH: crop,
205
+ 'bbox': [x1, y1, x2, y2],
206
+ 'bbox_normalized': bbox_normalized,
207
+ 'detection': detection,
208
+ 'index': i
209
+ })
210
+ else:
211
+ # Use manual coordinates (normalized 0.0-1.0)
212
+ norm_x1 = self.get_config_float('x1', 0.0)
213
+ norm_y1 = self.get_config_float('y1', 0.0)
214
+ norm_x2 = self.get_config_float('x2', 0.5)
215
+ norm_y2 = self.get_config_float('y2', 0.5)
216
+
217
+ x1 = int(norm_x1 * img_w)
218
+ y1 = int(norm_y1 * img_h)
219
+ x2 = int(norm_x2 * img_w)
220
+ y2 = int(norm_y2 * img_h)
221
+
222
+ crop = self._crop_image(image, x1, y1, x2, y2)
223
+ if crop is not None:
224
+ crops.append({
225
+ MessageKeys.IMAGE.PATH: crop,
226
+ 'bbox': [x1, y1, x2, y2],
227
+ 'bbox_normalized': [norm_x1, norm_y1, norm_x2, norm_y2],
228
+ 'index': 0
229
+ })
230
+
231
+ if not crops:
232
+ self.report_error("No valid crops produced")
233
+ return
234
+
235
+ output_mode = self.config.get('output_mode', 'separate')
236
+
237
+ # For manual mode with single crop, output as single object not array
238
+ if bbox_source == 'manual' and len(crops) == 1:
239
+ crop_data = crops[0]
240
+ encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
241
+ if encoded:
242
+ # Single crop output
243
+ msg[MessageKeys.PAYLOAD] = {
244
+ MessageKeys.IMAGE.PATH: encoded,
245
+ 'bbox': crop_data['bbox'],
246
+ 'bbox_normalized': crop_data.get('bbox_normalized'),
247
+ 'index': crop_data['index']
248
+ }
249
+ msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crop')
250
+ self.send(msg)
251
+ elif output_mode == 'separate':
252
+ # Send each crop as a separate message
253
+ for crop_data in crops:
254
+ encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
255
+ if encoded:
256
+ msg_copy = msg.copy()
257
+ crop_info = {
258
+ MessageKeys.IMAGE.PATH: encoded,
259
+ 'bbox': crop_data['bbox'],
260
+ 'bbox_normalized': crop_data.get('bbox_normalized'),
261
+ 'index': crop_data['index']
262
+ }
263
+ if 'detection' in crop_data:
264
+ crop_info['detection'] = crop_data['detection']
265
+
266
+ msg_copy[MessageKeys.PAYLOAD] = crop_info
267
+ msg_copy[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crop')
268
+ msg_copy['crop_count'] = len(crops)
269
+ msg_copy['parts'] = {'index': crop_data['index'], 'count': len(crops), 'id': msg.get(MessageKeys.MSG_ID)}
270
+ self.send(msg_copy)
271
+ else:
272
+ # Output all crops in one message as array
273
+ encoded_crops = []
274
+ for crop_data in crops:
275
+ encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
276
+ if encoded:
277
+ crop_info = {
278
+ MessageKeys.IMAGE.PATH: encoded,
279
+ 'bbox': crop_data['bbox'],
280
+ 'bbox_normalized': crop_data.get('bbox_normalized'),
281
+ 'index': crop_data['index']
282
+ }
283
+ if 'detection' in crop_data:
284
+ crop_info['detection'] = crop_data['detection']
285
+ encoded_crops.append(crop_info)
286
+
287
+ msg[MessageKeys.PAYLOAD] = encoded_crops
288
+ msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crops')
289
+ msg['crop_count'] = len(encoded_crops)
290
+ self.send(msg)
291
+
292
+ def _decode_image(self, payload: Dict[str, Any]):
293
+ """Decode image from payload using BaseNode helper. Returns (image, format_type) tuple."""
294
+ return self.decode_image(payload)
295
+
296
+ def _crop_image(self, image: np.ndarray, x1: int, y1: int, x2: int, y2: int) -> np.ndarray:
297
+ """Crop image to bounding box."""
298
+ try:
299
+ h, w = image.shape[:2]
300
+ # Clamp coordinates
301
+ x1 = max(0, min(x1, w))
302
+ y1 = max(0, min(y1, h))
303
+ x2 = max(0, min(x2, w))
304
+ y2 = max(0, min(y2, h))
305
+
306
+ if x2 <= x1 or y2 <= y1:
307
+ self.report_error(f"Invalid bbox: [{x1}, {y1}, {x2}, {y2}]")
308
+ return None
309
+
310
+ return image[y1:y2, x1:x2]
311
+ except Exception as e:
312
+ self.report_error(f"Error cropping image: {e}")
313
+ return None
314
+
315
+ def _encode_image(self, image: np.ndarray, format_type: str):
316
+ """Encode image using BaseNode helper matching input format."""
317
+ return self.encode_image(image, format_type)
@@ -0,0 +1,2 @@
1
+ opencv-python>=4.5.0
2
+ numpy>=1.19.0
@@ -0,0 +1,4 @@
1
+ from .debug_node import DebugNode
2
+
3
+ __all__ = ['DebugNode']
4
+
@@ -0,0 +1,169 @@
1
+ """
2
+ Debug node - prints messages to console.
3
+ Similar to Node-RED's debug node.
4
+ """
5
+
6
+ import time
7
+ import numpy as np
8
+ from typing import Any, Dict
9
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
10
+
11
+ _info = Info()
12
+ _info.add_text("Displays message content for debugging purposes. Similar to Node-RED's debug node.")
13
+ _info.add_header("Input")
14
+ _info.add_bullets(
15
+ ("msg:", "Any message to inspect. Large values are automatically truncated."),
16
+ )
17
+ _info.add_header("Output")
18
+ _info.add_text("This node has no outputs. Messages are displayed in the debug panel.")
19
+ _info.add_header("Properties")
20
+ _info.add_bullets(
21
+ ("Output:", "Show msg.payload only or complete message."),
22
+ )
23
+ _info.add_header("UI")
24
+ _info.add_text("Toggle the node on/off using the button. Last 10 messages are stored.")
25
+
26
+
27
+ class DebugNode(BaseNode):
28
+ """
29
+ Debug node - prints messages to console.
30
+ Similar to Node-RED's debug node.
31
+ """
32
+ info = str(_info)
33
+ display_name = 'Debug'
34
+ icon = '🐛'
35
+ category = 'common'
36
+ color = '#87A980'
37
+ border_color = '#5F7858'
38
+ text_color = '#000000'
39
+ input_count = 1
40
+ output_count = 0
41
+ ui_component = 'toggle'
42
+ ui_component_config = {
43
+ 'action': 'toggle_debug',
44
+ 'label': 'Enable'
45
+ }
46
+
47
+ api_routes = [
48
+ {'route': 'debug', 'methods': ['GET'], 'handler': 'get_debug_messages'},
49
+ {'route': 'debug', 'methods': ['DELETE'], 'handler': 'clear_debug_messages'},
50
+ ]
51
+
52
+ sse_handlers = [
53
+ {'type': 'messages', 'handler': 'consume_messages'},
54
+ ]
55
+
56
+ DEFAULT_CONFIG = {
57
+ 'console': True,
58
+ 'complete': MessageKeys.PAYLOAD,
59
+ 'drop_messages': False
60
+ }
61
+
62
+ properties = [
63
+ {
64
+ 'name': 'drop_messages',
65
+ 'label': 'Drop Messages When Busy',
66
+ 'type': 'checkbox',
67
+ 'default': False
68
+ },
69
+ {
70
+ 'name': 'complete',
71
+ 'label': 'Output',
72
+ 'type': 'select',
73
+ 'options': [
74
+ {'value': MessageKeys.PAYLOAD, 'label': f'msg.{MessageKeys.PAYLOAD}'},
75
+ {'value': 'msg', 'label': 'Complete msg'}
76
+ ],
77
+ 'default': DEFAULT_CONFIG['complete']
78
+ }
79
+ ]
80
+
81
+ def __init__(self, node_id=None, name="debug"):
82
+ super().__init__(node_id, name)
83
+ self.messages = [] # Store messages for API access
84
+
85
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
86
+ """
87
+ Print/store the message for debugging.
88
+ """
89
+ # Skip if debug node is disabled
90
+ if not self.enabled:
91
+ return
92
+
93
+ complete = self.config.get('complete', MessageKeys.PAYLOAD)
94
+
95
+ if complete == 'msg':
96
+ output = msg
97
+ display_key = 'Complete msg'
98
+ elif complete == MessageKeys.PAYLOAD:
99
+ output = msg.get(MessageKeys.PAYLOAD)
100
+ display_key = f'msg.{MessageKeys.PAYLOAD}'
101
+ else:
102
+ # Try to get nested property (only top-level for now)
103
+ output = msg.get(complete, msg.get(MessageKeys.PAYLOAD))
104
+ display_key = f"msg.{complete}"
105
+
106
+
107
+ # Recursively truncate large values in dicts/lists, but not the whole message
108
+ def truncate_values(val, maxlen=300):
109
+ if isinstance(val, (bytes, bytearray)):
110
+ return f"<binary data, {len(val)} bytes>"
111
+ elif isinstance(val, np.ndarray):
112
+ return f"<numpy array, shape={val.shape}, dtype={val.dtype}>"
113
+ elif isinstance(val, dict):
114
+ return {k: truncate_values(v, maxlen) for k, v in val.items()}
115
+ elif isinstance(val, list):
116
+ return [truncate_values(v, maxlen) for v in val]
117
+ else:
118
+ s = str(val)
119
+ if len(s) > maxlen:
120
+ return s[:maxlen] + f"... [truncated, {len(s)} chars]"
121
+ return val
122
+
123
+ display_output = truncate_values(output)
124
+
125
+ timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
126
+ debug_entry = {
127
+ 'timestamp': timestamp,
128
+ 'node': self.name,
129
+ 'node_id': self.id,
130
+ 'display_key': display_key,
131
+ 'output': display_output
132
+ }
133
+
134
+ self.messages.append(debug_entry)
135
+
136
+ # Keep only last 10 messages
137
+ if len(self.messages) > 10:
138
+ self.messages = self.messages[-10:]
139
+
140
+ # if self.config.get('console', True):
141
+ # print(f"[{timestamp}] [{self.name}] {output}")
142
+
143
+ # Pass through (optional)
144
+ # self.send(msg)
145
+
146
+ def set_enabled(self, enabled: bool):
147
+ """Set the enabled state of the debug node."""
148
+ self.enabled = enabled
149
+
150
+ def get_enabled(self) -> bool:
151
+ """Get the enabled state of the debug node."""
152
+ return self.enabled
153
+
154
+ def get_debug_messages(self):
155
+ """API route handler: return stored debug messages."""
156
+ return self.messages
157
+
158
+ def clear_debug_messages(self):
159
+ """API route handler: clear stored debug messages."""
160
+ self.messages.clear()
161
+ return None # 204 No Content
162
+
163
+ def consume_messages(self):
164
+ """SSE handler: return and clear pending messages."""
165
+ if self.messages:
166
+ msgs = self.messages.copy()
167
+ self.messages.clear()
168
+ return msgs
169
+ return None
@@ -0,0 +1 @@
1
+ numpy>=1.19.0