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,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)
|