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,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ultralytics YOLO node - performs object detection on images.
|
|
3
|
+
Supports YOLO26, YOLO11, YOLOv8 and other YOLO models.
|
|
4
|
+
Receives image data from camera nodes and outputs inference results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Dict, List
|
|
10
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
_info = Info()
|
|
16
|
+
_info.add_text("Performs YOLO object detection on input images using Ultralytics (YOLO26, YOLO11, YOLOv8, etc.).")
|
|
17
|
+
_info.add_header("Inputs")
|
|
18
|
+
_info.add_bullets(("Input 0:", "Image (base64 encoded)"))
|
|
19
|
+
_info.add_header("Outputs")
|
|
20
|
+
_info.add_bullets(
|
|
21
|
+
("Output 0:", "Detection results with:"),
|
|
22
|
+
)
|
|
23
|
+
_info.add_bullets(
|
|
24
|
+
("payload.image:", "Annotated image (if draw_results enabled)"),
|
|
25
|
+
("payload.detections:", "List of detections with class_id, class_name, confidence, bbox"),
|
|
26
|
+
("payload.detection_count:", "Number of detected objects"),
|
|
27
|
+
("payload.bbox_format:", "Bounding box format (xyxy)"),
|
|
28
|
+
)
|
|
29
|
+
_info.add_header("Configuration")
|
|
30
|
+
_info.add_bullets(
|
|
31
|
+
("Model:", "YOLO variant (YOLO26, YOLO11, YOLOv8 - nano to extra large)"),
|
|
32
|
+
("Device:", "CPU, CUDA GPU, or Intel OpenVINO device for inference"),
|
|
33
|
+
("Confidence:", "Detection confidence threshold"),
|
|
34
|
+
("IoU:", "Intersection over Union threshold for NMS"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class UltralyticsNode(BaseNode):
|
|
39
|
+
"""
|
|
40
|
+
Ultralytics YOLO node - performs object detection inference.
|
|
41
|
+
Supports YOLO26, YOLO11, YOLOv8 and other YOLO models.
|
|
42
|
+
Receives images and outputs detection results with optional visualization.
|
|
43
|
+
"""
|
|
44
|
+
# Visual properties
|
|
45
|
+
display_name = 'YOLO'
|
|
46
|
+
info = str(_info)
|
|
47
|
+
icon = '🎯'
|
|
48
|
+
category = 'vision'
|
|
49
|
+
color = '#00D9FF'
|
|
50
|
+
border_color = '#00A8CC'
|
|
51
|
+
text_color = '#000000'
|
|
52
|
+
input_count = 1
|
|
53
|
+
output_count = 1
|
|
54
|
+
|
|
55
|
+
DEFAULT_CONFIG = {
|
|
56
|
+
'model': 'yolo26n.pt',
|
|
57
|
+
'confidence': '0.25',
|
|
58
|
+
'iou': '0.45',
|
|
59
|
+
'draw_results': 'true',
|
|
60
|
+
'max_det': '300',
|
|
61
|
+
'include_image': True,
|
|
62
|
+
'include_predictions': True,
|
|
63
|
+
'drop_messages': 'true' # Enable by default for YOLO to prevent queue buildup
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def _get_device_options() -> List[Dict[str, str]]:
|
|
68
|
+
"""Get available hardware devices (CPU, CUDA, Intel OpenVINO) for the dropdown."""
|
|
69
|
+
devices = [{'value': 'cpu', 'label': 'CPU'}]
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if torch.cuda.is_available():
|
|
73
|
+
for i in range(torch.cuda.device_count()):
|
|
74
|
+
props = torch.cuda.get_device_properties(i)
|
|
75
|
+
memory_gb = props.total_memory / (1024**3)
|
|
76
|
+
label = f"CUDA:{i} - {props.name} ({memory_gb:.1f}GB)"
|
|
77
|
+
devices.append({'value': f'cuda:{i}', 'label': label})
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.warning(f"Error detecting CUDA devices: {e}")
|
|
80
|
+
|
|
81
|
+
# Add Intel OpenVINO devices (enumerated via openvino when available;
|
|
82
|
+
# falls back to the static CPU/GPU/NPU list otherwise)
|
|
83
|
+
try:
|
|
84
|
+
from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
|
|
85
|
+
get_intel_device_options,
|
|
86
|
+
)
|
|
87
|
+
devices.extend(get_intel_device_options())
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Error detecting Intel OpenVINO devices: {e}")
|
|
90
|
+
devices.extend([
|
|
91
|
+
{'value': 'intel:cpu', 'label': 'Intel CPU (OpenVINO)'},
|
|
92
|
+
{'value': 'intel:gpu', 'label': 'Intel GPU (OpenVINO)'},
|
|
93
|
+
{'value': 'intel:npu', 'label': 'Intel NPU (OpenVINO)'},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
return devices
|
|
97
|
+
|
|
98
|
+
def _resolve_configured_device(self) -> str:
|
|
99
|
+
"""Return the configured device with plain 'intel:gpu' resolved to the
|
|
100
|
+
first detected GPU (e.g. 'intel:gpu.0') so OpenVINO targets a real
|
|
101
|
+
device instead of falling back to AUTO on multi-GPU systems."""
|
|
102
|
+
device = self.config.get('device', 'cpu')
|
|
103
|
+
try:
|
|
104
|
+
from pynode.nodes.InferenceNode.InferenceEngine.device_detection import (
|
|
105
|
+
resolve_intel_device,
|
|
106
|
+
)
|
|
107
|
+
return resolve_intel_device(device)
|
|
108
|
+
except Exception:
|
|
109
|
+
return device
|
|
110
|
+
|
|
111
|
+
# Property schema for the properties panel
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_properties(cls):
|
|
114
|
+
"""Dynamic properties with device detection."""
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
'name': 'model',
|
|
118
|
+
'label': 'Model',
|
|
119
|
+
'type': 'select',
|
|
120
|
+
'options': [
|
|
121
|
+
{'value': 'yolo26n.pt', 'label': 'YOLO26 Nano (fastest, recommended)'},
|
|
122
|
+
{'value': 'yolo26s.pt', 'label': 'YOLO26 Small'},
|
|
123
|
+
{'value': 'yolo26m.pt', 'label': 'YOLO26 Medium'},
|
|
124
|
+
{'value': 'yolo26l.pt', 'label': 'YOLO26 Large'},
|
|
125
|
+
{'value': 'yolo26x.pt', 'label': 'YOLO26 Extra Large (most accurate)'},
|
|
126
|
+
{'value': 'yolo11n.pt', 'label': 'YOLO11 Nano'},
|
|
127
|
+
{'value': 'yolo11s.pt', 'label': 'YOLO11 Small'},
|
|
128
|
+
{'value': 'yolo11m.pt', 'label': 'YOLO11 Medium'},
|
|
129
|
+
{'value': 'yolo11l.pt', 'label': 'YOLO11 Large'},
|
|
130
|
+
{'value': 'yolo11x.pt', 'label': 'YOLO11 Extra Large'},
|
|
131
|
+
{'value': 'yolov8n.pt', 'label': 'YOLOv8 Nano (legacy)'},
|
|
132
|
+
{'value': 'yolov8s.pt', 'label': 'YOLOv8 Small (legacy)'},
|
|
133
|
+
{'value': 'yolov8m.pt', 'label': 'YOLOv8 Medium (legacy)'},
|
|
134
|
+
{'value': 'yolov8l.pt', 'label': 'YOLOv8 Large (legacy)'},
|
|
135
|
+
{'value': 'yolov8x.pt', 'label': 'YOLOv8 Extra Large (legacy)'}
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
'name': 'device',
|
|
140
|
+
'label': 'Device',
|
|
141
|
+
'type': 'select',
|
|
142
|
+
'options': cls._get_device_options()
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
'name': 'confidence',
|
|
146
|
+
'label': 'Confidence Threshold',
|
|
147
|
+
'type': 'text',
|
|
148
|
+
'placeholder': '0.25'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
'name': 'iou',
|
|
152
|
+
'label': 'IoU Threshold',
|
|
153
|
+
'type': 'text',
|
|
154
|
+
'placeholder': '0.45'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
'name': 'draw_results',
|
|
158
|
+
'label': 'Draw Results on Image',
|
|
159
|
+
'type': 'select',
|
|
160
|
+
'options': [
|
|
161
|
+
{'value': 'true', 'label': 'Yes'},
|
|
162
|
+
{'value': 'false', 'label': 'No'}
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
'name': 'max_det',
|
|
167
|
+
'label': 'Max Detections',
|
|
168
|
+
'type': 'text',
|
|
169
|
+
'placeholder': '300'
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
'name': 'include_image',
|
|
173
|
+
'label': 'Include Image in Output',
|
|
174
|
+
'type': 'checkbox',
|
|
175
|
+
'default': True
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
'name': 'include_predictions',
|
|
179
|
+
'label': 'Include Predictions in Output',
|
|
180
|
+
'type': 'checkbox',
|
|
181
|
+
'default': True
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
properties = property(lambda self: self.get_properties())
|
|
186
|
+
|
|
187
|
+
@staticmethod
|
|
188
|
+
def _get_default_device():
|
|
189
|
+
"""Get default device based on availability."""
|
|
190
|
+
return 'cuda:0' if torch.cuda.is_available() else 'cpu'
|
|
191
|
+
|
|
192
|
+
def __init__(self, node_id=None, name="yolo"):
|
|
193
|
+
super().__init__(node_id, name)
|
|
194
|
+
# Configure with defaults, then set device dynamically
|
|
195
|
+
config = self.DEFAULT_CONFIG.copy()
|
|
196
|
+
config['device'] = self._get_default_device()
|
|
197
|
+
self.configure(config)
|
|
198
|
+
self.model = None
|
|
199
|
+
self._model_loaded = False
|
|
200
|
+
|
|
201
|
+
def _load_model(self):
|
|
202
|
+
"""Load the YOLO model (lazy loading on first use)."""
|
|
203
|
+
if self._model_loaded:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
from ultralytics import YOLO # type: ignore
|
|
208
|
+
model_name = self.config.get('model', 'yolov8n.pt')
|
|
209
|
+
device = self._resolve_configured_device()
|
|
210
|
+
|
|
211
|
+
if isinstance(device, str) and device.lower().startswith('intel:'):
|
|
212
|
+
# Intel OpenVINO path: torch's model.to() does not understand
|
|
213
|
+
# 'intel:*' device strings, so export the model to OpenVINO
|
|
214
|
+
# format once and load the exported model instead. The
|
|
215
|
+
# resolved 'intel:gpu.N' string is passed to predict() at
|
|
216
|
+
# inference time; ultralytics maps it to the OpenVINO device
|
|
217
|
+
# name (e.g. intel:gpu.0 -> GPU.0).
|
|
218
|
+
base_model = YOLO(model_name)
|
|
219
|
+
source_path = str(getattr(base_model, 'ckpt_path', None) or model_name)
|
|
220
|
+
openvino_model_path = os.path.splitext(source_path)[0] + '_openvino_model'
|
|
221
|
+
if not os.path.isdir(openvino_model_path):
|
|
222
|
+
logger.info(f"Exporting {model_name} to OpenVINO format: {openvino_model_path}")
|
|
223
|
+
openvino_model_path = base_model.export(format='openvino')
|
|
224
|
+
self.model = YOLO(openvino_model_path, task=base_model.task)
|
|
225
|
+
logger.info(
|
|
226
|
+
f"Loaded OpenVINO model from {openvino_model_path} "
|
|
227
|
+
f"targeting device: {device}"
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
self.model = YOLO(model_name)
|
|
231
|
+
# Move model to specified device
|
|
232
|
+
self.model.to(device)
|
|
233
|
+
self._model_loaded = True
|
|
234
|
+
except ImportError:
|
|
235
|
+
error_msg = "ultralytics package not installed. Run: pip install ultralytics"
|
|
236
|
+
self.report_error(error_msg)
|
|
237
|
+
self.model = None
|
|
238
|
+
except Exception as e:
|
|
239
|
+
error_msg = f"Error loading model: {e}"
|
|
240
|
+
self.report_error(error_msg)
|
|
241
|
+
self.model = None
|
|
242
|
+
|
|
243
|
+
def configure(self, config: Dict[str, Any]):
|
|
244
|
+
"""Override configure to reload the model when model or device changes."""
|
|
245
|
+
old_model = self.config.get('model') if hasattr(self, 'config') else None
|
|
246
|
+
old_device = self.config.get('device') if hasattr(self, 'config') else None
|
|
247
|
+
super().configure(config)
|
|
248
|
+
|
|
249
|
+
# Mark for lazy reload on next inference if model or device changed.
|
|
250
|
+
# (The previous code called _load_model() directly, which no-ops while
|
|
251
|
+
# _model_loaded is True - so model changes never actually reloaded.)
|
|
252
|
+
if old_model is not None and (
|
|
253
|
+
old_model != self.config.get('model') or
|
|
254
|
+
old_device != self.config.get('device')
|
|
255
|
+
):
|
|
256
|
+
self._model_loaded = False
|
|
257
|
+
|
|
258
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
259
|
+
"""
|
|
260
|
+
Process incoming image messages and perform YOLO inference.
|
|
261
|
+
"""
|
|
262
|
+
# Lazy load model on first use
|
|
263
|
+
if not self._model_loaded:
|
|
264
|
+
self._load_model()
|
|
265
|
+
|
|
266
|
+
if self.model is None:
|
|
267
|
+
error_msg = "Model not loaded, skipping inference"
|
|
268
|
+
self.report_error(error_msg)
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
# Get image from message
|
|
272
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
273
|
+
if payload is None:
|
|
274
|
+
error_msg = "No image in payload"
|
|
275
|
+
self.report_error(error_msg)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
# Decode image using base node helper
|
|
279
|
+
image, input_format = self.decode_image(payload)
|
|
280
|
+
|
|
281
|
+
if image is None or input_format is None:
|
|
282
|
+
error_msg = "Failed to decode image from payload"
|
|
283
|
+
self.report_error(error_msg)
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Get inference parameters
|
|
288
|
+
confidence = self.get_config_float('confidence', 0.25)
|
|
289
|
+
iou = self.get_config_float('iou', 0.45)
|
|
290
|
+
max_det = self.get_config_int('max_det', 300)
|
|
291
|
+
draw_results = self.get_config_bool('draw_results', True)
|
|
292
|
+
|
|
293
|
+
# Perform inference on specified device ('intel:gpu' resolved to
|
|
294
|
+
# the first detected GPU, e.g. 'intel:gpu.0')
|
|
295
|
+
device = self._resolve_configured_device()
|
|
296
|
+
results = self.model.predict(
|
|
297
|
+
image,
|
|
298
|
+
conf=confidence,
|
|
299
|
+
iou=iou,
|
|
300
|
+
max_det=max_det,
|
|
301
|
+
device=device,
|
|
302
|
+
verbose=False
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Extract detection information. Pull the whole batch off the device
|
|
306
|
+
# once (each per-box .cpu() call is a separate sync + thread-pool
|
|
307
|
+
# wake-up) and iterate the resulting numpy arrays.
|
|
308
|
+
detections = []
|
|
309
|
+
if len(results) > 0:
|
|
310
|
+
result = results[0]
|
|
311
|
+
boxes = result.boxes
|
|
312
|
+
|
|
313
|
+
if boxes is not None and len(boxes) > 0:
|
|
314
|
+
xyxy = boxes.xyxy.cpu().numpy()
|
|
315
|
+
cls = boxes.cls.cpu().numpy().astype(int)
|
|
316
|
+
conf = boxes.conf.cpu().numpy()
|
|
317
|
+
for bbox, class_id, confidence_score in zip(xyxy, cls, conf):
|
|
318
|
+
detection = {
|
|
319
|
+
'class_id': int(class_id),
|
|
320
|
+
'class_name': result.names[int(class_id)],
|
|
321
|
+
'confidence': float(confidence_score),
|
|
322
|
+
'bbox': bbox.tolist(), # [x1, y1, x2, y2]
|
|
323
|
+
'bbox_format': 'xyxy'
|
|
324
|
+
}
|
|
325
|
+
detections.append(detection)
|
|
326
|
+
|
|
327
|
+
# Prepare output image
|
|
328
|
+
output_image = image
|
|
329
|
+
if draw_results and len(results) > 0:
|
|
330
|
+
output_image = results[0].plot()
|
|
331
|
+
|
|
332
|
+
# Encode image back to same format as input
|
|
333
|
+
include_image = self.config.get('include_image', True)
|
|
334
|
+
include_predictions = self.config.get('include_predictions', True)
|
|
335
|
+
|
|
336
|
+
payload_out = {}
|
|
337
|
+
|
|
338
|
+
if include_image:
|
|
339
|
+
# Encode image back to same format as input using base node helper
|
|
340
|
+
encoded_image = self.encode_image(output_image, input_format)
|
|
341
|
+
if encoded_image is not None:
|
|
342
|
+
payload_out[MessageKeys.IMAGE.PATH] = encoded_image
|
|
343
|
+
else:
|
|
344
|
+
self.report_error("Failed to encode output image")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Always include detections and detection_count if include_predictions is True (for backward compatibility)
|
|
348
|
+
if include_predictions or True:
|
|
349
|
+
payload_out['detections'] = detections
|
|
350
|
+
payload_out['detection_count'] = len(detections)
|
|
351
|
+
payload_out['bbox_format'] = 'xyxy' # Document the bbox format at payload level
|
|
352
|
+
|
|
353
|
+
# Preserve original message properties (like frame_count) and update payload
|
|
354
|
+
# Note: send() handles deep copying, so we modify msg directly
|
|
355
|
+
msg[MessageKeys.PAYLOAD] = payload_out
|
|
356
|
+
msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'yolo')
|
|
357
|
+
|
|
358
|
+
# Send the message
|
|
359
|
+
self.send(msg)
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
error_msg = f"Error during inference: {e}"
|
|
363
|
+
self.report_error(error_msg)
|
|
364
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unknown/Placeholder Node - represents a missing node type in the workflow.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pynode.nodes.base_node import BaseNode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnknownNode(BaseNode):
|
|
9
|
+
"""
|
|
10
|
+
Placeholder node for unknown/missing node types.
|
|
11
|
+
|
|
12
|
+
This node is created when loading a workflow that contains a node type
|
|
13
|
+
that is not currently registered (e.g., the node was removed or renamed).
|
|
14
|
+
|
|
15
|
+
The node preserves the original configuration and type information so the
|
|
16
|
+
workflow can still be loaded and the user can repair/replace the node.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
display_name = "Unknown"
|
|
20
|
+
icon = "❓"
|
|
21
|
+
category = "system"
|
|
22
|
+
color = "#3d3d3d"
|
|
23
|
+
border_color = "#ff6b6b"
|
|
24
|
+
text_color = "#999"
|
|
25
|
+
# input_count and output_count are handled via properties below
|
|
26
|
+
hidden = True # Don't show in palette - this is only created automatically
|
|
27
|
+
|
|
28
|
+
properties = [
|
|
29
|
+
{
|
|
30
|
+
'name': 'original_type',
|
|
31
|
+
'label': 'Original Type',
|
|
32
|
+
'type': 'text',
|
|
33
|
+
'default': '',
|
|
34
|
+
'readonly': True
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
'name': 'original_config',
|
|
38
|
+
'label': 'Original Config',
|
|
39
|
+
'type': 'json',
|
|
40
|
+
'default': {},
|
|
41
|
+
'readonly': True
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
info = """
|
|
46
|
+
<p>This is a placeholder for a node type that could not be found.</p>
|
|
47
|
+
<p>The original node type was not available when loading the workflow.</p>
|
|
48
|
+
<h4>Possible reasons:</h4>
|
|
49
|
+
<ul>
|
|
50
|
+
<li>The node was removed or renamed</li>
|
|
51
|
+
<li>A required dependency is not installed</li>
|
|
52
|
+
<li>The workflow was created with a different version</li>
|
|
53
|
+
</ul>
|
|
54
|
+
<h4>To fix:</h4>
|
|
55
|
+
<p>Delete this node and replace it with an appropriate alternative,
|
|
56
|
+
or install the missing node type.</p>
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, node_id=None, name="Unknown", original_type=None,
|
|
60
|
+
original_config=None, input_count=1, output_count=1):
|
|
61
|
+
super().__init__(node_id=node_id, name=name)
|
|
62
|
+
self.config['original_type'] = original_type or ''
|
|
63
|
+
self.config['original_config'] = original_config or {}
|
|
64
|
+
# Store the original port counts so connections can still be made
|
|
65
|
+
self._original_input_count = input_count
|
|
66
|
+
self._original_output_count = output_count
|
|
67
|
+
# Mark as disabled since it can't function
|
|
68
|
+
self.enabled = False
|
|
69
|
+
# Mark as unknown for frontend styling
|
|
70
|
+
self._is_unknown_node = True
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def input_count(self):
|
|
74
|
+
"""Return the original input count to preserve connections."""
|
|
75
|
+
return getattr(self, '_original_input_count', 1)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def output_count(self):
|
|
79
|
+
"""Return the original output count to preserve connections."""
|
|
80
|
+
return getattr(self, '_original_output_count', 1)
|
|
81
|
+
|
|
82
|
+
def process(self, msg):
|
|
83
|
+
"""Unknown nodes don't process messages - they're just placeholders."""
|
|
84
|
+
# Don't forward messages since we don't know what this node should do
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
def on_start(self):
|
|
88
|
+
"""No-op for unknown nodes."""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
def on_stop(self):
|
|
92
|
+
"""No-op for unknown nodes."""
|
|
93
|
+
pass
|