pynode-flow 0.2.5__tar.gz → 0.2.6__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.5 → pynode_flow-0.2.6}/PKG-INFO +1 -1
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/_version.py +3 -3
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/slice_collector_node.py +247 -9
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/slice_image_node.py +124 -15
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VideoReaderNode/video_reader_node.py +8 -2
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/debug.js +13 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/nodes.js +64 -20
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/PKG-INFO +1 -1
- pynode_flow-0.2.6/pynode_flow.egg-info/scm_version.json +8 -0
- pynode_flow-0.2.5/pynode_flow.egg-info/scm_version.json +0 -8
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/.claude/skills/pynode-workflows/SKILL.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/.dockerignore +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/.github/workflows/ci.yml +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/.github/workflows/publish.yaml +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/.gitignore +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/DOCKER.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/Dockerfile +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/Dockerfile.cpu +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/INSTALL.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/LICENSE +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/MANIFEST.in +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docker-compose.yml +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docker-entrypoint.sh +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docs/CREATING_NODES.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docs/CUSTOM_NODES.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docs/DOCKER.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docs/EXTENSIBILITY.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/docs/UI_COMPONENTS.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/examples/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/examples/camera_workflow.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/examples/camera_yolo_workflow.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/install_nodes.bat +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/install_nodes.sh +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/__main__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/_perf.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/helpers.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/nodes.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/services.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/sse.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/uploads.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/api/workflows.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/config.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/install_nodes.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/main.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/node_registry.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BatchNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BatchNode/batch_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/BatchNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CameraNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CameraNode/camera_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ChangeNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ChangeNode/change_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ChangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ConfidenceFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ControlSliderNode/control_slider_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CounterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CounterNode/counter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CropNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CropNode/crop_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/CropNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DebugNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DebugNode/debug_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DebugNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DelayNode/delay_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DelayNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ErrorNode/error_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ErrorNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FilterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FilterNode/filter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FunctionNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FunctionNode/function_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/FunctionNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/GateNode/gate_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/GateNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/ImageWriterNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/inference_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InferenceNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InjectNode/inject_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/InjectNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/JoinNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/JoinNode/join_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/JoinNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LabelFilterNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LinkNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LinkNode/link_bus.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LinkNode/link_in_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LinkNode/link_out_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/LinkNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MQTTNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MQTTNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/blend_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/blur_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/color_space_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/colormap_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/contours_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/draw_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/fft_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/histogram_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/in_range_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/morphology_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/paste_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/perspective_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/resize_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/rotate_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/template_match_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/OpenCV/threshold_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RangeNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RangeNode/range_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RangeNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RateProbeNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RateProbeNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SliceNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/interop/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/ndjson_protocol.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/nodered_snippets.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/tcp_in_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/tcp_out_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/udp_in_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/udp_out_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/udp_probe.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SocketNode/udp_protocol.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SplitNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SplitNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SplitNode/split_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SwitchNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SwitchNode/switch_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SyncNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/SyncNode/sync_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/TemplateNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/TemplateNode/template_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/TrackerNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/TrackerNode/supervision_tracker_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/TrackerNode/tracker_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/UltralyticsNode/ultralytics_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/UnknownNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VLLMNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/WebhookNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/base_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/image_utils.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/info.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/mDNSNode/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/mDNSNode/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/messages.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/nodes/pacing.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/server.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/01-hello-world.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/02-function-transform.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/03-switch-routing.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/04-flow-control.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/05-webcam-viewer.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/06-webcam-yolo.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/07-blur-people.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/16-socket-bridge.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/17-slider-crop.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/examples/manifest.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/images/favicon4.png +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/index.html +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/README.md +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/auth.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/clipboard.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/config.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/connections.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/events.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/history.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/main.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/minimap.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/mqtt-services.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/palette.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/properties.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/selection.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/state.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/ui-utils.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/viewport.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/workflow.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/js/workflows.js +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/static/style.css +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/workflow_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode/workflow_manager.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/SOURCES.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/dependency_links.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/entry_points.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/requires.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/scm_file_list.json +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pynode_flow.egg-info/top_level.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/pyproject.toml +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/requirements.txt +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/setup.bat +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/setup.cfg +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/setup.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/setup.sh +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/__init__.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/conftest.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_api_auth.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_api_errors.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_api_nodes.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_api_security.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_api_workflows.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_base_node_messaging.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_camera_node_backend.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_concurrency.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_config.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_control_slider_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_counter_sse.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_crop_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_debug_workflow_scope.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_delay_rate_limit.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_device_detection.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_frame_pacing.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_function_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_image_upload_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_link_nodes.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_message_reader_pickle.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_models_dir.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_mqtt_services.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_node_registration.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_node_types_api.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_persistence.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_range_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_send_copy_semantics.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_serialization.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_socket_nodes.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_video_reader_node.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_workflow_engine.py +0 -0
- {pynode_flow-0.2.5 → pynode_flow-0.2.6}/tests/test_workflow_roundtrip.py +0 -0
|
@@ -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.6'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 6)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gfd6285bef'
|
|
@@ -6,12 +6,20 @@ Designed to work with SliceImageNode in a slice-detect-merge workflow.
|
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
import time
|
|
8
8
|
from typing import Any, Dict, List, Optional
|
|
9
|
+
import numpy as np
|
|
9
10
|
from pynode.nodes.base_node import BaseNode, Info, MessageKeys
|
|
10
11
|
|
|
12
|
+
try:
|
|
13
|
+
import cv2
|
|
14
|
+
_HAS_CV2 = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
cv2 = None
|
|
17
|
+
_HAS_CV2 = False
|
|
18
|
+
|
|
11
19
|
_info = Info()
|
|
12
20
|
_info.add_text("Collects detection predictions from multiple image slices sent as separate messages, then merges them into a unified result.")
|
|
13
21
|
_info.add_header("Inputs")
|
|
14
|
-
_info.add_bullets(("Input 0:", "
|
|
22
|
+
_info.add_bullets(("Input 0:", "Slice predictions from SliceImageNode. Auto-detects 'array' mode (all slices in one message) or 'split' mode (separate messages)."))
|
|
15
23
|
_info.add_header("Outputs")
|
|
16
24
|
_info.add_bullets(
|
|
17
25
|
("Output 0:", "Merged predictions after all slices collected and NMS applied"),
|
|
@@ -22,9 +30,11 @@ _info.add_bullets(
|
|
|
22
30
|
("NMS IoU Threshold:", "Threshold for Non-Maximum Suppression"),
|
|
23
31
|
("Match Metric:", "IoU or IoS for duplicate detection matching"),
|
|
24
32
|
("Class Agnostic NMS:", "Apply NMS across all classes vs per-class"),
|
|
33
|
+
("Reconstruct Image from Slices:", "Rebuild the original parent image by stitching the collected tiles back together"),
|
|
34
|
+
("Draw Slice Bounds:", "Draw each slice's rectangle on the reconstructed image to visualize tile placement and overlap"),
|
|
25
35
|
)
|
|
26
36
|
_info.add_header("Usage")
|
|
27
|
-
_info.add_text("
|
|
37
|
+
_info.add_text("Works with either SliceImageNode output mode. Split: SliceImageNode (split) → Split → Inference → SliceCollectorNode. Array: SliceImageNode (array) → SliceCollectorNode")
|
|
28
38
|
|
|
29
39
|
@dataclass(frozen=True)
|
|
30
40
|
class NodeKeys:
|
|
@@ -45,6 +55,9 @@ class NodeKeys:
|
|
|
45
55
|
MATCH_METRIC = 'match_metric'
|
|
46
56
|
CLASS_AGNOSTIC = 'class_agnostic'
|
|
47
57
|
TIMEOUT = 'timeout'
|
|
58
|
+
RECONSTRUCT_IMAGE = 'reconstruct_image'
|
|
59
|
+
DRAW_SLICE_BOUNDS = 'draw_slice_bounds'
|
|
60
|
+
SLICE_IMAGE = 'slice_image'
|
|
48
61
|
|
|
49
62
|
|
|
50
63
|
class SliceCollectorNode(BaseNode):
|
|
@@ -74,6 +87,8 @@ class SliceCollectorNode(BaseNode):
|
|
|
74
87
|
NodeKeys.NMS_THRESHOLD: 0.5,
|
|
75
88
|
NodeKeys.MATCH_METRIC: 'iou',
|
|
76
89
|
NodeKeys.CLASS_AGNOSTIC: False,
|
|
90
|
+
NodeKeys.RECONSTRUCT_IMAGE: False,
|
|
91
|
+
NodeKeys.DRAW_SLICE_BOUNDS: False,
|
|
77
92
|
MessageKeys.DROP_MESSAGES: False
|
|
78
93
|
}
|
|
79
94
|
|
|
@@ -105,6 +120,19 @@ class SliceCollectorNode(BaseNode):
|
|
|
105
120
|
'label': 'Class Agnostic NMS',
|
|
106
121
|
'type': 'checkbox',
|
|
107
122
|
'default': DEFAULT_CONFIG[NodeKeys.CLASS_AGNOSTIC]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
'name': NodeKeys.RECONSTRUCT_IMAGE,
|
|
126
|
+
'label': 'Reconstruct Image from Slices',
|
|
127
|
+
'type': 'checkbox',
|
|
128
|
+
'default': DEFAULT_CONFIG[NodeKeys.RECONSTRUCT_IMAGE]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
'name': NodeKeys.DRAW_SLICE_BOUNDS,
|
|
132
|
+
'label': 'Draw Slice Bounds',
|
|
133
|
+
'type': 'checkbox',
|
|
134
|
+
'default': DEFAULT_CONFIG[NodeKeys.DRAW_SLICE_BOUNDS],
|
|
135
|
+
'showIf': {NodeKeys.RECONSTRUCT_IMAGE: True}
|
|
108
136
|
}
|
|
109
137
|
]
|
|
110
138
|
|
|
@@ -227,11 +255,14 @@ class SliceCollectorNode(BaseNode):
|
|
|
227
255
|
del self._collections[k]
|
|
228
256
|
|
|
229
257
|
def _process_collection(self, collection_id: str) -> Optional[Dict]:
|
|
230
|
-
"""Process a complete collection and return merged result."""
|
|
258
|
+
"""Process a complete collection (by id) and return merged result."""
|
|
231
259
|
collection = self._collections.get(collection_id)
|
|
232
260
|
if not collection:
|
|
233
261
|
return None
|
|
234
|
-
|
|
262
|
+
return self._merge_collection(collection)
|
|
263
|
+
|
|
264
|
+
def _merge_collection(self, collection: Dict) -> Optional[Dict]:
|
|
265
|
+
"""Merge all slices in a collection dict and return the merged result."""
|
|
235
266
|
nms_threshold = self.get_config_float(NodeKeys.NMS_THRESHOLD, self.DEFAULT_CONFIG[NodeKeys.NMS_THRESHOLD])
|
|
236
267
|
match_metric = self.config.get(NodeKeys.MATCH_METRIC, self.DEFAULT_CONFIG[NodeKeys.MATCH_METRIC])
|
|
237
268
|
class_agnostic = self.get_config_bool(NodeKeys.CLASS_AGNOSTIC, self.DEFAULT_CONFIG[NodeKeys.CLASS_AGNOSTIC])
|
|
@@ -263,7 +294,7 @@ class SliceCollectorNode(BaseNode):
|
|
|
263
294
|
# Sort by confidence
|
|
264
295
|
final_detections = sorted(final_detections, key=lambda x: x.get(MessageKeys.CV.CONFIDENCE, 0), reverse=True)
|
|
265
296
|
|
|
266
|
-
|
|
297
|
+
result = {
|
|
267
298
|
MessageKeys.CV.DETECTIONS: final_detections,
|
|
268
299
|
MessageKeys.CV.DETECTION_COUNT: len(final_detections),
|
|
269
300
|
NodeKeys.ORIGINAL_WIDTH: collection.get(NodeKeys.ORIGINAL_WIDTH, 0),
|
|
@@ -272,12 +303,204 @@ class SliceCollectorNode(BaseNode):
|
|
|
272
303
|
MessageKeys.CV.BBOX_FORMAT: 'xyxy',
|
|
273
304
|
MessageKeys.IMAGE.PATH: collection.get(MessageKeys.IMAGE.PATH)
|
|
274
305
|
}
|
|
306
|
+
|
|
307
|
+
# Optionally rebuild the parent image from the collected tiles
|
|
308
|
+
if self.get_config_bool(NodeKeys.RECONSTRUCT_IMAGE, self.DEFAULT_CONFIG[NodeKeys.RECONSTRUCT_IMAGE]):
|
|
309
|
+
reconstructed = self._reconstruct_image(collection)
|
|
310
|
+
if reconstructed is not None:
|
|
311
|
+
result[MessageKeys.IMAGE.PATH] = reconstructed
|
|
312
|
+
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
def _reconstruct_image(self, collection: Dict) -> Optional[Any]:
|
|
316
|
+
"""
|
|
317
|
+
Rebuild the original parent image by stitching the collected slice
|
|
318
|
+
tiles back into a single canvas sized to the original image.
|
|
319
|
+
|
|
320
|
+
Tiles are placed using their slice bbox (or offset). Overlapping
|
|
321
|
+
regions are overwritten; if a full-image slice is present it is used
|
|
322
|
+
as the base before overlaying the tiles.
|
|
323
|
+
"""
|
|
324
|
+
if not _HAS_CV2:
|
|
325
|
+
self.report_error("OpenCV (cv2) is required to reconstruct the image from slices")
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
width = collection.get(NodeKeys.ORIGINAL_WIDTH, 0)
|
|
329
|
+
height = collection.get(NodeKeys.ORIGINAL_HEIGHT, 0)
|
|
330
|
+
|
|
331
|
+
if width <= 0 or height <= 0:
|
|
332
|
+
self.report_error("Cannot reconstruct image: unknown original dimensions")
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
canvas = None
|
|
336
|
+
encode_format = None
|
|
337
|
+
tile_bounds: List[tuple] = []
|
|
338
|
+
|
|
339
|
+
# Place the full image (if any) first, then overlay tiles on top
|
|
340
|
+
ordered_slices = sorted(
|
|
341
|
+
collection[NodeKeys.SLICES].values(),
|
|
342
|
+
key=lambda s: 0 if s.get(NodeKeys.IS_FULL_IMAGE, False) else 1
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
for slice_data in ordered_slices:
|
|
346
|
+
img_payload = slice_data.get(NodeKeys.SLICE_IMAGE)
|
|
347
|
+
if img_payload is None:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
slice_img, fmt = self.decode_image(img_payload)
|
|
351
|
+
if slice_img is None:
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
if encode_format is None:
|
|
355
|
+
encode_format = fmt
|
|
356
|
+
|
|
357
|
+
if canvas is None:
|
|
358
|
+
if slice_img.ndim == 3:
|
|
359
|
+
canvas = np.zeros((height, width, slice_img.shape[2]), dtype=slice_img.dtype)
|
|
360
|
+
else:
|
|
361
|
+
canvas = np.zeros((height, width), dtype=slice_img.dtype)
|
|
362
|
+
|
|
363
|
+
sh, sw = slice_img.shape[:2]
|
|
364
|
+
is_full_image = slice_data.get(NodeKeys.IS_FULL_IMAGE, False)
|
|
365
|
+
|
|
366
|
+
if is_full_image:
|
|
367
|
+
x1, y1 = 0, 0
|
|
368
|
+
else:
|
|
369
|
+
bbox = slice_data.get(NodeKeys.SLICE_BBOX)
|
|
370
|
+
if bbox and len(bbox) >= 4:
|
|
371
|
+
x1, y1 = int(bbox[0]), int(bbox[1])
|
|
372
|
+
else:
|
|
373
|
+
offset = slice_data.get(NodeKeys.OFFSET, [0, 0])
|
|
374
|
+
x1, y1 = int(offset[0]), int(offset[1])
|
|
375
|
+
|
|
376
|
+
x1 = max(0, min(x1, width))
|
|
377
|
+
y1 = max(0, min(y1, height))
|
|
378
|
+
x2 = min(x1 + sw, width)
|
|
379
|
+
y2 = min(y1 + sh, height)
|
|
380
|
+
|
|
381
|
+
if x2 <= x1 or y2 <= y1:
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
canvas[y1:y2, x1:x2] = slice_img[0:(y2 - y1), 0:(x2 - x1)]
|
|
385
|
+
|
|
386
|
+
# Record tile bounds for optional overlay (exclude the full image)
|
|
387
|
+
if not is_full_image:
|
|
388
|
+
tile_bounds.append((x1, y1, x2, y2))
|
|
389
|
+
|
|
390
|
+
if canvas is None:
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
# Optionally draw the slice bounds so the overlap is visible
|
|
394
|
+
if self.get_config_bool(NodeKeys.DRAW_SLICE_BOUNDS, self.DEFAULT_CONFIG[NodeKeys.DRAW_SLICE_BOUNDS]):
|
|
395
|
+
self._draw_slice_bounds(canvas, tile_bounds)
|
|
396
|
+
|
|
397
|
+
return self.encode_image(canvas, encode_format)
|
|
398
|
+
|
|
399
|
+
def _draw_slice_bounds(self, canvas: Any, tile_bounds: List[tuple]):
|
|
400
|
+
"""
|
|
401
|
+
Draw a rectangle for each slice on the reconstructed canvas so the user
|
|
402
|
+
can see where each tile sits and how much adjacent tiles overlap.
|
|
403
|
+
"""
|
|
404
|
+
if not _HAS_CV2 or not tile_bounds:
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
# Cycle through distinct colors (BGR) so adjacent tiles are easy to tell apart
|
|
408
|
+
colors = [
|
|
409
|
+
(0, 255, 0), # green
|
|
410
|
+
(0, 0, 255), # red
|
|
411
|
+
(255, 0, 0), # blue
|
|
412
|
+
(0, 255, 255), # yellow
|
|
413
|
+
(255, 0, 255), # magenta
|
|
414
|
+
(255, 255, 0), # cyan
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
# Scale line thickness with image size for visibility on large images
|
|
418
|
+
max_dim = max(canvas.shape[0], canvas.shape[1])
|
|
419
|
+
thickness = max(1, max_dim // 500)
|
|
420
|
+
|
|
421
|
+
for i, (x1, y1, x2, y2) in enumerate(tile_bounds):
|
|
422
|
+
color = colors[i % len(colors)]
|
|
423
|
+
# Inset by half the thickness so edge tiles' borders stay visible
|
|
424
|
+
cv2.rectangle( # type: ignore[union-attr]
|
|
425
|
+
canvas,
|
|
426
|
+
(int(x1), int(y1)),
|
|
427
|
+
(int(x2) - 1, int(y2) - 1),
|
|
428
|
+
color,
|
|
429
|
+
thickness
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _handle_array(self, msg: Dict[str, Any], slices: List[Any]):
|
|
433
|
+
"""
|
|
434
|
+
Handle 'array' output mode from SliceImageNode where every slice arrives
|
|
435
|
+
together in a single message as a list. All slices are available at once,
|
|
436
|
+
so the collection is built and merged immediately (no buffering/timeout).
|
|
437
|
+
"""
|
|
438
|
+
collection: Dict[str, Any] = {
|
|
439
|
+
NodeKeys.SLICES: {},
|
|
440
|
+
NodeKeys.ORIGINAL_WIDTH: msg.get(NodeKeys.ORIGINAL_WIDTH, 0),
|
|
441
|
+
NodeKeys.ORIGINAL_HEIGHT: msg.get(NodeKeys.ORIGINAL_HEIGHT, 0),
|
|
442
|
+
MessageKeys.IMAGE.PATH: None
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
for i, item in enumerate(slices):
|
|
446
|
+
if not isinstance(item, dict):
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
item_payload = item.get(MessageKeys.PAYLOAD, {})
|
|
450
|
+
if not isinstance(item_payload, dict):
|
|
451
|
+
item_payload = {}
|
|
452
|
+
|
|
453
|
+
offset = item.get(NodeKeys.OFFSET, item_payload.get(NodeKeys.OFFSET, [0, 0]))
|
|
454
|
+
slice_bbox = item.get(NodeKeys.BBOX,
|
|
455
|
+
item.get(NodeKeys.SLICE_BBOX, item_payload.get(NodeKeys.BBOX)))
|
|
456
|
+
is_full_image = item.get(NodeKeys.IS_FULL_IMAGE, item_payload.get(NodeKeys.IS_FULL_IMAGE, False))
|
|
457
|
+
slice_index = item.get(NodeKeys.SLICE_INDEX, i)
|
|
458
|
+
detections = item_payload.get(MessageKeys.CV.DETECTIONS,
|
|
459
|
+
item.get(MessageKeys.CV.DETECTIONS, []))
|
|
460
|
+
image = item_payload.get(MessageKeys.IMAGE.PATH, item.get(MessageKeys.IMAGE.PATH))
|
|
461
|
+
|
|
462
|
+
ow = item.get(NodeKeys.ORIGINAL_WIDTH, item_payload.get(NodeKeys.ORIGINAL_WIDTH, 0))
|
|
463
|
+
oh = item.get(NodeKeys.ORIGINAL_HEIGHT, item_payload.get(NodeKeys.ORIGINAL_HEIGHT, 0))
|
|
464
|
+
if ow > 0:
|
|
465
|
+
collection[NodeKeys.ORIGINAL_WIDTH] = ow
|
|
466
|
+
if oh > 0:
|
|
467
|
+
collection[NodeKeys.ORIGINAL_HEIGHT] = oh
|
|
468
|
+
|
|
469
|
+
collection[NodeKeys.SLICES][slice_index] = {
|
|
470
|
+
MessageKeys.CV.DETECTIONS: detections,
|
|
471
|
+
NodeKeys.OFFSET: offset,
|
|
472
|
+
NodeKeys.SLICE_BBOX: slice_bbox,
|
|
473
|
+
NodeKeys.SLICE_IMAGE: image,
|
|
474
|
+
NodeKeys.IS_FULL_IMAGE: is_full_image
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if is_full_image and image is not None:
|
|
478
|
+
collection[MessageKeys.IMAGE.PATH] = image
|
|
479
|
+
|
|
480
|
+
if not collection[NodeKeys.SLICES]:
|
|
481
|
+
self.report_error("No slices found in array-mode message")
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
result = self._merge_collection(collection)
|
|
485
|
+
if result:
|
|
486
|
+
out_msg = msg.copy()
|
|
487
|
+
out_msg[MessageKeys.PAYLOAD] = result
|
|
488
|
+
out_msg[MessageKeys.TOPIC] = msg.get(MessageKeys.TOPIC, 'merged_predictions')
|
|
489
|
+
out_msg.pop('parts', None)
|
|
490
|
+
self.send(out_msg)
|
|
275
491
|
|
|
276
492
|
def on_input(self, msg: Dict[str, Any], input_index: int = 0):
|
|
277
493
|
"""
|
|
278
494
|
Collect slice predictions and merge when complete.
|
|
279
495
|
|
|
280
|
-
|
|
496
|
+
Works seamlessly with either SliceImageNode output mode:
|
|
497
|
+
|
|
498
|
+
- 'array' mode: all slices arrive together in one message where
|
|
499
|
+
'payload' is a list of slice dicts. Merged immediately.
|
|
500
|
+
- 'split' mode: each slice arrives as its own message (buffered by
|
|
501
|
+
parent id via 'parts', merged once all slices are collected).
|
|
502
|
+
|
|
503
|
+
Expected 'split' msg format (from Split of SliceImageNode, processed by YOLO):
|
|
281
504
|
{
|
|
282
505
|
'payload': {
|
|
283
506
|
'image': ...,
|
|
@@ -299,6 +522,14 @@ class SliceCollectorNode(BaseNode):
|
|
|
299
522
|
self._cleanup_expired()
|
|
300
523
|
|
|
301
524
|
payload = msg.get(MessageKeys.PAYLOAD, {})
|
|
525
|
+
|
|
526
|
+
# Auto-detect output mode from SliceImageNode:
|
|
527
|
+
# - 'array' mode: payload is a list of all slices in one message
|
|
528
|
+
# - 'split' mode: payload is a single slice dict across many messages
|
|
529
|
+
if isinstance(payload, list):
|
|
530
|
+
self._handle_array(msg, payload)
|
|
531
|
+
return
|
|
532
|
+
|
|
302
533
|
parts = msg.get('parts', {})
|
|
303
534
|
|
|
304
535
|
# Get collection ID (from parts or generate one)
|
|
@@ -306,9 +537,14 @@ class SliceCollectorNode(BaseNode):
|
|
|
306
537
|
expected_count = parts.get('count', 1)
|
|
307
538
|
slice_index = parts.get('index', msg.get(NodeKeys.SLICE_INDEX, 0))
|
|
308
539
|
|
|
309
|
-
# Get slice metadata (check message level first, then payload)
|
|
310
|
-
|
|
311
|
-
|
|
540
|
+
# Get slice metadata (check message level first, then payload).
|
|
541
|
+
# SliceImageNode emits these at message level as 'slice_offset' / 'slice_bbox'.
|
|
542
|
+
offset = msg.get(NodeKeys.SLICE_OFFSET,
|
|
543
|
+
msg.get(NodeKeys.OFFSET,
|
|
544
|
+
payload.get(NodeKeys.OFFSET, [0, 0]) if isinstance(payload, dict) else [0, 0]))
|
|
545
|
+
slice_bbox = msg.get(NodeKeys.SLICE_BBOX,
|
|
546
|
+
msg.get(NodeKeys.BBOX,
|
|
547
|
+
payload.get(NodeKeys.BBOX, None) if isinstance(payload, dict) else None))
|
|
312
548
|
is_full_image = msg.get(NodeKeys.IS_FULL_IMAGE, payload.get(NodeKeys.IS_FULL_IMAGE, False) if isinstance(payload, dict) else False)
|
|
313
549
|
original_width = msg.get(NodeKeys.ORIGINAL_WIDTH, payload.get(NodeKeys.ORIGINAL_WIDTH, 0) if isinstance(payload, dict) else 0)
|
|
314
550
|
original_height = msg.get(NodeKeys.ORIGINAL_HEIGHT, payload.get(NodeKeys.ORIGINAL_HEIGHT, 0) if isinstance(payload, dict) else 0)
|
|
@@ -339,6 +575,8 @@ class SliceCollectorNode(BaseNode):
|
|
|
339
575
|
collection[NodeKeys.SLICES][slice_index] = {
|
|
340
576
|
MessageKeys.CV.DETECTIONS: detections,
|
|
341
577
|
NodeKeys.OFFSET: offset,
|
|
578
|
+
NodeKeys.SLICE_BBOX: slice_bbox,
|
|
579
|
+
NodeKeys.SLICE_IMAGE: image,
|
|
342
580
|
NodeKeys.IS_FULL_IMAGE: is_full_image
|
|
343
581
|
}
|
|
344
582
|
|
|
@@ -26,7 +26,9 @@ _info.add_header("Configuration")
|
|
|
26
26
|
_info.add_bullets(
|
|
27
27
|
("Output Mode:", "Array (all slices in one message) or Split (separate messages)"),
|
|
28
28
|
("Auto Slice:", "Automatically calculate slice size based on image resolution"),
|
|
29
|
-
("Slice
|
|
29
|
+
("Slice By:", "Tile Size (pixels) or Tile Count (nx, ny)"),
|
|
30
|
+
("Slice Width/Height:", "Manual slice dimensions in pixels (Tile Size mode)"),
|
|
31
|
+
("Num Slices X/Y:", "Number of columns/rows of tiles (Tile Count mode)"),
|
|
30
32
|
("Overlap Ratios:", "Overlap between adjacent slices (0-1)"),
|
|
31
33
|
("Include Full Image:", "Also include the original full image in output"),
|
|
32
34
|
)
|
|
@@ -53,6 +55,9 @@ class SliceImageNode(BaseNode):
|
|
|
53
55
|
DEFAULT_CONFIG = {
|
|
54
56
|
'slice_width': 640,
|
|
55
57
|
'slice_height': 640,
|
|
58
|
+
'num_slices_x': 2,
|
|
59
|
+
'num_slices_y': 2,
|
|
60
|
+
'slice_mode': 'size',
|
|
56
61
|
'overlap_width_ratio': 0.2,
|
|
57
62
|
'overlap_height_ratio': 0.2,
|
|
58
63
|
'auto_slice': False,
|
|
@@ -79,19 +84,44 @@ class SliceImageNode(BaseNode):
|
|
|
79
84
|
'type': 'checkbox',
|
|
80
85
|
'default': False
|
|
81
86
|
},
|
|
87
|
+
{
|
|
88
|
+
'name': 'slice_mode',
|
|
89
|
+
'label': 'Slice By',
|
|
90
|
+
'type': 'select',
|
|
91
|
+
'options': [
|
|
92
|
+
{'value': 'size', 'label': 'Tile Size (pixels)'},
|
|
93
|
+
{'value': 'count', 'label': 'Tile Count (nx, ny)'}
|
|
94
|
+
],
|
|
95
|
+
'default': 'size',
|
|
96
|
+
'showIf': {'auto_slice': False}
|
|
97
|
+
},
|
|
82
98
|
{
|
|
83
99
|
'name': 'slice_width',
|
|
84
100
|
'label': 'Slice Width (pixels)',
|
|
85
101
|
'type': 'number',
|
|
86
102
|
'default': 640,
|
|
87
|
-
'showIf': {'auto_slice': False}
|
|
103
|
+
'showIf': {'auto_slice': False, 'slice_mode': 'size'}
|
|
88
104
|
},
|
|
89
105
|
{
|
|
90
106
|
'name': 'slice_height',
|
|
91
107
|
'label': 'Slice Height (pixels)',
|
|
92
108
|
'type': 'number',
|
|
93
109
|
'default': 640,
|
|
94
|
-
'showIf': {'auto_slice': False}
|
|
110
|
+
'showIf': {'auto_slice': False, 'slice_mode': 'size'}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
'name': 'num_slices_x',
|
|
114
|
+
'label': 'Num Slices X (columns)',
|
|
115
|
+
'type': 'number',
|
|
116
|
+
'default': 2,
|
|
117
|
+
'showIf': {'auto_slice': False, 'slice_mode': 'count'}
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
'name': 'num_slices_y',
|
|
121
|
+
'label': 'Num Slices Y (rows)',
|
|
122
|
+
'type': 'number',
|
|
123
|
+
'default': 2,
|
|
124
|
+
'showIf': {'auto_slice': False, 'slice_mode': 'count'}
|
|
95
125
|
},
|
|
96
126
|
{
|
|
97
127
|
'name': 'overlap_width_ratio',
|
|
@@ -231,6 +261,65 @@ class SliceImageNode(BaseNode):
|
|
|
231
261
|
|
|
232
262
|
return slice_bboxes
|
|
233
263
|
|
|
264
|
+
def _get_slice_bboxes_by_count(
|
|
265
|
+
self,
|
|
266
|
+
image_height: int,
|
|
267
|
+
image_width: int,
|
|
268
|
+
num_slices_x: int,
|
|
269
|
+
num_slices_y: int,
|
|
270
|
+
overlap_height_ratio: float,
|
|
271
|
+
overlap_width_ratio: float
|
|
272
|
+
) -> List[List[int]]:
|
|
273
|
+
"""
|
|
274
|
+
Generate slice bounding boxes for a fixed grid of nx x ny tiles.
|
|
275
|
+
|
|
276
|
+
Tile size is derived from the requested overlap ratio so that adjacent
|
|
277
|
+
tiles overlap by that fraction, while guaranteeing exactly
|
|
278
|
+
num_slices_x * num_slices_y tiles that together cover the whole image.
|
|
279
|
+
|
|
280
|
+
Returns list of [x_min, y_min, x_max, y_max] for each slice.
|
|
281
|
+
"""
|
|
282
|
+
if overlap_height_ratio >= 1.0 or overlap_width_ratio >= 1.0:
|
|
283
|
+
raise ValueError("Overlap ratio must be less than 1.0")
|
|
284
|
+
|
|
285
|
+
nx = max(1, int(num_slices_x))
|
|
286
|
+
ny = max(1, int(num_slices_y))
|
|
287
|
+
|
|
288
|
+
x_starts, slice_width = self._axis_starts(image_width, nx, overlap_width_ratio)
|
|
289
|
+
y_starts, slice_height = self._axis_starts(image_height, ny, overlap_height_ratio)
|
|
290
|
+
|
|
291
|
+
slice_bboxes = []
|
|
292
|
+
for y_min in y_starts:
|
|
293
|
+
for x_min in x_starts:
|
|
294
|
+
slice_bboxes.append([
|
|
295
|
+
x_min,
|
|
296
|
+
y_min,
|
|
297
|
+
min(x_min + slice_width, image_width),
|
|
298
|
+
min(y_min + slice_height, image_height)
|
|
299
|
+
])
|
|
300
|
+
|
|
301
|
+
return slice_bboxes
|
|
302
|
+
|
|
303
|
+
def _axis_starts(self, length: int, count: int, overlap_ratio: float) -> Tuple[List[int], int]:
|
|
304
|
+
"""
|
|
305
|
+
Compute evenly spaced tile start positions along one axis.
|
|
306
|
+
|
|
307
|
+
Returns (list of start coordinates, tile size in pixels) for ``count``
|
|
308
|
+
tiles overlapping by ``overlap_ratio`` that span ``length`` pixels.
|
|
309
|
+
"""
|
|
310
|
+
if count <= 1:
|
|
311
|
+
return [0], length
|
|
312
|
+
|
|
313
|
+
# size such that: (count-1) * size * (1 - overlap) + size = length
|
|
314
|
+
denom = (count - 1) * (1.0 - overlap_ratio) + 1.0
|
|
315
|
+
slice_size = int(round(length / denom)) if denom > 0 else length
|
|
316
|
+
slice_size = max(1, min(slice_size, length))
|
|
317
|
+
|
|
318
|
+
last_start = length - slice_size
|
|
319
|
+
starts = [int(round(i * last_start / (count - 1))) for i in range(count)]
|
|
320
|
+
|
|
321
|
+
return starts, slice_size
|
|
322
|
+
|
|
234
323
|
def _slice_image(self, image: np.ndarray) -> List[Dict[str, Any]]:
|
|
235
324
|
"""
|
|
236
325
|
Slice the image into tiles based on configuration.
|
|
@@ -253,21 +342,41 @@ class SliceImageNode(BaseNode):
|
|
|
253
342
|
}]
|
|
254
343
|
overlap_width_ratio = x_overlap / slice_width if slice_width > 0 else 0.2
|
|
255
344
|
overlap_height_ratio = y_overlap / slice_height if slice_height > 0 else 0.2
|
|
345
|
+
slice_bboxes = self._get_slice_bboxes(
|
|
346
|
+
image_height=height,
|
|
347
|
+
image_width=width,
|
|
348
|
+
slice_height=slice_height,
|
|
349
|
+
slice_width=slice_width,
|
|
350
|
+
overlap_height_ratio=overlap_height_ratio,
|
|
351
|
+
overlap_width_ratio=overlap_width_ratio
|
|
352
|
+
)
|
|
256
353
|
else:
|
|
257
|
-
|
|
258
|
-
slice_height = self.get_config_int('slice_height', 640)
|
|
354
|
+
slice_mode = self.config.get('slice_mode', 'size')
|
|
259
355
|
overlap_width_ratio = self.get_config_float('overlap_width_ratio', 0.2)
|
|
260
356
|
overlap_height_ratio = self.get_config_float('overlap_height_ratio', 0.2)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
357
|
+
|
|
358
|
+
if slice_mode == 'count':
|
|
359
|
+
num_slices_x = self.get_config_int('num_slices_x', 2)
|
|
360
|
+
num_slices_y = self.get_config_int('num_slices_y', 2)
|
|
361
|
+
slice_bboxes = self._get_slice_bboxes_by_count(
|
|
362
|
+
image_height=height,
|
|
363
|
+
image_width=width,
|
|
364
|
+
num_slices_x=num_slices_x,
|
|
365
|
+
num_slices_y=num_slices_y,
|
|
366
|
+
overlap_height_ratio=overlap_height_ratio,
|
|
367
|
+
overlap_width_ratio=overlap_width_ratio
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
slice_width = self.get_config_int('slice_width', 640)
|
|
371
|
+
slice_height = self.get_config_int('slice_height', 640)
|
|
372
|
+
slice_bboxes = self._get_slice_bboxes(
|
|
373
|
+
image_height=height,
|
|
374
|
+
image_width=width,
|
|
375
|
+
slice_height=slice_height,
|
|
376
|
+
slice_width=slice_width,
|
|
377
|
+
overlap_height_ratio=overlap_height_ratio,
|
|
378
|
+
overlap_width_ratio=overlap_width_ratio
|
|
379
|
+
)
|
|
271
380
|
|
|
272
381
|
slices = []
|
|
273
382
|
for bbox in slice_bboxes:
|
|
@@ -27,7 +27,9 @@ _info.add_bullets(
|
|
|
27
27
|
_info.add_header("Transport Controls")
|
|
28
28
|
_info.add_bullets(
|
|
29
29
|
("⏮ Step back:", "Show the previous frame (while paused/stopped)."),
|
|
30
|
-
("
|
|
30
|
+
("▶/⏸ Play/Pause:", "Toggle playback from the current position. The icon "
|
|
31
|
+
"shows what the next click will do: ▶ while paused/"
|
|
32
|
+
"stopped, ⏸ while playing."),
|
|
31
33
|
("⏹ Stop:", "Halt playback and seek back to the first frame."),
|
|
32
34
|
("⏭ Step forward:", "Show the next frame (while paused/stopped)."),
|
|
33
35
|
("Progress bar:", "Drag the slider along the bottom of the node to scrub "
|
|
@@ -76,7 +78,11 @@ class VideoReaderNode(BaseNode):
|
|
|
76
78
|
ui_component_config = {
|
|
77
79
|
'buttons': [
|
|
78
80
|
{'icon': '⏮', 'action': 'step_prev', 'title': 'Step back one frame'},
|
|
79
|
-
|
|
81
|
+
# activeIcon/activeTitle are swapped in by the frontend while the
|
|
82
|
+
# node reports playing=true, so the button always shows the action
|
|
83
|
+
# the next click will take.
|
|
84
|
+
{'icon': '▶', 'action': 'play_pause', 'title': 'Play',
|
|
85
|
+
'activeIcon': '⏸', 'activeTitle': 'Pause'},
|
|
80
86
|
{'icon': '⏹', 'action': 'stop', 'title': 'Stop (back to first frame)'},
|
|
81
87
|
{'icon': '⏭', 'action': 'step_next', 'title': 'Step forward one frame'},
|
|
82
88
|
],
|
|
@@ -89,6 +89,7 @@ export function updateCounterDisplay(nodeId, displayText) {
|
|
|
89
89
|
export function updateVideoPosition(nodeId, data) {
|
|
90
90
|
const posEl = document.getElementById(`transport-pos-${nodeId}`);
|
|
91
91
|
const sliderEl = document.getElementById(`transport-progress-${nodeId}`);
|
|
92
|
+
const playBtn = document.getElementById(`transport-btn-${nodeId}-play_pause`);
|
|
92
93
|
const total = data.total > 0 ? data.total : 0;
|
|
93
94
|
const frame = data.frame ?? 0;
|
|
94
95
|
if (posEl) {
|
|
@@ -107,6 +108,18 @@ export function updateVideoPosition(nodeId, data) {
|
|
|
107
108
|
sliderEl.max = total > 0 ? total - 1 : 0;
|
|
108
109
|
sliderEl.value = frame;
|
|
109
110
|
}
|
|
111
|
+
// Show the action the next click will take: pause while playing, play
|
|
112
|
+
// while paused/stopped. The icons come from the node's button config
|
|
113
|
+
// (see the transport-controls branch in nodes.js).
|
|
114
|
+
if (playBtn && playBtn.dataset.activeIcon) {
|
|
115
|
+
const playing = !!data.playing;
|
|
116
|
+
const icon = playing ? playBtn.dataset.activeIcon : playBtn.dataset.idleIcon;
|
|
117
|
+
const title = playing ? playBtn.dataset.activeTitle : playBtn.dataset.idleTitle;
|
|
118
|
+
if (playBtn.textContent !== icon) {
|
|
119
|
+
playBtn.textContent = icon;
|
|
120
|
+
}
|
|
121
|
+
playBtn.title = title;
|
|
122
|
+
}
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
export function updateImageViewer(nodeId, frameData) {
|