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,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenCV Morphology Node - applies morphological operations to images.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
from typing import Any, Dict
|
|
8
|
+
from pynode.nodes.base_node import BaseNode, process_image, Info
|
|
9
|
+
|
|
10
|
+
_info = Info()
|
|
11
|
+
_info.add_text("Applies morphological operations to binary or grayscale images. Used for noise removal, shape analysis, and image preprocessing.")
|
|
12
|
+
_info.add_header("Inputs")
|
|
13
|
+
_info.add_bullets(("Input 0:", "Binary or grayscale image"))
|
|
14
|
+
_info.add_header("Outputs")
|
|
15
|
+
_info.add_bullets(("Output 0:", "Processed image"))
|
|
16
|
+
_info.add_header("Operations")
|
|
17
|
+
_info.add_bullets(
|
|
18
|
+
("Erode:", "Shrinks white regions, removes small noise"),
|
|
19
|
+
("Dilate:", "Expands white regions, fills small holes"),
|
|
20
|
+
("Open:", "Erode then dilate - removes noise while preserving shape"),
|
|
21
|
+
("Close:", "Dilate then erode - fills holes while preserving shape"),
|
|
22
|
+
("Gradient:", "Difference between dilation and erosion - finds edges"),
|
|
23
|
+
("Top Hat:", "Difference between input and opening - finds bright spots"),
|
|
24
|
+
("Black Hat:", "Difference between closing and input - finds dark spots")
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class MorphologyNode(BaseNode):
|
|
28
|
+
"""
|
|
29
|
+
Morphology node - applies morphological operations to binary/grayscale images.
|
|
30
|
+
Supports erosion, dilation, opening, closing, gradient, tophat, and blackhat.
|
|
31
|
+
"""
|
|
32
|
+
info = str(_info)
|
|
33
|
+
display_name = 'Morphology'
|
|
34
|
+
icon = '⬡'
|
|
35
|
+
category = 'opencv'
|
|
36
|
+
color = '#4A90D9'
|
|
37
|
+
border_color = '#2E6BB0'
|
|
38
|
+
text_color = '#FFFFFF'
|
|
39
|
+
input_count = 1
|
|
40
|
+
output_count = 1
|
|
41
|
+
|
|
42
|
+
DEFAULT_CONFIG = {
|
|
43
|
+
'operation': 'dilate',
|
|
44
|
+
'kernel_shape': 'rect',
|
|
45
|
+
'kernel_size': 5,
|
|
46
|
+
'iterations': 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
properties = [
|
|
50
|
+
{
|
|
51
|
+
'name': 'operation',
|
|
52
|
+
'label': 'Operation',
|
|
53
|
+
'type': 'select',
|
|
54
|
+
'options': [
|
|
55
|
+
{'value': 'erode', 'label': 'Erode'},
|
|
56
|
+
{'value': 'dilate', 'label': 'Dilate'},
|
|
57
|
+
{'value': 'open', 'label': 'Open (erode then dilate)'},
|
|
58
|
+
{'value': 'close', 'label': 'Close (dilate then erode)'},
|
|
59
|
+
{'value': 'gradient', 'label': 'Gradient'},
|
|
60
|
+
{'value': 'tophat', 'label': 'Top Hat'},
|
|
61
|
+
{'value': 'blackhat', 'label': 'Black Hat'}
|
|
62
|
+
],
|
|
63
|
+
'default': DEFAULT_CONFIG['operation'],
|
|
64
|
+
'help': 'Morphological operation to apply'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
'name': 'kernel_shape',
|
|
68
|
+
'label': 'Kernel Shape',
|
|
69
|
+
'type': 'select',
|
|
70
|
+
'options': [
|
|
71
|
+
{'value': 'rect', 'label': 'Rectangle'},
|
|
72
|
+
{'value': 'ellipse', 'label': 'Ellipse'},
|
|
73
|
+
{'value': 'cross', 'label': 'Cross'}
|
|
74
|
+
],
|
|
75
|
+
'default': DEFAULT_CONFIG['kernel_shape'],
|
|
76
|
+
'help': 'Shape of the structuring element'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'name': 'kernel_size',
|
|
80
|
+
'label': 'Kernel Size',
|
|
81
|
+
'type': 'number',
|
|
82
|
+
'default': DEFAULT_CONFIG['kernel_size'],
|
|
83
|
+
'min': 1,
|
|
84
|
+
'max': 99,
|
|
85
|
+
'help': 'Size of the structuring element'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
'name': 'iterations',
|
|
89
|
+
'label': 'Iterations',
|
|
90
|
+
'type': 'number',
|
|
91
|
+
'default': DEFAULT_CONFIG['iterations'],
|
|
92
|
+
'min': 1,
|
|
93
|
+
'max': 20,
|
|
94
|
+
'help': 'Number of times to apply the operation'
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
def __init__(self, node_id=None, name="morphology"):
|
|
99
|
+
super().__init__(node_id, name)
|
|
100
|
+
|
|
101
|
+
@process_image()
|
|
102
|
+
def on_input(self, image: np.ndarray, msg: Dict[str, Any], input_index: int = 0):
|
|
103
|
+
"""Apply morphological operation to the input image."""
|
|
104
|
+
operation = self.config.get('operation', 'dilate')
|
|
105
|
+
kernel_shape = self.config.get('kernel_shape', 'rect')
|
|
106
|
+
kernel_size = self.get_config_int('kernel_size', 5)
|
|
107
|
+
iterations = self.get_config_int('iterations', 1)
|
|
108
|
+
|
|
109
|
+
# Create structuring element
|
|
110
|
+
shape_map = {
|
|
111
|
+
'rect': cv2.MORPH_RECT,
|
|
112
|
+
'ellipse': cv2.MORPH_ELLIPSE,
|
|
113
|
+
'cross': cv2.MORPH_CROSS
|
|
114
|
+
}
|
|
115
|
+
shape = shape_map.get(kernel_shape, cv2.MORPH_RECT)
|
|
116
|
+
kernel = cv2.getStructuringElement(shape, (kernel_size, kernel_size))
|
|
117
|
+
|
|
118
|
+
# Apply operation
|
|
119
|
+
if operation == 'erode':
|
|
120
|
+
result = cv2.erode(image, kernel, iterations=iterations)
|
|
121
|
+
elif operation == 'dilate':
|
|
122
|
+
result = cv2.dilate(image, kernel, iterations=iterations)
|
|
123
|
+
elif operation == 'open':
|
|
124
|
+
result = cv2.morphologyEx(image, cv2.MORPH_OPEN, kernel, iterations=iterations)
|
|
125
|
+
elif operation == 'close':
|
|
126
|
+
result = cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel, iterations=iterations)
|
|
127
|
+
elif operation == 'gradient':
|
|
128
|
+
result = cv2.morphologyEx(image, cv2.MORPH_GRADIENT, kernel, iterations=iterations)
|
|
129
|
+
elif operation == 'tophat':
|
|
130
|
+
result = cv2.morphologyEx(image, cv2.MORPH_TOPHAT, kernel, iterations=iterations)
|
|
131
|
+
elif operation == 'blackhat':
|
|
132
|
+
result = cv2.morphologyEx(image, cv2.MORPH_BLACKHAT, kernel, iterations=iterations)
|
|
133
|
+
else:
|
|
134
|
+
result = image
|
|
135
|
+
|
|
136
|
+
return result
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Normalize Coordinates Node - converts pixel coordinates to normalized (0.0-1.0).
|
|
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("Converts pixel coordinates to normalized coordinates (0.0-1.0). Useful for making detection outputs resolution-independent.")
|
|
10
|
+
_info.add_header("Inputs")
|
|
11
|
+
_info.add_bullets(
|
|
12
|
+
("Input 0:", "Message with coordinates to normalize"),
|
|
13
|
+
("payload.image:", "Source image (used to get dimensions if not specified)")
|
|
14
|
+
)
|
|
15
|
+
_info.add_header("Supported Formats")
|
|
16
|
+
_info.add_bullets(
|
|
17
|
+
("Detections:", "msg.payload.detections[].bbox -> [x1, y1, x2, y2]"),
|
|
18
|
+
("YOLO format:", "msg.detections[].bbox or msg.predictions[].bbox"),
|
|
19
|
+
("Shapes:", "msg.shapes[].x1, y1, x2, y2, radius, etc."),
|
|
20
|
+
("Single bbox:", "msg.bbox or msg.payload.bbox"),
|
|
21
|
+
("Points:", "msg.points[] or msg.payload.points[]")
|
|
22
|
+
)
|
|
23
|
+
_info.add_header("Output")
|
|
24
|
+
_info.add_text("Same structure with coordinates converted to normalized (0.0-1.0) values. Original pixel values preserved with '_px' suffix.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NormalizeCoordsNode(BaseNode):
|
|
28
|
+
"""
|
|
29
|
+
Normalize Coordinates node - converts pixel coordinates to normalized (0.0-1.0).
|
|
30
|
+
Handles detections, bboxes, shapes, points from various inference engines.
|
|
31
|
+
"""
|
|
32
|
+
info = str(_info)
|
|
33
|
+
display_name = 'Normalize Coords'
|
|
34
|
+
icon = '📐'
|
|
35
|
+
category = 'opencv'
|
|
36
|
+
color = '#4A90D9'
|
|
37
|
+
border_color = '#2E6BB0'
|
|
38
|
+
text_color = '#FFFFFF'
|
|
39
|
+
input_count = 1
|
|
40
|
+
output_count = 1
|
|
41
|
+
|
|
42
|
+
DEFAULT_CONFIG = {
|
|
43
|
+
'image_width': 0,
|
|
44
|
+
'image_height': 0,
|
|
45
|
+
'bbox_format': 'xyxy',
|
|
46
|
+
'preserve_pixels': True
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
properties = [
|
|
50
|
+
{
|
|
51
|
+
'name': 'image_width',
|
|
52
|
+
'label': 'Image Width',
|
|
53
|
+
'type': 'number',
|
|
54
|
+
'default': DEFAULT_CONFIG['image_width'],
|
|
55
|
+
'min': 0,
|
|
56
|
+
'help': 'Image width in pixels (0 = auto-detect from payload.image)'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
'name': 'image_height',
|
|
60
|
+
'label': 'Image Height',
|
|
61
|
+
'type': 'number',
|
|
62
|
+
'default': DEFAULT_CONFIG['image_height'],
|
|
63
|
+
'min': 0,
|
|
64
|
+
'help': 'Image height in pixels (0 = auto-detect from payload.image)'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
'name': 'bbox_format',
|
|
68
|
+
'label': 'BBox Format',
|
|
69
|
+
'type': 'select',
|
|
70
|
+
'options': [
|
|
71
|
+
{'value': 'xyxy', 'label': 'xyxy (x1, y1, x2, y2)'},
|
|
72
|
+
{'value': 'xywh', 'label': 'xywh (x, y, width, height)'},
|
|
73
|
+
{'value': 'cxcywh', 'label': 'cxcywh (center_x, center_y, w, h)'}
|
|
74
|
+
],
|
|
75
|
+
'default': DEFAULT_CONFIG['bbox_format'],
|
|
76
|
+
'help': 'Input bounding box format'
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
'name': 'preserve_pixels',
|
|
80
|
+
'label': 'Preserve Pixel Values',
|
|
81
|
+
'type': 'checkbox',
|
|
82
|
+
'default': DEFAULT_CONFIG['preserve_pixels'],
|
|
83
|
+
'help': 'Keep original pixel values with _px suffix'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
def __init__(self, node_id=None, name="normalize coords"):
|
|
88
|
+
super().__init__(node_id, name)
|
|
89
|
+
|
|
90
|
+
def _get_image_dimensions(self, msg: Dict[str, Any]) -> tuple:
|
|
91
|
+
"""Get image dimensions from config or message."""
|
|
92
|
+
w = self.get_config_int('image_width', 0)
|
|
93
|
+
h = self.get_config_int('image_height', 0)
|
|
94
|
+
|
|
95
|
+
if w > 0 and h > 0:
|
|
96
|
+
return w, h
|
|
97
|
+
|
|
98
|
+
# Try to get from payload.image
|
|
99
|
+
payload = msg.get(MessageKeys.PAYLOAD, {})
|
|
100
|
+
if isinstance(payload, dict):
|
|
101
|
+
img_data = payload.get(MessageKeys.IMAGE.PATH, payload)
|
|
102
|
+
if isinstance(img_data, dict):
|
|
103
|
+
w = img_data.get('width', 0)
|
|
104
|
+
h = img_data.get('height', 0)
|
|
105
|
+
if w > 0 and h > 0:
|
|
106
|
+
return w, h
|
|
107
|
+
|
|
108
|
+
# Try decoding image to get dimensions
|
|
109
|
+
img, _ = self.decode_image(payload)
|
|
110
|
+
if img is not None:
|
|
111
|
+
return img.shape[1], img.shape[0]
|
|
112
|
+
|
|
113
|
+
return 0, 0
|
|
114
|
+
|
|
115
|
+
def _normalize_bbox(self, bbox, w: int, h: int, preserve: bool) -> Any:
|
|
116
|
+
"""Normalize a bounding box."""
|
|
117
|
+
bbox_format = self.config.get('bbox_format', 'xyxy')
|
|
118
|
+
|
|
119
|
+
if isinstance(bbox, (list, tuple)) and len(bbox) >= 4:
|
|
120
|
+
x1, y1, x2, y2 = bbox[:4]
|
|
121
|
+
|
|
122
|
+
if bbox_format == 'xywh':
|
|
123
|
+
# Convert xywh to xyxy first
|
|
124
|
+
x2 = x1 + x2
|
|
125
|
+
y2 = y1 + y2
|
|
126
|
+
elif bbox_format == 'cxcywh':
|
|
127
|
+
# Convert center format to xyxy
|
|
128
|
+
cx, cy, bw, bh = bbox[:4]
|
|
129
|
+
x1 = cx - bw / 2
|
|
130
|
+
y1 = cy - bh / 2
|
|
131
|
+
x2 = cx + bw / 2
|
|
132
|
+
y2 = cy + bh / 2
|
|
133
|
+
|
|
134
|
+
result = [
|
|
135
|
+
float(x1) / w if w > 0 else 0,
|
|
136
|
+
float(y1) / h if h > 0 else 0,
|
|
137
|
+
float(x2) / w if w > 0 else 0,
|
|
138
|
+
float(y2) / h if h > 0 else 0
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
# Add extra elements if present (confidence, class_id, etc.)
|
|
142
|
+
if len(bbox) > 4:
|
|
143
|
+
result.extend(bbox[4:])
|
|
144
|
+
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
elif isinstance(bbox, dict):
|
|
148
|
+
result = {}
|
|
149
|
+
|
|
150
|
+
# Handle various bbox dict formats
|
|
151
|
+
if 'x1' in bbox:
|
|
152
|
+
result['x1'] = float(bbox['x1']) / w if w > 0 else 0
|
|
153
|
+
result['y1'] = float(bbox['y1']) / h if h > 0 else 0
|
|
154
|
+
result['x2'] = float(bbox.get('x2', 0)) / w if w > 0 else 0
|
|
155
|
+
result['y2'] = float(bbox.get('y2', 0)) / h if h > 0 else 0
|
|
156
|
+
if preserve:
|
|
157
|
+
result['x1_px'] = bbox['x1']
|
|
158
|
+
result['y1_px'] = bbox['y1']
|
|
159
|
+
result['x2_px'] = bbox.get('x2', 0)
|
|
160
|
+
result['y2_px'] = bbox.get('y2', 0)
|
|
161
|
+
|
|
162
|
+
if 'x' in bbox and 'width' in bbox:
|
|
163
|
+
result['x'] = float(bbox['x']) / w if w > 0 else 0
|
|
164
|
+
result['y'] = float(bbox['y']) / h if h > 0 else 0
|
|
165
|
+
result['width'] = float(bbox['width']) / w if w > 0 else 0
|
|
166
|
+
result['height'] = float(bbox['height']) / h if h > 0 else 0
|
|
167
|
+
if preserve:
|
|
168
|
+
result['x_px'] = bbox['x']
|
|
169
|
+
result['y_px'] = bbox['y']
|
|
170
|
+
result['width_px'] = bbox['width']
|
|
171
|
+
result['height_px'] = bbox['height']
|
|
172
|
+
|
|
173
|
+
# Copy non-coordinate fields
|
|
174
|
+
for key in bbox:
|
|
175
|
+
if key not in result and not key.endswith('_px'):
|
|
176
|
+
result[key] = bbox[key]
|
|
177
|
+
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
return bbox
|
|
181
|
+
|
|
182
|
+
def _normalize_point(self, point, w: int, h: int, preserve: bool) -> Any:
|
|
183
|
+
"""Normalize a point."""
|
|
184
|
+
if isinstance(point, (list, tuple)) and len(point) >= 2:
|
|
185
|
+
return [float(point[0]) / w if w > 0 else 0,
|
|
186
|
+
float(point[1]) / h if h > 0 else 0]
|
|
187
|
+
elif isinstance(point, dict):
|
|
188
|
+
result = {
|
|
189
|
+
'x': float(point.get('x', 0)) / w if w > 0 else 0,
|
|
190
|
+
'y': float(point.get('y', 0)) / h if h > 0 else 0
|
|
191
|
+
}
|
|
192
|
+
if preserve:
|
|
193
|
+
result['x_px'] = point.get('x', 0)
|
|
194
|
+
result['y_px'] = point.get('y', 0)
|
|
195
|
+
# Copy other fields
|
|
196
|
+
for key in point:
|
|
197
|
+
if key not in ['x', 'y'] and not key.endswith('_px'):
|
|
198
|
+
result[key] = point[key]
|
|
199
|
+
return result
|
|
200
|
+
return point
|
|
201
|
+
|
|
202
|
+
def _normalize_detection(self, det: Dict, w: int, h: int, preserve: bool) -> Dict:
|
|
203
|
+
"""Normalize a detection dict."""
|
|
204
|
+
result = det.copy()
|
|
205
|
+
|
|
206
|
+
if 'bbox' in det:
|
|
207
|
+
result['bbox'] = self._normalize_bbox(det['bbox'], w, h, preserve)
|
|
208
|
+
|
|
209
|
+
# Handle various coordinate fields
|
|
210
|
+
for key in ['x', 'cx', 'center_x']:
|
|
211
|
+
if key in det and not isinstance(det[key], dict):
|
|
212
|
+
result[key] = float(det[key]) / w if w > 0 else 0
|
|
213
|
+
if preserve:
|
|
214
|
+
result[f'{key}_px'] = det[key]
|
|
215
|
+
|
|
216
|
+
for key in ['y', 'cy', 'center_y']:
|
|
217
|
+
if key in det and not isinstance(det[key], dict):
|
|
218
|
+
result[key] = float(det[key]) / h if h > 0 else 0
|
|
219
|
+
if preserve:
|
|
220
|
+
result[f'{key}_px'] = det[key]
|
|
221
|
+
|
|
222
|
+
for key in ['width', 'w', 'radius']:
|
|
223
|
+
if key in det:
|
|
224
|
+
result[key] = float(det[key]) / w if w > 0 else 0
|
|
225
|
+
if preserve:
|
|
226
|
+
result[f'{key}_px'] = det[key]
|
|
227
|
+
|
|
228
|
+
for key in ['height', 'h']:
|
|
229
|
+
if key in det:
|
|
230
|
+
result[key] = float(det[key]) / h if h > 0 else 0
|
|
231
|
+
if preserve:
|
|
232
|
+
result[f'{key}_px'] = det[key]
|
|
233
|
+
|
|
234
|
+
# Handle x1, y1, x2, y2 directly on detection
|
|
235
|
+
if 'x1' in det:
|
|
236
|
+
result['x1'] = float(det['x1']) / w if w > 0 else 0
|
|
237
|
+
result['y1'] = float(det['y1']) / h if h > 0 else 0
|
|
238
|
+
if preserve:
|
|
239
|
+
result['x1_px'] = det['x1']
|
|
240
|
+
result['y1_px'] = det['y1']
|
|
241
|
+
if 'x2' in det:
|
|
242
|
+
result['x2'] = float(det['x2']) / w if w > 0 else 0
|
|
243
|
+
result['y2'] = float(det['y2']) / h if h > 0 else 0
|
|
244
|
+
if preserve:
|
|
245
|
+
result['x2_px'] = det['x2']
|
|
246
|
+
result['y2_px'] = det['y2']
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
def _normalize_shape(self, shape: Dict, w: int, h: int, preserve: bool) -> Dict:
|
|
251
|
+
"""Normalize a shape dict (for draw node compatibility)."""
|
|
252
|
+
return self._normalize_detection(shape, w, h, preserve)
|
|
253
|
+
|
|
254
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
255
|
+
"""Normalize coordinates in the message."""
|
|
256
|
+
w, h = self._get_image_dimensions(msg)
|
|
257
|
+
|
|
258
|
+
if w <= 0 or h <= 0:
|
|
259
|
+
self.report_error("Could not determine image dimensions. Set width/height in config or provide image in payload.")
|
|
260
|
+
self.send(msg)
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
preserve = self.get_config_bool('preserve_pixels', True)
|
|
264
|
+
|
|
265
|
+
# Store dimensions in message
|
|
266
|
+
msg['image_width'] = w
|
|
267
|
+
msg['image_height'] = h
|
|
268
|
+
|
|
269
|
+
# Handle payload.detections
|
|
270
|
+
payload = msg.get(MessageKeys.PAYLOAD, {})
|
|
271
|
+
if isinstance(payload, dict) and 'detections' in payload:
|
|
272
|
+
payload['detections'] = [
|
|
273
|
+
self._normalize_detection(d, w, h, preserve)
|
|
274
|
+
for d in payload['detections']
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
# Handle msg.detections (YOLO style)
|
|
278
|
+
if 'detections' in msg and isinstance(msg['detections'], list):
|
|
279
|
+
msg['detections'] = [
|
|
280
|
+
self._normalize_detection(d, w, h, preserve)
|
|
281
|
+
for d in msg['detections']
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
# Handle msg.predictions
|
|
285
|
+
if 'predictions' in msg and isinstance(msg['predictions'], list):
|
|
286
|
+
msg['predictions'] = [
|
|
287
|
+
self._normalize_detection(d, w, h, preserve)
|
|
288
|
+
for d in msg['predictions']
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
# Handle msg.shapes (draw node)
|
|
292
|
+
if 'shapes' in msg and isinstance(msg['shapes'], list):
|
|
293
|
+
msg['shapes'] = [
|
|
294
|
+
self._normalize_shape(s, w, h, preserve)
|
|
295
|
+
for s in msg['shapes']
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
# Handle single bbox
|
|
299
|
+
if 'bbox' in msg:
|
|
300
|
+
msg['bbox'] = self._normalize_bbox(msg['bbox'], w, h, preserve)
|
|
301
|
+
if isinstance(payload, dict) and 'bbox' in payload:
|
|
302
|
+
payload['bbox'] = self._normalize_bbox(payload['bbox'], w, h, preserve)
|
|
303
|
+
|
|
304
|
+
# Handle points
|
|
305
|
+
if 'points' in msg and isinstance(msg['points'], list):
|
|
306
|
+
msg['points'] = [
|
|
307
|
+
self._normalize_point(p, w, h, preserve)
|
|
308
|
+
for p in msg['points']
|
|
309
|
+
]
|
|
310
|
+
if isinstance(payload, dict) and 'points' in payload:
|
|
311
|
+
payload['points'] = [
|
|
312
|
+
self._normalize_point(p, w, h, preserve)
|
|
313
|
+
for p in payload['points']
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
# Handle circles
|
|
317
|
+
if 'circles' in msg and isinstance(msg['circles'], list):
|
|
318
|
+
msg['circles'] = [
|
|
319
|
+
self._normalize_detection(c, w, h, preserve)
|
|
320
|
+
for c in msg['circles']
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
# Handle lines
|
|
324
|
+
if 'lines' in msg and isinstance(msg['lines'], list):
|
|
325
|
+
msg['lines'] = [
|
|
326
|
+
self._normalize_detection(l, w, h, preserve)
|
|
327
|
+
for l in msg['lines']
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
# Handle contours
|
|
331
|
+
if 'contours' in msg and isinstance(msg['contours'], list):
|
|
332
|
+
msg['contours'] = [
|
|
333
|
+
self._normalize_detection(c, w, h, preserve)
|
|
334
|
+
for c in msg['contours']
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
# Handle blobs
|
|
338
|
+
if 'blobs' in msg and isinstance(msg['blobs'], list):
|
|
339
|
+
msg['blobs'] = [
|
|
340
|
+
self._normalize_detection(b, w, h, preserve)
|
|
341
|
+
for b in msg['blobs']
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
self.send(msg)
|