pynode-flow 0.2.7__tar.gz → 0.2.8__tar.gz
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_flow-0.2.7 → pynode_flow-0.2.8}/Dockerfile +4 -2
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/PKG-INFO +2 -2
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/README.md +1 -1
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/_version.py +3 -3
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/change_node.py +123 -24
- pynode_flow-0.2.8/pynode/nodes/ClassMapNode/__init__.py +3 -0
- pynode_flow-0.2.8/pynode/nodes/ClassMapNode/class_map_node.py +329 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +18 -7
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +21 -1
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +3 -1
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/inference_node.py +17 -3
- pynode_flow-0.2.8/pynode/nodes/InferenceNode/requirements.txt +16 -0
- pynode_flow-0.2.8/pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode_flow-0.2.8/pynode/nodes/detection_utils.py +207 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/properties.js +26 -3
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/style.css +31 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/PKG-INFO +2 -2
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/SOURCES.txt +7 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/scm_file_list.json +7 -0
- pynode_flow-0.2.8/pynode_flow.egg-info/scm_version.json +8 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pyproject.toml +3 -0
- pynode_flow-0.2.8/tests/test_change_node.py +256 -0
- pynode_flow-0.2.8/tests/test_class_map_node.py +250 -0
- pynode_flow-0.2.8/tests/test_detection_utils.py +268 -0
- pynode_flow-0.2.7/pynode/nodes/InferenceNode/requirements.txt +0 -12
- pynode_flow-0.2.7/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.claude/skills/pynode-workflows/SKILL.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.dockerignore +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.gitignore +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/DOCKER.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/INSTALL.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/LICENSE +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/MANIFEST.in +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docker-compose.yml +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docker-entrypoint.sh +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/README.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/install_nodes.bat +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/install_nodes.sh +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/helpers.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/services.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/workflows.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/config.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/main.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/camera_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.7/pynode/nodes/ConfidenceFilterNode → pynode_flow-0.2.8/pynode/nodes/ClassMapNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/DelayNode → pynode_flow-0.2.8/pynode/nodes/ConfidenceFilterNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ControlSliderNode/control_slider_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/debug_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DelayNode/delay_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/ErrorNode → pynode_flow-0.2.8/pynode/nodes/DelayNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/FilterNode → pynode_flow-0.2.8/pynode/nodes/ErrorNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/FunctionNode → pynode_flow-0.2.8/pynode/nodes/FilterNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/GateNode → pynode_flow-0.2.8/pynode/nodes/FunctionNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/ImageWriterNode → pynode_flow-0.2.8/pynode/nodes/GateNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/InjectNode → pynode_flow-0.2.8/pynode/nodes/ImageWriterNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/JoinNode → pynode_flow-0.2.8/pynode/nodes/InjectNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/LabelFilterNode → pynode_flow-0.2.8/pynode/nodes/JoinNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/LinkNode → pynode_flow-0.2.8/pynode/nodes/LabelFilterNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_bus.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_in_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_out_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/RangeNode → pynode_flow-0.2.8/pynode/nodes/LinkNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/RateProbeNode → pynode_flow-0.2.8/pynode/nodes/RangeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/SplitNode → pynode_flow-0.2.8/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/interop/README.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/ndjson_protocol.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/nodered_snippets.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/tcp_in_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/tcp_out_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_in_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_out_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_probe.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_protocol.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/SwitchNode → pynode_flow-0.2.8/pynode/nodes/SplitNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_annotate_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_filter_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_line_counter_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_sink_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_smoother_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_zone_node.py +0 -0
- {pynode_flow-0.2.7/pynode/nodes/TemplateNode → pynode_flow-0.2.8/pynode/nodes/SwitchNode}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SwitchNode/switch_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/ultralytics_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/base_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/messages.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/pacing.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/supervision_utils.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/server.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/01-hello-world.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/02-function-transform.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/03-switch-routing.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/04-flow-control.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/05-webcam-viewer.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/06-webcam-yolo.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/07-blur-people.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/16-socket-bridge.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/17-slider-crop.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/18-track-trace.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/19-line-counter.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/20-zone-occupancy.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/21-annotator-gallery.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/22-heatmap.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/23-privacy-blur.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/24-detection-log.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/README.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/manifest.json +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/index.html +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/debug.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/events.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/geometry-editor.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/main.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/mqtt-services.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/nodes.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/state.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/workflow.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/workflows.js +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/workflow_engine.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/workflow_manager.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/requirements.txt +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.bat +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.cfg +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.sh +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/__init__.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/conftest.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_workflows.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_camera_node_backend.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_config.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_control_slider_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_crop_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_debug_json_safety.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_debug_workflow_scope.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_delay_rate_limit.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_frame_pacing.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_function_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_image_upload_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_link_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_models_dir.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_mqtt_services.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_range_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_send_copy_semantics.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_socket_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_annotate_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_phase3_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_utils.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_zone_nodes.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_video_reader_node.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
FROM docker.io/nvidia/cuda:12.
|
|
1
|
+
FROM docker.io/nvidia/cuda:12.8.1-runtime-ubuntu22.04
|
|
2
2
|
|
|
3
3
|
# Set environment variables
|
|
4
4
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5
5
|
ENV PYTHONUNBUFFERED=1
|
|
6
|
-
|
|
6
|
+
# cu128 PyTorch wheels are the first to ship Blackwell (sm_120, e.g. RTX 50xx)
|
|
7
|
+
# kernels; older cu126 builds raise "no kernel image is available".
|
|
8
|
+
ENV CUDA_VERSION=12.8
|
|
7
9
|
|
|
8
10
|
# Install system dependencies
|
|
9
11
|
RUN apt-get update && apt-get install -y \
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pynode-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
4
4
|
Summary: Python based Node-RED-like visual workflow system
|
|
5
5
|
Author: olkham
|
|
6
6
|
License-Expression: MIT
|
|
@@ -94,7 +94,7 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
|
|
|
94
94
|
| **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
|
|
95
95
|
| **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
|
|
96
96
|
| **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
|
|
97
|
-
| **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
|
|
97
|
+
| **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Class Map (relabel classes), Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
|
|
98
98
|
| **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
|
|
99
99
|
| **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
|
|
100
100
|
| **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
|
|
@@ -29,7 +29,7 @@ carries its own documentation in the editor's **Information** panel (ℹ️ tab)
|
|
|
29
29
|
| **Output** | Image Viewer, Image Writer, Video Writer, Message Writer, Roboflow Upload |
|
|
30
30
|
| **Logic** | Switch (route on conditions), Gate, Sync, Auto Sync |
|
|
31
31
|
| **Network** | MQTT In / Out, UDP In / Out, TCP In / Out, REST Endpoint, Webhook, mDNS Broadcast / Discovery |
|
|
32
|
-
| **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
|
|
32
|
+
| **Vision** | YOLO, Inference, Crop, Slice Image / Slice Collector, Draw Predictions, Class Map (relabel classes), Confidence Filter, Label Filter, Merge Predictions, Image Format, Qwen3-VL, vLLM |
|
|
33
33
|
| **Supervision** | SV Tracker (ByteTrack), SV Annotate (15 visual styles: boxes, labels, traces, heat maps, privacy blur/pixelate), SV Line Counter (in/out crossing counts + events), SV Polygon Zone (occupancy, enter/exit events) — zone geometry drawn directly on a live frame — SV Smoother (de-jitter boxes), SV Detection Filter (confidence/class/area/aspect/top-K + NMS/NMM), SV Sink (CSV/JSON detection logging) |
|
|
34
34
|
| **Analysis** | BBox Metrics, Polygon Metrics, Point in Shape |
|
|
35
35
|
| **OpenCV** | ~28 image-processing nodes: Blur, Threshold, Morphology, Edge Detector, Find Contours, Perspective, Resize, Rotate, Colormap, Histogram, FFT, Template Match, RealSense Depth, and more |
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.8'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 8)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g7b8eff347'
|
|
@@ -5,7 +5,7 @@ Similar to Node-RED's change node.
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
|
-
from typing import Any, Dict
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
9
|
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
10
10
|
|
|
11
11
|
_info = Info()
|
|
@@ -25,6 +25,18 @@ _info.add_bullets(
|
|
|
25
25
|
("Delete:", "Remove a property from the message."),
|
|
26
26
|
("Move:", "Move a property to a different location."),
|
|
27
27
|
)
|
|
28
|
+
_info.add_header("Lists")
|
|
29
|
+
_info.add_text(
|
|
30
|
+
"Tick 'is list' on a rule when the property holds a list of objects, and "
|
|
31
|
+
"give the key to work on. The rule then applies to that key in every item, "
|
|
32
|
+
"however many there are - for example relabelling each detection:"
|
|
33
|
+
)
|
|
34
|
+
_info.add_code("in msg.payload.detections - is list, key class_name - search 2, replace drone").end()
|
|
35
|
+
_info.add_text(
|
|
36
|
+
"Search-and-replace works on substrings of text values. A value that is "
|
|
37
|
+
"not text (a number, a boolean) is replaced only when the search matches "
|
|
38
|
+
"the whole value, and the replacement keeps the type you selected."
|
|
39
|
+
)
|
|
28
40
|
|
|
29
41
|
|
|
30
42
|
class ChangeNode(BaseNode):
|
|
@@ -52,6 +64,9 @@ class ChangeNode(BaseNode):
|
|
|
52
64
|
ui_component = 'change-rules-editor'
|
|
53
65
|
ui_component_config = {
|
|
54
66
|
'operations': ['set', 'change', 'move', 'delete'],
|
|
67
|
+
# Per-rule list mode: apply the rule to `key` inside every item of the
|
|
68
|
+
# list at the rule's path (see _rule_targets).
|
|
69
|
+
'list_fields': ['isList', 'key'],
|
|
55
70
|
'value_types': [
|
|
56
71
|
{'value': 'str', 'label': 'String'},
|
|
57
72
|
{'value': 'num', 'label': 'Number'},
|
|
@@ -131,6 +146,87 @@ class ChangeNode(BaseNode):
|
|
|
131
146
|
|
|
132
147
|
return False
|
|
133
148
|
|
|
149
|
+
def _rule_targets(self, msg: Dict, rule: Dict, path: str) -> List[Tuple[Optional[Dict], str]]:
|
|
150
|
+
"""Work out the places a rule applies to.
|
|
151
|
+
|
|
152
|
+
Normally that is one place: the property path itself. With ``isList``
|
|
153
|
+
set, the path must hold a list, and the rule applies to ``key`` inside
|
|
154
|
+
every object in it - the way to rewrite a field on all detections
|
|
155
|
+
without knowing how many there are.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
msg: The message being modified.
|
|
159
|
+
rule: The rule definition.
|
|
160
|
+
path: The rule's property path.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
``(container, key)`` pairs. ``container`` is None for a plain
|
|
164
|
+
property path, in which case ``key`` is that path.
|
|
165
|
+
"""
|
|
166
|
+
if not rule.get('isList', rule.get('is_list', False)):
|
|
167
|
+
return [(None, path)]
|
|
168
|
+
|
|
169
|
+
key = str(rule.get('key', rule.get('itemKey', '')) or '').strip()
|
|
170
|
+
if not key:
|
|
171
|
+
self.report_error(f"List rule on {path} needs an item key")
|
|
172
|
+
return []
|
|
173
|
+
|
|
174
|
+
items = self._get_nested_value(msg, path)
|
|
175
|
+
if not isinstance(items, (list, tuple)):
|
|
176
|
+
self.report_error(f"List rule on {path}: not a list")
|
|
177
|
+
return []
|
|
178
|
+
|
|
179
|
+
return [(item, key) for item in items if isinstance(item, dict)]
|
|
180
|
+
|
|
181
|
+
def _target_get(self, msg: Dict, target: Tuple[Optional[Dict], str]) -> Any:
|
|
182
|
+
"""Read a target - a message path, or a key inside a list item."""
|
|
183
|
+
container, key = target
|
|
184
|
+
if container is None:
|
|
185
|
+
return self._get_nested_value(msg, key)
|
|
186
|
+
return container.get(key)
|
|
187
|
+
|
|
188
|
+
def _target_set(self, msg: Dict, target: Tuple[Optional[Dict], str], value: Any) -> None:
|
|
189
|
+
"""Write a target."""
|
|
190
|
+
container, key = target
|
|
191
|
+
if container is None:
|
|
192
|
+
self._set_nested_value(msg, key, value)
|
|
193
|
+
else:
|
|
194
|
+
container[key] = value
|
|
195
|
+
|
|
196
|
+
def _target_delete(self, msg: Dict, target: Tuple[Optional[Dict], str]) -> bool:
|
|
197
|
+
"""Remove a target, reporting whether anything was there."""
|
|
198
|
+
container, key = target
|
|
199
|
+
if container is None:
|
|
200
|
+
return self._delete_nested_value(msg, key)
|
|
201
|
+
if key in container:
|
|
202
|
+
del container[key]
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def _apply_change(self, msg: Dict, target: Tuple[Optional[Dict], str],
|
|
207
|
+
search: Any, search_type: str,
|
|
208
|
+
replacement_text: str, replacement_value: Any) -> None:
|
|
209
|
+
"""Search and replace inside one target.
|
|
210
|
+
|
|
211
|
+
Text is replaced by substring (or regex). Anything else - a number, a
|
|
212
|
+
boolean - has no substring to work on, so it is replaced only when the
|
|
213
|
+
search matches the whole value, and it takes the typed replacement.
|
|
214
|
+
"""
|
|
215
|
+
current = self._target_get(msg, target)
|
|
216
|
+
if current is None:
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
if isinstance(current, str):
|
|
220
|
+
if search_type == 'regex':
|
|
221
|
+
try:
|
|
222
|
+
self._target_set(msg, target, re.sub(str(search), replacement_text, current))
|
|
223
|
+
except re.error:
|
|
224
|
+
self.report_error(f"Invalid regex pattern: {search}")
|
|
225
|
+
else:
|
|
226
|
+
self._target_set(msg, target, current.replace(str(search), replacement_text))
|
|
227
|
+
elif not isinstance(current, (dict, list, tuple)) and str(current) == str(search):
|
|
228
|
+
self._target_set(msg, target, replacement_value)
|
|
229
|
+
|
|
134
230
|
def _resolve_value(self, msg: Dict, value: Any, value_type: str) -> Any:
|
|
135
231
|
"""
|
|
136
232
|
Resolve a value based on its type.
|
|
@@ -189,6 +285,11 @@ class ChangeNode(BaseNode):
|
|
|
189
285
|
property_path = rule.get('path', rule.get('p', rule.get('property', f'msg.{MessageKeys.PAYLOAD}')))
|
|
190
286
|
|
|
191
287
|
try:
|
|
288
|
+
# One target for a plain path; one per list item in list mode.
|
|
289
|
+
targets = self._rule_targets(msg, rule, property_path)
|
|
290
|
+
if not targets:
|
|
291
|
+
continue
|
|
292
|
+
|
|
192
293
|
if rule_type == 'set':
|
|
193
294
|
# Set property to a value
|
|
194
295
|
value = rule.get('value', rule.get('to', ''))
|
|
@@ -206,7 +307,8 @@ class ChangeNode(BaseNode):
|
|
|
206
307
|
else:
|
|
207
308
|
resolved_value = self._resolve_value(msg, value, value_type)
|
|
208
309
|
|
|
209
|
-
|
|
310
|
+
for target in targets:
|
|
311
|
+
self._target_set(msg, target, resolved_value)
|
|
210
312
|
|
|
211
313
|
elif rule_type == 'change':
|
|
212
314
|
# Search and replace within a property
|
|
@@ -215,39 +317,36 @@ class ChangeNode(BaseNode):
|
|
|
215
317
|
replace = rule.get('replace', rule.get('to', ''))
|
|
216
318
|
replace_type = rule.get('replaceType', rule.get('tot', 'str'))
|
|
217
319
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
#
|
|
320
|
+
# Resolve the replacement twice over: the text form drives
|
|
321
|
+
# substring and regex replacement, the typed form replaces a
|
|
322
|
+
# whole non-text value (see _apply_change).
|
|
221
323
|
if replace_type == 'path':
|
|
222
|
-
|
|
324
|
+
replacement_value = self._get_nested_value(msg, 'msg.' + str(replace) if not str(replace).startswith('msg.') else replace)
|
|
223
325
|
else:
|
|
224
|
-
|
|
326
|
+
replacement_value = self._resolve_value(msg, replace, replace_type)
|
|
327
|
+
replacement_text = str(replacement_value) if replacement_value is not None else ''
|
|
225
328
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
try:
|
|
230
|
-
new_value = re.sub(search, str(replace_value) if replace_value else '', current_value)
|
|
231
|
-
self._set_nested_value(msg, property_path, new_value)
|
|
232
|
-
except re.error as e:
|
|
233
|
-
self.report_error(f"Invalid regex pattern: {search}")
|
|
234
|
-
else:
|
|
235
|
-
new_value = current_value.replace(str(search), str(replace_value) if replace_value else '')
|
|
236
|
-
self._set_nested_value(msg, property_path, new_value)
|
|
329
|
+
for target in targets:
|
|
330
|
+
self._apply_change(msg, target, search, search_type,
|
|
331
|
+
replacement_text, replacement_value)
|
|
237
332
|
|
|
238
333
|
elif rule_type == 'delete':
|
|
239
334
|
# Delete a property
|
|
240
|
-
|
|
335
|
+
for target in targets:
|
|
336
|
+
self._target_delete(msg, target)
|
|
241
337
|
|
|
242
338
|
elif rule_type == 'move':
|
|
243
|
-
# Move property to another location
|
|
339
|
+
# Move property to another location. In list mode the
|
|
340
|
+
# destination is a key inside the same item.
|
|
244
341
|
to_path = rule.get('toPath', rule.get('to', rule.get('toProperty', '')))
|
|
245
342
|
|
|
246
343
|
if to_path:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
self.
|
|
250
|
-
|
|
344
|
+
for target in targets:
|
|
345
|
+
container, _ = target
|
|
346
|
+
current_value = self._target_get(msg, target)
|
|
347
|
+
if current_value is not None:
|
|
348
|
+
self._target_set(msg, (container, to_path), current_value)
|
|
349
|
+
self._target_delete(msg, target)
|
|
251
350
|
|
|
252
351
|
except Exception as e:
|
|
253
352
|
self.report_error(f"Error applying rule {rule_type} on {property_path}: {str(e)}")
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Class Map Node - relabels detections by class id or class name.
|
|
3
|
+
|
|
4
|
+
A model reports whatever it was trained with, and often not much: an ONNX model
|
|
5
|
+
carries no label file, so its detections arrive as ``class_id: 2`` /
|
|
6
|
+
``class_name: "2"``. This node applies a mapping of your own - ``2 = drone`` -
|
|
7
|
+
to every detection in the list, leaving boxes, scores and every other field
|
|
8
|
+
untouched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_info = Info()
|
|
21
|
+
_info.add_text(
|
|
22
|
+
"Renames detection classes. Give it a mapping from class id (or existing "
|
|
23
|
+
"class name) to the label you want, and every detection in the list is "
|
|
24
|
+
"relabelled - useful for models that report bare numeric classes."
|
|
25
|
+
)
|
|
26
|
+
_info.add_header("Inputs")
|
|
27
|
+
_info.add_bullets(
|
|
28
|
+
("Input 0:", "Message with a detections list (e.g. from Inference or a tracker)"),
|
|
29
|
+
)
|
|
30
|
+
_info.add_header("Outputs")
|
|
31
|
+
_info.add_bullets(
|
|
32
|
+
("Output 0:", "The same message with class_name rewritten on each detection"),
|
|
33
|
+
)
|
|
34
|
+
_info.add_header("Mapping")
|
|
35
|
+
_info.add_text("One entry per line, key first:")
|
|
36
|
+
_info.add_code("2 = drone").text(
|
|
37
|
+
"- a colon, arrow or comma works as the separator too, quotes are "
|
|
38
|
+
"optional, and lines starting with # are ignored."
|
|
39
|
+
).end()
|
|
40
|
+
_info.add_code('{"1": "bird", "2": "drone"}').text("A JSON object works too.").end()
|
|
41
|
+
_info.add_header("Options")
|
|
42
|
+
_info.add_bullets(
|
|
43
|
+
("Match On:", "Look the key up against the class id, the class name, or either"),
|
|
44
|
+
("Unmatched:", "Leave a detection's label alone, replace it with a fallback, or drop it"),
|
|
45
|
+
("Detections Path:", "Where the list lives (payload.detections, payload.tracks, ...)"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Key/value separators accepted in the line form, longest first so that '->'
|
|
49
|
+
# is not read as '-' followed by '>'.
|
|
50
|
+
_ENTRY_SEPARATOR = re.compile(r'\s*(?:=>|->|=|:|\t|,)\s*')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _unquote(text: str) -> str:
|
|
54
|
+
"""Strip surrounding whitespace and one layer of matching quotes."""
|
|
55
|
+
text = text.strip()
|
|
56
|
+
if len(text) >= 2 and text[0] == text[-1] and text[0] in ('"', "'"):
|
|
57
|
+
text = text[1:-1]
|
|
58
|
+
return text.strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_class_map(raw: Any) -> Tuple[Dict[str, str], List[str]]:
|
|
62
|
+
"""Parse the mapping config into ``{key: label}``.
|
|
63
|
+
|
|
64
|
+
Accepts a dict (already parsed by the UI), a JSON object, or one entry per
|
|
65
|
+
line in any of the ``2 = drone`` / ``2: drone`` / ``2 -> drone`` /
|
|
66
|
+
``2, drone`` spellings.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
raw: The raw config value.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
``(mapping, problems)`` - ``problems`` lists the lines that could not
|
|
73
|
+
be read, so the caller can report them once instead of per frame.
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(raw, dict):
|
|
76
|
+
return {str(k).strip(): str(v) for k, v in raw.items()}, []
|
|
77
|
+
|
|
78
|
+
text = str(raw or '').strip()
|
|
79
|
+
if not text:
|
|
80
|
+
return {}, []
|
|
81
|
+
|
|
82
|
+
if text.startswith('{'):
|
|
83
|
+
try:
|
|
84
|
+
data = json.loads(text)
|
|
85
|
+
except json.JSONDecodeError as e:
|
|
86
|
+
return {}, [f"invalid JSON ({e.msg} at line {e.lineno})"]
|
|
87
|
+
if isinstance(data, dict):
|
|
88
|
+
return {str(k).strip(): str(v) for k, v in data.items()}, []
|
|
89
|
+
return {}, ["JSON mapping must be an object"]
|
|
90
|
+
|
|
91
|
+
mapping: Dict[str, str] = {}
|
|
92
|
+
problems: List[str] = []
|
|
93
|
+
|
|
94
|
+
for line in text.splitlines():
|
|
95
|
+
line = line.strip()
|
|
96
|
+
if not line or line.startswith('#'):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
parts = _ENTRY_SEPARATOR.split(line, maxsplit=1)
|
|
100
|
+
if len(parts) != 2:
|
|
101
|
+
problems.append(line)
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
key, value = _unquote(parts[0]), _unquote(parts[1])
|
|
105
|
+
if not key or not value:
|
|
106
|
+
problems.append(line)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
mapping[key] = value
|
|
110
|
+
|
|
111
|
+
return mapping, problems
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ClassMapNode(BaseNode):
|
|
115
|
+
"""Rewrites ``class_name`` on every detection from a user-supplied map."""
|
|
116
|
+
|
|
117
|
+
display_name = 'Class Map'
|
|
118
|
+
info = str(_info)
|
|
119
|
+
icon = '🔤'
|
|
120
|
+
category = 'vision'
|
|
121
|
+
color = '#C7E9C0'
|
|
122
|
+
border_color = '#74C476'
|
|
123
|
+
text_color = '#000000'
|
|
124
|
+
input_count = 1
|
|
125
|
+
output_count = 1
|
|
126
|
+
|
|
127
|
+
DEFAULT_CONFIG = {
|
|
128
|
+
'mapping': '',
|
|
129
|
+
'match_on': 'either',
|
|
130
|
+
'unmatched': 'keep',
|
|
131
|
+
'unmatched_label': 'unknown',
|
|
132
|
+
'detections_path': 'payload.detections',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
properties = [
|
|
136
|
+
{
|
|
137
|
+
'name': 'mapping',
|
|
138
|
+
'label': 'Class Mapping',
|
|
139
|
+
'type': 'textarea',
|
|
140
|
+
'default': DEFAULT_CONFIG['mapping'],
|
|
141
|
+
'placeholder': '2 = drone\n1 = bird',
|
|
142
|
+
'help': 'One "id = label" per line, or a JSON object. Lines starting with # are ignored.'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
'name': 'match_on',
|
|
146
|
+
'label': 'Match On',
|
|
147
|
+
'type': 'select',
|
|
148
|
+
'options': [
|
|
149
|
+
{'value': 'either', 'label': 'Class ID, then class name'},
|
|
150
|
+
{'value': 'class_id', 'label': 'Class ID only'},
|
|
151
|
+
{'value': 'class_name', 'label': 'Class name only'},
|
|
152
|
+
],
|
|
153
|
+
'default': DEFAULT_CONFIG['match_on'],
|
|
154
|
+
'help': 'What the mapping keys are matched against. Name matching ignores case.'
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
'name': 'unmatched',
|
|
158
|
+
'label': 'Unmatched Detections',
|
|
159
|
+
'type': 'select',
|
|
160
|
+
'options': [
|
|
161
|
+
{'value': 'keep', 'label': 'Keep their current label'},
|
|
162
|
+
{'value': 'label', 'label': 'Relabel with a fallback'},
|
|
163
|
+
{'value': 'drop', 'label': 'Drop the detection'},
|
|
164
|
+
],
|
|
165
|
+
'default': DEFAULT_CONFIG['unmatched'],
|
|
166
|
+
'help': 'What to do with a detection whose class is not in the mapping'
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
'name': 'unmatched_label',
|
|
170
|
+
'label': 'Fallback Label',
|
|
171
|
+
'type': 'text',
|
|
172
|
+
'default': DEFAULT_CONFIG['unmatched_label'],
|
|
173
|
+
'showIf': {'unmatched': 'label'},
|
|
174
|
+
'help': 'Label given to detections that the mapping does not cover'
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
'name': 'detections_path',
|
|
178
|
+
'label': 'Detections Path',
|
|
179
|
+
'type': 'text',
|
|
180
|
+
'default': DEFAULT_CONFIG['detections_path'],
|
|
181
|
+
'help': f'Path to the detections list (e.g. "{DEFAULT_CONFIG["detections_path"]}")'
|
|
182
|
+
},
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
def __init__(self, node_id=None, name="class map"):
|
|
186
|
+
super().__init__(node_id, name)
|
|
187
|
+
# Parsing is cached against the raw config value: the map is re-read
|
|
188
|
+
# when the user edits it, not once per frame.
|
|
189
|
+
self._cached_raw: Any = None
|
|
190
|
+
self._cached_map: Dict[str, str] = {}
|
|
191
|
+
self._cached_lower: Dict[str, str] = {}
|
|
192
|
+
|
|
193
|
+
def _mapping(self) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
194
|
+
"""Return ``(mapping, lowercased mapping)``, re-parsing only on change."""
|
|
195
|
+
raw = self.config.get('mapping', self.DEFAULT_CONFIG['mapping'])
|
|
196
|
+
|
|
197
|
+
if raw != self._cached_raw or (raw and not self._cached_map):
|
|
198
|
+
mapping, problems = parse_class_map(raw)
|
|
199
|
+
self._cached_raw = raw
|
|
200
|
+
self._cached_map = mapping
|
|
201
|
+
self._cached_lower = {k.lower(): v for k, v in mapping.items()}
|
|
202
|
+
if problems:
|
|
203
|
+
self.report_error(
|
|
204
|
+
"Could not read class mapping entries: " + '; '.join(problems)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return self._cached_map, self._cached_lower
|
|
208
|
+
|
|
209
|
+
def _lookup(self, det: Dict[str, Any], mapping: Dict[str, str],
|
|
210
|
+
lower: Dict[str, str], match_on: str) -> Optional[str]:
|
|
211
|
+
"""Find the mapped label for one detection, or None when unmapped."""
|
|
212
|
+
if match_on in ('class_id', 'either'):
|
|
213
|
+
class_id = det.get(MessageKeys.CV.CLASS_ID)
|
|
214
|
+
if class_id is not None:
|
|
215
|
+
label = mapping.get(str(class_id).strip())
|
|
216
|
+
if label is not None:
|
|
217
|
+
return label
|
|
218
|
+
|
|
219
|
+
if match_on in ('class_name', 'either'):
|
|
220
|
+
class_name = det.get(MessageKeys.CV.CLASS_NAME)
|
|
221
|
+
if class_name is not None:
|
|
222
|
+
label = lower.get(str(class_name).strip().lower())
|
|
223
|
+
if label is not None:
|
|
224
|
+
return label
|
|
225
|
+
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
def _sync_supervision(self, payload: Dict[str, Any],
|
|
229
|
+
detections: List[Any], kept: List[bool]) -> None:
|
|
230
|
+
"""Mirror the new labels onto a live ``sv.Detections`` in the payload.
|
|
231
|
+
|
|
232
|
+
Supervision nodes prefer ``payload['sv']`` over the dict list, so an
|
|
233
|
+
annotator downstream would otherwise keep drawing the old names.
|
|
234
|
+
"""
|
|
235
|
+
sv_detections = payload.get('sv')
|
|
236
|
+
if sv_detections is None or not hasattr(sv_detections, 'xyxy'):
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
import numpy as np
|
|
241
|
+
|
|
242
|
+
if len(kept) != len(sv_detections):
|
|
243
|
+
# The list came from somewhere else; leave sv alone rather than
|
|
244
|
+
# pairing labels with the wrong boxes.
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
if not all(kept):
|
|
248
|
+
sv_detections = sv_detections[np.array(kept, dtype=bool)]
|
|
249
|
+
payload['sv'] = sv_detections
|
|
250
|
+
|
|
251
|
+
names = [
|
|
252
|
+
str(det.get(MessageKeys.CV.CLASS_NAME, ''))
|
|
253
|
+
for det in detections if isinstance(det, dict)
|
|
254
|
+
]
|
|
255
|
+
if len(names) == len(sv_detections) and isinstance(sv_detections.data, dict):
|
|
256
|
+
sv_detections.data['class_name'] = np.array(names)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.debug(f"Could not sync class names onto payload['sv']: {e}")
|
|
260
|
+
|
|
261
|
+
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
262
|
+
"""Relabel every detection at the configured path."""
|
|
263
|
+
mapping, lower = self._mapping()
|
|
264
|
+
unmatched = self.config.get('unmatched', self.DEFAULT_CONFIG['unmatched'])
|
|
265
|
+
|
|
266
|
+
if not mapping and unmatched != 'drop':
|
|
267
|
+
# Nothing configured yet - a pass-through beats an error on a node
|
|
268
|
+
# the user has only just dropped onto the canvas.
|
|
269
|
+
self.send(msg)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
path = self.config.get('detections_path', self.DEFAULT_CONFIG['detections_path'])
|
|
273
|
+
detections = self._get_nested_value(msg, path)
|
|
274
|
+
if not isinstance(detections, (list, tuple)):
|
|
275
|
+
self.send(msg)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
match_on = self.config.get('match_on', self.DEFAULT_CONFIG['match_on'])
|
|
279
|
+
fallback = self.config.get('unmatched_label', self.DEFAULT_CONFIG['unmatched_label'])
|
|
280
|
+
|
|
281
|
+
out: List[Any] = []
|
|
282
|
+
kept: List[bool] = []
|
|
283
|
+
changed = False
|
|
284
|
+
|
|
285
|
+
for det in detections:
|
|
286
|
+
if not isinstance(det, dict):
|
|
287
|
+
out.append(det)
|
|
288
|
+
kept.append(True)
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
label = self._lookup(det, mapping, lower, match_on)
|
|
292
|
+
|
|
293
|
+
if label is None:
|
|
294
|
+
if unmatched == 'drop':
|
|
295
|
+
kept.append(False)
|
|
296
|
+
changed = True
|
|
297
|
+
continue
|
|
298
|
+
if unmatched == 'label':
|
|
299
|
+
label = fallback
|
|
300
|
+
else:
|
|
301
|
+
out.append(det)
|
|
302
|
+
kept.append(True)
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
if det.get(MessageKeys.CV.CLASS_NAME) != label:
|
|
306
|
+
# Copy rather than mutate: the same detection dict may still be
|
|
307
|
+
# referenced by a message that branched upstream of this node.
|
|
308
|
+
det = dict(det)
|
|
309
|
+
det[MessageKeys.CV.CLASS_NAME] = label
|
|
310
|
+
changed = True
|
|
311
|
+
|
|
312
|
+
out.append(det)
|
|
313
|
+
kept.append(True)
|
|
314
|
+
|
|
315
|
+
if not changed:
|
|
316
|
+
self.send(msg)
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
self._set_nested_value(msg, path, out)
|
|
320
|
+
|
|
321
|
+
payload = msg.get(MessageKeys.PAYLOAD)
|
|
322
|
+
if isinstance(payload, dict):
|
|
323
|
+
# Only when the list we just wrote IS the payload's detections -
|
|
324
|
+
# a node pointed at payload.tracks must not touch detection_count.
|
|
325
|
+
if payload.get(MessageKeys.CV.DETECTIONS) is out:
|
|
326
|
+
payload[MessageKeys.CV.DETECTION_COUNT] = len(out)
|
|
327
|
+
self._sync_supervision(payload, out, kept)
|
|
328
|
+
|
|
329
|
+
self.send(msg)
|
{pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py
RENAMED
|
@@ -9,6 +9,7 @@ import logging
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
from typing import Any, Dict, Optional
|
|
11
11
|
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
12
|
+
from pynode.nodes.detection_utils import CANONICAL_BBOX_FORMAT, normalized_detection
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
@@ -18,6 +19,11 @@ _info.add_header("Inputs")
|
|
|
18
19
|
_info.add_bullets(
|
|
19
20
|
("Input 0:", "Message with payload.predictions (from YOLO or Inference) and payload.image (base64)"),
|
|
20
21
|
)
|
|
22
|
+
_info.add_text(
|
|
23
|
+
"Boxes are read in whatever format the producer labelled them with "
|
|
24
|
+
"(payload.bbox_format, or bbox_format on the detection itself) and "
|
|
25
|
+
"converted to xyxy before drawing."
|
|
26
|
+
)
|
|
21
27
|
_info.add_header("Outputs")
|
|
22
28
|
_info.add_bullets(
|
|
23
29
|
("Output 0:", "Annotated image with bounding boxes and labels drawn"),
|
|
@@ -264,16 +270,21 @@ class DrawPredictionsNode(BaseNode):
|
|
|
264
270
|
show_confidence = self.get_config_bool('show_confidence', True)
|
|
265
271
|
show_class = self.get_config_bool('show_class', True)
|
|
266
272
|
|
|
273
|
+
# Producers label their boxes differently (Ultralytics emits xyxy, the
|
|
274
|
+
# ONNX engine centre-based boxes under top_class/top_score), so take the
|
|
275
|
+
# payload-level format as the default and let each detection override it.
|
|
276
|
+
default_bbox_format = CANONICAL_BBOX_FORMAT
|
|
277
|
+
if isinstance(payload, dict):
|
|
278
|
+
default_bbox_format = payload.get(MessageKeys.CV.BBOX_FORMAT) or CANONICAL_BBOX_FORMAT
|
|
279
|
+
|
|
267
280
|
# Draw each detection/track
|
|
268
281
|
for i, det in enumerate(detections):
|
|
269
282
|
try:
|
|
270
|
-
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
class_name = det.get('class_name', 'unknown')
|
|
276
|
-
confidence = det.get('confidence', 0.0)
|
|
283
|
+
det = normalized_detection(det, default_bbox_format)
|
|
284
|
+
x1, y1, x2, y2 = det['bbox'] if det.get('bbox') is not None else (0, 0, 0, 0)
|
|
285
|
+
class_id = det['class_id']
|
|
286
|
+
class_name = det['class_name']
|
|
287
|
+
confidence = det['confidence']
|
|
277
288
|
track_id = det.get('track_id', None)
|
|
278
289
|
|
|
279
290
|
x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
|