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,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BBoxMetricsNode - Calculate metrics from bounding boxes
|
|
3
|
+
|
|
4
|
+
This node calculates various metrics from bounding box predictions including:
|
|
5
|
+
- Width and height
|
|
6
|
+
- Area
|
|
7
|
+
- Aspect ratio
|
|
8
|
+
- Center point coordinates
|
|
9
|
+
- Diagonal length
|
|
10
|
+
|
|
11
|
+
Input format:
|
|
12
|
+
msg['detections'] - List of detections with bounding boxes
|
|
13
|
+
OR
|
|
14
|
+
msg['boxes'] - Array of bounding boxes [x1, y1, x2, y2]
|
|
15
|
+
OR
|
|
16
|
+
msg['bbox'] - Single bounding box [x1, y1, x2, y2]
|
|
17
|
+
|
|
18
|
+
Output format:
|
|
19
|
+
msg['metrics'] - Dictionary containing calculated metrics
|
|
20
|
+
msg['detections'] - Original detections with added 'metrics' field
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
import os
|
|
25
|
+
from typing import Any, Dict
|
|
26
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
|
27
|
+
|
|
28
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
29
|
+
import numpy as np
|
|
30
|
+
|
|
31
|
+
_info = Info()
|
|
32
|
+
_info.add_text("Calculates geometric metrics from bounding boxes including dimensions, area, aspect ratio, center point, and diagonal length.")
|
|
33
|
+
_info.add_header("Input")
|
|
34
|
+
_info.add_bullets(
|
|
35
|
+
("detections:", "List of detection objects containing 'bbox' field."),
|
|
36
|
+
("boxes:", "Array of bounding boxes [x1, y1, x2, y2]."),
|
|
37
|
+
("bbox:", "Single bounding box [x1, y1, x2, y2]."),
|
|
38
|
+
)
|
|
39
|
+
_info.add_header("Output")
|
|
40
|
+
_info.add_bullets(
|
|
41
|
+
("metrics:", "Calculated metrics (width, height, area, aspect_ratio, center, diagonal)."),
|
|
42
|
+
("detections:", "Original detections with 'metrics' field added to each."),
|
|
43
|
+
)
|
|
44
|
+
_info.add_header("Properties")
|
|
45
|
+
_info.add_bullets(
|
|
46
|
+
("Metrics to Calculate:", "Select which metrics to compute."),
|
|
47
|
+
("Normalize:", "Normalize values by image dimensions."),
|
|
48
|
+
("Image Dimensions:", "Reference size for normalization."),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BBoxMetricsNode(BaseNode):
|
|
53
|
+
"""Calculate metrics from bounding boxes"""
|
|
54
|
+
info = str(_info)
|
|
55
|
+
display_name = 'BBox Metrics'
|
|
56
|
+
icon = '📏'
|
|
57
|
+
category = 'analysis'
|
|
58
|
+
color = '#FFE5B4'
|
|
59
|
+
border_color = '#D4AF37'
|
|
60
|
+
text_color = '#000000'
|
|
61
|
+
|
|
62
|
+
DEFAULT_CONFIG = {
|
|
63
|
+
'metrics': ['width', 'height', 'area', 'aspect_ratio', 'center', 'diagonal'],
|
|
64
|
+
'normalize': False,
|
|
65
|
+
'use_image_dimensions': False,
|
|
66
|
+
'image_width': 1920,
|
|
67
|
+
'image_height': 1080
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
properties = [
|
|
71
|
+
{
|
|
72
|
+
'name': 'metrics',
|
|
73
|
+
'label': 'Metrics to Calculate',
|
|
74
|
+
'type': 'multiselect',
|
|
75
|
+
'options': ['width', 'height', 'area', 'aspect_ratio', 'center', 'diagonal'],
|
|
76
|
+
'default': DEFAULT_CONFIG['metrics']
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'name': 'normalize',
|
|
80
|
+
'label': 'Normalize by Image Size',
|
|
81
|
+
'type': 'checkbox',
|
|
82
|
+
'default': DEFAULT_CONFIG['normalize']
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
'name': 'use_image_dimensions',
|
|
86
|
+
'label': 'Use Image Dimensions from Message',
|
|
87
|
+
'type': 'checkbox',
|
|
88
|
+
'default': DEFAULT_CONFIG['use_image_dimensions']
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
'name': 'image_width',
|
|
92
|
+
'label': 'Reference Image Width',
|
|
93
|
+
'type': 'number',
|
|
94
|
+
'default': DEFAULT_CONFIG['image_width'],
|
|
95
|
+
'showIf': {'use_image_dimensions': False}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
'name': 'image_height',
|
|
99
|
+
'label': 'Reference Image Height',
|
|
100
|
+
'type': 'number',
|
|
101
|
+
'default': DEFAULT_CONFIG['image_height'],
|
|
102
|
+
'showIf': {'use_image_dimensions': False}
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
def __init__(self, node_id=None, name="BBox Metrics"):
|
|
107
|
+
super().__init__(node_id, name)
|
|
108
|
+
|
|
109
|
+
def _calculate_bbox_metrics(self, bbox, normalize=False, img_w=1920, img_h=1080, bbox_format='xyxy'):
|
|
110
|
+
"""
|
|
111
|
+
Calculate metrics for a single bounding box
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
bbox: Bounding box in specified format
|
|
115
|
+
normalize: Whether to normalize metrics by image dimensions
|
|
116
|
+
img_w: Image width for normalization
|
|
117
|
+
img_h: Image height for normalization
|
|
118
|
+
bbox_format: Format of bbox - 'xyxy' [x1,y1,x2,y2], 'xywh' [x,y,w,h], 'cxcywh' [cx,cy,w,h]
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dictionary of metrics
|
|
122
|
+
"""
|
|
123
|
+
# Convert bbox to xyxy format for consistent processing
|
|
124
|
+
if bbox_format == 'xyxy':
|
|
125
|
+
x1, y1, x2, y2 = bbox[:4]
|
|
126
|
+
elif bbox_format == 'xywh':
|
|
127
|
+
x, y, w, h = bbox[:4]
|
|
128
|
+
x1, y1, x2, y2 = x, y, x + w, y + h
|
|
129
|
+
elif bbox_format == 'cxcywh':
|
|
130
|
+
cx, cy, w, h = bbox[:4]
|
|
131
|
+
x1 = cx - w / 2
|
|
132
|
+
y1 = cy - h / 2
|
|
133
|
+
x2 = cx + w / 2
|
|
134
|
+
y2 = cy + h / 2
|
|
135
|
+
else:
|
|
136
|
+
# Default to xyxy if unknown format
|
|
137
|
+
x1, y1, x2, y2 = bbox[:4]
|
|
138
|
+
|
|
139
|
+
# Basic dimensions
|
|
140
|
+
width = abs(x2 - x1)
|
|
141
|
+
height = abs(y2 - y1)
|
|
142
|
+
area = width * height
|
|
143
|
+
|
|
144
|
+
# Center point
|
|
145
|
+
center_x = (x1 + x2) / 2
|
|
146
|
+
center_y = (y1 + y2) / 2
|
|
147
|
+
|
|
148
|
+
# Aspect ratio (width/height)
|
|
149
|
+
aspect_ratio = width / height if height > 0 else 0
|
|
150
|
+
|
|
151
|
+
# Diagonal length
|
|
152
|
+
diagonal = np.sqrt(width**2 + height**2)
|
|
153
|
+
|
|
154
|
+
metrics = {
|
|
155
|
+
'width': width,
|
|
156
|
+
'height': height,
|
|
157
|
+
'area': area,
|
|
158
|
+
'aspect_ratio': aspect_ratio,
|
|
159
|
+
'center': {'x': center_x, 'y': center_y},
|
|
160
|
+
'diagonal': diagonal,
|
|
161
|
+
'bbox': [x1, y1, x2, y2],
|
|
162
|
+
'bbox_format': 'xyxy' # Output is always normalized to xyxy
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Normalize if requested
|
|
166
|
+
if normalize:
|
|
167
|
+
metrics['width_normalized'] = width / img_w
|
|
168
|
+
metrics['height_normalized'] = height / img_h
|
|
169
|
+
metrics['area_normalized'] = area / (img_w * img_h)
|
|
170
|
+
metrics['center_normalized'] = [center_x / img_w, center_y / img_h]
|
|
171
|
+
metrics['diagonal_normalized'] = diagonal / np.sqrt(img_w**2 + img_h**2)
|
|
172
|
+
|
|
173
|
+
# Filter to only requested metrics
|
|
174
|
+
selected_metrics = self.config.get('metrics', [])
|
|
175
|
+
if selected_metrics:
|
|
176
|
+
filtered = {k: v for k, v in metrics.items() if k in selected_metrics or k == 'bbox'}
|
|
177
|
+
# Always include normalized versions if normalize is enabled
|
|
178
|
+
if normalize:
|
|
179
|
+
for key in list(filtered.keys()):
|
|
180
|
+
norm_key = f"{key}_normalized"
|
|
181
|
+
if norm_key in metrics:
|
|
182
|
+
filtered[norm_key] = metrics[norm_key]
|
|
183
|
+
return filtered
|
|
184
|
+
|
|
185
|
+
return metrics
|
|
186
|
+
|
|
187
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
188
|
+
"""Calculate bounding box metrics"""
|
|
189
|
+
try:
|
|
190
|
+
normalize = self.config.get('normalize', False)
|
|
191
|
+
use_image_dimensions = self.config.get('use_image_dimensions', True)
|
|
192
|
+
img_w = self.get_config_int('image_width', 1920)
|
|
193
|
+
img_h = self.get_config_int('image_height', 1080)
|
|
194
|
+
|
|
195
|
+
# Check both top level and payload for data
|
|
196
|
+
data_source = msg.get(MessageKeys.PAYLOAD, msg)
|
|
197
|
+
|
|
198
|
+
# Get bbox format from message (default to xyxy)
|
|
199
|
+
bbox_format = data_source.get('bbox_format', 'xyxy')
|
|
200
|
+
|
|
201
|
+
# Try to get image dimensions from message if enabled
|
|
202
|
+
if use_image_dimensions:
|
|
203
|
+
if MessageKeys.IMAGE.PATH in data_source and hasattr(data_source[MessageKeys.IMAGE.PATH], 'shape'):
|
|
204
|
+
img_h, img_w = data_source[MessageKeys.IMAGE.PATH].shape[:2]
|
|
205
|
+
elif MessageKeys.IMAGE.PATH in data_source and isinstance(data_source[MessageKeys.IMAGE.PATH], dict):
|
|
206
|
+
# Handle nested image structure
|
|
207
|
+
img_w = data_source[MessageKeys.IMAGE.PATH].get(MessageKeys.IMAGE.WIDTH, img_w)
|
|
208
|
+
img_h = data_source[MessageKeys.IMAGE.PATH].get(MessageKeys.IMAGE.HEIGHT, img_h)
|
|
209
|
+
elif 'image_width' in data_source:
|
|
210
|
+
img_w = data_source['image_width']
|
|
211
|
+
img_h = data_source.get('image_height', img_h)
|
|
212
|
+
|
|
213
|
+
# Process detections with bounding boxes
|
|
214
|
+
if 'detections' in data_source:
|
|
215
|
+
detections = data_source['detections']
|
|
216
|
+
if isinstance(detections, list):
|
|
217
|
+
for det in detections:
|
|
218
|
+
if 'bbox' in det or 'box' in det:
|
|
219
|
+
bbox = det.get('bbox', det.get('box'))
|
|
220
|
+
# Check for detection-specific bbox_format, fallback to message-level
|
|
221
|
+
det_bbox_format = det.get('bbox_format', bbox_format)
|
|
222
|
+
det['metrics'] = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, det_bbox_format)
|
|
223
|
+
data_source['detections'] = detections
|
|
224
|
+
self.send(msg)
|
|
225
|
+
|
|
226
|
+
# Process array of boxes
|
|
227
|
+
elif 'boxes' in data_source:
|
|
228
|
+
boxes = data_source['boxes']
|
|
229
|
+
if isinstance(boxes, (list, np.ndarray)):
|
|
230
|
+
metrics_list = []
|
|
231
|
+
for bbox in boxes:
|
|
232
|
+
metrics = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, bbox_format)
|
|
233
|
+
metrics_list.append(metrics)
|
|
234
|
+
data_source['metrics'] = metrics_list
|
|
235
|
+
self.send(msg)
|
|
236
|
+
|
|
237
|
+
# Process single bbox
|
|
238
|
+
elif 'bbox' in data_source:
|
|
239
|
+
bbox = data_source['bbox']
|
|
240
|
+
metrics = self._calculate_bbox_metrics(bbox, normalize, img_w, img_h, bbox_format)
|
|
241
|
+
data_source['metrics'] = metrics
|
|
242
|
+
self.send(msg)
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
self.report_error(f"No bounding boxes found in message. Expected 'detections', 'boxes', or 'bbox' field.")
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
self.report_error(f"Error calculating bbox metrics: {str(e)}")
|
|
249
|
+
import traceback
|
|
250
|
+
self.report_error(traceback.format_exc())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Node - groups messages into batches.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
7
|
+
|
|
8
|
+
_info = Info()
|
|
9
|
+
_info.add_text("Collects incoming messages into batches and sends them together as an array.")
|
|
10
|
+
_info.add_header("Input")
|
|
11
|
+
_info.add_bullets(
|
|
12
|
+
("payload:", "Any message payload to be collected into a batch."),
|
|
13
|
+
)
|
|
14
|
+
_info.add_header("Output")
|
|
15
|
+
_info.add_bullets(
|
|
16
|
+
("payload:", "Array of payloads from the batched messages."),
|
|
17
|
+
("batch_size:", "Number of messages in the batch."),
|
|
18
|
+
)
|
|
19
|
+
_info.add_header("Properties")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("Batch Size:", "Number of messages to collect before sending."),
|
|
22
|
+
("Overlap:", "Number of messages to retain for the next batch."),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BatchNode(BaseNode):
|
|
27
|
+
"""
|
|
28
|
+
Batch Node - collects messages into batches and sends them together.
|
|
29
|
+
"""
|
|
30
|
+
info = str(_info)
|
|
31
|
+
display_name = 'Batch'
|
|
32
|
+
icon = '📦'
|
|
33
|
+
category = 'function'
|
|
34
|
+
color = '#DDA0DD'
|
|
35
|
+
border_color = '#BA55D3'
|
|
36
|
+
text_color = '#000000'
|
|
37
|
+
input_count = 1
|
|
38
|
+
output_count = 1
|
|
39
|
+
|
|
40
|
+
DEFAULT_CONFIG = {
|
|
41
|
+
'batch_size': 10,
|
|
42
|
+
'overlap': 0,
|
|
43
|
+
MessageKeys.DROP_MESSAGES: False
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
properties = [
|
|
47
|
+
{
|
|
48
|
+
'name': 'batch_size',
|
|
49
|
+
'label': 'Batch Size',
|
|
50
|
+
'type': 'number',
|
|
51
|
+
'default': DEFAULT_CONFIG['batch_size']
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
'name': 'overlap',
|
|
55
|
+
'label': 'Overlap (messages)',
|
|
56
|
+
'type': 'number',
|
|
57
|
+
'default': DEFAULT_CONFIG['overlap'],
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
def __init__(self, node_id=None, name="batch"):
|
|
62
|
+
super().__init__(node_id, name)
|
|
63
|
+
self.buffer = []
|
|
64
|
+
|
|
65
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
66
|
+
"""Collect messages into batches."""
|
|
67
|
+
self.buffer.append(msg.copy())
|
|
68
|
+
|
|
69
|
+
batch_size = self.get_config_int('batch_size', 10)
|
|
70
|
+
|
|
71
|
+
if len(self.buffer) >= batch_size:
|
|
72
|
+
# Send batch
|
|
73
|
+
batch = self.buffer[:batch_size]
|
|
74
|
+
out_msg = self.create_message(
|
|
75
|
+
payload=[m.get(MessageKeys.PAYLOAD) for m in batch],
|
|
76
|
+
topic=batch[0].get(MessageKeys.TOPIC, '') if batch else '',
|
|
77
|
+
batch_size=len(batch)
|
|
78
|
+
)
|
|
79
|
+
self.send(out_msg)
|
|
80
|
+
|
|
81
|
+
# Handle overlap
|
|
82
|
+
overlap = self.get_config_int('overlap', 0)
|
|
83
|
+
if overlap > 0 and overlap < batch_size:
|
|
84
|
+
self.buffer = self.buffer[batch_size - overlap:]
|
|
85
|
+
else:
|
|
86
|
+
self.buffer = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# No external dependencies - uses only Python standard library
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Camera node - captures frames from a webcam using OpenCV.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import base64
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict
|
|
10
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
11
|
+
|
|
12
|
+
_info = Info()
|
|
13
|
+
_info.add_text("Captures video frames from a webcam or video device using OpenCV and outputs them as messages.")
|
|
14
|
+
_info.add_header("Inputs")
|
|
15
|
+
_info.add_text("This node has no inputs. It generates frames automatically when the workflow starts.")
|
|
16
|
+
_info.add_header("Output")
|
|
17
|
+
_info.add_bullets(
|
|
18
|
+
("payload.image:", "Image data (JPEG base64 or numpy array depending on config)."),
|
|
19
|
+
("payload.image.width:", "Frame width in pixels."),
|
|
20
|
+
("payload.image.height:", "Frame height in pixels."),
|
|
21
|
+
)
|
|
22
|
+
_info.add_header("Properties")
|
|
23
|
+
_info.add_bullets(
|
|
24
|
+
("Camera Index:", "Device index (0 for default camera)."),
|
|
25
|
+
("Frame Rate:", "Target FPS for capture."),
|
|
26
|
+
("Width/Height:", "Resolution settings."),
|
|
27
|
+
("Encode as JPEG:", "Output as base64 JPEG or raw numpy array."),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CameraNode(BaseNode):
|
|
32
|
+
"""
|
|
33
|
+
Camera node - captures frames from a webcam and outputs them as messages.
|
|
34
|
+
"""
|
|
35
|
+
info = str(_info)
|
|
36
|
+
display_name = 'Camera'
|
|
37
|
+
icon = '📷'
|
|
38
|
+
category = 'input'
|
|
39
|
+
color = '#C0DEED'
|
|
40
|
+
border_color = '#7FA7C9'
|
|
41
|
+
text_color = '#000000'
|
|
42
|
+
input_count = 0 # No input
|
|
43
|
+
output_count = 1
|
|
44
|
+
|
|
45
|
+
DEFAULT_CONFIG = {
|
|
46
|
+
MessageKeys.CAMERA.DEVICE_INDEX: 0,
|
|
47
|
+
MessageKeys.CAMERA.FPS: 30,
|
|
48
|
+
MessageKeys.CAMERA.WIDTH: 640,
|
|
49
|
+
MessageKeys.CAMERA.HEIGHT: 480,
|
|
50
|
+
MessageKeys.CAMERA.ENCODE_JPEG: False,
|
|
51
|
+
MessageKeys.CAMERA.JPEG_QUALITY: 75
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
properties = [
|
|
55
|
+
{
|
|
56
|
+
'name': MessageKeys.CAMERA.DEVICE_INDEX,
|
|
57
|
+
'label': 'Camera Index',
|
|
58
|
+
'type': 'number',
|
|
59
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.DEVICE_INDEX]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
'name': MessageKeys.CAMERA.FPS,
|
|
63
|
+
'label': 'Frame Rate (FPS)',
|
|
64
|
+
'type': 'number',
|
|
65
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.FPS]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
'name': MessageKeys.CAMERA.WIDTH,
|
|
69
|
+
'label': 'Width',
|
|
70
|
+
'type': 'number',
|
|
71
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.WIDTH]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
'name': MessageKeys.CAMERA.HEIGHT,
|
|
75
|
+
'label': 'Height',
|
|
76
|
+
'type': 'number',
|
|
77
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.HEIGHT]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
'name': MessageKeys.CAMERA.ENCODE_JPEG,
|
|
81
|
+
'label': 'Encode as JPEG',
|
|
82
|
+
'type': 'checkbox',
|
|
83
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.ENCODE_JPEG]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
'name': MessageKeys.CAMERA.JPEG_QUALITY,
|
|
87
|
+
'label': 'JPEG Quality (1-100)',
|
|
88
|
+
'type': 'number',
|
|
89
|
+
'default': DEFAULT_CONFIG[MessageKeys.CAMERA.JPEG_QUALITY]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def __init__(self, node_id=None, name="camera"):
|
|
94
|
+
super().__init__(node_id, name)
|
|
95
|
+
self.camera = None
|
|
96
|
+
self.capture_thread = None
|
|
97
|
+
self.running = False
|
|
98
|
+
|
|
99
|
+
def on_start(self):
|
|
100
|
+
"""Start the camera capture when workflow starts."""
|
|
101
|
+
super().on_start() # Start base node worker thread
|
|
102
|
+
|
|
103
|
+
camera_index = self.get_config_int(MessageKeys.CAMERA.DEVICE_INDEX, 0)
|
|
104
|
+
fps = self.get_config_int(MessageKeys.CAMERA.FPS, 10)
|
|
105
|
+
width = self.get_config_int(MessageKeys.CAMERA.WIDTH, 640)
|
|
106
|
+
height = self.get_config_int(MessageKeys.CAMERA.HEIGHT, 480)
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Open the camera
|
|
110
|
+
self.camera = cv2.VideoCapture(camera_index)
|
|
111
|
+
|
|
112
|
+
if not self.camera.isOpened():
|
|
113
|
+
self.report_error(f"Failed to open camera {camera_index}")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Set resolution
|
|
117
|
+
self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
|
118
|
+
self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
|
119
|
+
|
|
120
|
+
# Start capture thread
|
|
121
|
+
self.running = True
|
|
122
|
+
self.capture_thread = threading.Thread(target=self._capture_loop, args=(fps,), daemon=True)
|
|
123
|
+
self.capture_thread.start()
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
self.report_error(f"Error starting camera: {e}")
|
|
127
|
+
|
|
128
|
+
def on_stop(self):
|
|
129
|
+
"""Stop the camera capture when workflow stops."""
|
|
130
|
+
super().on_stop() # Stop base node worker thread
|
|
131
|
+
|
|
132
|
+
self.running = False
|
|
133
|
+
|
|
134
|
+
if self.capture_thread:
|
|
135
|
+
self.capture_thread.join(timeout=2.0)
|
|
136
|
+
self.capture_thread = None
|
|
137
|
+
|
|
138
|
+
if self.camera:
|
|
139
|
+
self.camera.release()
|
|
140
|
+
self.camera = None
|
|
141
|
+
|
|
142
|
+
def on_close(self):
|
|
143
|
+
"""Cleanup when node is deleted."""
|
|
144
|
+
self.on_stop()
|
|
145
|
+
|
|
146
|
+
def _capture_loop(self, fps):
|
|
147
|
+
"""Capture frames in a loop and send them as messages."""
|
|
148
|
+
frame_interval = 1.0 / fps
|
|
149
|
+
encode_jpeg = self.config.get(MessageKeys.CAMERA.ENCODE_JPEG, True)
|
|
150
|
+
|
|
151
|
+
while self.running and self.camera and self.camera.isOpened():
|
|
152
|
+
start_time = time.time()
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
ret, frame = self.camera.read()
|
|
156
|
+
|
|
157
|
+
if not ret or frame is None:
|
|
158
|
+
self.report_error("Failed to capture frame")
|
|
159
|
+
time.sleep(frame_interval)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Prepare the payload
|
|
163
|
+
if encode_jpeg:
|
|
164
|
+
# Encode frame as JPEG with quality setting
|
|
165
|
+
jpeg_quality = self.get_config_int(MessageKeys.CAMERA.JPEG_QUALITY, 75)
|
|
166
|
+
encode_params = (cv2.IMWRITE_JPEG_QUALITY, jpeg_quality)
|
|
167
|
+
ret, buffer = cv2.imencode('.jpg', frame, encode_params)
|
|
168
|
+
if ret:
|
|
169
|
+
# Convert to bytes (more efficient than base64)
|
|
170
|
+
jpeg_bytes = buffer.tobytes()
|
|
171
|
+
# Convert to base64 for JSON transmission
|
|
172
|
+
jpeg_base64 = base64.b64encode(jpeg_bytes).decode('utf-8')
|
|
173
|
+
payload = {
|
|
174
|
+
MessageKeys.IMAGE.FORMAT: 'jpeg',
|
|
175
|
+
MessageKeys.IMAGE.ENCODING: 'base64',
|
|
176
|
+
MessageKeys.IMAGE.DATA: jpeg_base64,
|
|
177
|
+
MessageKeys.IMAGE.WIDTH: frame.shape[1],
|
|
178
|
+
MessageKeys.IMAGE.HEIGHT: frame.shape[0]
|
|
179
|
+
}
|
|
180
|
+
else:
|
|
181
|
+
self.report_error("Failed to encode JPEG")
|
|
182
|
+
continue
|
|
183
|
+
else:
|
|
184
|
+
# Send raw frame as numpy array
|
|
185
|
+
payload = {
|
|
186
|
+
MessageKeys.IMAGE.FORMAT: 'bgr',
|
|
187
|
+
MessageKeys.IMAGE.ENCODING: 'numpy',
|
|
188
|
+
MessageKeys.IMAGE.DATA: frame,
|
|
189
|
+
MessageKeys.IMAGE.WIDTH: frame.shape[1],
|
|
190
|
+
MessageKeys.IMAGE.HEIGHT: frame.shape[0]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Create and send message with image wrapped in payload.image
|
|
194
|
+
msg = self.create_message(payload={MessageKeys.IMAGE.PATH: payload}, topic='camera/frame')
|
|
195
|
+
self.send(msg)
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.report_error(f"Error capturing frame: {e}")
|
|
199
|
+
|
|
200
|
+
# Maintain frame rate
|
|
201
|
+
elapsed = time.time() - start_time
|
|
202
|
+
sleep_time = max(0, frame_interval - elapsed)
|
|
203
|
+
if sleep_time > 0:
|
|
204
|
+
time.sleep(sleep_time)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .change_node import ChangeNode
|