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,167 @@
1
+ """
2
+ OpenCV Threshold Node - applies thresholding to images.
3
+ Converts grayscale images to binary using various thresholding methods.
4
+ """
5
+
6
+ import cv2
7
+ import numpy as np
8
+ from typing import Any, Dict
9
+ from pynode.nodes.base_node import BaseNode, process_image, Info
10
+
11
+ _info = Info()
12
+ _info.add_text("Converts grayscale images to binary using various thresholding methods. Essential for image segmentation and preparing images for contour detection.")
13
+ _info.add_header("Inputs")
14
+ _info.add_bullets(("Input 0:", "Grayscale image (auto-converts color if enabled)"))
15
+ _info.add_header("Outputs")
16
+ _info.add_bullets(("Output 0:", "Binary/thresholded image"))
17
+ _info.add_header("Methods")
18
+ _info.add_bullets(
19
+ ("Binary:", "Pixels above threshold become white, others black"),
20
+ ("Binary Inv:", "Inverse of binary"),
21
+ ("Truncate:", "Cap values at threshold"),
22
+ ("To Zero:", "Below threshold becomes 0"),
23
+ ("Otsu:", "Automatic threshold calculation"),
24
+ ("Adaptive Mean/Gaussian:", "Local threshold based on neighborhood")
25
+ )
26
+
27
+ class ThresholdNode(BaseNode):
28
+ """
29
+ Threshold node - applies thresholding to grayscale images.
30
+ Supports binary, binary inverted, truncate, to-zero, and adaptive methods.
31
+ """
32
+ info = str(_info)
33
+ display_name = 'Threshold'
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
+ 'method': 'binary',
44
+ 'threshold': 127,
45
+ 'max_value': 255,
46
+ 'block_size': 11,
47
+ 'c_value': 2,
48
+ 'convert_gray': 'yes'
49
+ }
50
+
51
+ properties = [
52
+ {
53
+ 'name': 'method',
54
+ 'label': 'Method',
55
+ 'type': 'select',
56
+ 'options': [
57
+ {'value': 'binary', 'label': 'Binary'},
58
+ {'value': 'binary_inv', 'label': 'Binary Inverted'},
59
+ {'value': 'trunc', 'label': 'Truncate'},
60
+ {'value': 'tozero', 'label': 'To Zero'},
61
+ {'value': 'tozero_inv', 'label': 'To Zero Inverted'},
62
+ {'value': 'otsu', 'label': 'Otsu (auto)'},
63
+ {'value': 'adaptive_mean', 'label': 'Adaptive Mean'},
64
+ {'value': 'adaptive_gaussian', 'label': 'Adaptive Gaussian'}
65
+ ],
66
+ 'default': DEFAULT_CONFIG['method'],
67
+ 'help': 'Thresholding method to apply'
68
+ },
69
+ {
70
+ 'name': 'threshold',
71
+ 'label': 'Threshold Value',
72
+ 'type': 'number',
73
+ 'default': DEFAULT_CONFIG['threshold'],
74
+ 'min': 0,
75
+ 'max': 255,
76
+ 'help': 'Threshold value (ignored for Otsu and adaptive methods)',
77
+ 'showIf': {'method': ['binary', 'binary_inv', 'trunc', 'tozero', 'tozero_inv']}
78
+ },
79
+ {
80
+ 'name': 'max_value',
81
+ 'label': 'Max Value',
82
+ 'type': 'number',
83
+ 'default': DEFAULT_CONFIG['max_value'],
84
+ 'min': 0,
85
+ 'max': 255,
86
+ 'help': 'Maximum value for binary thresholding'
87
+ },
88
+ {
89
+ 'name': 'block_size',
90
+ 'label': 'Block Size',
91
+ 'type': 'number',
92
+ 'default': DEFAULT_CONFIG['block_size'],
93
+ 'min': 3,
94
+ 'max': 99,
95
+ 'help': 'Block size for adaptive methods (must be odd)',
96
+ 'showIf': {'method': ['adaptive_mean', 'adaptive_gaussian']}
97
+ },
98
+ {
99
+ 'name': 'c_value',
100
+ 'label': 'C Value',
101
+ 'type': 'number',
102
+ 'default': DEFAULT_CONFIG['c_value'],
103
+ 'help': 'Constant subtracted from mean (adaptive methods)',
104
+ 'showIf': {'method': ['adaptive_mean', 'adaptive_gaussian']}
105
+ },
106
+ {
107
+ 'name': 'convert_gray',
108
+ 'label': 'Auto Convert to Grayscale',
109
+ 'type': 'select',
110
+ 'options': [
111
+ {'value': 'yes', 'label': 'Yes'},
112
+ {'value': 'no', 'label': 'No'}
113
+ ],
114
+ 'default': DEFAULT_CONFIG['convert_gray'],
115
+ 'help': 'Automatically convert color images to grayscale'
116
+ }
117
+ ]
118
+
119
+ def __init__(self, node_id=None, name="threshold"):
120
+ super().__init__(node_id, name)
121
+
122
+ @process_image()
123
+ def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
124
+ """Apply thresholding to the input image."""
125
+ method = self.config.get('method', 'binary')
126
+ thresh_val = self.get_config_int('threshold', 127)
127
+ max_val = self.get_config_int('max_value', 255)
128
+ block_size = self.get_config_int('block_size', 11)
129
+ c_value = self.get_config_int('c_value', 2)
130
+ convert_gray = self.get_config_bool('convert_gray', True)
131
+
132
+ # Ensure block_size is odd
133
+ if block_size % 2 == 0:
134
+ block_size += 1
135
+
136
+ # Convert to grayscale if needed
137
+ if len(image.shape) == 3 and convert_gray:
138
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
139
+ elif len(image.shape) == 2:
140
+ gray = image
141
+ else:
142
+ # Can't process, pass through
143
+ return image
144
+
145
+ # Apply thresholding based on method
146
+ if method == 'binary':
147
+ _, result = cv2.threshold(gray, thresh_val, max_val, cv2.THRESH_BINARY)
148
+ elif method == 'binary_inv':
149
+ _, result = cv2.threshold(gray, thresh_val, max_val, cv2.THRESH_BINARY_INV)
150
+ elif method == 'trunc':
151
+ _, result = cv2.threshold(gray, thresh_val, max_val, cv2.THRESH_TRUNC)
152
+ elif method == 'tozero':
153
+ _, result = cv2.threshold(gray, thresh_val, max_val, cv2.THRESH_TOZERO)
154
+ elif method == 'tozero_inv':
155
+ _, result = cv2.threshold(gray, thresh_val, max_val, cv2.THRESH_TOZERO_INV)
156
+ elif method == 'otsu':
157
+ _, result = cv2.threshold(gray, 0, max_val, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
158
+ elif method == 'adaptive_mean':
159
+ result = cv2.adaptiveThreshold(gray, max_val, cv2.ADAPTIVE_THRESH_MEAN_C,
160
+ cv2.THRESH_BINARY, block_size, c_value)
161
+ elif method == 'adaptive_gaussian':
162
+ result = cv2.adaptiveThreshold(gray, max_val, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
163
+ cv2.THRESH_BINARY, block_size, c_value)
164
+ else:
165
+ result = gray
166
+
167
+ return result
@@ -0,0 +1,3 @@
1
+ from .point_in_shape_node import PointInShapeNode
2
+
3
+ __all__ = ['PointInShapeNode']
@@ -0,0 +1,313 @@
1
+ """
2
+ PointInShapeNode - Check if a point is inside a shape
3
+
4
+ This node tests whether a point (x, y coordinate) is inside a shape.
5
+ Supported shapes: rectangle, polygon, circle
6
+
7
+ Shapes can be:
8
+ - Manually defined in node properties
9
+ - Read from the incoming message
10
+
11
+ Useful for:
12
+ - Region of interest detection
13
+ - Zone/boundary testing
14
+ - Spatial filtering
15
+ - Click/touch detection in UI regions
16
+ """
17
+
18
+ import sys
19
+ import os
20
+ import json
21
+ from typing import Any, Dict
22
+
23
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
24
+
25
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
26
+ import numpy as np
27
+ import cv2
28
+
29
+ _info = Info()
30
+ _info.add_text("Check if a point is inside a shape (rectangle, polygon, or circle).")
31
+ _info.add_header("Configuration")
32
+ _info.add_bullets(
33
+ ("Shape Source:", "Manual (define in properties) or From Message (read from msg)"),
34
+ ("Shape Type:", "Rectangle, Polygon, or Circle"),
35
+ ("Point Path:", "Path to point in message (e.g., 'payload.point' or 'click')"),
36
+ ("Shape Path:", "Path to shape in message (when using From Message)")
37
+ )
38
+ _info.add_header("Input")
39
+ _info.add_bullets(
40
+ ("Point:", "Point coordinates [x, y] at configured path"),
41
+ ("Shape:", "Shape data at configured path (when using From Message)")
42
+ )
43
+ _info.add_header("Output")
44
+ _info.add_bullets(
45
+ ("payload.inside:", "Boolean - true if point is inside shape"),
46
+ ("payload.point:", "The tested point [x, y]"),
47
+ ("payload.shape_type:", "Type of shape tested against")
48
+ )
49
+
50
+
51
+ class PointInShapeNode(BaseNode):
52
+ """Check if points are inside shapes"""
53
+
54
+ info = str(_info)
55
+ display_name = 'Point in Shape'
56
+ icon = '📍'
57
+ category = 'analysis'
58
+ color = '#FFE4E1'
59
+ border_color = '#DC143C'
60
+ text_color = '#000000'
61
+
62
+ DEFAULT_CONFIG = {
63
+ 'point_path': 'payload.point',
64
+ 'shape_source': 'manual',
65
+ 'shape_type': 'rect',
66
+ 'shape_path': 'payload.shape',
67
+ 'rect': '100,100,300,300',
68
+ 'polygon': '[[100,100],[300,100],[300,300],[100,300]]',
69
+ 'circle': '200,200,100'
70
+ }
71
+
72
+ properties = [
73
+ {
74
+ 'name': 'point_path',
75
+ 'label': 'Point Path',
76
+ 'type': 'text',
77
+ 'default': DEFAULT_CONFIG['point_path'],
78
+ 'help': 'Path to point in message (e.g., "payload.point", "click")'
79
+ },
80
+ {
81
+ 'name': 'shape_source',
82
+ 'label': 'Shape Source',
83
+ 'type': 'select',
84
+ 'options': [
85
+ {'value': 'manual', 'label': 'Manual (define below)'},
86
+ {'value': 'message', 'label': 'From Message'}
87
+ ],
88
+ 'default': DEFAULT_CONFIG['shape_source']
89
+ },
90
+ {
91
+ 'name': 'shape_type',
92
+ 'label': 'Shape Type',
93
+ 'type': 'select',
94
+ 'options': [
95
+ {'value': 'rect', 'label': 'Rectangle'},
96
+ {'value': 'polygon', 'label': 'Polygon'},
97
+ {'value': 'circle', 'label': 'Circle'}
98
+ ],
99
+ 'default': DEFAULT_CONFIG['shape_type'],
100
+ 'showIf': {'shape_source': 'manual'}
101
+ },
102
+ {
103
+ 'name': 'shape_path',
104
+ 'label': 'Shape Path',
105
+ 'type': 'text',
106
+ 'default': DEFAULT_CONFIG['shape_path'],
107
+ 'help': 'Path to shape in message',
108
+ 'showIf': {'shape_source': 'message'}
109
+ },
110
+ {
111
+ 'name': 'rect',
112
+ 'label': 'Rectangle (x1,y1,x2,y2)',
113
+ 'type': 'text',
114
+ 'default': DEFAULT_CONFIG['rect'],
115
+ 'help': 'Format: x1,y1,x2,y2',
116
+ 'showIf': {'shape_source': 'manual', 'shape_type': 'rect'}
117
+ },
118
+ {
119
+ 'name': 'polygon',
120
+ 'label': 'Polygon Points',
121
+ 'type': 'text',
122
+ 'default': DEFAULT_CONFIG['polygon'],
123
+ 'help': 'Format: [[x1,y1],[x2,y2],[x3,y3],...]',
124
+ 'showIf': {'shape_source': 'manual', 'shape_type': 'polygon'}
125
+ },
126
+ {
127
+ 'name': 'circle',
128
+ 'label': 'Circle (x,y,radius)',
129
+ 'type': 'text',
130
+ 'default': DEFAULT_CONFIG['circle'],
131
+ 'help': 'Format: center_x,center_y,radius',
132
+ 'showIf': {'shape_source': 'manual', 'shape_type': 'circle'}
133
+ }
134
+ ]
135
+
136
+ def __init__(self, node_id=None, name="Point in Shape"):
137
+ super().__init__(node_id, name)
138
+
139
+ def _point_in_rect(self, point, rect):
140
+ """
141
+ Check if a point is inside a rectangle
142
+
143
+ Args:
144
+ point: [x, y]
145
+ rect: [x1, y1, x2, y2]
146
+
147
+ Returns:
148
+ Boolean
149
+ """
150
+ x, y = point
151
+ x1, y1, x2, y2 = rect
152
+
153
+ # Ensure correct order
154
+ x_min = min(x1, x2)
155
+ x_max = max(x1, x2)
156
+ y_min = min(y1, y2)
157
+ y_max = max(y1, y2)
158
+
159
+ return x_min <= x <= x_max and y_min <= y <= y_max
160
+
161
+ def _point_in_polygon(self, point, polygon):
162
+ """
163
+ Check if a point is inside a polygon
164
+
165
+ Args:
166
+ point: [x, y]
167
+ polygon: [[x1, y1], [x2, y2], ...]
168
+
169
+ Returns:
170
+ Boolean
171
+ """
172
+ if isinstance(polygon, list):
173
+ polygon = np.array(polygon, dtype=np.float32)
174
+
175
+ if len(polygon) < 3:
176
+ return False
177
+
178
+ # Use OpenCV's pointPolygonTest
179
+ # Returns: positive (inside), negative (outside), or zero (on edge)
180
+ result = cv2.pointPolygonTest(polygon, (float(point[0]), float(point[1])), False)
181
+ return result >= 0
182
+
183
+ def _point_in_circle(self, point, circle):
184
+ """
185
+ Check if a point is inside a circle
186
+
187
+ Args:
188
+ point: [x, y]
189
+ circle: [center_x, center_y, radius]
190
+
191
+ Returns:
192
+ Boolean
193
+ """
194
+ x, y = point
195
+ cx, cy, radius = circle
196
+
197
+ # Calculate distance from center
198
+ distance = np.sqrt((x - cx) ** 2 + (y - cy) ** 2)
199
+
200
+ return distance <= radius
201
+
202
+ def _parse_manual_shape(self, shape_type, shape_str):
203
+ """
204
+ Parse manually configured shape from string
205
+
206
+ Args:
207
+ shape_type: 'rect', 'polygon', or 'circle'
208
+ shape_str: String representation of the shape
209
+
210
+ Returns:
211
+ Parsed shape data
212
+ """
213
+ try:
214
+ if shape_type == 'rect':
215
+ # Format: "x1,y1,x2,y2"
216
+ parts = [float(x.strip()) for x in shape_str.split(',')]
217
+ if len(parts) == 4:
218
+ return parts
219
+
220
+ elif shape_type == 'polygon':
221
+ # Format: "[[x1,y1],[x2,y2],...]"
222
+ return json.loads(shape_str)
223
+
224
+ elif shape_type == 'circle':
225
+ # Format: "cx,cy,radius"
226
+ parts = [float(x.strip()) for x in shape_str.split(',')]
227
+ if len(parts) == 3:
228
+ return parts
229
+
230
+ except Exception as e:
231
+ self.report_error(f"Error parsing shape: {str(e)}")
232
+
233
+ return None
234
+
235
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
236
+ """Check if a point is inside a shape"""
237
+ try:
238
+ # Get configuration
239
+ point_path = self.config.get('point_path', 'payload.point')
240
+ shape_source = self.config.get('shape_source', 'manual')
241
+ shape_type = self.config.get('shape_type', 'rect')
242
+
243
+ # Get the point from the message
244
+ point = self._get_nested_value(msg, point_path)
245
+
246
+ if point is None:
247
+ self.report_error(f"Point not found at path: {point_path}")
248
+ return
249
+
250
+ # Validate point format
251
+ if not isinstance(point, (list, tuple)) or len(point) != 2:
252
+ self.report_error(f"Point must be [x, y], got: {point}")
253
+ return
254
+
255
+ # Get the shape
256
+ shape_data = None
257
+
258
+ if shape_source == 'manual':
259
+ # Parse manually configured shape
260
+ shape_str = self.config.get(shape_type, '')
261
+ shape_data = self._parse_manual_shape(shape_type, shape_str)
262
+
263
+ if shape_data is None:
264
+ self.report_error(f"Invalid {shape_type} configuration: {shape_str}")
265
+ return
266
+
267
+ else: # from message
268
+ shape_path = self.config.get('shape_path', 'payload.shape')
269
+ shape_data = self._get_nested_value(msg, shape_path)
270
+
271
+ if shape_data is None:
272
+ self.report_error(f"Shape not found at path: {shape_path}")
273
+ return
274
+
275
+ # Try to determine shape type from data if it's a dict with type info
276
+ if isinstance(shape_data, dict):
277
+ if 'rect' in shape_data or 'bbox' in shape_data:
278
+ shape_type = 'rect'
279
+ shape_data = shape_data.get('rect', shape_data.get('bbox'))
280
+ elif 'polygon' in shape_data:
281
+ shape_type = 'polygon'
282
+ shape_data = shape_data['polygon']
283
+ elif 'circle' in shape_data:
284
+ shape_type = 'circle'
285
+ shape_data = shape_data['circle']
286
+
287
+ # Test the point against the shape
288
+ inside = False
289
+
290
+ if shape_type == 'rect':
291
+ inside = self._point_in_rect(point, shape_data)
292
+ elif shape_type == 'polygon':
293
+ inside = self._point_in_polygon(point, shape_data)
294
+ elif shape_type == 'circle':
295
+ inside = self._point_in_circle(point, shape_data)
296
+ else:
297
+ self.report_error(f"Unknown shape type: {shape_type}")
298
+ return
299
+
300
+ # Create output
301
+ if MessageKeys.PAYLOAD not in msg:
302
+ msg[MessageKeys.PAYLOAD] = {}
303
+
304
+ msg[MessageKeys.PAYLOAD]['inside'] = inside
305
+ msg[MessageKeys.PAYLOAD]['point'] = list(point)
306
+ msg[MessageKeys.PAYLOAD]['shape_type'] = shape_type
307
+
308
+ self.send(msg)
309
+
310
+ except Exception as e:
311
+ self.report_error(f"Error checking point in shape: {str(e)}")
312
+ import traceback
313
+ self.report_error(traceback.format_exc())
@@ -0,0 +1,2 @@
1
+ numpy
2
+ opencv-python
@@ -0,0 +1,3 @@
1
+ from .polygon_metrics_node import PolygonMetricsNode
2
+
3
+ __all__ = ['PolygonMetricsNode']