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,136 @@
1
+ """
2
+ OpenCV Morphology Node - applies morphological operations to images.
3
+ """
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from typing import Any, Dict
8
+ from pynode.nodes.base_node import BaseNode, process_image, Info
9
+
10
+ _info = Info()
11
+ _info.add_text("Applies morphological operations to binary or grayscale images. Used for noise removal, shape analysis, and image preprocessing.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Binary or grayscale image"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(("Output 0:", "Processed image"))
16
+ _info.add_header("Operations")
17
+ _info.add_bullets(
18
+ ("Erode:", "Shrinks white regions, removes small noise"),
19
+ ("Dilate:", "Expands white regions, fills small holes"),
20
+ ("Open:", "Erode then dilate - removes noise while preserving shape"),
21
+ ("Close:", "Dilate then erode - fills holes while preserving shape"),
22
+ ("Gradient:", "Difference between dilation and erosion - finds edges"),
23
+ ("Top Hat:", "Difference between input and opening - finds bright spots"),
24
+ ("Black Hat:", "Difference between closing and input - finds dark spots")
25
+ )
26
+
27
+ class MorphologyNode(BaseNode):
28
+ """
29
+ Morphology node - applies morphological operations to binary/grayscale images.
30
+ Supports erosion, dilation, opening, closing, gradient, tophat, and blackhat.
31
+ """
32
+ info = str(_info)
33
+ display_name = 'Morphology'
34
+ icon = '⬡'
35
+ category = 'opencv'
36
+ color = '#4A90D9'
37
+ border_color = '#2E6BB0'
38
+ text_color = '#FFFFFF'
39
+ input_count = 1
40
+ output_count = 1
41
+
42
+ DEFAULT_CONFIG = {
43
+ 'operation': 'dilate',
44
+ 'kernel_shape': 'rect',
45
+ 'kernel_size': 5,
46
+ 'iterations': 1
47
+ }
48
+
49
+ properties = [
50
+ {
51
+ 'name': 'operation',
52
+ 'label': 'Operation',
53
+ 'type': 'select',
54
+ 'options': [
55
+ {'value': 'erode', 'label': 'Erode'},
56
+ {'value': 'dilate', 'label': 'Dilate'},
57
+ {'value': 'open', 'label': 'Open (erode then dilate)'},
58
+ {'value': 'close', 'label': 'Close (dilate then erode)'},
59
+ {'value': 'gradient', 'label': 'Gradient'},
60
+ {'value': 'tophat', 'label': 'Top Hat'},
61
+ {'value': 'blackhat', 'label': 'Black Hat'}
62
+ ],
63
+ 'default': DEFAULT_CONFIG['operation'],
64
+ 'help': 'Morphological operation to apply'
65
+ },
66
+ {
67
+ 'name': 'kernel_shape',
68
+ 'label': 'Kernel Shape',
69
+ 'type': 'select',
70
+ 'options': [
71
+ {'value': 'rect', 'label': 'Rectangle'},
72
+ {'value': 'ellipse', 'label': 'Ellipse'},
73
+ {'value': 'cross', 'label': 'Cross'}
74
+ ],
75
+ 'default': DEFAULT_CONFIG['kernel_shape'],
76
+ 'help': 'Shape of the structuring element'
77
+ },
78
+ {
79
+ 'name': 'kernel_size',
80
+ 'label': 'Kernel Size',
81
+ 'type': 'number',
82
+ 'default': DEFAULT_CONFIG['kernel_size'],
83
+ 'min': 1,
84
+ 'max': 99,
85
+ 'help': 'Size of the structuring element'
86
+ },
87
+ {
88
+ 'name': 'iterations',
89
+ 'label': 'Iterations',
90
+ 'type': 'number',
91
+ 'default': DEFAULT_CONFIG['iterations'],
92
+ 'min': 1,
93
+ 'max': 20,
94
+ 'help': 'Number of times to apply the operation'
95
+ }
96
+ ]
97
+
98
+ def __init__(self, node_id=None, name="morphology"):
99
+ super().__init__(node_id, name)
100
+
101
+ @process_image()
102
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
103
+ """Apply morphological operation to the input image."""
104
+ operation = self.config.get('operation', 'dilate')
105
+ kernel_shape = self.config.get('kernel_shape', 'rect')
106
+ kernel_size = self.get_config_int('kernel_size', 5)
107
+ iterations = self.get_config_int('iterations', 1)
108
+
109
+ # Create structuring element
110
+ shape_map = {
111
+ 'rect': cv2.MORPH_RECT,
112
+ 'ellipse': cv2.MORPH_ELLIPSE,
113
+ 'cross': cv2.MORPH_CROSS
114
+ }
115
+ shape = shape_map.get(kernel_shape, cv2.MORPH_RECT)
116
+ kernel = cv2.getStructuringElement(shape, (kernel_size, kernel_size))
117
+
118
+ # Apply operation
119
+ if operation == 'erode':
120
+ result = cv2.erode(image, kernel, iterations=iterations)
121
+ elif operation == 'dilate':
122
+ result = cv2.dilate(image, kernel, iterations=iterations)
123
+ elif operation == 'open':
124
+ result = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel, iterations=iterations)
125
+ elif operation == 'close':
126
+ result = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel, iterations=iterations)
127
+ elif operation == 'gradient':
128
+ result = cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel, iterations=iterations)
129
+ elif operation == 'tophat':
130
+ result = cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel, iterations=iterations)
131
+ elif operation == 'blackhat':
132
+ result = cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel, iterations=iterations)
133
+ else:
134
+ result = image
135
+
136
+ return result
@@ -0,0 +1,344 @@
1
+ """
2
+ Normalize Coordinates Node - converts pixel coordinates to normalized (0.0-1.0).
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("Converts pixel coordinates to normalized coordinates (0.0-1.0). Useful for making detection outputs resolution-independent.")
10
+ _info.add_header("Inputs")
11
+ _info.add_bullets(
12
+ ("Input 0:", "Message with coordinates to normalize"),
13
+ ("payload.image:", "Source image (used to get dimensions if not specified)")
14
+ )
15
+ _info.add_header("Supported Formats")
16
+ _info.add_bullets(
17
+ ("Detections:", "msg.payload.detections[].bbox -> [x1, y1, x2, y2]"),
18
+ ("YOLO format:", "msg.detections[].bbox or msg.predictions[].bbox"),
19
+ ("Shapes:", "msg.shapes[].x1, y1, x2, y2, radius, etc."),
20
+ ("Single bbox:", "msg.bbox or msg.payload.bbox"),
21
+ ("Points:", "msg.points[] or msg.payload.points[]")
22
+ )
23
+ _info.add_header("Output")
24
+ _info.add_text("Same structure with coordinates converted to normalized (0.0-1.0) values. Original pixel values preserved with '_px' suffix.")
25
+
26
+
27
+ class NormalizeCoordsNode(BaseNode):
28
+ """
29
+ Normalize Coordinates node - converts pixel coordinates to normalized (0.0-1.0).
30
+ Handles detections, bboxes, shapes, points from various inference engines.
31
+ """
32
+ info = str(_info)
33
+ display_name = 'Normalize Coords'
34
+ icon = '📐'
35
+ category = 'opencv'
36
+ color = '#4A90D9'
37
+ border_color = '#2E6BB0'
38
+ text_color = '#FFFFFF'
39
+ input_count = 1
40
+ output_count = 1
41
+
42
+ DEFAULT_CONFIG = {
43
+ 'image_width': 0,
44
+ 'image_height': 0,
45
+ 'bbox_format': 'xyxy',
46
+ 'preserve_pixels': True
47
+ }
48
+
49
+ properties = [
50
+ {
51
+ 'name': 'image_width',
52
+ 'label': 'Image Width',
53
+ 'type': 'number',
54
+ 'default': DEFAULT_CONFIG['image_width'],
55
+ 'min': 0,
56
+ 'help': 'Image width in pixels (0 = auto-detect from payload.image)'
57
+ },
58
+ {
59
+ 'name': 'image_height',
60
+ 'label': 'Image Height',
61
+ 'type': 'number',
62
+ 'default': DEFAULT_CONFIG['image_height'],
63
+ 'min': 0,
64
+ 'help': 'Image height in pixels (0 = auto-detect from payload.image)'
65
+ },
66
+ {
67
+ 'name': 'bbox_format',
68
+ 'label': 'BBox Format',
69
+ 'type': 'select',
70
+ 'options': [
71
+ {'value': 'xyxy', 'label': 'xyxy (x1, y1, x2, y2)'},
72
+ {'value': 'xywh', 'label': 'xywh (x, y, width, height)'},
73
+ {'value': 'cxcywh', 'label': 'cxcywh (center_x, center_y, w, h)'}
74
+ ],
75
+ 'default': DEFAULT_CONFIG['bbox_format'],
76
+ 'help': 'Input bounding box format'
77
+ },
78
+ {
79
+ 'name': 'preserve_pixels',
80
+ 'label': 'Preserve Pixel Values',
81
+ 'type': 'checkbox',
82
+ 'default': DEFAULT_CONFIG['preserve_pixels'],
83
+ 'help': 'Keep original pixel values with _px suffix'
84
+ }
85
+ ]
86
+
87
+ def __init__(self, node_id=None, name="normalize coords"):
88
+ super().__init__(node_id, name)
89
+
90
+ def _get_image_dimensions(self, msg: Dict[str, Any]) -> tuple:
91
+ """Get image dimensions from config or message."""
92
+ w = self.get_config_int('image_width', 0)
93
+ h = self.get_config_int('image_height', 0)
94
+
95
+ if w > 0 and h > 0:
96
+ return w, h
97
+
98
+ # Try to get from payload.image
99
+ payload = msg.get(MessageKeys.PAYLOAD, {})
100
+ if isinstance(payload, dict):
101
+ img_data = payload.get(MessageKeys.IMAGE.PATH, payload)
102
+ if isinstance(img_data, dict):
103
+ w = img_data.get('width', 0)
104
+ h = img_data.get('height', 0)
105
+ if w > 0 and h > 0:
106
+ return w, h
107
+
108
+ # Try decoding image to get dimensions
109
+ img, _ = self.decode_image(payload)
110
+ if img is not None:
111
+ return img.shape[1], img.shape[0]
112
+
113
+ return 0, 0
114
+
115
+ def _normalize_bbox(self, bbox, w: int, h: int, preserve: bool) -> Any:
116
+ """Normalize a bounding box."""
117
+ bbox_format = self.config.get('bbox_format', 'xyxy')
118
+
119
+ if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
120
+ x1, y1, x2, y2 = bbox[:4]
121
+
122
+ if bbox_format == 'xywh':
123
+ # Convert xywh to xyxy first
124
+ x2 = x1 + x2
125
+ y2 = y1 + y2
126
+ elif bbox_format == 'cxcywh':
127
+ # Convert center format to xyxy
128
+ cx, cy, bw, bh = bbox[:4]
129
+ x1 = cx - bw / 2
130
+ y1 = cy - bh / 2
131
+ x2 = cx + bw / 2
132
+ y2 = cy + bh / 2
133
+
134
+ result = [
135
+ float(x1) / w if w > 0 else 0,
136
+ float(y1) / h if h > 0 else 0,
137
+ float(x2) / w if w > 0 else 0,
138
+ float(y2) / h if h > 0 else 0
139
+ ]
140
+
141
+ # Add extra elements if present (confidence, class_id, etc.)
142
+ if len(bbox) > 4:
143
+ result.extend(bbox[4:])
144
+
145
+ return result
146
+
147
+ elif isinstance(bbox, dict):
148
+ result = {}
149
+
150
+ # Handle various bbox dict formats
151
+ if 'x1' in bbox:
152
+ result['x1'] = float(bbox['x1']) / w if w > 0 else 0
153
+ result['y1'] = float(bbox['y1']) / h if h > 0 else 0
154
+ result['x2'] = float(bbox.get('x2', 0)) / w if w > 0 else 0
155
+ result['y2'] = float(bbox.get('y2', 0)) / h if h > 0 else 0
156
+ if preserve:
157
+ result['x1_px'] = bbox['x1']
158
+ result['y1_px'] = bbox['y1']
159
+ result['x2_px'] = bbox.get('x2', 0)
160
+ result['y2_px'] = bbox.get('y2', 0)
161
+
162
+ if 'x' in bbox and 'width' in bbox:
163
+ result['x'] = float(bbox['x']) / w if w > 0 else 0
164
+ result['y'] = float(bbox['y']) / h if h > 0 else 0
165
+ result['width'] = float(bbox['width']) / w if w > 0 else 0
166
+ result['height'] = float(bbox['height']) / h if h > 0 else 0
167
+ if preserve:
168
+ result['x_px'] = bbox['x']
169
+ result['y_px'] = bbox['y']
170
+ result['width_px'] = bbox['width']
171
+ result['height_px'] = bbox['height']
172
+
173
+ # Copy non-coordinate fields
174
+ for key in bbox:
175
+ if key not in result and not key.endswith('_px'):
176
+ result[key] = bbox[key]
177
+
178
+ return result
179
+
180
+ return bbox
181
+
182
+ def _normalize_point(self, point, w: int, h: int, preserve: bool) -> Any:
183
+ """Normalize a point."""
184
+ if isinstance(point, (list, tuple)) and len(point) >= 2:
185
+ return [float(point[0]) / w if w > 0 else 0,
186
+ float(point[1]) / h if h > 0 else 0]
187
+ elif isinstance(point, dict):
188
+ result = {
189
+ 'x': float(point.get('x', 0)) / w if w > 0 else 0,
190
+ 'y': float(point.get('y', 0)) / h if h > 0 else 0
191
+ }
192
+ if preserve:
193
+ result['x_px'] = point.get('x', 0)
194
+ result['y_px'] = point.get('y', 0)
195
+ # Copy other fields
196
+ for key in point:
197
+ if key not in ['x', 'y'] and not key.endswith('_px'):
198
+ result[key] = point[key]
199
+ return result
200
+ return point
201
+
202
+ def _normalize_detection(self, det: Dict, w: int, h: int, preserve: bool) -> Dict:
203
+ """Normalize a detection dict."""
204
+ result = det.copy()
205
+
206
+ if 'bbox' in det:
207
+ result['bbox'] = self._normalize_bbox(det['bbox'], w, h, preserve)
208
+
209
+ # Handle various coordinate fields
210
+ for key in ['x', 'cx', 'center_x']:
211
+ if key in det and not isinstance(det[key], dict):
212
+ result[key] = float(det[key]) / w if w > 0 else 0
213
+ if preserve:
214
+ result[f'{key}_px'] = det[key]
215
+
216
+ for key in ['y', 'cy', 'center_y']:
217
+ if key in det and not isinstance(det[key], dict):
218
+ result[key] = float(det[key]) / h if h > 0 else 0
219
+ if preserve:
220
+ result[f'{key}_px'] = det[key]
221
+
222
+ for key in ['width', 'w', 'radius']:
223
+ if key in det:
224
+ result[key] = float(det[key]) / w if w > 0 else 0
225
+ if preserve:
226
+ result[f'{key}_px'] = det[key]
227
+
228
+ for key in ['height', 'h']:
229
+ if key in det:
230
+ result[key] = float(det[key]) / h if h > 0 else 0
231
+ if preserve:
232
+ result[f'{key}_px'] = det[key]
233
+
234
+ # Handle x1, y1, x2, y2 directly on detection
235
+ if 'x1' in det:
236
+ result['x1'] = float(det['x1']) / w if w > 0 else 0
237
+ result['y1'] = float(det['y1']) / h if h > 0 else 0
238
+ if preserve:
239
+ result['x1_px'] = det['x1']
240
+ result['y1_px'] = det['y1']
241
+ if 'x2' in det:
242
+ result['x2'] = float(det['x2']) / w if w > 0 else 0
243
+ result['y2'] = float(det['y2']) / h if h > 0 else 0
244
+ if preserve:
245
+ result['x2_px'] = det['x2']
246
+ result['y2_px'] = det['y2']
247
+
248
+ return result
249
+
250
+ def _normalize_shape(self, shape: Dict, w: int, h: int, preserve: bool) -> Dict:
251
+ """Normalize a shape dict (for draw node compatibility)."""
252
+ return self._normalize_detection(shape, w, h, preserve)
253
+
254
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
255
+ """Normalize coordinates in the message."""
256
+ w, h = self._get_image_dimensions(msg)
257
+
258
+ if w <= 0 or h <= 0:
259
+ self.report_error("Could not determine image dimensions. Set width/height in config or provide image in payload.")
260
+ self.send(msg)
261
+ return
262
+
263
+ preserve = self.get_config_bool('preserve_pixels', True)
264
+
265
+ # Store dimensions in message
266
+ msg['image_width'] = w
267
+ msg['image_height'] = h
268
+
269
+ # Handle payload.detections
270
+ payload = msg.get(MessageKeys.PAYLOAD, {})
271
+ if isinstance(payload, dict) and 'detections' in payload:
272
+ payload['detections'] = [
273
+ self._normalize_detection(d, w, h, preserve)
274
+ for d in payload['detections']
275
+ ]
276
+
277
+ # Handle msg.detections (YOLO style)
278
+ if 'detections' in msg and isinstance(msg['detections'], list):
279
+ msg['detections'] = [
280
+ self._normalize_detection(d, w, h, preserve)
281
+ for d in msg['detections']
282
+ ]
283
+
284
+ # Handle msg.predictions
285
+ if 'predictions' in msg and isinstance(msg['predictions'], list):
286
+ msg['predictions'] = [
287
+ self._normalize_detection(d, w, h, preserve)
288
+ for d in msg['predictions']
289
+ ]
290
+
291
+ # Handle msg.shapes (draw node)
292
+ if 'shapes' in msg and isinstance(msg['shapes'], list):
293
+ msg['shapes'] = [
294
+ self._normalize_shape(s, w, h, preserve)
295
+ for s in msg['shapes']
296
+ ]
297
+
298
+ # Handle single bbox
299
+ if 'bbox' in msg:
300
+ msg['bbox'] = self._normalize_bbox(msg['bbox'], w, h, preserve)
301
+ if isinstance(payload, dict) and 'bbox' in payload:
302
+ payload['bbox'] = self._normalize_bbox(payload['bbox'], w, h, preserve)
303
+
304
+ # Handle points
305
+ if 'points' in msg and isinstance(msg['points'], list):
306
+ msg['points'] = [
307
+ self._normalize_point(p, w, h, preserve)
308
+ for p in msg['points']
309
+ ]
310
+ if isinstance(payload, dict) and 'points' in payload:
311
+ payload['points'] = [
312
+ self._normalize_point(p, w, h, preserve)
313
+ for p in payload['points']
314
+ ]
315
+
316
+ # Handle circles
317
+ if 'circles' in msg and isinstance(msg['circles'], list):
318
+ msg['circles'] = [
319
+ self._normalize_detection(c, w, h, preserve)
320
+ for c in msg['circles']
321
+ ]
322
+
323
+ # Handle lines
324
+ if 'lines' in msg and isinstance(msg['lines'], list):
325
+ msg['lines'] = [
326
+ self._normalize_detection(l, w, h, preserve)
327
+ for l in msg['lines']
328
+ ]
329
+
330
+ # Handle contours
331
+ if 'contours' in msg and isinstance(msg['contours'], list):
332
+ msg['contours'] = [
333
+ self._normalize_detection(c, w, h, preserve)
334
+ for c in msg['contours']
335
+ ]
336
+
337
+ # Handle blobs
338
+ if 'blobs' in msg and isinstance(msg['blobs'], list):
339
+ msg['blobs'] = [
340
+ self._normalize_detection(b, w, h, preserve)
341
+ for b in msg['blobs']
342
+ ]
343
+
344
+ self.send(msg)