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.
Files changed (353) hide show
  1. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/Dockerfile +4 -2
  2. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/PKG-INFO +2 -2
  3. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/README.md +1 -1
  4. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/_version.py +3 -3
  5. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/change_node.py +123 -24
  6. pynode_flow-0.2.8/pynode/nodes/ClassMapNode/__init__.py +3 -0
  7. pynode_flow-0.2.8/pynode/nodes/ClassMapNode/class_map_node.py +329 -0
  8. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +18 -7
  9. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +21 -1
  10. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +3 -1
  11. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/inference_node.py +17 -3
  12. pynode_flow-0.2.8/pynode/nodes/InferenceNode/requirements.txt +16 -0
  13. pynode_flow-0.2.8/pynode/nodes/TemplateNode/requirements.txt +1 -0
  14. pynode_flow-0.2.8/pynode/nodes/detection_utils.py +207 -0
  15. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/properties.js +26 -3
  16. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/style.css +31 -0
  17. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/PKG-INFO +2 -2
  18. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/SOURCES.txt +7 -0
  19. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/scm_file_list.json +7 -0
  20. pynode_flow-0.2.8/pynode_flow.egg-info/scm_version.json +8 -0
  21. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pyproject.toml +3 -0
  22. pynode_flow-0.2.8/tests/test_change_node.py +256 -0
  23. pynode_flow-0.2.8/tests/test_class_map_node.py +250 -0
  24. pynode_flow-0.2.8/tests/test_detection_utils.py +268 -0
  25. pynode_flow-0.2.7/pynode/nodes/InferenceNode/requirements.txt +0 -12
  26. pynode_flow-0.2.7/pynode_flow.egg-info/scm_version.json +0 -8
  27. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.claude/skills/pynode-workflows/SKILL.md +0 -0
  28. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.dockerignore +0 -0
  29. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.github/workflows/ci.yml +0 -0
  30. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.github/workflows/publish.yaml +0 -0
  31. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/.gitignore +0 -0
  32. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/DOCKER.md +0 -0
  33. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/Dockerfile.cpu +0 -0
  34. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/INSTALL.md +0 -0
  35. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/LICENSE +0 -0
  36. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/MANIFEST.in +0 -0
  37. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docker-compose.yml +0 -0
  38. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docker-entrypoint.sh +0 -0
  39. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/CREATING_NODES.md +0 -0
  40. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/CUSTOM_NODES.md +0 -0
  41. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/DOCKER.md +0 -0
  42. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/EXTENSIBILITY.md +0 -0
  43. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/docs/UI_COMPONENTS.md +0 -0
  44. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/README.md +0 -0
  45. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/camera_workflow.py +0 -0
  46. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/examples/camera_yolo_workflow.py +0 -0
  47. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/install_nodes.bat +0 -0
  48. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/install_nodes.sh +0 -0
  49. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/__init__.py +0 -0
  50. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/__main__.py +0 -0
  51. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/_perf.py +0 -0
  52. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/__init__.py +0 -0
  53. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/helpers.py +0 -0
  54. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/nodes.py +0 -0
  55. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/services.py +0 -0
  56. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/sse.py +0 -0
  57. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/uploads.py +0 -0
  58. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/api/workflows.py +0 -0
  59. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/config.py +0 -0
  60. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/install_nodes.py +0 -0
  61. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/main.py +0 -0
  62. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/node_registry.py +0 -0
  63. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/AutoSyncNode/auto_sync_node.py +0 -0
  64. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/__init__.py +0 -0
  65. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +0 -0
  66. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BBoxMetricsNode/requirements.txt +0 -0
  67. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/__init__.py +0 -0
  68. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/batch_node.py +0 -0
  69. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/BatchNode/requirements.txt +0 -0
  70. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/__init__.py +0 -0
  71. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/camera_node.py +0 -0
  72. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CameraNode/requirements.txt +0 -0
  73. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/__init__.py +0 -0
  74. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ChangeNode/requirements.txt +0 -0
  75. {pynode_flow-0.2.7/pynode/nodes/ConfidenceFilterNode → pynode_flow-0.2.8/pynode/nodes/ClassMapNode}/requirements.txt +0 -0
  76. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ConfidenceFilterNode/__init__.py +0 -0
  77. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +0 -0
  78. {pynode_flow-0.2.7/pynode/nodes/DelayNode → pynode_flow-0.2.8/pynode/nodes/ConfidenceFilterNode}/requirements.txt +0 -0
  79. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ControlSliderNode/control_slider_node.py +0 -0
  80. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CounterNode/__init__.py +0 -0
  81. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CounterNode/counter_node.py +0 -0
  82. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/__init__.py +0 -0
  83. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/crop_node.py +0 -0
  84. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/CropNode/requirements.txt +0 -0
  85. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/__init__.py +0 -0
  86. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/debug_node.py +0 -0
  87. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DebugNode/requirements.txt +0 -0
  88. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DelayNode/delay_node.py +0 -0
  89. {pynode_flow-0.2.7/pynode/nodes/ErrorNode → pynode_flow-0.2.8/pynode/nodes/DelayNode}/requirements.txt +0 -0
  90. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/__init__.py +0 -0
  91. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/DrawPredictionsNode/requirements.txt +0 -0
  92. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ErrorNode/error_node.py +0 -0
  93. {pynode_flow-0.2.7/pynode/nodes/FilterNode → pynode_flow-0.2.8/pynode/nodes/ErrorNode}/requirements.txt +0 -0
  94. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FilterNode/__init__.py +0 -0
  95. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FilterNode/filter_node.py +0 -0
  96. {pynode_flow-0.2.7/pynode/nodes/FunctionNode → pynode_flow-0.2.8/pynode/nodes/FilterNode}/requirements.txt +0 -0
  97. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/__init__.py +0 -0
  98. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/frame_source_node.py +0 -0
  99. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FrameSourceNode/requirements.txt +0 -0
  100. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FunctionNode/__init__.py +0 -0
  101. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/FunctionNode/function_node.py +0 -0
  102. {pynode_flow-0.2.7/pynode/nodes/GateNode → pynode_flow-0.2.8/pynode/nodes/FunctionNode}/requirements.txt +0 -0
  103. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/GateNode/gate_node.py +0 -0
  104. {pynode_flow-0.2.7/pynode/nodes/ImageWriterNode → pynode_flow-0.2.8/pynode/nodes/GateNode}/requirements.txt +0 -0
  105. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/__init__.py +0 -0
  106. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/image_format_node.py +0 -0
  107. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageFormatNode/requirements.txt +0 -0
  108. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageUploadNode/image_upload_node.py +0 -0
  109. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/__init__.py +0 -0
  110. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/image_viewer_node.py +0 -0
  111. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageViewerNode/requirements.txt +0 -0
  112. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageWriterNode/__init__.py +0 -0
  113. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/ImageWriterNode/imagewriter_node.py +0 -0
  114. {pynode_flow-0.2.7/pynode/nodes/InjectNode → pynode_flow-0.2.8/pynode/nodes/ImageWriterNode}/requirements.txt +0 -0
  115. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/__init__.py +0 -0
  116. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +0 -0
  117. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +0 -0
  118. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +0 -0
  119. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +0 -0
  120. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +0 -0
  121. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/readme.md +0 -0
  122. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +0 -0
  123. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/InjectNode/inject_node.py +0 -0
  124. {pynode_flow-0.2.7/pynode/nodes/JoinNode → pynode_flow-0.2.8/pynode/nodes/InjectNode}/requirements.txt +0 -0
  125. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/JoinNode/__init__.py +0 -0
  126. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/JoinNode/join_node.py +0 -0
  127. {pynode_flow-0.2.7/pynode/nodes/LabelFilterNode → pynode_flow-0.2.8/pynode/nodes/JoinNode}/requirements.txt +0 -0
  128. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LabelFilterNode/__init__.py +0 -0
  129. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LabelFilterNode/label_filter_node.py +0 -0
  130. {pynode_flow-0.2.7/pynode/nodes/LinkNode → pynode_flow-0.2.8/pynode/nodes/LabelFilterNode}/requirements.txt +0 -0
  131. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/__init__.py +0 -0
  132. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_bus.py +0 -0
  133. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_in_node.py +0 -0
  134. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/LinkNode/link_out_node.py +0 -0
  135. {pynode_flow-0.2.7/pynode/nodes/RangeNode → pynode_flow-0.2.8/pynode/nodes/LinkNode}/requirements.txt +0 -0
  136. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/__init__.py +0 -0
  137. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_in_node.py +0 -0
  138. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_out_node.py +0 -0
  139. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/mqtt_service.py +0 -0
  140. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MQTTNode/requirements.txt +0 -0
  141. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/__init__.py +0 -0
  142. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/messagereader_node.py +0 -0
  143. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/MessageWriterNode/messagewriter_node.py +0 -0
  144. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/__init__.py +0 -0
  145. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/omron_camera_node.py +0 -0
  146. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OmronCameraNode/requirements.txt +0 -0
  147. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/__init__.py +0 -0
  148. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/arithmetic_node.py +0 -0
  149. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/bitwise_node.py +0 -0
  150. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blend_node.py +0 -0
  151. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blob_detector_node.py +0 -0
  152. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/blur_node.py +0 -0
  153. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/color_space_node.py +0 -0
  154. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/colormap_node.py +0 -0
  155. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/contours_node.py +0 -0
  156. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/denormalize_coords_node.py +0 -0
  157. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/draw_node.py +0 -0
  158. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/edge_detector_node.py +0 -0
  159. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/equirectangular_node.py +0 -0
  160. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/fft_node.py +0 -0
  161. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/focus_estimation_node.py +0 -0
  162. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/frequency_filter_node.py +0 -0
  163. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/histogram_node.py +0 -0
  164. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/hough_circles_node.py +0 -0
  165. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/hough_lines_node.py +0 -0
  166. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/in_range_node.py +0 -0
  167. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/morphology_node.py +0 -0
  168. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/normalize_coords_node.py +0 -0
  169. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/paste_node.py +0 -0
  170. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/perspective_node.py +0 -0
  171. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/realsense_depth_node.py +0 -0
  172. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/requirements.txt +0 -0
  173. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/resize_node.py +0 -0
  174. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/rotate_node.py +0 -0
  175. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/template_match_node.py +0 -0
  176. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/OpenCV/threshold_node.py +0 -0
  177. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/__init__.py +0 -0
  178. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/point_in_shape_node.py +0 -0
  179. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PointInShapeNode/requirements.txt +0 -0
  180. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/__init__.py +0 -0
  181. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +0 -0
  182. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/PolygonMetricsNode/requirements.txt +0 -0
  183. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/QueueLengthProbeNode/__init__.py +0 -0
  184. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +0 -0
  185. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/__init__.py +0 -0
  186. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +0 -0
  187. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Qwen3VLMNode/requirements.txt +0 -0
  188. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RESTEndpointNode/__init__.py +0 -0
  189. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +0 -0
  190. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RangeNode/__init__.py +0 -0
  191. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RangeNode/range_node.py +0 -0
  192. {pynode_flow-0.2.7/pynode/nodes/RateProbeNode → pynode_flow-0.2.8/pynode/nodes/RangeNode}/requirements.txt +0 -0
  193. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RateProbeNode/__init__.py +0 -0
  194. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RateProbeNode/rate_probe_node.py +0 -0
  195. {pynode_flow-0.2.7/pynode/nodes/SplitNode → pynode_flow-0.2.8/pynode/nodes/RateProbeNode}/requirements.txt +0 -0
  196. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/__init__.py +0 -0
  197. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/requirements.txt +0 -0
  198. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +0 -0
  199. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/README.md +0 -0
  200. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/__init__.py +0 -0
  201. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/merge_predictions_node.py +0 -0
  202. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/requirements.txt +0 -0
  203. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/slice_collector_node.py +0 -0
  204. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SliceNode/slice_image_node.py +0 -0
  205. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/__init__.py +0 -0
  206. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/interop/README.md +0 -0
  207. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/interop/nodered-example-flow.json +0 -0
  208. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/ndjson_protocol.py +0 -0
  209. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/nodered_snippets.py +0 -0
  210. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/requirements.txt +0 -0
  211. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/tcp_in_node.py +0 -0
  212. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/tcp_out_node.py +0 -0
  213. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_in_node.py +0 -0
  214. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_out_node.py +0 -0
  215. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_probe.py +0 -0
  216. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SocketNode/udp_protocol.py +0 -0
  217. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SplitNode/__init__.py +0 -0
  218. {pynode_flow-0.2.7/pynode/nodes/SwitchNode → pynode_flow-0.2.8/pynode/nodes/SplitNode}/requirements.txt +0 -0
  219. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SplitNode/split_node.py +0 -0
  220. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/__init__.py +0 -0
  221. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/requirements.txt +0 -0
  222. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_annotate_node.py +0 -0
  223. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_filter_node.py +0 -0
  224. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_line_counter_node.py +0 -0
  225. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_sink_node.py +0 -0
  226. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_smoother_node.py +0 -0
  227. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_tracker_node.py +0 -0
  228. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/Supervision/supervision_zone_node.py +0 -0
  229. {pynode_flow-0.2.7/pynode/nodes/TemplateNode → pynode_flow-0.2.8/pynode/nodes/SwitchNode}/requirements.txt +0 -0
  230. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SwitchNode/switch_node.py +0 -0
  231. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SyncNode/__init__.py +0 -0
  232. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/SyncNode/sync_node.py +0 -0
  233. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/TemplateNode/template_node.py +0 -0
  234. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/__init__.py +0 -0
  235. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/model_paths.py +0 -0
  236. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/requirements.txt +0 -0
  237. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UltralyticsNode/ultralytics_node.py +0 -0
  238. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/UnknownNode/__init__.py +0 -0
  239. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VLLMNode/__init__.py +0 -0
  240. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VLLMNode/vllm_node.py +0 -0
  241. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoReaderNode/__init__.py +0 -0
  242. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoReaderNode/video_reader_node.py +0 -0
  243. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoWriterNode/__init__.py +0 -0
  244. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/VideoWriterNode/video_writer_node.py +0 -0
  245. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/WebhookNode/__init__.py +0 -0
  246. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/WebhookNode/webhook_node.py +0 -0
  247. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/__init__.py +0 -0
  248. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/base_node.py +0 -0
  249. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/image_utils.py +0 -0
  250. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/info.py +0 -0
  251. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/__init__.py +0 -0
  252. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_broadcast_node.py +0 -0
  253. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_discovery_node.py +0 -0
  254. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/mdns_manager.py +0 -0
  255. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/mDNSNode/requirements.txt +0 -0
  256. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/messages.py +0 -0
  257. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/pacing.py +0 -0
  258. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/nodes/supervision_utils.py +0 -0
  259. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/server.py +0 -0
  260. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/01-hello-world.json +0 -0
  261. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/02-function-transform.json +0 -0
  262. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/03-switch-routing.json +0 -0
  263. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/04-flow-control.json +0 -0
  264. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/05-webcam-viewer.json +0 -0
  265. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/06-webcam-yolo.json +0 -0
  266. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/07-blur-people.json +0 -0
  267. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/08-video-yolo-recorder.json +0 -0
  268. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/09-mqtt-pub-sub.json +0 -0
  269. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/10-person-alert-mqtt.json +0 -0
  270. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/16-socket-bridge.json +0 -0
  271. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/17-slider-crop.json +0 -0
  272. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/18-track-trace.json +0 -0
  273. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/19-line-counter.json +0 -0
  274. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/20-zone-occupancy.json +0 -0
  275. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/21-annotator-gallery.json +0 -0
  276. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/22-heatmap.json +0 -0
  277. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/23-privacy-blur.json +0 -0
  278. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/24-detection-log.json +0 -0
  279. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/README.md +0 -0
  280. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/examples/manifest.json +0 -0
  281. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/images/favicon4.png +0 -0
  282. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/index.html +0 -0
  283. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/README.md +0 -0
  284. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/auth.js +0 -0
  285. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/clipboard.js +0 -0
  286. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/config.js +0 -0
  287. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/connections.js +0 -0
  288. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/debug.js +0 -0
  289. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/events.js +0 -0
  290. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/geometry-editor.js +0 -0
  291. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/history.js +0 -0
  292. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/main.js +0 -0
  293. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/minimap.js +0 -0
  294. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/mqtt-services.js +0 -0
  295. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/nodes.js +0 -0
  296. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/palette.js +0 -0
  297. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/selection.js +0 -0
  298. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/state.js +0 -0
  299. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/ui-utils.js +0 -0
  300. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/viewport.js +0 -0
  301. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/workflow.js +0 -0
  302. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/static/js/workflows.js +0 -0
  303. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/workflow_engine.py +0 -0
  304. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode/workflow_manager.py +0 -0
  305. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/dependency_links.txt +0 -0
  306. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/entry_points.txt +0 -0
  307. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/requires.txt +0 -0
  308. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/pynode_flow.egg-info/top_level.txt +0 -0
  309. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/requirements.txt +0 -0
  310. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.bat +0 -0
  311. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.cfg +0 -0
  312. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.py +0 -0
  313. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/setup.sh +0 -0
  314. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/__init__.py +0 -0
  315. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/conftest.py +0 -0
  316. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_auth.py +0 -0
  317. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_errors.py +0 -0
  318. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_nodes.py +0 -0
  319. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_security.py +0 -0
  320. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_api_workflows.py +0 -0
  321. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_base_node_messaging.py +0 -0
  322. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_camera_node_backend.py +0 -0
  323. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_concurrency.py +0 -0
  324. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_config.py +0 -0
  325. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_control_slider_node.py +0 -0
  326. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_counter_sse.py +0 -0
  327. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_crop_node.py +0 -0
  328. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_debug_json_safety.py +0 -0
  329. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_debug_workflow_scope.py +0 -0
  330. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_delay_rate_limit.py +0 -0
  331. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_device_detection.py +0 -0
  332. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_frame_pacing.py +0 -0
  333. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_function_node.py +0 -0
  334. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_image_upload_node.py +0 -0
  335. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_link_nodes.py +0 -0
  336. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_message_reader_pickle.py +0 -0
  337. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_models_dir.py +0 -0
  338. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_mqtt_services.py +0 -0
  339. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_node_registration.py +0 -0
  340. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_node_types_api.py +0 -0
  341. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_persistence.py +0 -0
  342. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_range_node.py +0 -0
  343. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_send_copy_semantics.py +0 -0
  344. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_serialization.py +0 -0
  345. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_socket_nodes.py +0 -0
  346. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_annotate_node.py +0 -0
  347. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_phase3_nodes.py +0 -0
  348. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_tracker_node.py +0 -0
  349. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_utils.py +0 -0
  350. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_supervision_zone_nodes.py +0 -0
  351. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_video_reader_node.py +0 -0
  352. {pynode_flow-0.2.7 → pynode_flow-0.2.8}/tests/test_workflow_engine.py +0 -0
  353. {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.6.0-runtime-ubuntu22.04
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
- ENV CUDA_VERSION=12.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.7
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.7'
22
- __version_tuple__ = version_tuple = (0, 2, 7)
21
+ __version__ = version = '0.2.8'
22
+ __version_tuple__ = version_tuple = (0, 2, 8)
23
23
 
24
- __commit_id__ = commit_id = 'g6f8c52570'
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
- self._set_nested_value(msg, property_path, resolved_value)
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
- current_value = self._get_nested_value(msg, property_path)
219
-
220
- # Resolve replace value
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
- replace_value = self._get_nested_value(msg, 'msg.' + str(replace) if not str(replace).startswith('msg.') else replace)
324
+ replacement_value = self._get_nested_value(msg, 'msg.' + str(replace) if not str(replace).startswith('msg.') else replace)
223
325
  else:
224
- replace_value = str(replace)
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
- if current_value is not None:
227
- if isinstance(current_value, str):
228
- if search_type == 'regex':
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
- self._delete_nested_value(msg, property_path)
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
- current_value = self._get_nested_value(msg, property_path)
248
- if current_value is not None:
249
- self._set_nested_value(msg, to_path, current_value)
250
- self._delete_nested_value(msg, property_path)
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,3 @@
1
+ from ..ClassMapNode.class_map_node import ClassMapNode
2
+
3
+ __all__ = ['ClassMapNode']
@@ -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)
@@ -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
- bbox = det.get('bbox', [0, 0, 0, 0])
271
- if isinstance(bbox, np.ndarray):
272
- bbox = bbox.tolist()
273
- x1, y1, x2, y2 = bbox
274
- class_id = det.get('class_id', 0)
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)