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,224 @@
1
+ """
2
+ OpenCV Hough Circles Node - detects circles in 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, Info, MessageKeys
9
+
10
+ _info = Info()
11
+ _info.add_text("Detects circles in images using the Hough Circle Transform. Applies median blur internally to reduce noise.")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Image (color or grayscale)"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(
16
+ ("Output 0:", "Image with circles drawn (if enabled)"),
17
+ ("msg.circles:", "Array of detected circles with normalized x, y, radius (0.0-1.0)")
18
+ )
19
+ _info.add_header("Normalized Output")
20
+ _info.add_text("Circle positions and radii are normalized (0.0-1.0) for resolution independence.")
21
+ _info.add_header("Key Parameters")
22
+ _info.add_bullets(
23
+ ("DP:", "Accumulator resolution ratio (1 = same as image, 2 = half)"),
24
+ ("Min Distance:", "Minimum distance between circle centers (normalized 0.0-1.0)"),
25
+ ("Canny Threshold:", "Higher threshold for internal Canny edge detection"),
26
+ ("Accumulator Threshold:", "Lower = more circles detected (may include false positives)"),
27
+ ("Min/Max Radius:", "Circle radius constraints (normalized 0.0-1.0, relative to image width)")
28
+ )
29
+ _info.add_header("Tips")
30
+ _info.add_bullets(
31
+ ("No circles found?", "Try lowering Accumulator Threshold"),
32
+ ("Too many false positives?", "Increase Accumulator Threshold or Min Distance")
33
+ )
34
+
35
+
36
+ class HoughCirclesNode(BaseNode):
37
+ info = str(_info)
38
+ """
39
+ Hough Circles node - detects circles in images using Hough transform.
40
+ """
41
+ display_name = 'Hough Circles'
42
+ icon = '◯'
43
+ category = 'opencv'
44
+ color = '#4A90D9'
45
+ border_color = '#2E6BB0'
46
+ text_color = '#FFFFFF'
47
+ input_count = 1
48
+ output_count = 1
49
+
50
+ DEFAULT_CONFIG = {
51
+ 'dp': 1.2,
52
+ 'min_dist': 0.1,
53
+ 'param1': 100,
54
+ 'param2': 30,
55
+ 'min_radius': 0.02,
56
+ 'max_radius': 0.0,
57
+ 'draw_circles': 'yes',
58
+ 'circle_color': '0,255,0'
59
+ }
60
+
61
+ properties = [
62
+ {
63
+ 'name': 'dp',
64
+ 'label': 'DP (resolution ratio)',
65
+ 'type': 'number',
66
+ 'default': DEFAULT_CONFIG['dp'],
67
+ 'min': 1,
68
+ 'step': 0.1,
69
+ 'help': 'Inverse ratio of accumulator resolution (1 = same as image)'
70
+ },
71
+ {
72
+ 'name': 'min_dist',
73
+ 'label': 'Min Distance',
74
+ 'type': 'number',
75
+ 'default': DEFAULT_CONFIG['min_dist'],
76
+ 'min': 0.01,
77
+ 'max': 1.0,
78
+ 'step': 0.01,
79
+ 'help': 'Minimum distance between circle centers (normalized 0.0-1.0)'
80
+ },
81
+ {
82
+ 'name': 'param1',
83
+ 'label': 'Canny Threshold',
84
+ 'type': 'number',
85
+ 'default': DEFAULT_CONFIG['param1'],
86
+ 'min': 1,
87
+ 'help': 'Higher threshold for Canny edge detector'
88
+ },
89
+ {
90
+ 'name': 'param2',
91
+ 'label': 'Accumulator Threshold',
92
+ 'type': 'number',
93
+ 'default': DEFAULT_CONFIG['param2'],
94
+ 'min': 1,
95
+ 'help': 'Accumulator threshold for circle detection'
96
+ },
97
+ {
98
+ 'name': 'min_radius',
99
+ 'label': 'Min Radius',
100
+ 'type': 'number',
101
+ 'default': DEFAULT_CONFIG['min_radius'],
102
+ 'min': 0.0,
103
+ 'max': 1.0,
104
+ 'step': 0.01,
105
+ 'help': 'Minimum circle radius (normalized 0.0-1.0, 0 = no minimum)'
106
+ },
107
+ {
108
+ 'name': 'max_radius',
109
+ 'label': 'Max Radius',
110
+ 'type': 'number',
111
+ 'default': DEFAULT_CONFIG['max_radius'],
112
+ 'min': 0.0,
113
+ 'max': 1.0,
114
+ 'step': 0.01,
115
+ 'help': 'Maximum circle radius (normalized 0.0-1.0, 0 = no maximum)'
116
+ },
117
+ {
118
+ 'name': 'draw_circles',
119
+ 'label': 'Draw Circles',
120
+ 'type': 'select',
121
+ 'options': [
122
+ {'value': 'yes', 'label': 'Yes'},
123
+ {'value': 'no', 'label': 'No'}
124
+ ],
125
+ 'default': DEFAULT_CONFIG['draw_circles'],
126
+ 'help': 'Draw detected circles on output'
127
+ },
128
+ {
129
+ 'name': 'circle_color',
130
+ 'label': 'Circle Color (B,G,R)',
131
+ 'type': 'text',
132
+ 'default': DEFAULT_CONFIG['circle_color'],
133
+ 'help': 'Color for drawn circles'
134
+ }
135
+ ]
136
+
137
+ def __init__(self, node_id=None, name="hough circles"):
138
+ super().__init__(node_id, name)
139
+
140
+ def _parse_color(self, color_str):
141
+ """Parse color string to BGR tuple."""
142
+ try:
143
+ parts = [int(x.strip()) for x in str(color_str).split(',')]
144
+ if len(parts) >= 3:
145
+ return (parts[0], parts[1], parts[2])
146
+ return (0, 255, 0)
147
+ except Exception:
148
+ return (0, 255, 0)
149
+
150
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
151
+ """Detect circles in the input image."""
152
+ if MessageKeys.PAYLOAD not in msg:
153
+ self.send(msg)
154
+ return
155
+
156
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
157
+ if img is None:
158
+ self.send(msg)
159
+ return
160
+
161
+ h, w = img.shape[:2]
162
+
163
+ dp = self.get_config_float('dp', 1.2)
164
+ # Convert normalized min_dist to pixels
165
+ min_dist = int(self.get_config_float('min_dist', 0.1) * w)
166
+ min_dist = max(1, min_dist) # Ensure at least 1 pixel
167
+ param1 = self.get_config_int('param1', 100)
168
+ param2 = self.get_config_int('param2', 30)
169
+ # Convert normalized radius to pixels
170
+ min_radius = int(self.get_config_float('min_radius', 0.02) * w)
171
+ max_radius_norm = self.get_config_float('max_radius', 0.0)
172
+ max_radius = int(max_radius_norm * w) if max_radius_norm > 0 else 0
173
+ draw = self.get_config_bool('draw_circles', True)
174
+ circle_color = self._parse_color(self.config.get('circle_color', '0,255,0'))
175
+
176
+ # Convert to grayscale if needed
177
+ if len(img.shape) == 3:
178
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
179
+ else:
180
+ gray = img
181
+
182
+ # Apply slight blur to reduce noise
183
+ gray = cv2.medianBlur(gray, 5)
184
+
185
+ # Detect circles
186
+ circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp, min_dist,
187
+ param1=param1, param2=param2,
188
+ minRadius=min_radius, maxRadius=max_radius)
189
+
190
+ circles_data = []
191
+ if circles is not None:
192
+ circles = np.uint16(np.around(circles))
193
+ for circle in circles[0, :]:
194
+ cx, cy, radius = circle
195
+ # Output normalized coordinates
196
+ circles_data.append({
197
+ 'x': float(cx) / w,
198
+ 'y': float(cy) / h,
199
+ 'radius': float(radius) / w,
200
+ 'x_px': int(cx),
201
+ 'y_px': int(cy),
202
+ 'radius_px': int(radius),
203
+ 'area': float(np.pi * radius * radius)
204
+ })
205
+
206
+ # Draw circles if requested
207
+ if draw:
208
+ if len(img.shape) == 2:
209
+ output = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
210
+ else:
211
+ output = img.copy()
212
+
213
+ for circle in circles[0, :]:
214
+ cx, cy, radius = circle
215
+ cv2.circle(output, (cx, cy), radius, circle_color, 2)
216
+ cv2.circle(output, (cx, cy), 2, (0, 0, 255), 3) # center
217
+
218
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
219
+ msg[MessageKeys.PAYLOAD] = {}
220
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(output, format_type)
221
+
222
+ msg['circles'] = circles_data
223
+ msg['circle_count'] = len(circles_data)
224
+ self.send(msg)
@@ -0,0 +1,243 @@
1
+ """
2
+ OpenCV Hough Lines Node - detects lines in 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, Info, MessageKeys
9
+
10
+ _info = Info()
11
+ _info.add_text("Detects lines in images using the Hough Line Transform. Works best on edge-detected images (use Edge Detector node first).")
12
+ _info.add_header("Inputs")
13
+ _info.add_bullets(("Input 0:", "Edge-detected or binary image recommended"))
14
+ _info.add_header("Outputs")
15
+ _info.add_bullets(
16
+ ("Output 0:", "Image with lines drawn (if enabled)"),
17
+ ("msg.lines:", "Array of detected lines with normalized coordinates (0.0-1.0)")
18
+ )
19
+ _info.add_header("Normalized Coordinates")
20
+ _info.add_text("Line coordinates and parameters are normalized for resolution independence.")
21
+ _info.add_header("Methods")
22
+ _info.add_bullets(
23
+ ("Standard:", "Returns lines in polar form (rho, theta)"),
24
+ ("Probabilistic:", "Returns line segments with normalized endpoints (x1,y1,x2,y2)")
25
+ )
26
+ _info.add_header("Key Parameters")
27
+ _info.add_bullets(
28
+ ("Rho:", "Distance resolution in pixels (typically 1)"),
29
+ ("Theta:", "Angle resolution in degrees (typically 1)"),
30
+ ("Threshold:", "Minimum votes needed to detect a line"),
31
+ ("Min Length:", "Minimum line segment length (normalized 0.0-1.0, probabilistic only)"),
32
+ ("Max Gap:", "Maximum gap to connect segments (normalized 0.0-1.0, probabilistic only)")
33
+ )
34
+
35
+
36
+ class HoughLinesNode(BaseNode):
37
+ info = str(_info)
38
+ """
39
+ Hough Lines node - detects lines in images using Hough transform.
40
+ Works best on edge-detected images.
41
+ """
42
+ display_name = 'Hough Lines'
43
+ icon = '╱'
44
+ category = 'opencv'
45
+ color = '#4A90D9'
46
+ border_color = '#2E6BB0'
47
+ text_color = '#FFFFFF'
48
+ input_count = 1
49
+ output_count = 1
50
+
51
+ DEFAULT_CONFIG = {
52
+ 'method': 'probabilistic',
53
+ 'rho': 1,
54
+ 'theta_degrees': 1,
55
+ 'threshold': 100,
56
+ 'min_length': 0.1,
57
+ 'max_gap': 0.02,
58
+ 'draw_lines': 'yes',
59
+ 'line_color': '0,0,255'
60
+ }
61
+
62
+ properties = [
63
+ {
64
+ 'name': 'method',
65
+ 'label': 'Method',
66
+ 'type': 'select',
67
+ 'options': [
68
+ {'value': 'standard', 'label': 'Standard'},
69
+ {'value': 'probabilistic', 'label': 'Probabilistic'}
70
+ ],
71
+ 'default': DEFAULT_CONFIG['method'],
72
+ 'help': 'Hough transform method'
73
+ },
74
+ {
75
+ 'name': 'rho',
76
+ 'label': 'Rho (pixels)',
77
+ 'type': 'number',
78
+ 'default': DEFAULT_CONFIG['rho'],
79
+ 'min': 1,
80
+ 'help': 'Distance resolution in pixels'
81
+ },
82
+ {
83
+ 'name': 'theta_degrees',
84
+ 'label': 'Theta (degrees)',
85
+ 'type': 'number',
86
+ 'default': DEFAULT_CONFIG['theta_degrees'],
87
+ 'min': 0.1,
88
+ 'step': 0.1,
89
+ 'help': 'Angle resolution in degrees'
90
+ },
91
+ {
92
+ 'name': 'threshold',
93
+ 'label': 'Threshold',
94
+ 'type': 'number',
95
+ 'default': DEFAULT_CONFIG['threshold'],
96
+ 'min': 1,
97
+ 'help': 'Accumulator threshold'
98
+ },
99
+ {
100
+ 'name': 'min_length',
101
+ 'label': 'Min Line Length',
102
+ 'type': 'number',
103
+ 'default': DEFAULT_CONFIG['min_length'],
104
+ 'min': 0.01,
105
+ 'max': 1.0,
106
+ 'step': 0.01,
107
+ 'help': 'Minimum line length (normalized 0.0-1.0, probabilistic only)',
108
+ 'showIf': {'method': 'probabilistic'}
109
+ },
110
+ {
111
+ 'name': 'max_gap',
112
+ 'label': 'Max Line Gap',
113
+ 'type': 'number',
114
+ 'default': DEFAULT_CONFIG['max_gap'],
115
+ 'min': 0.01,
116
+ 'max': 1.0,
117
+ 'step': 0.01,
118
+ 'help': 'Maximum gap between line segments (normalized 0.0-1.0, probabilistic only)',
119
+ 'showIf': {'method': 'probabilistic'}
120
+ },
121
+ {
122
+ 'name': 'draw_lines',
123
+ 'label': 'Draw Lines',
124
+ 'type': 'select',
125
+ 'options': [
126
+ {'value': 'yes', 'label': 'Yes'},
127
+ {'value': 'no', 'label': 'No'}
128
+ ],
129
+ 'default': DEFAULT_CONFIG['draw_lines'],
130
+ 'help': 'Draw detected lines on output'
131
+ },
132
+ {
133
+ 'name': 'line_color',
134
+ 'label': 'Line Color (B,G,R)',
135
+ 'type': 'text',
136
+ 'default': DEFAULT_CONFIG['line_color'],
137
+ 'help': 'Color for drawn lines'
138
+ }
139
+ ]
140
+
141
+ def __init__(self, node_id=None, name="hough lines"):
142
+ super().__init__(node_id, name)
143
+
144
+ def _parse_color(self, color_str):
145
+ """Parse color string to BGR tuple."""
146
+ try:
147
+ parts = [int(x.strip()) for x in str(color_str).split(',')]
148
+ if len(parts) >= 3:
149
+ return (parts[0], parts[1], parts[2])
150
+ return (0, 0, 255)
151
+ except Exception:
152
+ return (0, 0, 255)
153
+
154
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
155
+ """Detect lines in the input image."""
156
+ if MessageKeys.PAYLOAD not in msg:
157
+ self.send(msg)
158
+ return
159
+
160
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
161
+ if img is None:
162
+ self.send(msg)
163
+ return
164
+
165
+ h, w = img.shape[:2]
166
+ diag = np.sqrt(w*w + h*h) # Diagonal for length normalization
167
+
168
+ method = self.config.get('method', 'probabilistic')
169
+ rho = self.get_config_float('rho', 1)
170
+ theta = self.get_config_float('theta_degrees', 1) * np.pi / 180
171
+ threshold = self.get_config_int('threshold', 100)
172
+ # Convert normalized length/gap to pixels
173
+ min_length = int(self.get_config_float('min_length', 0.1) * diag)
174
+ max_gap = int(self.get_config_float('max_gap', 0.02) * diag)
175
+ draw = self.get_config_bool('draw_lines', True)
176
+ line_color = self._parse_color(self.config.get('line_color', '0,0,255'))
177
+
178
+ # Convert to grayscale if needed
179
+ if len(img.shape) == 3:
180
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
181
+ else:
182
+ gray = img
183
+
184
+ lines_data = []
185
+
186
+ if method == 'standard':
187
+ lines = cv2.HoughLines(gray, rho, theta, threshold)
188
+ if lines is not None:
189
+ for line in lines:
190
+ r, t = line[0]
191
+ lines_data.append({
192
+ 'rho': float(r),
193
+ 'theta': float(t),
194
+ 'theta_degrees': float(t * 180 / np.pi)
195
+ })
196
+ else: # probabilistic
197
+ lines = cv2.HoughLinesP(gray, rho, theta, threshold,
198
+ minLineLength=min_length, maxLineGap=max_gap)
199
+ if lines is not None:
200
+ for line in lines:
201
+ x1, y1, x2, y2 = line[0]
202
+ length = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
203
+ angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
204
+ # Output normalized coordinates
205
+ lines_data.append({
206
+ 'x1': float(x1) / w, 'y1': float(y1) / h,
207
+ 'x2': float(x2) / w, 'y2': float(y2) / h,
208
+ 'x1_px': int(x1), 'y1_px': int(y1),
209
+ 'x2_px': int(x2), 'y2_px': int(y2),
210
+ 'length': float(length) / diag,
211
+ 'length_px': float(length),
212
+ 'angle': float(angle)
213
+ })
214
+
215
+ # Draw lines if requested
216
+ if draw and lines is not None:
217
+ if len(img.shape) == 2:
218
+ output = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
219
+ else:
220
+ output = img.copy()
221
+
222
+ if method == 'standard':
223
+ for line in lines:
224
+ r, t = line[0]
225
+ a, b = np.cos(t), np.sin(t)
226
+ x0, y0 = a * r, b * r
227
+ x1 = int(x0 + 1000 * (-b))
228
+ y1 = int(y0 + 1000 * (a))
229
+ x2 = int(x0 - 1000 * (-b))
230
+ y2 = int(y0 - 1000 * (a))
231
+ cv2.line(output, (x1, y1), (x2, y2), line_color, 2)
232
+ else:
233
+ for line in lines:
234
+ x1, y1, x2, y2 = line[0]
235
+ cv2.line(output, (x1, y1), (x2, y2), line_color, 2)
236
+
237
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
238
+ msg[MessageKeys.PAYLOAD] = {}
239
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(output, format_type)
240
+
241
+ msg['lines'] = lines_data
242
+ msg['line_count'] = len(lines_data)
243
+ self.send(msg)
@@ -0,0 +1,186 @@
1
+ """
2
+ OpenCV In Range Node - filters image pixels by color/intensity range.
3
+ Useful for color-based object detection and masking.
4
+ """
5
+
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("Creates a binary mask from pixels within a specified color/intensity range. Useful for color-based object detection and masking. Works best with HSV color space for color filtering.")
13
+ _info.add_header("Inputs")
14
+ _info.add_bullets(("Input 0:", "Image (any color space, HSV recommended)"))
15
+ _info.add_header("Outputs")
16
+ _info.add_bullets(
17
+ ("Output 0:", "Binary mask (white = in range, black = out of range)"),
18
+ ("Output 1:", "Masked image (original with out-of-range pixels blacked out)")
19
+ )
20
+ _info.add_header("Properties")
21
+ _info.add_bullets(
22
+ ("Channel 1 (H/B):", "Min/Max for first channel (Hue 0-179 in HSV)"),
23
+ ("Channel 2 (S/G):", "Min/Max for second channel (Saturation in HSV)"),
24
+ ("Channel 3 (V/R):", "Min/Max for third channel (Value in HSV)"),
25
+ ("Output Mode:", "Choose mask only, masked image only, or both")
26
+ )
27
+
28
+ class InRangeNode(BaseNode):
29
+ """
30
+ In Range node - creates a binary mask from pixels within a color range.
31
+ Works with any color space (HSV recommended for color filtering).
32
+ """
33
+ info = str(_info)
34
+ display_name = 'In Range'
35
+ icon = '🎯'
36
+ category = 'opencv'
37
+ color = '#4A90D9'
38
+ border_color = '#2E6BB0'
39
+ text_color = '#FFFFFF'
40
+ input_count = 1
41
+ output_count = 2 # Output 0: mask, Output 1: masked image
42
+
43
+ DEFAULT_CONFIG = {
44
+ 'channel1_min': 0,
45
+ 'channel1_max': 180,
46
+ 'channel2_min': 50,
47
+ 'channel2_max': 255,
48
+ 'channel3_min': 50,
49
+ 'channel3_max': 255,
50
+ 'output_mode': 'both'
51
+ }
52
+
53
+ properties = [
54
+ {
55
+ 'name': 'channel1_min',
56
+ 'label': 'Channel 1 Min (H/B)',
57
+ 'type': 'number',
58
+ 'default': DEFAULT_CONFIG['channel1_min'],
59
+ 'min': 0,
60
+ 'max': 255,
61
+ 'help': 'Minimum value for channel 1 (Hue in HSV: 0-179)'
62
+ },
63
+ {
64
+ 'name': 'channel1_max',
65
+ 'label': 'Channel 1 Max (H/B)',
66
+ 'type': 'number',
67
+ 'default': DEFAULT_CONFIG['channel1_max'],
68
+ 'min': 0,
69
+ 'max': 255,
70
+ 'help': 'Maximum value for channel 1'
71
+ },
72
+ {
73
+ 'name': 'channel2_min',
74
+ 'label': 'Channel 2 Min (S/G)',
75
+ 'type': 'number',
76
+ 'default': DEFAULT_CONFIG['channel2_min'],
77
+ 'min': 0,
78
+ 'max': 255,
79
+ 'help': 'Minimum value for channel 2 (Saturation in HSV)'
80
+ },
81
+ {
82
+ 'name': 'channel2_max',
83
+ 'label': 'Channel 2 Max (S/G)',
84
+ 'type': 'number',
85
+ 'default': DEFAULT_CONFIG['channel2_max'],
86
+ 'min': 0,
87
+ 'max': 255,
88
+ 'help': 'Maximum value for channel 2'
89
+ },
90
+ {
91
+ 'name': 'channel3_min',
92
+ 'label': 'Channel 3 Min (V/R)',
93
+ 'type': 'number',
94
+ 'default': DEFAULT_CONFIG['channel3_min'],
95
+ 'min': 0,
96
+ 'max': 255,
97
+ 'help': 'Minimum value for channel 3 (Value in HSV)'
98
+ },
99
+ {
100
+ 'name': 'channel3_max',
101
+ 'label': 'Channel 3 Max (V/R)',
102
+ 'type': 'number',
103
+ 'default': DEFAULT_CONFIG['channel3_max'],
104
+ 'min': 0,
105
+ 'max': 255,
106
+ 'help': 'Maximum value for channel 3'
107
+ },
108
+ {
109
+ 'name': 'output_mode',
110
+ 'label': 'Output Mode',
111
+ 'type': 'select',
112
+ 'options': [
113
+ {'value': 'mask', 'label': 'Mask only'},
114
+ {'value': 'masked', 'label': 'Masked image only'},
115
+ {'value': 'both', 'label': 'Both (mask + masked)'}
116
+ ],
117
+ 'default': DEFAULT_CONFIG['output_mode'],
118
+ 'help': 'What to include in output'
119
+ }
120
+ ]
121
+
122
+ def __init__(self, node_id=None, name="in range"):
123
+ super().__init__(node_id, name)
124
+
125
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
126
+ """Apply color range filter to the input image."""
127
+ if MessageKeys.PAYLOAD not in msg:
128
+ self.send(msg)
129
+ return
130
+
131
+ img, format_type = self.decode_image(msg[MessageKeys.PAYLOAD])
132
+ if img is None:
133
+ self.send(msg)
134
+ return
135
+
136
+ # Get range values
137
+ lower = np.array([
138
+ self.get_config_int('channel1_min', 0),
139
+ self.get_config_int('channel2_min', 0),
140
+ self.get_config_int('channel3_min', 0)
141
+ ])
142
+ upper = np.array([
143
+ self.get_config_int('channel1_max', 255),
144
+ self.get_config_int('channel2_max', 255),
145
+ self.get_config_int('channel3_max', 255)
146
+ ])
147
+
148
+ output_mode = self.config.get('output_mode', 'both')
149
+
150
+ # Handle grayscale images
151
+ if len(img.shape) == 2:
152
+ mask = cv2.inRange(img, lower[0], upper[0])
153
+ else:
154
+ mask = cv2.inRange(img, lower, upper)
155
+
156
+ # Apply mask to get masked image
157
+ if len(img.shape) == 2:
158
+ masked = cv2.bitwise_and(img, img, mask=mask)
159
+ else:
160
+ masked = cv2.bitwise_and(img, img, mask=mask)
161
+
162
+ # Count non-zero pixels
163
+ pixel_count = cv2.countNonZero(mask)
164
+ total_pixels = mask.shape[0] * mask.shape[1]
165
+ coverage = pixel_count / total_pixels if total_pixels > 0 else 0
166
+
167
+ msg['mask_pixels'] = pixel_count
168
+ msg['mask_coverage'] = coverage
169
+
170
+ if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
171
+ msg[MessageKeys.PAYLOAD] = {}
172
+
173
+ if output_mode == 'mask':
174
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(mask, format_type)
175
+ self.send(msg, 0)
176
+ elif output_mode == 'masked':
177
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(masked, format_type)
178
+ self.send(msg, 0)
179
+ else: # both
180
+ msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(mask, format_type)
181
+ msg[MessageKeys.PAYLOAD]['masked_image'] = self.encode_image(masked, format_type)
182
+ self.send(msg, 0)
183
+
184
+ msg2 = msg.copy()
185
+ msg2[MessageKeys.PAYLOAD] = {MessageKeys.IMAGE.PATH: self.encode_image(masked, format_type)}
186
+ self.send(msg2, 1)