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,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slice Image Node - slices images into tiles for improved detection of small objects.
|
|
3
|
+
Based on SAHI (Slicing Aided Hyper Inference) methodology.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from typing import Any, Dict, List, Tuple, Optional
|
|
8
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import cv2
|
|
12
|
+
_HAS_CV2 = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
cv2 = None
|
|
15
|
+
_HAS_CV2 = False
|
|
16
|
+
|
|
17
|
+
_info = Info()
|
|
18
|
+
_info.add_text("Divides images into overlapping tiles for improved detection of small objects. Based on SAHI (Slicing Aided Hyper Inference) methodology.")
|
|
19
|
+
_info.add_header("Inputs")
|
|
20
|
+
_info.add_bullets(("Input 0:", "Image message with 'image' field"))
|
|
21
|
+
_info.add_header("Outputs")
|
|
22
|
+
_info.add_bullets(
|
|
23
|
+
("Output 0:", "Array of image slices with offset metadata, or split messages per slice"),
|
|
24
|
+
)
|
|
25
|
+
_info.add_header("Configuration")
|
|
26
|
+
_info.add_bullets(
|
|
27
|
+
("Output Mode:", "Array (all slices in one message) or Split (separate messages)"),
|
|
28
|
+
("Auto Slice:", "Automatically calculate slice size based on image resolution"),
|
|
29
|
+
("Slice Width/Height:", "Manual slice dimensions in pixels"),
|
|
30
|
+
("Overlap Ratios:", "Overlap between adjacent slices (0-1)"),
|
|
31
|
+
("Include Full Image:", "Also include the original full image in output"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SliceImageNode(BaseNode):
|
|
36
|
+
"""
|
|
37
|
+
Slice Image Node - divides images into overlapping tiles for better detection
|
|
38
|
+
of small objects in large images.
|
|
39
|
+
|
|
40
|
+
Implements SAHI-style slicing with configurable tile size and overlap.
|
|
41
|
+
Outputs an array of image tiles with their offset information.
|
|
42
|
+
"""
|
|
43
|
+
display_name = 'Slice Image'
|
|
44
|
+
info = str(_info)
|
|
45
|
+
icon = '🔲'
|
|
46
|
+
category = 'vision'
|
|
47
|
+
color = '#4ECDC4'
|
|
48
|
+
border_color = '#26A69A'
|
|
49
|
+
text_color = '#000000'
|
|
50
|
+
input_count = 1
|
|
51
|
+
output_count = 1
|
|
52
|
+
|
|
53
|
+
DEFAULT_CONFIG = {
|
|
54
|
+
'slice_width': 640,
|
|
55
|
+
'slice_height': 640,
|
|
56
|
+
'overlap_width_ratio': 0.2,
|
|
57
|
+
'overlap_height_ratio': 0.2,
|
|
58
|
+
'auto_slice': False,
|
|
59
|
+
'include_full_image': True,
|
|
60
|
+
'output_mode': 'array',
|
|
61
|
+
'min_area_ratio': 0.1,
|
|
62
|
+
'drop_messages': False
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
properties = [
|
|
66
|
+
{
|
|
67
|
+
'name': 'output_mode',
|
|
68
|
+
'label': 'Output Mode',
|
|
69
|
+
'type': 'select',
|
|
70
|
+
'options': [
|
|
71
|
+
{'value': 'array', 'label': 'Array (all slices in one message)'},
|
|
72
|
+
{'value': 'split', 'label': 'Split (separate message per slice)'}
|
|
73
|
+
],
|
|
74
|
+
'default': 'array'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
'name': 'auto_slice',
|
|
78
|
+
'label': 'Auto Slice Resolution',
|
|
79
|
+
'type': 'checkbox',
|
|
80
|
+
'default': False
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
'name': 'slice_width',
|
|
84
|
+
'label': 'Slice Width (pixels)',
|
|
85
|
+
'type': 'number',
|
|
86
|
+
'default': 640,
|
|
87
|
+
'showIf': {'auto_slice': False}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
'name': 'slice_height',
|
|
91
|
+
'label': 'Slice Height (pixels)',
|
|
92
|
+
'type': 'number',
|
|
93
|
+
'default': 640,
|
|
94
|
+
'showIf': {'auto_slice': False}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
'name': 'overlap_width_ratio',
|
|
98
|
+
'label': 'Overlap Width Ratio (0-1)',
|
|
99
|
+
'type': 'number',
|
|
100
|
+
'default': 0.2,
|
|
101
|
+
'showIf': {'auto_slice': False}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
'name': 'overlap_height_ratio',
|
|
105
|
+
'label': 'Overlap Height Ratio (0-1)',
|
|
106
|
+
'type': 'number',
|
|
107
|
+
'default': 0.2,
|
|
108
|
+
'showIf': {'auto_slice': False}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
'name': 'include_full_image',
|
|
112
|
+
'label': 'Include Full Image in Output',
|
|
113
|
+
'type': 'checkbox',
|
|
114
|
+
'default': True
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
'name': 'min_area_ratio',
|
|
118
|
+
'label': 'Min Area Ratio (for auto slice)',
|
|
119
|
+
'type': 'number',
|
|
120
|
+
'default': 0.1,
|
|
121
|
+
'showIf': {'auto_slice': True}
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
def __init__(self, node_id=None, name="slice_image"):
|
|
126
|
+
super().__init__(node_id, name)
|
|
127
|
+
|
|
128
|
+
def _get_auto_slice_params(self, height: int, width: int) -> Tuple[int, int, int, int]:
|
|
129
|
+
"""
|
|
130
|
+
Calculate automatic slice parameters based on image resolution.
|
|
131
|
+
Returns (x_overlap, y_overlap, slice_width, slice_height)
|
|
132
|
+
"""
|
|
133
|
+
resolution = height * width
|
|
134
|
+
factor = self._calc_resolution_factor(resolution)
|
|
135
|
+
|
|
136
|
+
orientation = self._calc_aspect_ratio_orientation(width, height)
|
|
137
|
+
|
|
138
|
+
if factor <= 18: # Low resolution (e.g., 300x300, 640x640)
|
|
139
|
+
return self._get_resolution_params("low", height, width, orientation)
|
|
140
|
+
elif factor < 21: # Medium resolution (e.g., 1024x1024)
|
|
141
|
+
return self._get_resolution_params("medium", height, width, orientation)
|
|
142
|
+
elif factor < 24: # High resolution (e.g., 2048x2048)
|
|
143
|
+
return self._get_resolution_params("high", height, width, orientation)
|
|
144
|
+
else: # Ultra-high resolution (e.g., 4096x4096+)
|
|
145
|
+
return self._get_resolution_params("ultra-high", height, width, orientation)
|
|
146
|
+
|
|
147
|
+
def _calc_resolution_factor(self, resolution: int) -> int:
|
|
148
|
+
"""Calculate power of 2 closest to image resolution."""
|
|
149
|
+
expo = 0
|
|
150
|
+
while 2 ** expo < resolution:
|
|
151
|
+
expo += 1
|
|
152
|
+
return expo - 1
|
|
153
|
+
|
|
154
|
+
def _calc_aspect_ratio_orientation(self, width: int, height: int) -> str:
|
|
155
|
+
"""Determine image orientation based on aspect ratio."""
|
|
156
|
+
if width < height:
|
|
157
|
+
return "vertical"
|
|
158
|
+
elif width > height:
|
|
159
|
+
return "horizontal"
|
|
160
|
+
return "square"
|
|
161
|
+
|
|
162
|
+
def _calc_ratio_and_slice(self, orientation: str, slide: int = 1, ratio: float = 0.1) -> Tuple[int, int, float, float]:
|
|
163
|
+
"""Calculate slice rows/cols and overlap ratios based on orientation."""
|
|
164
|
+
if orientation == "vertical":
|
|
165
|
+
return slide, slide * 2, ratio, ratio
|
|
166
|
+
elif orientation == "horizontal":
|
|
167
|
+
return slide * 2, slide, ratio, ratio
|
|
168
|
+
return slide, slide, ratio, ratio
|
|
169
|
+
|
|
170
|
+
def _get_resolution_params(self, resolution: str, height: int, width: int, orientation: str) -> Tuple[int, int, int, int]:
|
|
171
|
+
"""Get slice parameters based on resolution category."""
|
|
172
|
+
if resolution == "medium":
|
|
173
|
+
split_row, split_col, overlap_h, overlap_w = self._calc_ratio_and_slice(orientation, 1, 0.8)
|
|
174
|
+
elif resolution == "high":
|
|
175
|
+
split_row, split_col, overlap_h, overlap_w = self._calc_ratio_and_slice(orientation, 2, 0.4)
|
|
176
|
+
elif resolution == "ultra-high":
|
|
177
|
+
split_row, split_col, overlap_h, overlap_w = self._calc_ratio_and_slice(orientation, 4, 0.4)
|
|
178
|
+
else: # low
|
|
179
|
+
return 0, 0, width, height # No slicing needed
|
|
180
|
+
|
|
181
|
+
slice_height = height // split_col
|
|
182
|
+
slice_width = width // split_row
|
|
183
|
+
x_overlap = int(slice_width * overlap_w)
|
|
184
|
+
y_overlap = int(slice_height * overlap_h)
|
|
185
|
+
|
|
186
|
+
return x_overlap, y_overlap, slice_width, slice_height
|
|
187
|
+
|
|
188
|
+
def _get_slice_bboxes(
|
|
189
|
+
self,
|
|
190
|
+
image_height: int,
|
|
191
|
+
image_width: int,
|
|
192
|
+
slice_height: int,
|
|
193
|
+
slice_width: int,
|
|
194
|
+
overlap_height_ratio: float,
|
|
195
|
+
overlap_width_ratio: float
|
|
196
|
+
) -> List[List[int]]:
|
|
197
|
+
"""
|
|
198
|
+
Generate bounding boxes for slicing an image.
|
|
199
|
+
|
|
200
|
+
Returns list of [x_min, y_min, x_max, y_max] for each slice.
|
|
201
|
+
"""
|
|
202
|
+
if overlap_height_ratio >= 1.0 or overlap_width_ratio >= 1.0:
|
|
203
|
+
raise ValueError("Overlap ratio must be less than 1.0")
|
|
204
|
+
|
|
205
|
+
y_overlap = int(overlap_height_ratio * slice_height)
|
|
206
|
+
x_overlap = int(overlap_width_ratio * slice_width)
|
|
207
|
+
|
|
208
|
+
slice_bboxes = []
|
|
209
|
+
y_min = 0
|
|
210
|
+
|
|
211
|
+
while y_min < image_height:
|
|
212
|
+
y_max = y_min + slice_height
|
|
213
|
+
x_min = 0
|
|
214
|
+
|
|
215
|
+
while x_min < image_width:
|
|
216
|
+
x_max = x_min + slice_width
|
|
217
|
+
|
|
218
|
+
# Handle edge cases - ensure slices don't exceed image bounds
|
|
219
|
+
if y_max > image_height or x_max > image_width:
|
|
220
|
+
xmax = min(image_width, x_max)
|
|
221
|
+
ymax = min(image_height, y_max)
|
|
222
|
+
xmin = max(0, xmax - slice_width)
|
|
223
|
+
ymin = max(0, ymax - slice_height)
|
|
224
|
+
slice_bboxes.append([xmin, ymin, xmax, ymax])
|
|
225
|
+
else:
|
|
226
|
+
slice_bboxes.append([x_min, y_min, x_max, y_max])
|
|
227
|
+
|
|
228
|
+
x_min = x_max - x_overlap
|
|
229
|
+
|
|
230
|
+
y_min = y_max - y_overlap
|
|
231
|
+
|
|
232
|
+
return slice_bboxes
|
|
233
|
+
|
|
234
|
+
def _slice_image(self, image: np.ndarray) -> List[Dict[str, Any]]:
|
|
235
|
+
"""
|
|
236
|
+
Slice the image into tiles based on configuration.
|
|
237
|
+
|
|
238
|
+
Returns list of dicts with 'image', 'offset', and 'bbox' for each slice.
|
|
239
|
+
"""
|
|
240
|
+
height, width = image.shape[:2]
|
|
241
|
+
|
|
242
|
+
auto_slice = self.get_config_bool('auto_slice', False)
|
|
243
|
+
|
|
244
|
+
if auto_slice:
|
|
245
|
+
x_overlap, y_overlap, slice_width, slice_height = self._get_auto_slice_params(height, width)
|
|
246
|
+
if slice_width >= width and slice_height >= height:
|
|
247
|
+
# No slicing needed - image is small enough
|
|
248
|
+
return [{
|
|
249
|
+
'image': image,
|
|
250
|
+
'offset': [0, 0],
|
|
251
|
+
'bbox': [0, 0, width, height],
|
|
252
|
+
'is_full_image': True
|
|
253
|
+
}]
|
|
254
|
+
overlap_width_ratio = x_overlap / slice_width if slice_width > 0 else 0.2
|
|
255
|
+
overlap_height_ratio = y_overlap / slice_height if slice_height > 0 else 0.2
|
|
256
|
+
else:
|
|
257
|
+
slice_width = self.get_config_int('slice_width', 640)
|
|
258
|
+
slice_height = self.get_config_int('slice_height', 640)
|
|
259
|
+
overlap_width_ratio = self.get_config_float('overlap_width_ratio', 0.2)
|
|
260
|
+
overlap_height_ratio = self.get_config_float('overlap_height_ratio', 0.2)
|
|
261
|
+
|
|
262
|
+
# Get slice bounding boxes
|
|
263
|
+
slice_bboxes = self._get_slice_bboxes(
|
|
264
|
+
image_height=height,
|
|
265
|
+
image_width=width,
|
|
266
|
+
slice_height=slice_height,
|
|
267
|
+
slice_width=slice_width,
|
|
268
|
+
overlap_height_ratio=overlap_height_ratio,
|
|
269
|
+
overlap_width_ratio=overlap_width_ratio
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
slices = []
|
|
273
|
+
for bbox in slice_bboxes:
|
|
274
|
+
x_min, y_min, x_max, y_max = bbox
|
|
275
|
+
slice_img = image[y_min:y_max, x_min:x_max].copy()
|
|
276
|
+
slices.append({
|
|
277
|
+
'image': slice_img,
|
|
278
|
+
'offset': [x_min, y_min],
|
|
279
|
+
'bbox': bbox,
|
|
280
|
+
'is_full_image': False
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
return slices
|
|
284
|
+
|
|
285
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
286
|
+
"""Process incoming image and output slices."""
|
|
287
|
+
if not _HAS_CV2:
|
|
288
|
+
self.report_error("OpenCV (cv2) is required for image slicing")
|
|
289
|
+
return
|
|
290
|
+
|
|
291
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
292
|
+
if payload is None:
|
|
293
|
+
self.report_error("No payload in message")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Decode the image
|
|
297
|
+
image, input_format = self.decode_image(payload)
|
|
298
|
+
if image is None or input_format is None:
|
|
299
|
+
self.report_error("Failed to decode image from payload")
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Slice the image
|
|
303
|
+
slices = self._slice_image(image)
|
|
304
|
+
|
|
305
|
+
# Check if we should include the full image
|
|
306
|
+
include_full = self.get_config_bool('include_full_image', True)
|
|
307
|
+
output_mode = self.config.get('output_mode', 'array')
|
|
308
|
+
|
|
309
|
+
# Get original message ID for tracking
|
|
310
|
+
parent_msg_id = msg.get(MessageKeys.MSG_ID, '')
|
|
311
|
+
|
|
312
|
+
# Prepare output slices
|
|
313
|
+
output_slices = []
|
|
314
|
+
|
|
315
|
+
# Add full image first if configured (index 0)
|
|
316
|
+
if include_full:
|
|
317
|
+
full_encoded = self.encode_image(image, input_format)
|
|
318
|
+
output_slices.append({
|
|
319
|
+
MessageKeys.PAYLOAD: {MessageKeys.IMAGE.PATH: full_encoded}, # Standard payload.image format
|
|
320
|
+
'offset': [0, 0],
|
|
321
|
+
'bbox': [0, 0, image.shape[1], image.shape[0]],
|
|
322
|
+
'slice_index': 0,
|
|
323
|
+
'is_full_image': True,
|
|
324
|
+
'original_width': image.shape[1],
|
|
325
|
+
'original_height': image.shape[0]
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
# Add slices
|
|
329
|
+
start_index = 1 if include_full else 0
|
|
330
|
+
for i, slice_data in enumerate(slices):
|
|
331
|
+
encoded = self.encode_image(slice_data[MessageKeys.IMAGE.PATH], input_format)
|
|
332
|
+
output_slices.append({
|
|
333
|
+
MessageKeys.PAYLOAD: {MessageKeys.IMAGE.PATH: encoded}, # Standard payload.image format
|
|
334
|
+
'offset': slice_data['offset'],
|
|
335
|
+
'bbox': slice_data['bbox'],
|
|
336
|
+
'slice_index': start_index + i,
|
|
337
|
+
'is_full_image': slice_data.get('is_full_image', False),
|
|
338
|
+
'original_width': image.shape[1],
|
|
339
|
+
'original_height': image.shape[0]
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
total_count = len(output_slices)
|
|
343
|
+
|
|
344
|
+
if output_mode == 'split':
|
|
345
|
+
# Send each slice as a separate message with parts metadata
|
|
346
|
+
for i, slice_info in enumerate(output_slices):
|
|
347
|
+
slice_msg = msg.copy()
|
|
348
|
+
# Put image directly in payload for YOLO compatibility
|
|
349
|
+
slice_msg[MessageKeys.PAYLOAD] = slice_info[MessageKeys.PAYLOAD]
|
|
350
|
+
slice_msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'slice')
|
|
351
|
+
|
|
352
|
+
# Add parts metadata for SliceCollectorNode to track
|
|
353
|
+
slice_msg['parts'] = {
|
|
354
|
+
'index': i,
|
|
355
|
+
'count': total_count,
|
|
356
|
+
'id': parent_msg_id
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Add slice metadata at message level for SliceCollector
|
|
360
|
+
slice_msg['slice_offset'] = slice_info['offset']
|
|
361
|
+
slice_msg['slice_bbox'] = slice_info['bbox']
|
|
362
|
+
slice_msg['slice_index'] = slice_info['slice_index']
|
|
363
|
+
slice_msg['is_full_image'] = slice_info['is_full_image']
|
|
364
|
+
slice_msg['original_width'] = slice_info['original_width']
|
|
365
|
+
slice_msg['original_height'] = slice_info['original_height']
|
|
366
|
+
|
|
367
|
+
self.send(slice_msg)
|
|
368
|
+
else:
|
|
369
|
+
# Array mode - send slices as array for Split node
|
|
370
|
+
# Each item has 'payload' key which Split will extract
|
|
371
|
+
out_msg = msg.copy()
|
|
372
|
+
out_msg[MessageKeys.PAYLOAD] = output_slices # Direct array for Split node
|
|
373
|
+
out_msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'sliced')
|
|
374
|
+
out_msg['slice_count'] = total_count
|
|
375
|
+
out_msg['original_width'] = image.shape[1]
|
|
376
|
+
out_msg['original_height'] = image.shape[0]
|
|
377
|
+
out_msg['include_full_image'] = include_full
|
|
378
|
+
|
|
379
|
+
self.send(out_msg)
|
|
380
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# No external dependencies - uses only Python standard library
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Split Node - splits arrays or strings into separate messages.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
7
|
+
|
|
8
|
+
_info = Info()
|
|
9
|
+
_info.add_text("Splits arrays, strings, or objects into separate messages. Each element becomes an individual message with parts metadata for reassembly.")
|
|
10
|
+
_info.add_header("Inputs")
|
|
11
|
+
_info.add_bullets(
|
|
12
|
+
("Input 0:", "Message with array, string, or object payload")
|
|
13
|
+
)
|
|
14
|
+
_info.add_header("Outputs")
|
|
15
|
+
_info.add_bullets(
|
|
16
|
+
("Output 0:", "Multiple messages, one per element")
|
|
17
|
+
)
|
|
18
|
+
_info.add_header("Configuration")
|
|
19
|
+
_info.add_bullets(
|
|
20
|
+
("Split Type:", "Auto-detect, Array, String, or Object (key-value pairs)"),
|
|
21
|
+
("Delimiter:", "Character to split strings on (default: comma)")
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("Output Message")
|
|
24
|
+
_info.add_code('msg.payload').text(" - Individual element from the split").end()
|
|
25
|
+
_info.add_code('msg.parts.index').text(" - Position in the original sequence").end()
|
|
26
|
+
_info.add_code('msg.parts.count').text(" - Total number of parts").end()
|
|
27
|
+
_info.add_code('msg.parts.id').text(" - ID to group related parts").end()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SplitNode(BaseNode):
|
|
31
|
+
"""
|
|
32
|
+
Split Node - splits arrays or strings into separate messages.
|
|
33
|
+
Similar to Node-RED's split node.
|
|
34
|
+
"""
|
|
35
|
+
info = str(_info)
|
|
36
|
+
display_name = 'Split'
|
|
37
|
+
icon = '✂️'
|
|
38
|
+
category = 'function'
|
|
39
|
+
color = '#F0E68C'
|
|
40
|
+
border_color = '#DAA520'
|
|
41
|
+
text_color = '#000000'
|
|
42
|
+
input_count = 1
|
|
43
|
+
output_count = 1
|
|
44
|
+
|
|
45
|
+
DEFAULT_CONFIG = {
|
|
46
|
+
'split_type': 'auto',
|
|
47
|
+
'delimiter': ',',
|
|
48
|
+
'drop_messages': False,
|
|
49
|
+
'split_path': MessageKeys.PAYLOAD
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
properties = [
|
|
53
|
+
{
|
|
54
|
+
'name': 'split_path',
|
|
55
|
+
'label': 'Message Path',
|
|
56
|
+
'type': 'text',
|
|
57
|
+
'default': DEFAULT_CONFIG['split_path']
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
'name': 'split_type',
|
|
61
|
+
'label': 'Split Type',
|
|
62
|
+
'type': 'select',
|
|
63
|
+
'options': [
|
|
64
|
+
{'value': 'auto', 'label': 'Auto-detect'},
|
|
65
|
+
{'value': 'array', 'label': 'Array'},
|
|
66
|
+
{'value': 'string', 'label': 'String'},
|
|
67
|
+
{'value': 'object', 'label': 'Object (key-value pairs)'}
|
|
68
|
+
],
|
|
69
|
+
'default': DEFAULT_CONFIG['split_type']
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
'name': 'delimiter',
|
|
73
|
+
'label': 'String Delimiter',
|
|
74
|
+
'type': 'text',
|
|
75
|
+
'default': DEFAULT_CONFIG['delimiter'],
|
|
76
|
+
'showIf': {'split_type': 'string'},
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def __init__(self, node_id=None, name="split"):
|
|
81
|
+
super().__init__(node_id, name)
|
|
82
|
+
|
|
83
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
84
|
+
"""Split incoming message into multiple messages."""
|
|
85
|
+
split_path = self.config.get('split_path', MessageKeys.PAYLOAD)
|
|
86
|
+
payload = self._get_nested_value(msg, split_path)
|
|
87
|
+
|
|
88
|
+
if payload is None:
|
|
89
|
+
self.report_error(f"No value found at path: {split_path}")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
split_type = self.config.get('split_type', 'auto')
|
|
93
|
+
|
|
94
|
+
items = []
|
|
95
|
+
|
|
96
|
+
# Auto-detect or explicit type
|
|
97
|
+
if split_type == 'auto':
|
|
98
|
+
if isinstance(payload, list):
|
|
99
|
+
items = payload
|
|
100
|
+
elif isinstance(payload, dict):
|
|
101
|
+
items = list(payload.items())
|
|
102
|
+
elif isinstance(payload, str):
|
|
103
|
+
delimiter = self.config.get('delimiter', ',')
|
|
104
|
+
items = payload.split(delimiter)
|
|
105
|
+
else:
|
|
106
|
+
self.report_error(f"Cannot split payload of type {type(payload)}")
|
|
107
|
+
return
|
|
108
|
+
elif split_type == 'array':
|
|
109
|
+
if isinstance(payload, list):
|
|
110
|
+
items = payload
|
|
111
|
+
else:
|
|
112
|
+
self.report_error("Payload is not an array")
|
|
113
|
+
return
|
|
114
|
+
elif split_type == 'string':
|
|
115
|
+
if isinstance(payload, str):
|
|
116
|
+
delimiter = self.config.get('delimiter', ',')
|
|
117
|
+
items = payload.split(delimiter)
|
|
118
|
+
else:
|
|
119
|
+
items = str(payload).split(self.config.get('delimiter', ','))
|
|
120
|
+
elif split_type == 'object':
|
|
121
|
+
if isinstance(payload, dict):
|
|
122
|
+
items = list(payload.items())
|
|
123
|
+
else:
|
|
124
|
+
self.report_error("Payload is not an object")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# Send each item as a separate message
|
|
128
|
+
total_count = len(items)
|
|
129
|
+
original_msgid = msg.get(MessageKeys.MSG_ID)
|
|
130
|
+
|
|
131
|
+
for i, item in enumerate(items):
|
|
132
|
+
# Create a fresh copy of the message for each item
|
|
133
|
+
out_msg = msg.copy()
|
|
134
|
+
|
|
135
|
+
# For object splits, item is a tuple (key, value)
|
|
136
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
137
|
+
item_payload = {'key': item[0], 'value': item[1]}
|
|
138
|
+
# For slice objects with embedded payload (from SliceImageNode)
|
|
139
|
+
elif isinstance(item, dict) and MessageKeys.PAYLOAD in item:
|
|
140
|
+
item_payload = item[MessageKeys.PAYLOAD]
|
|
141
|
+
# Copy other keys to message level (slice metadata)
|
|
142
|
+
for key, value in item.items():
|
|
143
|
+
if key != MessageKeys.PAYLOAD:
|
|
144
|
+
out_msg[key] = value
|
|
145
|
+
else:
|
|
146
|
+
item_payload = item
|
|
147
|
+
|
|
148
|
+
out_msg[MessageKeys.PAYLOAD] = item_payload
|
|
149
|
+
out_msg['parts'] = {
|
|
150
|
+
'index': i,
|
|
151
|
+
'count': total_count,
|
|
152
|
+
'id': original_msgid
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
self.send(out_msg)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# No external dependencies - uses only Python standard library
|