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,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PolygonMetricsNode - Calculate metrics from polygons
|
|
3
|
+
|
|
4
|
+
This node calculates various metrics from polygon predictions including:
|
|
5
|
+
- Polygon area
|
|
6
|
+
- Bounding box (width, height, area)
|
|
7
|
+
- Maximum internal rectangle
|
|
8
|
+
- Minimum external rectangle
|
|
9
|
+
- Perimeter
|
|
10
|
+
- Centroid
|
|
11
|
+
|
|
12
|
+
Input format:
|
|
13
|
+
msg['detections'] - List of detections with polygons
|
|
14
|
+
OR
|
|
15
|
+
msg['polygons'] - List of polygons (array of points)
|
|
16
|
+
OR
|
|
17
|
+
msg['polygon'] - Single polygon (array of points [[x1,y1], [x2,y2], ...])
|
|
18
|
+
|
|
19
|
+
Output format:
|
|
20
|
+
msg['metrics'] - Dictionary containing calculated metrics
|
|
21
|
+
msg['detections'] - Original detections with added 'metrics' field
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
import os
|
|
26
|
+
from typing import Any, Dict
|
|
27
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
|
|
28
|
+
|
|
29
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
30
|
+
import numpy as np
|
|
31
|
+
import cv2
|
|
32
|
+
|
|
33
|
+
_info = Info()
|
|
34
|
+
_info.add_text("Calculate metrics from polygons including area, perimeter, bounding box, centroid, and rectangles.")
|
|
35
|
+
_info.add_header("Inputs")
|
|
36
|
+
_info.add_bullets(
|
|
37
|
+
("detections:", "List of detections with polygons"),
|
|
38
|
+
("polygons:", "List of polygons"),
|
|
39
|
+
("polygon:", "Single polygon [[x1,y1], [x2,y2], ...]")
|
|
40
|
+
)
|
|
41
|
+
_info.add_header("Outputs")
|
|
42
|
+
_info.add_bullets(
|
|
43
|
+
("metrics:", "Dictionary with calculated metrics"),
|
|
44
|
+
("detections:", "Original detections with added 'metrics' field")
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PolygonMetricsNode(BaseNode):
|
|
49
|
+
"""Calculate metrics from polygons"""
|
|
50
|
+
|
|
51
|
+
info = str(_info)
|
|
52
|
+
display_name = 'Polygon Metrics'
|
|
53
|
+
icon = '⬡'
|
|
54
|
+
category = 'analysis'
|
|
55
|
+
color = '#E6E6FA'
|
|
56
|
+
border_color = '#9370DB'
|
|
57
|
+
text_color = '#000000'
|
|
58
|
+
|
|
59
|
+
DEFAULT_CONFIG = {
|
|
60
|
+
'metrics': ['area', 'perimeter', 'bbox', 'centroid', 'max_internal_rect', 'min_external_rect'],
|
|
61
|
+
'normalize': False,
|
|
62
|
+
'image_width': 1920,
|
|
63
|
+
'image_height': 1080
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
properties = [
|
|
67
|
+
{
|
|
68
|
+
'name': 'metrics',
|
|
69
|
+
'label': 'Metrics to Calculate',
|
|
70
|
+
'type': 'multiselect',
|
|
71
|
+
'options': ['area', 'perimeter', 'bbox', 'centroid', 'max_internal_rect', 'min_external_rect'],
|
|
72
|
+
'default': DEFAULT_CONFIG['metrics']
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
'name': 'normalize',
|
|
76
|
+
'label': 'Normalize by Image Size',
|
|
77
|
+
'type': 'checkbox',
|
|
78
|
+
'default': DEFAULT_CONFIG['normalize']
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
'name': 'image_width',
|
|
82
|
+
'label': 'Image Width (for normalization)',
|
|
83
|
+
'type': 'number',
|
|
84
|
+
'default': DEFAULT_CONFIG['image_width']
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
'name': 'image_height',
|
|
88
|
+
'label': 'Image Height (for normalization)',
|
|
89
|
+
'type': 'number',
|
|
90
|
+
'default': DEFAULT_CONFIG['image_height']
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def __init__(self, node_id=None, name="Polygon Metrics"):
|
|
95
|
+
super().__init__(node_id, name)
|
|
96
|
+
|
|
97
|
+
def _polygon_area(self, points):
|
|
98
|
+
"""Calculate polygon area using shoelace formula"""
|
|
99
|
+
if len(points) < 3:
|
|
100
|
+
return 0.0
|
|
101
|
+
|
|
102
|
+
points = np.array(points)
|
|
103
|
+
x = points[:, 0]
|
|
104
|
+
y = points[:, 1]
|
|
105
|
+
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
|
106
|
+
|
|
107
|
+
def _polygon_perimeter(self, points):
|
|
108
|
+
"""Calculate polygon perimeter"""
|
|
109
|
+
if len(points) < 2:
|
|
110
|
+
return 0.0
|
|
111
|
+
|
|
112
|
+
points = np.array(points)
|
|
113
|
+
# Calculate distance between consecutive points
|
|
114
|
+
diffs = np.diff(points, axis=0, append=[points[0]])
|
|
115
|
+
distances = np.sqrt(np.sum(diffs**2, axis=1))
|
|
116
|
+
return np.sum(distances)
|
|
117
|
+
|
|
118
|
+
def _polygon_centroid(self, points):
|
|
119
|
+
"""Calculate polygon centroid"""
|
|
120
|
+
if len(points) < 3:
|
|
121
|
+
return [0.0, 0.0]
|
|
122
|
+
|
|
123
|
+
points = np.array(points)
|
|
124
|
+
M = cv2.moments(points.astype(np.float32))
|
|
125
|
+
|
|
126
|
+
if M['m00'] != 0:
|
|
127
|
+
cx = M['m10'] / M['m00']
|
|
128
|
+
cy = M['m01'] / M['m00']
|
|
129
|
+
return [float(cx), float(cy)]
|
|
130
|
+
else:
|
|
131
|
+
# Fallback to simple average
|
|
132
|
+
return [float(np.mean(points[:, 0])), float(np.mean(points[:, 1]))]
|
|
133
|
+
|
|
134
|
+
def _polygon_bounding_box(self, points):
|
|
135
|
+
"""Get bounding box of polygon"""
|
|
136
|
+
if len(points) < 1:
|
|
137
|
+
return [0, 0, 0, 0]
|
|
138
|
+
|
|
139
|
+
points = np.array(points)
|
|
140
|
+
x_min = np.min(points[:, 0])
|
|
141
|
+
y_min = np.min(points[:, 1])
|
|
142
|
+
x_max = np.max(points[:, 0])
|
|
143
|
+
y_max = np.max(points[:, 1])
|
|
144
|
+
|
|
145
|
+
return [float(x_min), float(y_min), float(x_max), float(y_max)]
|
|
146
|
+
|
|
147
|
+
def _minimum_external_rect(self, points):
|
|
148
|
+
"""Calculate minimum area rectangle that contains the polygon"""
|
|
149
|
+
if len(points) < 3:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
points = np.array(points, dtype=np.float32)
|
|
153
|
+
rect = cv2.minAreaRect(points)
|
|
154
|
+
box = cv2.boxPoints(rect)
|
|
155
|
+
|
|
156
|
+
# rect is ((center_x, center_y), (width, height), angle)
|
|
157
|
+
center, size, angle = rect
|
|
158
|
+
width, height = size
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'center': [float(center[0]), float(center[1])],
|
|
162
|
+
'width': float(width),
|
|
163
|
+
'height': float(height),
|
|
164
|
+
'area': float(width * height),
|
|
165
|
+
'angle': float(angle),
|
|
166
|
+
'corners': box.tolist()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def _maximum_internal_rect(self, points, image_width=1920, image_height=1080):
|
|
170
|
+
"""
|
|
171
|
+
Estimate maximum inscribed rectangle.
|
|
172
|
+
This is a simplified approximation using the bounding box approach.
|
|
173
|
+
For more accurate results, more complex algorithms would be needed.
|
|
174
|
+
"""
|
|
175
|
+
if len(points) < 3:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Get bounding box as approximation
|
|
179
|
+
bbox = self._polygon_bounding_box(points)
|
|
180
|
+
x1, y1, x2, y2 = bbox
|
|
181
|
+
width = x2 - x1
|
|
182
|
+
height = y2 - y1
|
|
183
|
+
|
|
184
|
+
# For convex polygons, we could do better, but this is a reasonable approximation
|
|
185
|
+
# Scale down by a factor to ensure it fits inside
|
|
186
|
+
scale_factor = 0.9
|
|
187
|
+
scaled_width = width * scale_factor
|
|
188
|
+
scaled_height = height * scale_factor
|
|
189
|
+
|
|
190
|
+
center = [(x1 + x2) / 2, (y1 + y2) / 2]
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
'center': center,
|
|
194
|
+
'width': float(scaled_width),
|
|
195
|
+
'height': float(scaled_height),
|
|
196
|
+
'area': float(scaled_width * scaled_height),
|
|
197
|
+
'bbox': [
|
|
198
|
+
center[0] - scaled_width/2,
|
|
199
|
+
center[1] - scaled_height/2,
|
|
200
|
+
center[0] + scaled_width/2,
|
|
201
|
+
center[1] + scaled_height/2
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
def _calculate_polygon_metrics(self, polygon, normalize=False, img_w=1920, img_h=1080):
|
|
206
|
+
"""
|
|
207
|
+
Calculate metrics for a single polygon
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
polygon: List of points [[x1, y1], [x2, y2], ...]
|
|
211
|
+
normalize: Whether to normalize metrics by image dimensions
|
|
212
|
+
img_w: Image width for normalization
|
|
213
|
+
img_h: Image height for normalization
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dictionary of metrics
|
|
217
|
+
"""
|
|
218
|
+
if isinstance(polygon, np.ndarray):
|
|
219
|
+
if polygon.ndim == 1:
|
|
220
|
+
# Reshape flat array to points
|
|
221
|
+
polygon = polygon.reshape(-1, 2)
|
|
222
|
+
polygon = polygon.tolist()
|
|
223
|
+
|
|
224
|
+
# Calculate all metrics
|
|
225
|
+
area = self._polygon_area(polygon)
|
|
226
|
+
perimeter = self._polygon_perimeter(polygon)
|
|
227
|
+
centroid = self._polygon_centroid(polygon)
|
|
228
|
+
bbox = self._polygon_bounding_box(polygon)
|
|
229
|
+
min_rect = self._minimum_external_rect(polygon)
|
|
230
|
+
max_rect = self._maximum_internal_rect(polygon, img_w, img_h)
|
|
231
|
+
|
|
232
|
+
bbox_width = bbox[2] - bbox[0]
|
|
233
|
+
bbox_height = bbox[3] - bbox[1]
|
|
234
|
+
bbox_area = bbox_width * bbox_height
|
|
235
|
+
|
|
236
|
+
metrics = {
|
|
237
|
+
'area': float(area),
|
|
238
|
+
'perimeter': float(perimeter),
|
|
239
|
+
'centroid': centroid,
|
|
240
|
+
'bbox': bbox,
|
|
241
|
+
'bbox_width': float(bbox_width),
|
|
242
|
+
'bbox_height': float(bbox_height),
|
|
243
|
+
'bbox_area': float(bbox_area),
|
|
244
|
+
'min_external_rect': min_rect,
|
|
245
|
+
'max_internal_rect': max_rect,
|
|
246
|
+
'num_points': len(polygon)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# Normalize if requested
|
|
250
|
+
if normalize:
|
|
251
|
+
metrics['area_normalized'] = area / (img_w * img_h)
|
|
252
|
+
metrics['perimeter_normalized'] = perimeter / (img_w + img_h)
|
|
253
|
+
metrics['centroid_normalized'] = [centroid[0] / img_w, centroid[1] / img_h]
|
|
254
|
+
metrics['bbox_width_normalized'] = bbox_width / img_w
|
|
255
|
+
metrics['bbox_height_normalized'] = bbox_height / img_h
|
|
256
|
+
metrics['bbox_area_normalized'] = bbox_area / (img_w * img_h)
|
|
257
|
+
|
|
258
|
+
# Filter to only requested metrics
|
|
259
|
+
selected_metrics = self.config.get('metrics', [])
|
|
260
|
+
if selected_metrics and isinstance(selected_metrics, list):
|
|
261
|
+
filtered = {k: v for k, v in metrics.items()
|
|
262
|
+
if any(sel in k for sel in selected_metrics) or k == 'num_points'}
|
|
263
|
+
return filtered
|
|
264
|
+
|
|
265
|
+
return metrics
|
|
266
|
+
|
|
267
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
268
|
+
"""Calculate polygon metrics"""
|
|
269
|
+
try:
|
|
270
|
+
normalize = self.config.get('normalize', False)
|
|
271
|
+
img_w = self.get_config_int('image_width', 1920)
|
|
272
|
+
img_h = self.get_config_int('image_height', 1080)
|
|
273
|
+
|
|
274
|
+
# Check both top level and payload for data
|
|
275
|
+
data_source = msg.get(MessageKeys.PAYLOAD, msg)
|
|
276
|
+
|
|
277
|
+
# Try to get image dimensions from message
|
|
278
|
+
if MessageKeys.IMAGE.PATH in data_source and hasattr(data_source[MessageKeys.IMAGE.PATH], 'shape'):
|
|
279
|
+
img_h, img_w = data_source[MessageKeys.IMAGE.PATH].shape[:2]
|
|
280
|
+
elif MessageKeys.IMAGE.PATH in data_source and isinstance(data_source[MessageKeys.IMAGE.PATH], dict):
|
|
281
|
+
img_w = data_source[MessageKeys.IMAGE.PATH].get('width', img_w)
|
|
282
|
+
img_h = data_source[MessageKeys.IMAGE.PATH].get('height', img_h)
|
|
283
|
+
elif 'image_width' in data_source:
|
|
284
|
+
img_w = data_source['image_width']
|
|
285
|
+
img_h = data_source.get('image_height', img_h)
|
|
286
|
+
|
|
287
|
+
# Process detections with polygons
|
|
288
|
+
if 'detections' in data_source:
|
|
289
|
+
detections = data_source['detections']
|
|
290
|
+
if isinstance(detections, list):
|
|
291
|
+
for det in detections:
|
|
292
|
+
# Look for polygon data in various formats
|
|
293
|
+
polygon = None
|
|
294
|
+
if 'polygon' in det:
|
|
295
|
+
polygon = det['polygon']
|
|
296
|
+
elif 'segmentation' in det:
|
|
297
|
+
polygon = det['segmentation']
|
|
298
|
+
elif 'mask' in det and isinstance(det['mask'], list):
|
|
299
|
+
polygon = det['mask']
|
|
300
|
+
|
|
301
|
+
if polygon is not None:
|
|
302
|
+
det['metrics'] = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
|
|
303
|
+
|
|
304
|
+
data_source['detections'] = detections
|
|
305
|
+
self.send(msg)
|
|
306
|
+
|
|
307
|
+
# Process array of polygons
|
|
308
|
+
elif 'polygons' in data_source:
|
|
309
|
+
polygons = data_source['polygons']
|
|
310
|
+
if isinstance(polygons, list):
|
|
311
|
+
metrics_list = []
|
|
312
|
+
for polygon in polygons:
|
|
313
|
+
metrics = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
|
|
314
|
+
metrics_list.append(metrics)
|
|
315
|
+
data_source['metrics'] = metrics_list
|
|
316
|
+
self.send(msg)
|
|
317
|
+
|
|
318
|
+
# Process single polygon
|
|
319
|
+
elif 'polygon' in data_source:
|
|
320
|
+
polygon = data_source['polygon']
|
|
321
|
+
metrics = self._calculate_polygon_metrics(polygon, normalize, img_w, img_h)
|
|
322
|
+
data_source['metrics'] = metrics
|
|
323
|
+
self.send(msg)
|
|
324
|
+
|
|
325
|
+
else:
|
|
326
|
+
self.report_error(f"No polygons found in message. Expected 'detections', 'polygons', or 'polygon' field.")
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
self.report_error(f"Error calculating polygon metrics: {str(e)}")
|
|
330
|
+
import traceback
|
|
331
|
+
self.report_error(traceback.format_exc())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queue Length Probe Node - monitors message queue length.
|
|
3
|
+
Passes messages through while tracking and displaying the queue length.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
8
|
+
|
|
9
|
+
_info = Info()
|
|
10
|
+
_info.add_text("Monitors message queue length by displaying the _queue_length value from incoming messages. Displays the queue length on the node and outputs queue length information.")
|
|
11
|
+
_info.add_header("Inputs")
|
|
12
|
+
_info.add_bullets(
|
|
13
|
+
("Input 0:", "Any message to monitor (should contain _queue_length)")
|
|
14
|
+
)
|
|
15
|
+
_info.add_header("Outputs")
|
|
16
|
+
_info.add_bullets(
|
|
17
|
+
("Output 0:", f"Original message with queue length statistics in {MessageKeys.PAYLOAD}")
|
|
18
|
+
)
|
|
19
|
+
_info.add_header("Configuration")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("None:", "No configuration options")
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("Output Message")
|
|
24
|
+
_info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.queue_length').text(" - Current queue length as a number").end()
|
|
25
|
+
_info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.display').text(" - Human-readable queue length string (e.g., '5 queued', '0 queued')").end()
|
|
26
|
+
_info.add_code(f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}.source_node').text(" - ID of the node where the queue length was measured").end()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class QueueLengthProbeNode(BaseNode):
|
|
30
|
+
"""
|
|
31
|
+
Queue Length Probe node - monitors and displays message queue length.
|
|
32
|
+
Passes all messages through unchanged while displaying queue length.
|
|
33
|
+
"""
|
|
34
|
+
info = str(_info)
|
|
35
|
+
display_name = 'Queue Length Probe'
|
|
36
|
+
icon = '📊'
|
|
37
|
+
category = 'node probes'
|
|
38
|
+
color = '#E2D96E'
|
|
39
|
+
border_color = '#B8AF4A'
|
|
40
|
+
text_color = '#000000'
|
|
41
|
+
input_count = 1
|
|
42
|
+
output_count = 1
|
|
43
|
+
ui_component = 'queue-length-display'
|
|
44
|
+
ui_component_config = {
|
|
45
|
+
'format': '{value} queued',
|
|
46
|
+
'precision': 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sse_handlers = [
|
|
50
|
+
{'type': 'queue_length', 'handler': 'get_queue_length_sse', 'throttle': 0.5},
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
DEFAULT_CONFIG = {
|
|
54
|
+
MessageKeys.DROP_MESSAGES: 'false'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
properties = [
|
|
58
|
+
{
|
|
59
|
+
'name': MessageKeys.DROP_MESSAGES,
|
|
60
|
+
'label': 'Drop Messages When Busy',
|
|
61
|
+
'type': 'checkbox',
|
|
62
|
+
'default': False
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def __init__(self, node_id=None, name="queue length probe"):
|
|
67
|
+
super().__init__(node_id, name)
|
|
68
|
+
self._current_queue_length = 0
|
|
69
|
+
|
|
70
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
71
|
+
"""
|
|
72
|
+
Pass through the message and extract queue length for display.
|
|
73
|
+
Sends queue length information as the payload.
|
|
74
|
+
"""
|
|
75
|
+
# Extract queue length from the message
|
|
76
|
+
queue_length = msg.get(MessageKeys.QUEUE_LENGTH, 0)
|
|
77
|
+
self._current_queue_length = queue_length
|
|
78
|
+
|
|
79
|
+
# Replace payload with queue length statistics (like RateProbeNode)
|
|
80
|
+
msg[MessageKeys.PAYLOAD] = {
|
|
81
|
+
'queue_length': queue_length,
|
|
82
|
+
'display': self.get_queue_length_display(queue_length),
|
|
83
|
+
'source_node': msg.get('source_node_id', 'unknown')
|
|
84
|
+
}
|
|
85
|
+
msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'queue_length')
|
|
86
|
+
|
|
87
|
+
# Send the message through
|
|
88
|
+
self.send(msg)
|
|
89
|
+
|
|
90
|
+
def get_queue_length(self) -> int:
|
|
91
|
+
"""Get the current queue length."""
|
|
92
|
+
return self._current_queue_length
|
|
93
|
+
|
|
94
|
+
def get_queue_length_display(self, queue_length: Optional[int] = None) -> str:
|
|
95
|
+
"""Get formatted queue length string for display."""
|
|
96
|
+
if queue_length is None:
|
|
97
|
+
queue_length = self._current_queue_length
|
|
98
|
+
|
|
99
|
+
if queue_length == 0:
|
|
100
|
+
return "No items queued"
|
|
101
|
+
elif queue_length == 1:
|
|
102
|
+
return "1 item queued"
|
|
103
|
+
else:
|
|
104
|
+
return f"{queue_length} items queued"
|
|
105
|
+
|
|
106
|
+
def on_stop(self):
|
|
107
|
+
"""Reset queue length when stopped."""
|
|
108
|
+
self._current_queue_length = 0
|
|
109
|
+
super().on_stop()
|
|
110
|
+
|
|
111
|
+
def get_queue_length_sse(self):
|
|
112
|
+
"""SSE handler: return queue length data for broadcast."""
|
|
113
|
+
return {
|
|
114
|
+
'display': self.get_queue_length_display(),
|
|
115
|
+
'queue_length': self.get_queue_length(),
|
|
116
|
+
}
|