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,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenCV Resize Node - resizes images.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
from pynode.nodes.base_node import BaseNode, process_image, Info
|
|
9
|
+
|
|
10
|
+
_info = Info()
|
|
11
|
+
_info.add_text("Resizes images using various modes and interpolation methods. Supports absolute dimensions, scale factors, and aspect-ratio preserving modes.")
|
|
12
|
+
_info.add_header("Inputs")
|
|
13
|
+
_info.add_bullets(("Input 0:", "Source image"))
|
|
14
|
+
_info.add_header("Outputs")
|
|
15
|
+
_info.add_bullets(("Output 0:", "Resized image"))
|
|
16
|
+
_info.add_header("Modes")
|
|
17
|
+
_info.add_bullets(
|
|
18
|
+
("Absolute:", "Resize to exact width and height"),
|
|
19
|
+
("Scale:", "Multiply dimensions by scale factor"),
|
|
20
|
+
("Fit:", "Fit within bounds, preserve aspect ratio"),
|
|
21
|
+
("Fill:", "Fill bounds with cropping, preserve aspect ratio")
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("Interpolation")
|
|
24
|
+
_info.add_bullets(
|
|
25
|
+
("Nearest:", "Fast, pixelated (good for pixel art)"),
|
|
26
|
+
("Linear:", "Balanced quality and speed"),
|
|
27
|
+
("Area:", "Best for shrinking images"),
|
|
28
|
+
("Cubic:", "High quality for enlarging"),
|
|
29
|
+
("Lanczos:", "Highest quality, slowest")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class ResizeNode(BaseNode):
|
|
33
|
+
"""
|
|
34
|
+
Resize node - resizes images to specified dimensions or scale.
|
|
35
|
+
"""
|
|
36
|
+
info = str(_info)
|
|
37
|
+
display_name = 'Resize'
|
|
38
|
+
icon = '⤡'
|
|
39
|
+
category = 'opencv'
|
|
40
|
+
color = '#4A90D9'
|
|
41
|
+
border_color = '#2E6BB0'
|
|
42
|
+
text_color = '#FFFFFF'
|
|
43
|
+
input_count = 1
|
|
44
|
+
output_count = 1
|
|
45
|
+
|
|
46
|
+
DEFAULT_CONFIG = {
|
|
47
|
+
'mode': 'scale',
|
|
48
|
+
'width': 640,
|
|
49
|
+
'height': 480,
|
|
50
|
+
'scale': 0.5,
|
|
51
|
+
'interpolation': 'linear'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
properties = [
|
|
55
|
+
{
|
|
56
|
+
'name': 'mode',
|
|
57
|
+
'label': 'Mode',
|
|
58
|
+
'type': 'select',
|
|
59
|
+
'options': [
|
|
60
|
+
{'value': 'absolute', 'label': 'Absolute size'},
|
|
61
|
+
{'value': 'scale', 'label': 'Scale factor'},
|
|
62
|
+
{'value': 'fit', 'label': 'Fit within bounds'},
|
|
63
|
+
{'value': 'fill', 'label': 'Fill bounds (crop)'}
|
|
64
|
+
],
|
|
65
|
+
'default': DEFAULT_CONFIG['mode'],
|
|
66
|
+
'help': 'How to determine new size'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
'name': 'width',
|
|
70
|
+
'label': 'Width',
|
|
71
|
+
'type': 'number',
|
|
72
|
+
'default': DEFAULT_CONFIG['width'],
|
|
73
|
+
'min': 1,
|
|
74
|
+
'help': 'Target width in pixels',
|
|
75
|
+
'showIf': {'mode': ['absolute', 'fit', 'fill']}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
'name': 'height',
|
|
79
|
+
'label': 'Height',
|
|
80
|
+
'type': 'number',
|
|
81
|
+
'default': DEFAULT_CONFIG['height'],
|
|
82
|
+
'min': 1,
|
|
83
|
+
'help': 'Target height in pixels',
|
|
84
|
+
'showIf': {'mode': ['absolute', 'fit', 'fill']}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
'name': 'scale',
|
|
88
|
+
'label': 'Scale Factor',
|
|
89
|
+
'type': 'number',
|
|
90
|
+
'default': DEFAULT_CONFIG['scale'],
|
|
91
|
+
'min': 0.01,
|
|
92
|
+
'max': 10,
|
|
93
|
+
'step': 0.1,
|
|
94
|
+
'help': 'Scale multiplier (1.0 = original size)',
|
|
95
|
+
'showIf': {'mode': 'scale'}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
'name': 'interpolation',
|
|
99
|
+
'label': 'Interpolation',
|
|
100
|
+
'type': 'select',
|
|
101
|
+
'options': [
|
|
102
|
+
{'value': 'nearest', 'label': 'Nearest neighbor'},
|
|
103
|
+
{'value': 'linear', 'label': 'Bilinear'},
|
|
104
|
+
{'value': 'area', 'label': 'Area (best for shrinking)'},
|
|
105
|
+
{'value': 'cubic', 'label': 'Bicubic'},
|
|
106
|
+
{'value': 'lanczos', 'label': 'Lanczos'}
|
|
107
|
+
],
|
|
108
|
+
'default': DEFAULT_CONFIG['interpolation'],
|
|
109
|
+
'help': 'Interpolation method'
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def __init__(self, node_id=None, name="resize"):
|
|
114
|
+
super().__init__(node_id, name)
|
|
115
|
+
|
|
116
|
+
@process_image()
|
|
117
|
+
def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
|
|
118
|
+
"""Resize the input image."""
|
|
119
|
+
mode = self.config.get('mode', 'scale')
|
|
120
|
+
target_width = self.get_config_int('width', 640)
|
|
121
|
+
target_height = self.get_config_int('height', 480)
|
|
122
|
+
scale = self.get_config_float('scale', 0.5)
|
|
123
|
+
interp_str = self.config.get('interpolation', 'linear')
|
|
124
|
+
|
|
125
|
+
# Map interpolation string to OpenCV constant
|
|
126
|
+
interp_map = {
|
|
127
|
+
'nearest': cv2.INTER_NEAREST,
|
|
128
|
+
'linear': cv2.INTER_LINEAR,
|
|
129
|
+
'area': cv2.INTER_AREA,
|
|
130
|
+
'cubic': cv2.INTER_CUBIC,
|
|
131
|
+
'lanczos': cv2.INTER_LANCZOS4
|
|
132
|
+
}
|
|
133
|
+
interpolation = interp_map.get(interp_str, cv2.INTER_LINEAR)
|
|
134
|
+
|
|
135
|
+
h, w = image.shape[:2]
|
|
136
|
+
|
|
137
|
+
if mode == 'absolute':
|
|
138
|
+
new_width = target_width
|
|
139
|
+
new_height = target_height
|
|
140
|
+
elif mode == 'scale':
|
|
141
|
+
new_width = int(w * scale)
|
|
142
|
+
new_height = int(h * scale)
|
|
143
|
+
elif mode == 'fit':
|
|
144
|
+
# Fit within bounds while maintaining aspect ratio
|
|
145
|
+
ratio = min(target_width / w, target_height / h)
|
|
146
|
+
new_width = int(w * ratio)
|
|
147
|
+
new_height = int(h * ratio)
|
|
148
|
+
elif mode == 'fill':
|
|
149
|
+
# Fill bounds while maintaining aspect ratio (may crop)
|
|
150
|
+
ratio = max(target_width / w, target_height / h)
|
|
151
|
+
new_width = int(w * ratio)
|
|
152
|
+
new_height = int(h * ratio)
|
|
153
|
+
else:
|
|
154
|
+
new_width = w
|
|
155
|
+
new_height = h
|
|
156
|
+
|
|
157
|
+
# Ensure minimum size
|
|
158
|
+
new_width = max(1, new_width)
|
|
159
|
+
new_height = max(1, new_height)
|
|
160
|
+
|
|
161
|
+
result = cv2.resize(image, (new_width, new_height), interpolation=interpolation)
|
|
162
|
+
|
|
163
|
+
# For fill mode, crop to target size
|
|
164
|
+
if mode == 'fill':
|
|
165
|
+
y_offset = (new_height - target_height) // 2
|
|
166
|
+
x_offset = (new_width - target_width) // 2
|
|
167
|
+
result = result[y_offset:y_offset + target_height,
|
|
168
|
+
x_offset:x_offset + target_width]
|
|
169
|
+
|
|
170
|
+
extra_fields = {
|
|
171
|
+
'original_size': {'width': w, 'height': h},
|
|
172
|
+
'new_size': {'width': result.shape[1], 'height': result.shape[0]}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result, extra_fields
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenCV Rotate Node - rotates images.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
from pynode.nodes.base_node import BaseNode, process_image, Info
|
|
9
|
+
|
|
10
|
+
_info = Info()
|
|
11
|
+
_info.add_text("Rotates and flips images. Supports preset rotations (90°, 180°), custom angles, and flip operations.")
|
|
12
|
+
_info.add_header("Inputs")
|
|
13
|
+
_info.add_bullets(("Input 0:", "Source image"))
|
|
14
|
+
_info.add_header("Outputs")
|
|
15
|
+
_info.add_bullets(("Output 0:", "Rotated/flipped image"))
|
|
16
|
+
_info.add_header("Modes")
|
|
17
|
+
_info.add_bullets(
|
|
18
|
+
("90° CW/CCW:", "Fast 90-degree rotations"),
|
|
19
|
+
("180°:", "Rotate upside down"),
|
|
20
|
+
("Custom Angle:", "Rotate by any angle in degrees"),
|
|
21
|
+
("Flip H/V/Both:", "Mirror image horizontally, vertically, or both")
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("Custom Angle Options")
|
|
24
|
+
_info.add_bullets(
|
|
25
|
+
("Expand Canvas:", "Grow canvas to fit rotated image"),
|
|
26
|
+
("Fill Color:", "Color for empty areas (B,G,R format)")
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class RotateNode(BaseNode):
|
|
30
|
+
"""
|
|
31
|
+
Rotate node - rotates images by specified angle.
|
|
32
|
+
"""
|
|
33
|
+
info = str(_info)
|
|
34
|
+
display_name = 'Rotate'
|
|
35
|
+
icon = '↻'
|
|
36
|
+
category = 'opencv'
|
|
37
|
+
color = '#4A90D9'
|
|
38
|
+
border_color = '#2E6BB0'
|
|
39
|
+
text_color = '#FFFFFF'
|
|
40
|
+
input_count = 1
|
|
41
|
+
output_count = 1
|
|
42
|
+
|
|
43
|
+
DEFAULT_CONFIG = {
|
|
44
|
+
'mode': '90cw',
|
|
45
|
+
'angle': 45,
|
|
46
|
+
'expand': 'yes',
|
|
47
|
+
'fill_color': '0,0,0'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
properties = [
|
|
51
|
+
{
|
|
52
|
+
'name': 'mode',
|
|
53
|
+
'label': 'Mode',
|
|
54
|
+
'type': 'select',
|
|
55
|
+
'options': [
|
|
56
|
+
{'value': 'angle', 'label': 'Custom angle'},
|
|
57
|
+
{'value': '90cw', 'label': '90° clockwise'},
|
|
58
|
+
{'value': '90ccw', 'label': '90° counter-clockwise'},
|
|
59
|
+
{'value': '180', 'label': '180°'},
|
|
60
|
+
{'value': 'flip_h', 'label': 'Flip horizontal'},
|
|
61
|
+
{'value': 'flip_v', 'label': 'Flip vertical'},
|
|
62
|
+
{'value': 'flip_both', 'label': 'Flip both'}
|
|
63
|
+
],
|
|
64
|
+
'default': DEFAULT_CONFIG['mode'],
|
|
65
|
+
'help': 'Rotation mode'
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
'name': 'angle',
|
|
69
|
+
'label': 'Angle (degrees)',
|
|
70
|
+
'type': 'number',
|
|
71
|
+
'default': DEFAULT_CONFIG['angle'],
|
|
72
|
+
'min': -360,
|
|
73
|
+
'max': 360,
|
|
74
|
+
'help': 'Rotation angle in degrees (positive = counter-clockwise)',
|
|
75
|
+
'showIf': {'mode': 'angle'}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
'name': 'expand',
|
|
79
|
+
'label': 'Expand Canvas',
|
|
80
|
+
'type': 'select',
|
|
81
|
+
'options': [
|
|
82
|
+
{'value': 'yes', 'label': 'Yes'},
|
|
83
|
+
{'value': 'no', 'label': 'No'}
|
|
84
|
+
],
|
|
85
|
+
'default': DEFAULT_CONFIG['expand'],
|
|
86
|
+
'help': 'Expand canvas to fit rotated image',
|
|
87
|
+
'showIf': {'mode': 'angle'}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
'name': 'fill_color',
|
|
91
|
+
'label': 'Fill Color (B,G,R)',
|
|
92
|
+
'type': 'text',
|
|
93
|
+
'default': DEFAULT_CONFIG['fill_color'],
|
|
94
|
+
'help': 'Color to fill empty areas',
|
|
95
|
+
'showIf': {'mode': 'angle'}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
def __init__(self, node_id=None, name="rotate"):
|
|
100
|
+
super().__init__(node_id, name)
|
|
101
|
+
|
|
102
|
+
def _parse_color(self, color_str):
|
|
103
|
+
"""Parse color string to BGR tuple."""
|
|
104
|
+
try:
|
|
105
|
+
parts = [int(x.strip()) for x in str(color_str).split(',')]
|
|
106
|
+
if len(parts) >= 3:
|
|
107
|
+
return (parts[0], parts[1], parts[2])
|
|
108
|
+
return (0, 0, 0)
|
|
109
|
+
except Exception:
|
|
110
|
+
return (0, 0, 0)
|
|
111
|
+
|
|
112
|
+
@process_image()
|
|
113
|
+
def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
|
|
114
|
+
"""Rotate the input image."""
|
|
115
|
+
mode = self.config.get('mode', '90cw')
|
|
116
|
+
|
|
117
|
+
if mode == '90cw':
|
|
118
|
+
result = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
|
|
119
|
+
elif mode == '90ccw':
|
|
120
|
+
result = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
|
121
|
+
elif mode == '180':
|
|
122
|
+
result = cv2.rotate(image, cv2.ROTATE_180)
|
|
123
|
+
elif mode == 'flip_h':
|
|
124
|
+
result = cv2.flip(image, 1)
|
|
125
|
+
elif mode == 'flip_v':
|
|
126
|
+
result = cv2.flip(image, 0)
|
|
127
|
+
elif mode == 'flip_both':
|
|
128
|
+
result = cv2.flip(image, -1)
|
|
129
|
+
elif mode == 'angle':
|
|
130
|
+
angle = self.get_config_float('angle', 45)
|
|
131
|
+
expand = self.get_config_bool('expand', True)
|
|
132
|
+
fill_color = self._parse_color(self.config.get('fill_color', '0,0,0'))
|
|
133
|
+
|
|
134
|
+
h, w = image.shape[:2]
|
|
135
|
+
center = (w // 2, h // 2)
|
|
136
|
+
|
|
137
|
+
# Get rotation matrix
|
|
138
|
+
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
|
139
|
+
|
|
140
|
+
if expand:
|
|
141
|
+
# Calculate new bounding box size
|
|
142
|
+
cos = np.abs(M[0, 0])
|
|
143
|
+
sin = np.abs(M[0, 1])
|
|
144
|
+
new_w = int(h * sin + w * cos)
|
|
145
|
+
new_h = int(h * cos + w * sin)
|
|
146
|
+
|
|
147
|
+
# Adjust the rotation matrix
|
|
148
|
+
M[0, 2] += (new_w - w) / 2
|
|
149
|
+
M[1, 2] += (new_h - h) / 2
|
|
150
|
+
|
|
151
|
+
result = cv2.warpAffine(image, M, (new_w, new_h),
|
|
152
|
+
borderValue=fill_color)
|
|
153
|
+
else:
|
|
154
|
+
result = cv2.warpAffine(image, M, (w, h),
|
|
155
|
+
borderValue=fill_color)
|
|
156
|
+
else:
|
|
157
|
+
result = image
|
|
158
|
+
|
|
159
|
+
return result
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenCV Template Matching Node - finds template in image.
|
|
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("Finds occurrences of a template image within a larger image. Useful for object detection, pattern recognition, and image search.")
|
|
12
|
+
_info.add_header("Inputs")
|
|
13
|
+
_info.add_bullets(
|
|
14
|
+
("Input 0:", "Source image to search in"),
|
|
15
|
+
("Input 1:", "Template image to find")
|
|
16
|
+
)
|
|
17
|
+
_info.add_header("Outputs")
|
|
18
|
+
_info.add_bullets(("Output 0:", "Image with matches drawn (if enabled)"))
|
|
19
|
+
_info.add_header("Properties")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("Method:", "Matching algorithm (normalized methods recommended)"),
|
|
22
|
+
("Threshold:", "Match confidence threshold (0-1)"),
|
|
23
|
+
("Multi Match:", "Find all matches or best only"),
|
|
24
|
+
("Draw Matches:", "Draw rectangles around found matches")
|
|
25
|
+
)
|
|
26
|
+
_info.add_header("Message Fields")
|
|
27
|
+
_info.add_bullets(
|
|
28
|
+
("matches:", "List of match locations and scores"),
|
|
29
|
+
("match_count:", "Number of matches found")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
class TemplateMatchNode(BaseNode):
|
|
33
|
+
"""
|
|
34
|
+
Template Match node - finds occurrences of a template image in the input.
|
|
35
|
+
"""
|
|
36
|
+
info = str(_info)
|
|
37
|
+
display_name = 'Template Match'
|
|
38
|
+
icon = '🔍'
|
|
39
|
+
category = 'opencv'
|
|
40
|
+
color = '#4A90D9'
|
|
41
|
+
border_color = '#2E6BB0'
|
|
42
|
+
text_color = '#FFFFFF'
|
|
43
|
+
input_count = 2 # Input 0: image, Input 1: template
|
|
44
|
+
output_count = 1
|
|
45
|
+
|
|
46
|
+
DEFAULT_CONFIG = {
|
|
47
|
+
'method': 'ccoeff_normed',
|
|
48
|
+
'threshold': 0.8,
|
|
49
|
+
'multi_match': 'no',
|
|
50
|
+
'draw_matches': 'yes',
|
|
51
|
+
'match_color': '0,255,0'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
properties = [
|
|
55
|
+
{
|
|
56
|
+
'name': 'method',
|
|
57
|
+
'label': 'Method',
|
|
58
|
+
'type': 'select',
|
|
59
|
+
'options': [
|
|
60
|
+
{'value': 'ccoeff_normed', 'label': 'Correlation Coefficient (normalized)'},
|
|
61
|
+
{'value': 'ccorr_normed', 'label': 'Cross Correlation (normalized)'},
|
|
62
|
+
{'value': 'sqdiff_normed', 'label': 'Squared Difference (normalized)'},
|
|
63
|
+
{'value': 'ccoeff', 'label': 'Correlation Coefficient'},
|
|
64
|
+
{'value': 'ccorr', 'label': 'Cross Correlation'},
|
|
65
|
+
{'value': 'sqdiff', 'label': 'Squared Difference'}
|
|
66
|
+
],
|
|
67
|
+
'default': DEFAULT_CONFIG['method'],
|
|
68
|
+
'help': 'Template matching method'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
'name': 'threshold',
|
|
72
|
+
'label': 'Threshold',
|
|
73
|
+
'type': 'number',
|
|
74
|
+
'default': DEFAULT_CONFIG['threshold'],
|
|
75
|
+
'min': 0,
|
|
76
|
+
'max': 1,
|
|
77
|
+
'step': 0.05,
|
|
78
|
+
'help': 'Match threshold (0-1)'
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
'name': 'multi_match',
|
|
82
|
+
'label': 'Multi Match',
|
|
83
|
+
'type': 'select',
|
|
84
|
+
'options': [
|
|
85
|
+
{'value': 'yes', 'label': 'Yes (find all)'},
|
|
86
|
+
{'value': 'no', 'label': 'No (best only)'}
|
|
87
|
+
],
|
|
88
|
+
'default': DEFAULT_CONFIG['multi_match'],
|
|
89
|
+
'help': 'Find multiple matches above threshold'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
'name': 'draw_matches',
|
|
93
|
+
'label': 'Draw Matches',
|
|
94
|
+
'type': 'select',
|
|
95
|
+
'options': [
|
|
96
|
+
{'value': 'yes', 'label': 'Yes'},
|
|
97
|
+
{'value': 'no', 'label': 'No'}
|
|
98
|
+
],
|
|
99
|
+
'default': DEFAULT_CONFIG['draw_matches'],
|
|
100
|
+
'help': 'Draw rectangles around matches'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
'name': 'match_color',
|
|
104
|
+
'label': 'Match Color (B,G,R)',
|
|
105
|
+
'type': 'text',
|
|
106
|
+
'default': DEFAULT_CONFIG['match_color'],
|
|
107
|
+
'help': 'Color for match rectangles'
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
def __init__(self, node_id=None, name="template match"):
|
|
112
|
+
super().__init__(node_id, name)
|
|
113
|
+
self._template = None
|
|
114
|
+
|
|
115
|
+
def _parse_color(self, color_str):
|
|
116
|
+
"""Parse color string to BGR tuple."""
|
|
117
|
+
try:
|
|
118
|
+
parts = [int(x.strip()) for x in str(color_str).split(',')]
|
|
119
|
+
if len(parts) >= 3:
|
|
120
|
+
return (parts[0], parts[1], parts[2])
|
|
121
|
+
return (0, 255, 0)
|
|
122
|
+
except Exception:
|
|
123
|
+
return (0, 255, 0)
|
|
124
|
+
|
|
125
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
126
|
+
"""Process template matching."""
|
|
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
|
+
# Input 1 is the template
|
|
137
|
+
if input_index == 1:
|
|
138
|
+
self._template = img
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Input 0 is the image to search
|
|
142
|
+
if self._template is None:
|
|
143
|
+
msg['matches'] = []
|
|
144
|
+
msg['match_count'] = 0
|
|
145
|
+
self.send(msg)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
method_str = self.config.get('method', 'ccoeff_normed')
|
|
149
|
+
threshold = self.get_config_float('threshold', 0.8)
|
|
150
|
+
multi_match = self.get_config_bool('multi_match', False)
|
|
151
|
+
draw = self.get_config_bool('draw_matches', True)
|
|
152
|
+
match_color = self._parse_color(self.config.get('match_color', '0,255,0'))
|
|
153
|
+
|
|
154
|
+
# Map method string to OpenCV constant
|
|
155
|
+
method_map = {
|
|
156
|
+
'ccoeff_normed': cv2.TM_CCOEFF_NORMED,
|
|
157
|
+
'ccorr_normed': cv2.TM_CCORR_NORMED,
|
|
158
|
+
'sqdiff_normed': cv2.TM_SQDIFF_NORMED,
|
|
159
|
+
'ccoeff': cv2.TM_CCOEFF,
|
|
160
|
+
'ccorr': cv2.TM_CCORR,
|
|
161
|
+
'sqdiff': cv2.TM_SQDIFF
|
|
162
|
+
}
|
|
163
|
+
method = method_map.get(method_str, cv2.TM_CCOEFF_NORMED)
|
|
164
|
+
|
|
165
|
+
# Perform template matching
|
|
166
|
+
result = cv2.matchTemplate(img, self._template, method)
|
|
167
|
+
|
|
168
|
+
h, w = self._template.shape[:2]
|
|
169
|
+
matches = []
|
|
170
|
+
|
|
171
|
+
# For SQDIFF methods, lower is better
|
|
172
|
+
is_sqdiff = method in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]
|
|
173
|
+
|
|
174
|
+
if multi_match:
|
|
175
|
+
if is_sqdiff:
|
|
176
|
+
locations = np.where(result <= (1 - threshold))
|
|
177
|
+
else:
|
|
178
|
+
locations = np.where(result >= threshold)
|
|
179
|
+
|
|
180
|
+
for pt in zip(*locations[::-1]):
|
|
181
|
+
score = float(result[pt[1], pt[0]])
|
|
182
|
+
matches.append({
|
|
183
|
+
'x': int(pt[0]),
|
|
184
|
+
'y': int(pt[1]),
|
|
185
|
+
'width': w,
|
|
186
|
+
'height': h,
|
|
187
|
+
'score': score if not is_sqdiff else 1 - score
|
|
188
|
+
})
|
|
189
|
+
else:
|
|
190
|
+
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
|
|
191
|
+
|
|
192
|
+
if is_sqdiff:
|
|
193
|
+
best_loc = min_loc
|
|
194
|
+
best_score = 1 - min_val
|
|
195
|
+
else:
|
|
196
|
+
best_loc = max_loc
|
|
197
|
+
best_score = max_val
|
|
198
|
+
|
|
199
|
+
if (is_sqdiff and min_val <= (1 - threshold)) or \
|
|
200
|
+
(not is_sqdiff and max_val >= threshold):
|
|
201
|
+
matches.append({
|
|
202
|
+
'x': int(best_loc[0]),
|
|
203
|
+
'y': int(best_loc[1]),
|
|
204
|
+
'width': w,
|
|
205
|
+
'height': h,
|
|
206
|
+
'score': float(best_score)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
# Draw matches if requested
|
|
210
|
+
if draw and matches:
|
|
211
|
+
if len(img.shape) == 2:
|
|
212
|
+
output = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
|
|
213
|
+
else:
|
|
214
|
+
output = img.copy()
|
|
215
|
+
|
|
216
|
+
for match in matches:
|
|
217
|
+
pt1 = (match['x'], match['y'])
|
|
218
|
+
pt2 = (match['x'] + w, match['y'] + h)
|
|
219
|
+
cv2.rectangle(output, pt1, pt2, match_color, 2)
|
|
220
|
+
|
|
221
|
+
if MessageKeys.PAYLOAD not in msg or not isinstance(msg[MessageKeys.PAYLOAD], dict):
|
|
222
|
+
msg[MessageKeys.PAYLOAD] = {}
|
|
223
|
+
msg[MessageKeys.PAYLOAD][MessageKeys.IMAGE.PATH] = self.encode_image(output, format_type)
|
|
224
|
+
|
|
225
|
+
msg['matches'] = matches
|
|
226
|
+
msg['match_count'] = len(matches)
|
|
227
|
+
self.send(msg)
|