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.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- 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,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())
|