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,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Crop Node - crops bounding boxes from images.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
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("Extracts cropped regions from images based on bounding box coordinates from detections or manual input.")
|
|
13
|
+
_info.add_header("Input")
|
|
14
|
+
_info.add_bullets(
|
|
15
|
+
("payload.image:", "Source image to crop from."),
|
|
16
|
+
("payload.detections:", "List of detections with 'bbox' field (when using detection mode)."),
|
|
17
|
+
)
|
|
18
|
+
_info.add_header("Output")
|
|
19
|
+
_info.add_bullets(
|
|
20
|
+
("payload.image:", "Cropped image data."),
|
|
21
|
+
("payload.bbox:", "Bounding box coordinates used for the crop (pixels)."),
|
|
22
|
+
("payload.bbox_normalized:", "Bounding box in normalized coordinates (0.0-1.0)."),
|
|
23
|
+
("payload.detection:", "Original detection info (if from detections)."),
|
|
24
|
+
("parts:", "Sequence info when outputting separate messages."),
|
|
25
|
+
)
|
|
26
|
+
_info.add_header("Coordinate Handling")
|
|
27
|
+
_info.add_text("Automatically detects normalized (0.0-1.0) vs pixel coordinates:")
|
|
28
|
+
_info.add_bullets(
|
|
29
|
+
("Normalized:", "All bbox values are between 0.0 and 1.0"),
|
|
30
|
+
("Pixels:", "Any bbox value > 1.0 indicates pixel coordinates"),
|
|
31
|
+
)
|
|
32
|
+
_info.add_header("Manual Coordinates")
|
|
33
|
+
_info.add_text("When using manual mode, coordinates are normalized (0.0-1.0):")
|
|
34
|
+
_info.add_bullets(
|
|
35
|
+
("0.0:", "Left/Top edge"),
|
|
36
|
+
("0.5:", "Center"),
|
|
37
|
+
("1.0:", "Right/Bottom edge"),
|
|
38
|
+
)
|
|
39
|
+
_info.add_header("Properties")
|
|
40
|
+
_info.add_bullets(
|
|
41
|
+
("Bounding Box Source:", "Use detections from message or manual coordinates."),
|
|
42
|
+
("Manual Coordinates:", "Normalized X1, Y1, X2, Y2 values (0.0-1.0)."),
|
|
43
|
+
("Output Mode:", "Send all crops in one message or separate messages."),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CropNode(BaseNode):
|
|
48
|
+
"""
|
|
49
|
+
Crop Node - extracts bounding box regions from images.
|
|
50
|
+
Receives image data and bbox coordinates, outputs cropped images.
|
|
51
|
+
"""
|
|
52
|
+
info = str(_info)
|
|
53
|
+
display_name = 'Crop'
|
|
54
|
+
icon = '✂️'
|
|
55
|
+
category = 'vision'
|
|
56
|
+
color = '#98D8C8'
|
|
57
|
+
border_color = '#6DB6A3'
|
|
58
|
+
text_color = '#000000'
|
|
59
|
+
input_count = 1
|
|
60
|
+
output_count = 1
|
|
61
|
+
|
|
62
|
+
DEFAULT_CONFIG = {
|
|
63
|
+
'bbox_source': 'detections',
|
|
64
|
+
'x1': 0.0,
|
|
65
|
+
'y1': 0.0,
|
|
66
|
+
'x2': 0.5,
|
|
67
|
+
'y2': 0.5,
|
|
68
|
+
'output_mode': 'separate',
|
|
69
|
+
'drop_messages': False
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
properties = [
|
|
73
|
+
{
|
|
74
|
+
'name': 'bbox_source',
|
|
75
|
+
'label': 'Bounding Box Source',
|
|
76
|
+
'type': 'select',
|
|
77
|
+
'options': [
|
|
78
|
+
{'value': 'detections', 'label': 'From msg.payload.detections'},
|
|
79
|
+
{'value': 'manual', 'label': 'Manual coordinates'}
|
|
80
|
+
],
|
|
81
|
+
'default': DEFAULT_CONFIG['bbox_source']
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
'name': 'x1',
|
|
85
|
+
'label': 'X1 (left)',
|
|
86
|
+
'type': 'number',
|
|
87
|
+
'default': DEFAULT_CONFIG['x1'],
|
|
88
|
+
'min': 0.0,
|
|
89
|
+
'max': 1.0,
|
|
90
|
+
'step': 0.01,
|
|
91
|
+
'help': 'Normalized X coordinate (0.0-1.0)',
|
|
92
|
+
'showIf': {'bbox_source': 'manual'}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
'name': 'y1',
|
|
96
|
+
'label': 'Y1 (top)',
|
|
97
|
+
'type': 'number',
|
|
98
|
+
'default': DEFAULT_CONFIG['y1'],
|
|
99
|
+
'min': 0.0,
|
|
100
|
+
'max': 1.0,
|
|
101
|
+
'step': 0.01,
|
|
102
|
+
'help': 'Normalized Y coordinate (0.0-1.0)',
|
|
103
|
+
'showIf': {'bbox_source': 'manual'}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
'name': 'x2',
|
|
107
|
+
'label': 'X2 (right)',
|
|
108
|
+
'type': 'number',
|
|
109
|
+
'default': DEFAULT_CONFIG['x2'],
|
|
110
|
+
'min': 0.0,
|
|
111
|
+
'max': 1.0,
|
|
112
|
+
'step': 0.01,
|
|
113
|
+
'help': 'Normalized X coordinate (0.0-1.0)',
|
|
114
|
+
'showIf': {'bbox_source': 'manual'}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
'name': 'y2',
|
|
118
|
+
'label': 'Y2 (bottom)',
|
|
119
|
+
'type': 'number',
|
|
120
|
+
'default': DEFAULT_CONFIG['y2'],
|
|
121
|
+
'min': 0.0,
|
|
122
|
+
'max': 1.0,
|
|
123
|
+
'step': 0.01,
|
|
124
|
+
'help': 'Normalized Y coordinate (0.0-1.0)',
|
|
125
|
+
'showIf': {'bbox_source': 'manual'}
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
'name': 'output_mode',
|
|
129
|
+
'label': 'Output Mode',
|
|
130
|
+
'type': 'select',
|
|
131
|
+
'options': [
|
|
132
|
+
{'value': 'all', 'label': 'All crops in one message'},
|
|
133
|
+
{'value': 'separate', 'label': 'Separate messages per crop'}
|
|
134
|
+
],
|
|
135
|
+
'default': DEFAULT_CONFIG['output_mode']
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
def __init__(self, node_id=None, name="crop"):
|
|
140
|
+
super().__init__(node_id, name)
|
|
141
|
+
|
|
142
|
+
def _is_normalized_bbox(self, bbox) -> bool:
|
|
143
|
+
"""Check if bbox appears to be normalized (all values 0.0-1.0)."""
|
|
144
|
+
if not bbox or len(bbox) < 4:
|
|
145
|
+
return False
|
|
146
|
+
# If all coordinate values are <= 1.0, assume normalized
|
|
147
|
+
return all(0.0 <= float(v) <= 1.0 for v in bbox[:4])
|
|
148
|
+
|
|
149
|
+
def _convert_bbox_to_pixels(self, bbox, img_w: int, img_h: int) -> tuple:
|
|
150
|
+
"""Convert bbox to pixel coordinates, handling both normalized and pixel inputs."""
|
|
151
|
+
if not bbox or len(bbox) < 4:
|
|
152
|
+
return 0, 0, 0, 0
|
|
153
|
+
|
|
154
|
+
x1, y1, x2, y2 = float(bbox[0]), float(bbox[1]), float(bbox[2]), float(bbox[3])
|
|
155
|
+
|
|
156
|
+
# Auto-detect normalized vs pixel coordinates
|
|
157
|
+
if self._is_normalized_bbox(bbox):
|
|
158
|
+
# Convert normalized to pixels
|
|
159
|
+
x1 = int(x1 * img_w)
|
|
160
|
+
y1 = int(y1 * img_h)
|
|
161
|
+
x2 = int(x2 * img_w)
|
|
162
|
+
y2 = int(y2 * img_h)
|
|
163
|
+
else:
|
|
164
|
+
# Already pixels, just convert to int
|
|
165
|
+
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
|
|
166
|
+
|
|
167
|
+
return x1, y1, x2, y2
|
|
168
|
+
|
|
169
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
170
|
+
"""Process incoming image and crop based on bounding boxes."""
|
|
171
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
172
|
+
if not payload or not isinstance(payload, dict):
|
|
173
|
+
self.report_error("No image payload found")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
# Decode image and track input format
|
|
177
|
+
image, input_format = self._decode_image(payload)
|
|
178
|
+
if image is None:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
bbox_source = self.config.get('bbox_source', 'detections')
|
|
182
|
+
crops = []
|
|
183
|
+
|
|
184
|
+
# Get image dimensions for coordinate conversion
|
|
185
|
+
img_h, img_w = image.shape[:2]
|
|
186
|
+
|
|
187
|
+
if bbox_source == 'detections':
|
|
188
|
+
# Extract from msg.payload.detections
|
|
189
|
+
detections = payload.get('detections', [])
|
|
190
|
+
if not detections:
|
|
191
|
+
self.report_error("No detections found in payload")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
for i, detection in enumerate(detections):
|
|
195
|
+
bbox = detection.get('bbox')
|
|
196
|
+
if bbox and len(bbox) >= 4:
|
|
197
|
+
# Auto-convert normalized coords to pixels
|
|
198
|
+
x1, y1, x2, y2 = self._convert_bbox_to_pixels(bbox, img_w, img_h)
|
|
199
|
+
crop = self._crop_image(image, x1, y1, x2, y2)
|
|
200
|
+
if crop is not None:
|
|
201
|
+
# Store both pixel and normalized bbox
|
|
202
|
+
bbox_normalized = [x1 / img_w, y1 / img_h, x2 / img_w, y2 / img_h]
|
|
203
|
+
crops.append({
|
|
204
|
+
MessageKeys.IMAGE.PATH: crop,
|
|
205
|
+
'bbox': [x1, y1, x2, y2],
|
|
206
|
+
'bbox_normalized': bbox_normalized,
|
|
207
|
+
'detection': detection,
|
|
208
|
+
'index': i
|
|
209
|
+
})
|
|
210
|
+
else:
|
|
211
|
+
# Use manual coordinates (normalized 0.0-1.0)
|
|
212
|
+
norm_x1 = self.get_config_float('x1', 0.0)
|
|
213
|
+
norm_y1 = self.get_config_float('y1', 0.0)
|
|
214
|
+
norm_x2 = self.get_config_float('x2', 0.5)
|
|
215
|
+
norm_y2 = self.get_config_float('y2', 0.5)
|
|
216
|
+
|
|
217
|
+
x1 = int(norm_x1 * img_w)
|
|
218
|
+
y1 = int(norm_y1 * img_h)
|
|
219
|
+
x2 = int(norm_x2 * img_w)
|
|
220
|
+
y2 = int(norm_y2 * img_h)
|
|
221
|
+
|
|
222
|
+
crop = self._crop_image(image, x1, y1, x2, y2)
|
|
223
|
+
if crop is not None:
|
|
224
|
+
crops.append({
|
|
225
|
+
MessageKeys.IMAGE.PATH: crop,
|
|
226
|
+
'bbox': [x1, y1, x2, y2],
|
|
227
|
+
'bbox_normalized': [norm_x1, norm_y1, norm_x2, norm_y2],
|
|
228
|
+
'index': 0
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
if not crops:
|
|
232
|
+
self.report_error("No valid crops produced")
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
output_mode = self.config.get('output_mode', 'separate')
|
|
236
|
+
|
|
237
|
+
# For manual mode with single crop, output as single object not array
|
|
238
|
+
if bbox_source == 'manual' and len(crops) == 1:
|
|
239
|
+
crop_data = crops[0]
|
|
240
|
+
encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
|
|
241
|
+
if encoded:
|
|
242
|
+
# Single crop output
|
|
243
|
+
msg[MessageKeys.PAYLOAD] = {
|
|
244
|
+
MessageKeys.IMAGE.PATH: encoded,
|
|
245
|
+
'bbox': crop_data['bbox'],
|
|
246
|
+
'bbox_normalized': crop_data.get('bbox_normalized'),
|
|
247
|
+
'index': crop_data['index']
|
|
248
|
+
}
|
|
249
|
+
msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crop')
|
|
250
|
+
self.send(msg)
|
|
251
|
+
elif output_mode == 'separate':
|
|
252
|
+
# Send each crop as a separate message
|
|
253
|
+
for crop_data in crops:
|
|
254
|
+
encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
|
|
255
|
+
if encoded:
|
|
256
|
+
msg_copy = msg.copy()
|
|
257
|
+
crop_info = {
|
|
258
|
+
MessageKeys.IMAGE.PATH: encoded,
|
|
259
|
+
'bbox': crop_data['bbox'],
|
|
260
|
+
'bbox_normalized': crop_data.get('bbox_normalized'),
|
|
261
|
+
'index': crop_data['index']
|
|
262
|
+
}
|
|
263
|
+
if 'detection' in crop_data:
|
|
264
|
+
crop_info['detection'] = crop_data['detection']
|
|
265
|
+
|
|
266
|
+
msg_copy[MessageKeys.PAYLOAD] = crop_info
|
|
267
|
+
msg_copy[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crop')
|
|
268
|
+
msg_copy['crop_count'] = len(crops)
|
|
269
|
+
msg_copy['parts'] = {'index': crop_data['index'], 'count': len(crops), 'id': msg.get(MessageKeys.MSG_ID)}
|
|
270
|
+
self.send(msg_copy)
|
|
271
|
+
else:
|
|
272
|
+
# Output all crops in one message as array
|
|
273
|
+
encoded_crops = []
|
|
274
|
+
for crop_data in crops:
|
|
275
|
+
encoded = self._encode_image(crop_data[MessageKeys.IMAGE.PATH], input_format)
|
|
276
|
+
if encoded:
|
|
277
|
+
crop_info = {
|
|
278
|
+
MessageKeys.IMAGE.PATH: encoded,
|
|
279
|
+
'bbox': crop_data['bbox'],
|
|
280
|
+
'bbox_normalized': crop_data.get('bbox_normalized'),
|
|
281
|
+
'index': crop_data['index']
|
|
282
|
+
}
|
|
283
|
+
if 'detection' in crop_data:
|
|
284
|
+
crop_info['detection'] = crop_data['detection']
|
|
285
|
+
encoded_crops.append(crop_info)
|
|
286
|
+
|
|
287
|
+
msg[MessageKeys.PAYLOAD] = encoded_crops
|
|
288
|
+
msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'crops')
|
|
289
|
+
msg['crop_count'] = len(encoded_crops)
|
|
290
|
+
self.send(msg)
|
|
291
|
+
|
|
292
|
+
def _decode_image(self, payload: Dict[str, Any]):
|
|
293
|
+
"""Decode image from payload using BaseNode helper. Returns (image, format_type) tuple."""
|
|
294
|
+
return self.decode_image(payload)
|
|
295
|
+
|
|
296
|
+
def _crop_image(self, image: np.ndarray, x1: int, y1: int, x2: int, y2: int) -> np.ndarray:
|
|
297
|
+
"""Crop image to bounding box."""
|
|
298
|
+
try:
|
|
299
|
+
h, w = image.shape[:2]
|
|
300
|
+
# Clamp coordinates
|
|
301
|
+
x1 = max(0, min(x1, w))
|
|
302
|
+
y1 = max(0, min(y1, h))
|
|
303
|
+
x2 = max(0, min(x2, w))
|
|
304
|
+
y2 = max(0, min(y2, h))
|
|
305
|
+
|
|
306
|
+
if x2 <= x1 or y2 <= y1:
|
|
307
|
+
self.report_error(f"Invalid bbox: [{x1}, {y1}, {x2}, {y2}]")
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
return image[y1:y2, x1:x2]
|
|
311
|
+
except Exception as e:
|
|
312
|
+
self.report_error(f"Error cropping image: {e}")
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def _encode_image(self, image: np.ndarray, format_type: str):
|
|
316
|
+
"""Encode image using BaseNode helper matching input format."""
|
|
317
|
+
return self.encode_image(image, format_type)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Debug node - prints messages to console.
|
|
3
|
+
Similar to Node-RED's debug node.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
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("Displays message content for debugging purposes. Similar to Node-RED's debug node.")
|
|
13
|
+
_info.add_header("Input")
|
|
14
|
+
_info.add_bullets(
|
|
15
|
+
("msg:", "Any message to inspect. Large values are automatically truncated."),
|
|
16
|
+
)
|
|
17
|
+
_info.add_header("Output")
|
|
18
|
+
_info.add_text("This node has no outputs. Messages are displayed in the debug panel.")
|
|
19
|
+
_info.add_header("Properties")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("Output:", "Show msg.payload only or complete message."),
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("UI")
|
|
24
|
+
_info.add_text("Toggle the node on/off using the button. Last 10 messages are stored.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DebugNode(BaseNode):
|
|
28
|
+
"""
|
|
29
|
+
Debug node - prints messages to console.
|
|
30
|
+
Similar to Node-RED's debug node.
|
|
31
|
+
"""
|
|
32
|
+
info = str(_info)
|
|
33
|
+
display_name = 'Debug'
|
|
34
|
+
icon = '🐛'
|
|
35
|
+
category = 'common'
|
|
36
|
+
color = '#87A980'
|
|
37
|
+
border_color = '#5F7858'
|
|
38
|
+
text_color = '#000000'
|
|
39
|
+
input_count = 1
|
|
40
|
+
output_count = 0
|
|
41
|
+
ui_component = 'toggle'
|
|
42
|
+
ui_component_config = {
|
|
43
|
+
'action': 'toggle_debug',
|
|
44
|
+
'label': 'Enable'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
api_routes = [
|
|
48
|
+
{'route': 'debug', 'methods': ['GET'], 'handler': 'get_debug_messages'},
|
|
49
|
+
{'route': 'debug', 'methods': ['DELETE'], 'handler': 'clear_debug_messages'},
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
sse_handlers = [
|
|
53
|
+
{'type': 'messages', 'handler': 'consume_messages'},
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
DEFAULT_CONFIG = {
|
|
57
|
+
'console': True,
|
|
58
|
+
'complete': MessageKeys.PAYLOAD,
|
|
59
|
+
'drop_messages': False
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
properties = [
|
|
63
|
+
{
|
|
64
|
+
'name': 'drop_messages',
|
|
65
|
+
'label': 'Drop Messages When Busy',
|
|
66
|
+
'type': 'checkbox',
|
|
67
|
+
'default': False
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
'name': 'complete',
|
|
71
|
+
'label': 'Output',
|
|
72
|
+
'type': 'select',
|
|
73
|
+
'options': [
|
|
74
|
+
{'value': MessageKeys.PAYLOAD, 'label': f'msg.{MessageKeys.PAYLOAD}'},
|
|
75
|
+
{'value': 'msg', 'label': 'Complete msg'}
|
|
76
|
+
],
|
|
77
|
+
'default': DEFAULT_CONFIG['complete']
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
def __init__(self, node_id=None, name="debug"):
|
|
82
|
+
super().__init__(node_id, name)
|
|
83
|
+
self.messages = [] # Store messages for API access
|
|
84
|
+
|
|
85
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
86
|
+
"""
|
|
87
|
+
Print/store the message for debugging.
|
|
88
|
+
"""
|
|
89
|
+
# Skip if debug node is disabled
|
|
90
|
+
if not self.enabled:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
complete = self.config.get('complete', MessageKeys.PAYLOAD)
|
|
94
|
+
|
|
95
|
+
if complete == 'msg':
|
|
96
|
+
output = msg
|
|
97
|
+
display_key = 'Complete msg'
|
|
98
|
+
elif complete == MessageKeys.PAYLOAD:
|
|
99
|
+
output = msg.get(MessageKeys.PAYLOAD)
|
|
100
|
+
display_key = f'msg.{MessageKeys.PAYLOAD}'
|
|
101
|
+
else:
|
|
102
|
+
# Try to get nested property (only top-level for now)
|
|
103
|
+
output = msg.get(complete, msg.get(MessageKeys.PAYLOAD))
|
|
104
|
+
display_key = f"msg.{complete}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Recursively truncate large values in dicts/lists, but not the whole message
|
|
108
|
+
def truncate_values(val, maxlen=300):
|
|
109
|
+
if isinstance(val, (bytes, bytearray)):
|
|
110
|
+
return f"<binary data, {len(val)} bytes>"
|
|
111
|
+
elif isinstance(val, np.ndarray):
|
|
112
|
+
return f"<numpy array, shape={val.shape}, dtype={val.dtype}>"
|
|
113
|
+
elif isinstance(val, dict):
|
|
114
|
+
return {k: truncate_values(v, maxlen) for k, v in val.items()}
|
|
115
|
+
elif isinstance(val, list):
|
|
116
|
+
return [truncate_values(v, maxlen) for v in val]
|
|
117
|
+
else:
|
|
118
|
+
s = str(val)
|
|
119
|
+
if len(s) > maxlen:
|
|
120
|
+
return s[:maxlen] + f"... [truncated, {len(s)} chars]"
|
|
121
|
+
return val
|
|
122
|
+
|
|
123
|
+
display_output = truncate_values(output)
|
|
124
|
+
|
|
125
|
+
timestamp = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
126
|
+
debug_entry = {
|
|
127
|
+
'timestamp': timestamp,
|
|
128
|
+
'node': self.name,
|
|
129
|
+
'node_id': self.id,
|
|
130
|
+
'display_key': display_key,
|
|
131
|
+
'output': display_output
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
self.messages.append(debug_entry)
|
|
135
|
+
|
|
136
|
+
# Keep only last 10 messages
|
|
137
|
+
if len(self.messages) > 10:
|
|
138
|
+
self.messages = self.messages[-10:]
|
|
139
|
+
|
|
140
|
+
# if self.config.get('console', True):
|
|
141
|
+
# print(f"[{timestamp}] [{self.name}] {output}")
|
|
142
|
+
|
|
143
|
+
# Pass through (optional)
|
|
144
|
+
# self.send(msg)
|
|
145
|
+
|
|
146
|
+
def set_enabled(self, enabled: bool):
|
|
147
|
+
"""Set the enabled state of the debug node."""
|
|
148
|
+
self.enabled = enabled
|
|
149
|
+
|
|
150
|
+
def get_enabled(self) -> bool:
|
|
151
|
+
"""Get the enabled state of the debug node."""
|
|
152
|
+
return self.enabled
|
|
153
|
+
|
|
154
|
+
def get_debug_messages(self):
|
|
155
|
+
"""API route handler: return stored debug messages."""
|
|
156
|
+
return self.messages
|
|
157
|
+
|
|
158
|
+
def clear_debug_messages(self):
|
|
159
|
+
"""API route handler: clear stored debug messages."""
|
|
160
|
+
self.messages.clear()
|
|
161
|
+
return None # 204 No Content
|
|
162
|
+
|
|
163
|
+
def consume_messages(self):
|
|
164
|
+
"""SSE handler: return and clear pending messages."""
|
|
165
|
+
if self.messages:
|
|
166
|
+
msgs = self.messages.copy()
|
|
167
|
+
self.messages.clear()
|
|
168
|
+
return msgs
|
|
169
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.19.0
|