From 0bed51704a74c4c9b2358aab7fecdc6ef1978c89 Mon Sep 17 00:00:00 2001 From: Ivan Kud Date: Thu, 24 Oct 2024 10:32:52 +0200 Subject: [PATCH] 154 Bbox solely owned area (#155) * implemented solely owned area computation for bboxes * unified package versions and upgraded PyO3 to 0.22 * fixed opentelemetry init for null output (was to stdout) * implemented bbox association function * implemented ordering and fixed review comments --- Cargo.toml | 16 +- python/frame_pipeline_evolution.py | 92 ++++- python/primitives/bbox/utils.py | 41 +++ savant_algebra/Cargo.toml | 39 --- savant_algebra/build.rs | 3 - savant_algebra/python/frame_objs_from_np.py | 67 ---- savant_algebra/python/numpy_ops.py | 51 --- savant_algebra/src/conversions.rs | 49 --- savant_algebra/src/lib.rs | 49 --- savant_algebra/src/np.rs | 36 -- savant_algebra/src/np/bbox.rs | 269 --------------- savant_algebra/src/np/np_nalgebra.rs | 325 ------------------ savant_algebra/src/np/np_ndarray.rs | 240 ------------- savant_core/Cargo.toml | 30 +- savant_core/benches/bench_bbox_utils.rs | 71 ++++ savant_core/src/primitives/bbox.rs | 2 + savant_core/src/primitives/bbox/utils.rs | 180 ++++++++++ savant_core/src/primitives/frame.rs | 8 +- savant_core/src/telemetry.rs | 12 +- savant_core/src/transport/zeromq/writer.rs | 1 + savant_core_py/Cargo.toml | 33 +- savant_core_py/src/draw_spec.rs | 4 +- savant_core_py/src/logging.rs | 4 +- savant_core_py/src/pipeline.rs | 5 +- .../src/primitives/attribute_value.rs | 4 +- savant_core_py/src/primitives/bbox.rs | 8 +- savant_core_py/src/primitives/bbox/utils.rs | 29 ++ savant_core_py/src/primitives/frame.rs | 7 +- savant_core_py/src/primitives/frame_update.rs | 8 +- savant_core_py/src/primitives/object.rs | 7 +- savant_core_py/src/primitives/objects_view.rs | 4 +- .../src/primitives/polygonal_area.rs | 1 + savant_core_py/src/primitives/segment.rs | 4 +- savant_core_py/src/telemetry.rs | 8 +- savant_core_py/src/utils/byte_buffer.rs | 1 + savant_core_py/src/utils/otlp.rs | 7 +- savant_core_py/src/utils/symbol_mapper.rs | 4 +- savant_core_py/src/zmq/basic_types.rs | 8 +- savant_core_py/src/zmq/configs.rs | 2 + .../savant_plugin_sample/Cargo.toml | 8 +- savant_python/Cargo.toml | 7 +- .../primitives/geometry/geometry.pyi | 114 ++---- savant_python/src/lib.rs | 12 +- 43 files changed, 550 insertions(+), 1320 deletions(-) create mode 100644 python/primitives/bbox/utils.py delete mode 100644 savant_algebra/Cargo.toml delete mode 100644 savant_algebra/build.rs delete mode 100644 savant_algebra/python/frame_objs_from_np.py delete mode 100644 savant_algebra/python/numpy_ops.py delete mode 100644 savant_algebra/src/conversions.rs delete mode 100644 savant_algebra/src/lib.rs delete mode 100644 savant_algebra/src/np.rs delete mode 100644 savant_algebra/src/np/bbox.rs delete mode 100644 savant_algebra/src/np/np_nalgebra.rs delete mode 100644 savant_algebra/src/np/np_ndarray.rs create mode 100644 savant_core/benches/bench_bbox_utils.rs create mode 100644 savant_core/src/primitives/bbox/utils.rs create mode 100644 savant_core_py/src/primitives/bbox/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 032cacaa..e28605bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,25 @@ members = [ ] [workspace.dependencies] +anyhow = "1.0" +evalexpr = { version = "11", features = ["rand", "regex_support"] } +geo = "0.28" +lazy_static = "1.5" +log = "0.4" savant_core = { path = "savant_core" } savant_core_py = { path = "savant_core_py" } +hashbrown = { version = "0.15", features = ["serde"] } +opentelemetry = "0.24" +opentelemetry-otlp = { version = "0.17", features = ["http-json", "http-proto", "tls", "reqwest-rustls"] } +parking_lot = { version = "0.12", features = ["deadlock_detection"] } +pyo3 = "0.22" +pyo3-build-config = "0.22" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" [workspace.package] -version = "0.4.0" +version = "0.4.1" edition = "2021" authors = ["Ivan Kudriavtsev "] description = "Savant Rust core functions library" diff --git a/python/frame_pipeline_evolution.py b/python/frame_pipeline_evolution.py index 22799533..a25479b7 100644 --- a/python/frame_pipeline_evolution.py +++ b/python/frame_pipeline_evolution.py @@ -4,42 +4,96 @@ set_log_level(LogLevel.Trace) -from savant_rs.pipeline import VideoPipelineStagePayloadType, VideoPipeline, VideoPipelineConfiguration, StageFunction -from savant_rs.primitives import VideoFrame, VideoFrameContent, VideoFrameTranscodingMethod, VideoFrameTransformation, \ - Attribute, AttributeValue +from savant_rs.pipeline import ( + VideoPipelineStagePayloadType, + VideoPipeline, + VideoPipelineConfiguration, + StageFunction, +) +from savant_rs.primitives import ( + VideoFrame, + VideoFrameContent, + VideoFrameTranscodingMethod, + VideoFrameTransformation, + Attribute, + AttributeValue, +) from savant_rs.utils import gen_frame, TelemetrySpan, enable_dl_detection -from savant_rs import init_jaeger_tracer if __name__ == "__main__": savant_rs.version() enable_dl_detection() # enables internal DL detection (checks every 5 secs) - log(LogLevel.Info, "root", "Begin operation", dict(savant_rs_version=savant_rs.version())) - init_jaeger_tracer("demo-pipeline", "localhost:6831") + log( + LogLevel.Info, + "root", + "Begin operation", + dict(savant_rs_version=savant_rs.version()), + ) conf = VideoPipelineConfiguration() conf.append_frame_meta_to_otlp_span = True - frame = VideoFrame(source_id="test", framerate="30/1", width=1400, height=720, - content=VideoFrameContent.internal(bytes("this is it", 'utf-8')), - transcoding_method=VideoFrameTranscodingMethod.Encoded, codec="h264", keyframe=True, - time_base=(1, 1000000), pts=10000, dts=10000, duration=10) + frame = VideoFrame( + source_id="test", + framerate="30/1", + width=1400, + height=720, + content=VideoFrameContent.internal(bytes("this is it", "utf-8")), + transcoding_method=VideoFrameTranscodingMethod.Encoded, + codec="h264", + keyframe=True, + time_base=(1, 1000000), + pts=10000, + dts=10000, + duration=10, + ) frame.add_transformation(VideoFrameTransformation.initial_size(1920, 1080)) frame.add_transformation(VideoFrameTransformation.scale(1280, 720)) frame.add_transformation(VideoFrameTransformation.padding(120, 0, 0, 0)) frame.add_transformation(VideoFrameTransformation.resulting_size(1400, 720)) - frame.set_attribute(Attribute("Configuration", "CamMode", - [AttributeValue.string("fisheye"), AttributeValue.integers([180, 180])], - "PlatformConfig", True)) + frame.set_attribute( + Attribute( + "Configuration", + "CamMode", + [AttributeValue.string("fisheye"), AttributeValue.integers([180, 180])], + "PlatformConfig", + True, + ) + ) print(frame.json_pretty) - p = VideoPipeline("video-pipeline-root", [ - ("input", VideoPipelineStagePayloadType.Frame, StageFunction.none(), StageFunction.none()), - ("proc1", VideoPipelineStagePayloadType.Batch, StageFunction.none(), StageFunction.none()), - ("proc2", VideoPipelineStagePayloadType.Batch, StageFunction.none(), StageFunction.none()), - ("output", VideoPipelineStagePayloadType.Frame, StageFunction.none(), StageFunction.none()), - ], conf) + p = VideoPipeline( + "video-pipeline-root", + [ + ( + "input", + VideoPipelineStagePayloadType.Frame, + StageFunction.none(), + StageFunction.none(), + ), + ( + "proc1", + VideoPipelineStagePayloadType.Batch, + StageFunction.none(), + StageFunction.none(), + ), + ( + "proc2", + VideoPipelineStagePayloadType.Batch, + StageFunction.none(), + StageFunction.none(), + ), + ( + "output", + VideoPipelineStagePayloadType.Frame, + StageFunction.none(), + StageFunction.none(), + ), + ], + conf, + ) p.sampling_period = 10 root_span = TelemetrySpan("new-telemetry") diff --git a/python/primitives/bbox/utils.py b/python/primitives/bbox/utils.py new file mode 100644 index 00000000..99dd5efc --- /dev/null +++ b/python/primitives/bbox/utils.py @@ -0,0 +1,41 @@ +from savant_rs.primitives.geometry import ( + RBBox, + solely_owned_areas, + associate_bboxes, +) +from savant_rs.utils import BBoxMetricType + +red = RBBox.ltrb(0.0, 2.0, 2.0, 4.0) +green = RBBox.ltrb(1.0, 3.0, 5.0, 5.0) +yellow = RBBox.ltrb(1.0, 1.0, 3.0, 6.0) +purple = RBBox.ltrb(4.0, 0.0, 7.0, 2.0) + +areas = solely_owned_areas([red, green, yellow, purple], parallel=True) + +red = areas[0] +green = areas[1] +yellow = areas[2] +purple = areas[3] + +assert red == 2.0 +assert green == 4.0 +assert yellow == 5.0 +assert purple == 6.0 + +lp1 = RBBox.ltrb(0.0, 1.0, 2.0, 2.0) +lp2 = RBBox.ltrb(5.0, 2.0, 8.0, 3.0) +lp3 = RBBox.ltrb(100.0, 0.0, 6.0, 3.0) +owner1 = RBBox.ltrb(1.0, 0.0, 6.0, 3.0) +owner2 = RBBox.ltrb(6.0, 1.0, 9.0, 4.0) + +associations_iou = associate_bboxes( + [lp1, lp2, lp3], [owner1, owner2], BBoxMetricType.IoU, 0.01 +) + +lp1_associations = associations_iou[0] +lp2_associations = associations_iou[1] +lp3_associations = associations_iou[2] + +assert list(map(lambda t: t[0], lp1_associations)) == [0] +assert list(map(lambda t: t[0], lp2_associations)) == [1, 0] +assert lp3_associations == [] diff --git a/savant_algebra/Cargo.toml b/savant_algebra/Cargo.toml deleted file mode 100644 index b9cba665..00000000 --- a/savant_algebra/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "savant_algebra" -version.workspace = true -edition.workspace = true -authors.workspace = true -description.workspace = true -homepage.workspace = true -repository.workspace = true -readme.workspace = true -keywords.workspace = true -categories.workspace = true -license.workspace = true -rust-version.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -savant_rs = { path = "../savant_python" } -parking_lot = "0.12" -anyhow = "1" -opentelemetry = "0.24.0" -numpy = { version = "0.19", features = ["nalgebra"] } -nalgebra = "0.32" -ndarray = "0.15" -num-traits = "0.2" -log = "0.4" - -[lib] -crate-type = ["cdylib", "lib"] - -[dependencies.pyo3] -version = "0.12" - -[features] -extension-module = ["pyo3/extension-module"] -default = ["extension-module"] - -[build-dependencies] -pyo3-build-config = "0.21" diff --git a/savant_algebra/build.rs b/savant_algebra/build.rs deleted file mode 100644 index dace4a9b..00000000 --- a/savant_algebra/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - pyo3_build_config::add_extension_module_link_args(); -} diff --git a/savant_algebra/python/frame_objs_from_np.py b/savant_algebra/python/frame_objs_from_np.py deleted file mode 100644 index 3199e486..00000000 --- a/savant_algebra/python/frame_objs_from_np.py +++ /dev/null @@ -1,67 +0,0 @@ -import numpy as np -from savant_rs.primitives import VideoFrame, VideoFrameContent -from savant_rs.utils.symbol_mapper import register_model_objects, RegistrationPolicy -from savant_rs.video_object_query import MatchQuery as Q - -from savant_rs.logging import LogLevel, set_log_level -set_log_level(LogLevel.Trace) - -register_model_objects("detector", { 1: "person" }, RegistrationPolicy.ErrorIfNonUnique) - -frame = VideoFrame( - source_id="Test", - framerate="30/1", - width=1920, - height=1080, - content=VideoFrameContent.external("s3", "s3://some-bucket/some-key.jpeg"), - codec="jpeg", - keyframe=True, - pts=0, - dts=None, - duration=None, -) - -objs = np.array([ - [1, 0.75, 0.0, 0.0, 10.0, 20.0] -]) - -frame.create_objects_from_numpy("detector", objs) -objs = frame.access_objects(Q.idle()) -assert len(objs) == 1 - -incorrect_objs = np.array([ - [1, 0.75, 0.0, 0.0, 10.0] -]) - -try: - frame.create_objects_from_numpy("detector", incorrect_objs) - assert False -except: - assert True - -try: - # not registered model - frame.create_objects_from_numpy("detector2", objs) - assert False -except: - assert True - -unregistered_objs = np.array([ - [2, 0.75, 0.0, 0.0, 10.0, 20.0] -]) - -try: - # not registered class - frame.create_objects_from_numpy("detector", objs) - assert False -except: - assert True - -objs_with_angle = np.array([ - [1, 0.75, 0.0, 0.0, 10.0, 20.0, 35] -]) - -frame.create_objects_from_numpy("detector", objs_with_angle) -objs = frame.access_objects(Q.idle()) -assert len(objs) == 2 - diff --git a/savant_algebra/python/numpy_ops.py b/savant_algebra/python/numpy_ops.py deleted file mode 100644 index 7e2e4f9a..00000000 --- a/savant_algebra/python/numpy_ops.py +++ /dev/null @@ -1,51 +0,0 @@ -import numpy as np -from savant_rs.utils.numpy import * -from timeit import default_timer as timer - -num = 10_000 -dims = (1024, 1) - -from savant_rs.logging import LogLevel, set_log_level -set_log_level(LogLevel.Trace) - -def bench_matrix(dtype, dims, num): - t = timer() - v = np.zeros(dims, dtype=dtype) - m = None - for _ in range(num): - m = np_to_matrix(v) - print(f"NP {dtype} > NALGEBRA {dtype}: {num} Time:", timer() - t) - - t = timer() - for _ in range(num): - v = matrix_to_np(m) - - print(f"NALGEBRA {dtype} > NP {dtype}: {num} Time:", timer() - t) - - -def bench_ndarray(dtype, dims, num): - t = timer() - v = np.zeros(dims, dtype=dtype) - m = None - for _ in range(num): - m = np_to_ndarray(v) - print(f"NP {dtype} > NDARRAY {dtype}: {num} Time:", timer() - t) - - t = timer() - for _ in range(num): - v = ndarray_to_np(m) - - print(f"NDARRAY {dtype} > NP {dtype}: {num} Time:", timer() - t) - - -print("Bench Nalgebra Matrix") -for dt in ['uint8', 'int8', 'uint16', 'int16', 'uint32', 'int32', 'uint64', 'int64', 'float32', 'float64']: - bench_matrix(dt, dims, num) - -print("Bench NDArray") -for dt in ['uint8', 'int8', 'uint16', 'int16', 'uint32', 'int32', 'uint64', 'int64', 'float32', 'float64']: - bench_ndarray(dt, dims, num) - - -arr = np.zeros((3, 4), dtype='float32') -print(np_to_ndarray(arr)) diff --git a/savant_algebra/src/conversions.rs b/savant_algebra/src/conversions.rs deleted file mode 100644 index 5d18616f..00000000 --- a/savant_algebra/src/conversions.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::np::{ConvF32, RConvF32}; - -impl ConvF32 for f32 { - fn conv_f32(self) -> f32 { - self - } -} - -impl ConvF32 for f64 { - fn conv_f32(self) -> f32 { - self as f32 - } -} - -impl ConvF32 for i32 { - fn conv_f32(self) -> f32 { - self as f32 - } -} - -impl ConvF32 for i64 { - fn conv_f32(self) -> f32 { - self as f32 - } -} - -impl RConvF32 for f32 { - fn conv_from_f32(f: f32) -> Self { - f - } -} - -impl RConvF32 for f64 { - fn conv_from_f32(f: f32) -> Self { - f as f64 - } -} - -impl RConvF32 for i32 { - fn conv_from_f32(f: f32) -> Self { - f as i32 - } -} - -impl RConvF32 for i64 { - fn conv_from_f32(f: f32) -> Self { - f as i64 - } -} diff --git a/savant_algebra/src/lib.rs b/savant_algebra/src/lib.rs deleted file mode 100644 index 0bf2cd32..00000000 --- a/savant_algebra/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::np::np_nalgebra::NalgebraDMatrix; -use crate::np::np_ndarray::NDarray; -use pyo3::prelude::*; -use pyo3::pyclass; - -/// The format of a bounding box passed as a parameter or requested as a return type. -/// -/// LeftTopRightBottom -/// The format is [left, top, right, bottom]. -/// LeftTopWidthHeight -/// The format is [left, top, width, height]. -/// XcYcWidthHeight -/// The format is [xcenter, ycenter, width, height]. -/// -#[pyclass] -#[derive(Debug, Clone)] -pub enum BBoxFormat { - LeftTopRightBottom, - LeftTopWidthHeight, - XcYcWidthHeight, -} - -pub(crate) mod conversions; -mod np; - -use np::bbox::*; -use np::np_nalgebra::*; -use np::np_ndarray::*; - -#[pymodule] -pub fn savant_nalgebra(_py: Python, m: &PyModule) -> PyResult<()> { - // bbox batch ops - m.add_function(wrap_pyfunction!(rotated_bboxes_to_ndarray_gil, m)?)?; - m.add_function(wrap_pyfunction!(bboxes_to_ndarray_gil, m)?)?; - m.add_function(wrap_pyfunction!(ndarray_to_bboxes_py, m)?)?; - m.add_function(wrap_pyfunction!(ndarray_to_rotated_bboxes_py, m)?)?; - // - // // numpy utils - m.add_function(wrap_pyfunction!(np_to_matrix_gil, m)?)?; - m.add_function(wrap_pyfunction!(matrix_to_np_py, m)?)?; - m.add_function(wrap_pyfunction!(np_to_ndarray_gil, m)?)?; - m.add_function(wrap_pyfunction!(ndarray_to_np_py, m)?)?; - - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - - Ok(()) -} diff --git a/savant_algebra/src/np.rs b/savant_algebra/src/np.rs deleted file mode 100644 index 2b32d3d0..00000000 --- a/savant_algebra/src/np.rs +++ /dev/null @@ -1,36 +0,0 @@ -use nalgebra::Scalar; -use std::fmt::Debug; - -pub mod bbox; -pub mod np_nalgebra; -pub mod np_ndarray; - -pub trait ConvF32 { - fn conv_f32(self) -> f32; -} - -pub trait RConvF32 { - fn conv_from_f32(f: f32) -> Self; -} - -pub trait ElementType: numpy::Element + Scalar + Copy + Clone + Debug {} - -impl ElementType for f32 {} - -impl ElementType for f64 {} - -impl ElementType for i8 {} - -impl ElementType for i16 {} - -impl ElementType for i32 {} - -impl ElementType for i64 {} - -impl ElementType for u8 {} - -impl ElementType for u16 {} - -impl ElementType for u32 {} - -impl ElementType for u64 {} diff --git a/savant_algebra/src/np/bbox.rs b/savant_algebra/src/np/bbox.rs deleted file mode 100644 index ee3e96bf..00000000 --- a/savant_algebra/src/np/bbox.rs +++ /dev/null @@ -1,269 +0,0 @@ -use crate::np::{ConvF32, ElementType, RConvF32}; -use crate::BBoxFormat; -use numpy::ndarray::ArrayD; -use numpy::{IxDyn, PyArray, PyReadonlyArrayDyn}; -use pyo3::prelude::*; -use savant_rs::primitives::bbox::{BBox, RBBox}; -use savant_rs::with_gil; - -pub fn ndarray_to_rotated_bboxes( - arr: &PyReadonlyArrayDyn, -) -> Vec { - let dims = arr.shape(); - assert!(dims.len() == 2 && dims[1] >= 5); - arr.as_array() - .rows() - .into_iter() - .map(|r| { - RBBox::new( - r[0].conv_f32(), - r[1].conv_f32(), - r[2].conv_f32(), - r[3].conv_f32(), - Some(r[4].conv_f32()), - ) - }) - .collect::>() -} - -pub fn ndarray_to_bboxes( - arr: &PyReadonlyArrayDyn, - format: &BBoxFormat, -) -> Vec { - let dims = arr.shape(); - assert!(dims.len() == 2 && dims[1] >= 4); - arr.as_array() - .rows() - .into_iter() - .map(|r| match format { - BBoxFormat::LeftTopRightBottom => BBox::ltrb( - r[0].conv_f32(), - r[1].conv_f32(), - r[2].conv_f32(), - r[3].conv_f32(), - ), - BBoxFormat::LeftTopWidthHeight => BBox::ltwh( - r[0].conv_f32(), - r[1].conv_f32(), - r[2].conv_f32(), - r[3].conv_f32(), - ), - BBoxFormat::XcYcWidthHeight => BBox::new( - r[0].conv_f32(), - r[1].conv_f32(), - r[2].conv_f32(), - r[3].conv_f32(), - ), - }) - .collect::>() -} - -pub fn bboxes_to_ndarray( - boxes: &Vec, - format: &BBoxFormat, -) -> Py> { - let arr = { - let mut arr = ArrayD::::zeros(IxDyn(&[boxes.len(), 4])); - for (i, bbox) in boxes.iter().enumerate() { - let (v0, v1, v2, v3) = match format { - BBoxFormat::LeftTopRightBottom => bbox.as_ltrb(), - BBoxFormat::LeftTopWidthHeight => bbox.as_ltwh(), - BBoxFormat::XcYcWidthHeight => bbox.as_xcycwh(), - }; - - arr[[i, 0]] = RConvF32::conv_from_f32(v0); - arr[[i, 1]] = RConvF32::conv_from_f32(v1); - arr[[i, 2]] = RConvF32::conv_from_f32(v2); - arr[[i, 3]] = RConvF32::conv_from_f32(v3); - } - arr - }; - - with_gil!(|py| { - let arr = PyArray::from_array(py, &arr); - arr.into_py(py) - }) -} - -pub fn rotated_bboxes_to_ndarray( - boxes: Vec, -) -> Py> { - let arr = { - let mut arr = ArrayD::::zeros(IxDyn(&[boxes.len(), 5])); - for (i, bbox) in boxes.iter().enumerate() { - arr[[i, 0]] = RConvF32::conv_from_f32(bbox.get_xc()); - arr[[i, 1]] = RConvF32::conv_from_f32(bbox.get_yc()); - arr[[i, 2]] = RConvF32::conv_from_f32(bbox.get_width()); - arr[[i, 3]] = RConvF32::conv_from_f32(bbox.get_height()); - arr[[i, 4]] = RConvF32::conv_from_f32(bbox.get_angle().unwrap_or(0.0)); - } - arr - }; - - with_gil!(|py| { - let arr = PyArray::from_array(py, &arr); - arr.into_py(py) - }) -} - -/// Converts a list of :class:`savant_rs.primitives.geometry.RBBox`-es to a numpy -/// array with rows represented by ``[xc, yc, width, height, angle]``. -/// -/// Parameters -/// ---------- -/// arr : List[savant_rs.primitives.geometry.RBBox] -/// The numpy array with rows represented by ``[xc, yc, width, height, angle]``. -/// dtype : str -/// The data type of the numpy array. Can be ``float32``, ``float64``, ``int32`` or ``int64``. -/// -/// Returns -/// ------- -/// numpy.ndarray -/// The numpy array with rows represented by ``[xc, yc, width, height, angle]``. -/// -/// -/// Panics when a data type is not ``float32``, ``float64``, ``int32`` or ``int64``. -/// -#[pyfunction] -#[pyo3(name = "rotated_bboxes_to_ndarray")] -pub fn rotated_bboxes_to_ndarray_gil(boxes: Vec, dtype: String) -> PyObject { - match dtype.as_str() { - "float32" => { - let arr = rotated_bboxes_to_ndarray::(boxes); - with_gil!(|py| arr.to_object(py)) - } - "float64" => { - let arr = rotated_bboxes_to_ndarray::(boxes); - with_gil!(|py| arr.to_object(py)) - } - "int32" => { - let arr = rotated_bboxes_to_ndarray::(boxes); - with_gil!(|py| arr.to_object(py)) - } - "int64" => { - let arr = rotated_bboxes_to_ndarray::(boxes); - with_gil!(|py| arr.to_object(py)) - } - _ => panic!("Unsupported dtype"), - } -} - -/// Converts a numpy array with rows in a format specified by ``format`` to a list of :class:`savant_rs.primitives.geometry.BBox`-es. -/// -/// Parameters -/// ---------- -/// arr : numpy.ndarray -/// The numpy array with rows in a format specified by ``format``. -/// format : BBoxFormat -/// The format of the numpy array. Can be ``BBoxFormat.LeftTopRightBottom``, ``BBoxFormat.LeftTopWidthHeight`` or ``BBoxFormat.XcYcWidthHeight``. -/// -/// Returns -/// ------- -/// List[savant_rs.primitives.geometry.BBox] -/// The list of :class:`savant_rs.primitives.geometry.BBox`-es. -/// -#[pyfunction] -#[pyo3(name = "ndarray_to_bboxes")] -pub fn ndarray_to_bboxes_py(arr: &PyAny, format: &BBoxFormat) -> PyResult> { - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_bboxes(&arr.readonly(), format)); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_bboxes(&arr.readonly(), format)); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_bboxes(&arr.readonly(), format)); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_bboxes(&arr.readonly(), format)); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32, f64, i32 or i64", - )) -} - -/// Converts numpy array with rows represented by ``[xc, yc, width, height, angle]`` to a list of :class:`savant_rs.primitives.geometry.RBBox`-es. -/// -/// Parameters -/// ---------- -/// arr : numpy.ndarray -/// The numpy array with rows represented by ``[xc, yc, width, height, angle]``. -/// -/// Returns -/// ------- -/// List[savant_rs.primitives.geometry.RBBox] -/// The list of :class:`savant_rs.primitives.geometry.RBBox`-es. -/// -/// Raises -/// ------ -/// TypeError -/// If the numpy array is not of type ``float32``, ``float64``, ``int32`` or ``int64``. -/// -#[pyfunction] -#[pyo3(name = "ndarray_to_rotated_bboxes")] -pub fn ndarray_to_rotated_bboxes_py(arr: &PyAny) -> PyResult> { - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_rotated_bboxes(&arr.readonly())); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_rotated_bboxes(&arr.readonly())); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_rotated_bboxes(&arr.readonly())); - } - - if let Ok(arr) = arr.downcast::>() { - return Ok(ndarray_to_rotated_bboxes(&arr.readonly())); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32, f64, i32 or i64", - )) -} - -/// Converts a list of :class:`savant_rs.primitives.geometry.BBox`-es to a numpy ndarray. The format of the ndarray is determined by the ``format`` parameter. -/// -/// Parameters -/// ---------- -/// boxes : List[savant_rs.primitives.geometry.BBox] -/// The list of :class:`savant_rs.primitives.geometry.BBox`-es. -/// format : BBoxFormat -/// The format of bbox representation. One -/// of :class:`BBoxFormat.LeftTopRightBottom`, :class:`BBoxFormat.LeftTopWidthHeight`, or :class:`BBoxFormat.XcYcWidthHeight`. -/// dtype : str -/// The data type of the numpy array. Can be ``float32``, ``float64``, ``int32`` or ``int64``. -/// -/// Returns -/// ------- -/// numpy.ndarray -/// The numpy array with rows in a specified format. -/// -#[pyfunction] -#[pyo3(name = "bboxes_to_ndarray")] -pub fn bboxes_to_ndarray_gil(boxes: Vec, format: &BBoxFormat, dtype: String) -> PyObject { - match dtype.as_str() { - "float32" => { - let arr = bboxes_to_ndarray::(&boxes, format); - with_gil!(|py| arr.to_object(py)) - } - "float64" => { - let arr = bboxes_to_ndarray::(&boxes, format); - with_gil!(|py| arr.to_object(py)) - } - "int32" => { - let arr = bboxes_to_ndarray::(&boxes, format); - with_gil!(|py| arr.to_object(py)) - } - "int64" => { - let arr = bboxes_to_ndarray::(&boxes, format); - with_gil!(|py| arr.to_object(py)) - } - _ => panic!("Unsupported dtype"), - } -} diff --git a/savant_algebra/src/np/np_nalgebra.rs b/savant_algebra/src/np/np_nalgebra.rs deleted file mode 100644 index b032d115..00000000 --- a/savant_algebra/src/np/np_nalgebra.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::np::ElementType; -use nalgebra::DMatrix; -use numpy::ndarray::ArrayD; -use numpy::{IxDyn, PyArray, PyReadonlyArrayDyn}; -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; -use savant_rs::with_gil; -use std::fmt::Debug; -use std::ops::Deref; -use std::sync::Arc; - -macro_rules! pretty_print { - ($arr:expr) => {{ - let indent = 4; - let prefix = String::from_utf8(vec![b' '; indent]).unwrap(); - let mut result_els = vec!["".to_string()]; - for i in 0..$arr.nrows() { - let mut row_els = vec![]; - for j in 0..$arr.ncols() { - row_els.push(format!("{:12.3}", $arr[(i, j)])); - } - let row_str = row_els.into_iter().collect::>().join(" "); - let row_str = format!("{}{}", prefix, row_str); - result_els.push(row_str); - } - result_els.into_iter().collect::>().join("\n") - }}; -} - -#[derive(Debug, Clone)] -pub enum NalgebraDMatrixVariant { - Float64(DMatrix), - Float32(DMatrix), - Int64(DMatrix), - Int32(DMatrix), - Int16(DMatrix), - Int8(DMatrix), - UnsignedInt64(DMatrix), - UnsignedInt32(DMatrix), - UnsignedInt16(DMatrix), - UnsignedInt8(DMatrix), -} - -/// The class is a wrapper class to handle Rust's Nalgebra DMatrix in Python. -/// The class doesn't have methods intended to be used directly by the user. Instead, it is used -/// by Rust functions to which the user passes a :class:`NalgebraDMatrix` object for optimized processing. -/// -/// Examples -/// -------- -/// .. code-block:: python -/// -/// from savant_rs.utils.numpy import matrix_to_np, NalgebraDMatrix, np_to_matrix -/// import numpy as np -/// numpy_ndarray = np.zeros((4, 10), dtype='int32') -/// matrix = np_to_matrix(numpy_ndarray) -/// new_numpy_ndarray = matrix_to_np(matrix) -/// assert np.array_equal(numpy_ndarray, new_numpy_ndarray) -/// -#[pyclass] -#[derive(Debug, Clone)] -pub struct NalgebraDMatrix { - inner: Arc, -} - -impl NalgebraDMatrix { - pub fn from_fp64(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Float64(m)), - } - } - pub fn from_fp32(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Float32(m)), - } - } - - pub fn from_i64(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Int64(m)), - } - } - - pub fn from_i32(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Int32(m)), - } - } - - pub fn from_i16(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Int16(m)), - } - } - - pub fn from_i8(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::Int8(m)), - } - } - - pub fn from_u64(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::UnsignedInt64(m)), - } - } - - pub fn from_u32(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::UnsignedInt32(m)), - } - } - - pub fn from_u16(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::UnsignedInt16(m)), - } - } - - pub fn from_u8(m: DMatrix) -> Self { - Self { - inner: Arc::new(NalgebraDMatrixVariant::UnsignedInt8(m)), - } - } -} - -#[pymethods] -impl NalgebraDMatrix { - #[classattr] - const __hash__: Option> = None; - - fn __repr__(&self) -> String { - match self.inner.deref() { - NalgebraDMatrixVariant::Float64(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::Float32(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::Int64(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::Int32(m) => { - pretty_print!(m) - } - - NalgebraDMatrixVariant::Int16(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::Int8(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::UnsignedInt64(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::UnsignedInt32(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::UnsignedInt16(m) => { - pretty_print!(m) - } - NalgebraDMatrixVariant::UnsignedInt8(m) => { - pretty_print!(m) - } - } - } - - fn __str__(&self) -> String { - self.__repr__() - } -} - -pub fn matrix_to_np(m: &DMatrix) -> PyObject { - let arr = - ArrayD::::from_shape_vec(IxDyn(&[m.nrows(), m.ncols()]), m.as_slice().to_vec()).unwrap(); - - with_gil!(|py| { - let arr = PyArray::from_array(py, &arr); - arr.into_py(py) - }) -} - -pub fn np_to_matrix(arr: PyReadonlyArrayDyn) -> PyResult> { - let shape = arr.shape().to_vec(); - let slice = arr.as_slice(); - let slice = match slice { - Ok(slice) => slice, - Err(_) => { - return Err(PyValueError::new_err( - "Non-contiguous array cannot be converted to DMatrix", - )) - } - } - .to_vec(); - - with_gil!(|py| py.allow_threads(|| Ok(nalgebra::DMatrix::from_vec(shape[0], shape[1], slice)))) -} - -/// Converts a Numpy array to a :class:`NalgebraDMatrix`. This function is GIL-free. It supports the following Numpy dtypes: -/// ``f32``, ``f64``, ``i8``, ``i16``, ``i32``, ``i64``, ``u8``, ``u16``, ``u32``, ``u64``. -/// -/// Parameters -/// ---------- -/// arr : np.ndarray -/// The array to convert -/// -/// Returns -/// ------- -/// :class:`NalgebraDMatrix` -/// The python handle to the converted matrix -/// -/// Raises -/// ------ -/// ValueError -/// If the array is not compatible. -/// -#[pyfunction] -#[pyo3(name = "np_to_matrix")] -pub fn np_to_matrix_gil(arr: &PyAny) -> PyResult { - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_fp32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_fp64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_i8)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_i16)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_i32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_i64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_u8)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_u16)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_u32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_matrix(arr.readonly()).map(NalgebraDMatrix::from_u64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32/64, i8/16/32/64, or u8/16/32/64", - )) -} - -/// Converts a :class:`NalgebraDMatrix` to a numpy array. This function is GIL-free. It supports the following Numpy dtypes: -/// ``f32``, ``f64``, ``i8``, ``i16``, ``i32``, ``i64``, ``u8``, ``u16``, ``u32``, ``u64``. -/// -/// Parameters -/// ---------- -/// m : :class:`NalgebraDMatrix` -/// The matrix to convert -/// -/// Returns -/// ------- -/// np.ndarray -/// The numpy array -/// -/// Raises -/// ------ -/// TypeError -/// If the matrix is not of a supported type -/// -#[pyfunction] -#[pyo3(name = "matrix_to_np")] -pub fn matrix_to_np_py(m: &PyAny) -> PyResult { - if let Ok(m) = m.extract::() { - let m = match m.inner.deref() { - NalgebraDMatrixVariant::Float64(m) => matrix_to_np(m), - NalgebraDMatrixVariant::Float32(m) => matrix_to_np(m), - NalgebraDMatrixVariant::Int64(m) => matrix_to_np(m), - NalgebraDMatrixVariant::Int32(m) => matrix_to_np(m), - NalgebraDMatrixVariant::Int16(m) => matrix_to_np(m), - NalgebraDMatrixVariant::Int8(m) => matrix_to_np(m), - NalgebraDMatrixVariant::UnsignedInt64(m) => matrix_to_np(m), - NalgebraDMatrixVariant::UnsignedInt32(m) => matrix_to_np(m), - NalgebraDMatrixVariant::UnsignedInt16(m) => matrix_to_np(m), - NalgebraDMatrixVariant::UnsignedInt8(m) => matrix_to_np(m), - }; - return Ok(m); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32/64, i8/16/32/64, or u8/16/32/64", - )) -} - -#[cfg(test)] -mod tests { - #[test] - fn test_cast() { - let m = nalgebra::DMatrix::::from_row_slice(2, 2, &[1, 2, 3, 4]); - let _n = m.clone().cast::(); - let _n = m.clone().cast::(); - let _n = m.cast::(); - } -} diff --git a/savant_algebra/src/np/np_ndarray.rs b/savant_algebra/src/np/np_ndarray.rs deleted file mode 100644 index 81b5a075..00000000 --- a/savant_algebra/src/np/np_ndarray.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::np::ElementType; -use ndarray::{ArrayBase, IxDyn, OwnedRepr}; -use numpy::{PyArray, PyReadonlyArrayDyn}; -use pyo3::prelude::*; -use savant_rs::with_gil; -use std::ops::Deref; -use std::sync::Arc; - -type DynamicArray = ArrayBase, IxDyn>; - -#[derive(Debug, Clone)] -pub enum NDarrayVariant { - Float64(DynamicArray), - Float32(DynamicArray), - Int64(DynamicArray), - Int32(DynamicArray), - Int16(DynamicArray), - Int8(DynamicArray), - UnsignedInt64(DynamicArray), - UnsignedInt32(DynamicArray), - UnsignedInt16(DynamicArray), - UnsignedInt8(DynamicArray), -} - -/// The class is a wrapper class to handle Rust's NDarray in Python. -/// The class doesn't have methods intended to be used directly by the user. Instead, it is used -/// by Rust functions to which the user passes a NDarray object for optimized processing. -/// -/// Examples -/// -------- -/// .. code-block:: python -/// -/// from savant_rs.utils.numpy import ndarray_to_np, NalgebraDMatrix, np_to_ndarray -/// import numpy as np -/// numpy_ndarray = np.zeros((4, 10), dtype='int32') -/// rust_ndarray = np_to_ndarray(numpy_ndarray) -/// new_numpy_ndarray = ndarray_to_np(rust_ndarray) -/// assert np.array_equal(numpy_ndarray, new_numpy_ndarray) -/// -#[pyclass] -#[derive(Debug, Clone)] -pub struct NDarray { - inner: Arc, -} - -#[pymethods] -impl NDarray { - #[classattr] - const __hash__: Option> = None; - - fn __repr__(&self) -> String { - format!("{:#?}", self.inner.deref()) - } - - fn __str__(&self) -> String { - self.__repr__() - } -} - -impl NDarray { - pub fn from_fp64(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Float64(m)), - } - } - pub fn from_fp32(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Float32(m)), - } - } - - pub fn from_i64(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Int64(m)), - } - } - - pub fn from_i32(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Int32(m)), - } - } - - pub fn from_i16(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Int16(m)), - } - } - - pub fn from_i8(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::Int8(m)), - } - } - - pub fn from_u64(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::UnsignedInt64(m)), - } - } - - pub fn from_u32(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::UnsignedInt32(m)), - } - } - - pub fn from_u16(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::UnsignedInt16(m)), - } - } - - pub fn from_u8(m: DynamicArray) -> Self { - Self { - inner: Arc::new(NDarrayVariant::UnsignedInt8(m)), - } - } -} - -pub fn ndarray_to_np(m: &DynamicArray) -> PyObject { - let arr = m.clone(); - with_gil!(|py| { - let arr = PyArray::from_array(py, &arr); - arr.into_py(py) - }) -} - -pub fn np_to_ndarray(arr: PyReadonlyArrayDyn) -> PyResult> { - let arr = arr.as_array().to_owned(); - Ok(arr) -} - -/// Converts a Numpy Ndarray to a Rust :class:`NDarray` object. This function is GIL-free. It supports the following Numpy dtypes: -/// ``f32``, ``f64``, ``i8``, ``i16``, ``i32``, ``i64``, ``u8``, ``u16``, ``u32``, ``u64``. -/// -/// Parameters -/// ---------- -/// arr : numpy.ndarray -/// The Numpy Ndarray to be converted. -/// -/// Returns -/// ------- -/// :class:`NDarray` -/// The Rust NDarray object. -/// -#[pyfunction] -#[pyo3(name = "np_to_ndarray")] -pub fn np_to_ndarray_gil(arr: &PyAny) -> PyResult { - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_fp32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_fp64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_i8)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_i16)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_i32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_i64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_u8)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_u16)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_u32)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - if let Ok(arr) = arr.downcast::>() { - let m = np_to_ndarray(arr.readonly()).map(NDarray::from_u64)?; - return with_gil!(|py| Ok(m.into_py(py))); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32/64, i8/16/32/64, or u8/16/32/64", - )) -} - -/// Converts a Rust :class:`NDarray` object to a Numpy Ndarray. This function is GIL-free. It supports the following Numpy dtypes: -/// ``f32``, ``f64``, ``i8``, ``i16``, ``i32``, ``i64``, ``u8``, ``u16``, ``u32``, ``u64``. -/// -/// Parameters -/// ---------- -/// m : :class:`NDarray` -/// The Rust NDarray object to be converted. -/// -/// Returns -/// ------- -/// numpy.ndarray -/// The Numpy Ndarray. -/// -#[pyfunction] -#[pyo3(name = "ndarray_to_np")] -pub fn ndarray_to_np_py(m: &PyAny) -> PyResult { - if let Ok(m) = m.extract::() { - let m = match m.inner.deref() { - NDarrayVariant::Float64(m) => ndarray_to_np(m), - NDarrayVariant::Float32(m) => ndarray_to_np(m), - NDarrayVariant::Int64(m) => ndarray_to_np(m), - NDarrayVariant::Int32(m) => ndarray_to_np(m), - NDarrayVariant::Int16(m) => ndarray_to_np(m), - NDarrayVariant::Int8(m) => ndarray_to_np(m), - NDarrayVariant::UnsignedInt64(m) => ndarray_to_np(m), - NDarrayVariant::UnsignedInt32(m) => ndarray_to_np(m), - NDarrayVariant::UnsignedInt16(m) => ndarray_to_np(m), - NDarrayVariant::UnsignedInt8(m) => ndarray_to_np(m), - }; - return Ok(m); - } - - Err(pyo3::exceptions::PyTypeError::new_err( - "Expected ndarray of type f32/64, i8/16/32/i64, or u8/16/32/64", - )) -} diff --git a/savant_core/Cargo.toml b/savant_core/Cargo.toml index 52c32be9..bed97abe 100644 --- a/savant_core/Cargo.toml +++ b/savant_core/Cargo.toml @@ -15,40 +15,42 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1" -bytes = "1.5" +anyhow = { workspace = true } +evalexpr = { workspace = true } +hashbrown = { workspace = true } +geo = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-otlp = { workspace = true } +parking_lot = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } + +# unique to savant_core +bytes = "1.8" crc32fast = "1" crossbeam = "0.8" derive_builder = "0.13" -evalexpr = { version = "11", features = ["rand", "regex_support"] } etcd_dynamic_state = { git = "https://github.com/insight-platform/etcd_dynamic_state", tag = "0.2.12" } etcd-client = { version = "0.13", features = ["tls"] } -geo = "0.26" -hashbrown = { version = "0.14", features = ["raw", "serde"] } jmespath = { version = "0.3", features = ["sync"] } -lazy_static = "1.4" libloading = "0.8" -log = "0.4" lru = { version = "0.12", features = ["hashbrown"] } -opentelemetry = "0.24.0" opentelemetry_sdk = { version = "0.24.1", features = ["rt-tokio"] } -opentelemetry-otlp = { version = "0.17.0", features = ["http-json", "http-proto", "tls", "reqwest-rustls"] } tonic = { version = "0.12.2", features = ["tls-native-roots"] } -reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] } +reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls-native-roots"] } opentelemetry-stdout = { version = "0.5.0", features = ["trace"] } opentelemetry-semantic-conventions = "0.16.0" opentelemetry-jaeger-propagator = "0.3.0" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } prost = "0.12" prost-types = "0.12" rayon = "1.8" regex = "1" savant-protobuf = { git = "https://github.com/insight-platform/savant-protobuf", tag = "0.2.0" } -serde_json = "1.0" serde_yaml = "0.9" -serde = { version = "1.0", features = ["derive"] } -thiserror = "1" uuid = { version = "1.7", features = ["fast-rng", "v7"] } zmq = "0.10" rand = "0.8.5" diff --git a/savant_core/benches/bench_bbox_utils.rs b/savant_core/benches/bench_bbox_utils.rs new file mode 100644 index 00000000..721d21b5 --- /dev/null +++ b/savant_core/benches/bench_bbox_utils.rs @@ -0,0 +1,71 @@ +#![feature(test)] + +extern crate test; + +use rand::Rng; +use savant_core::primitives::utils::solely_owned_areas; +use savant_core::primitives::RBBox; +use test::Bencher; + +fn bench_solely_owned_areas(bbox_count: usize, parallel: bool) { + let pos_x_range = 0.0..1920.0; + let pos_y_range = 0.0..1080.0; + let width_range = 50.0..600.0; + let height_range = 50.0..400.0; + let mut rng = rand::thread_rng(); + let bboxes: Vec = (0..bbox_count) + .map(|_| { + RBBox::new( + rng.gen_range(pos_x_range.clone()), + rng.gen_range(pos_y_range.clone()), + rng.gen_range(width_range.clone()), + rng.gen_range(height_range.clone()), + Some(0.0), + ) + }) + .collect(); + let bbox_refs = bboxes.iter().collect::>(); + solely_owned_areas(&bbox_refs, parallel); +} + +#[bench] +fn bench_seq_solely_owned_areas_010(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(10, false); + }); +} + +#[bench] +fn bench_seq_solely_owned_areas_020(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(20, false); + }); +} + +#[bench] +fn bench_seq_solely_owned_areas_050(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(50, false); + }); +} + +#[bench] +fn bench_par_solely_owned_areas_010(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(10, true); + }); +} + +#[bench] +fn bench_par_solely_owned_areas_020(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(20, true); + }); +} + +#[bench] +fn bench_par_solely_owned_areas_050(b: &mut Bencher) { + b.iter(|| { + bench_solely_owned_areas(50, true); + }); +} diff --git a/savant_core/src/primitives/bbox.rs b/savant_core/src/primitives/bbox.rs index 575ad309..baf160fa 100644 --- a/savant_core/src/primitives/bbox.rs +++ b/savant_core/src/primitives/bbox.rs @@ -1,3 +1,5 @@ +pub mod utils; + use crate::atomic_f32::AtomicF32; use crate::draw::PaddingDraw; use crate::primitives::{Point, PolygonalArea}; diff --git a/savant_core/src/primitives/bbox/utils.rs b/savant_core/src/primitives/bbox/utils.rs new file mode 100644 index 00000000..6684a19f --- /dev/null +++ b/savant_core/src/primitives/bbox/utils.rs @@ -0,0 +1,180 @@ +use crate::primitives::{BBoxMetricType, RBBox}; +use geo::{Area, BooleanOps, MultiPolygon}; +use rayon::iter::ParallelIterator; +use rayon::prelude::IntoParallelRefIterator; +use std::collections::HashMap; +use std::sync::Arc; + +fn sequential_solely_owned_areas(bboxes: &[&RBBox]) -> Vec { + let mut areas = Vec::with_capacity(bboxes.len()); + for (i, bbox) in bboxes.iter().enumerate() { + let others = bboxes + .iter() + .enumerate() + .filter(|(j, _)| i != *j) + .filter(|(_, b)| matches!(bbox.calculate_intersection(b), Ok(x) if x > 0.0)) + .map(|(_, b)| *b) + .collect::>(); + let union = calculate_union_area(&others); + let bbox_area = MultiPolygon::new(vec![bbox.get_as_polygonal_area().get_polygon()]); + let area = bbox_area.difference(&union).unsigned_area(); + areas.push(area); + } + areas +} + +fn rayon_solely_owned_areas(bboxes: &[&RBBox]) -> Vec { + bboxes + .par_iter() + .map(|bbox| { + let others = bboxes + .iter() + .filter(|b| { + !Arc::ptr_eq(&b.0, &bbox.0) + && matches!(bbox.calculate_intersection(b), Ok(x) if x > 0.0) + }) + .copied() + .collect::>(); + let union = calculate_union_area(&others); + let bbox_area = MultiPolygon::new(vec![bbox.get_as_polygonal_area().get_polygon()]); + bbox_area.difference(&union).unsigned_area() + }) + .collect() +} + +pub fn solely_owned_areas(bboxes: &[&RBBox], parallel: bool) -> Vec { + if parallel { + rayon_solely_owned_areas(bboxes) + } else { + sequential_solely_owned_areas(bboxes) + } +} + +pub fn calculate_union_area(bboxes: &[&RBBox]) -> MultiPolygon { + if bboxes.is_empty() { + return MultiPolygon::new(Vec::new()); + } + + let mut union = MultiPolygon::new(vec![bboxes[0].get_as_polygonal_area().get_polygon()]); + + for bbox in &bboxes[1..] { + let mp = MultiPolygon::new(vec![bbox.get_as_polygonal_area().get_polygon()]); + union = union.union(&mp); + } + + union +} + +pub fn associate_bboxes( + candidates: &[&RBBox], + owners: &[&RBBox], + metric: BBoxMetricType, + threshold: f32, +) -> HashMap> { + let mut associations = HashMap::new(); + for (ci, c) in candidates.iter().enumerate() { + associations.insert(ci, Vec::new()); + for (co, o) in owners.iter().enumerate() { + let mv = match metric { + BBoxMetricType::IoU => c.iou(o), + BBoxMetricType::IoSelf => c.ios(o), + BBoxMetricType::IoOther => c.ioo(o), + }; + if let Ok(mv) = mv { + if mv > threshold { + associations.entry(ci).and_modify(|v| v.push((co, mv))); + } + } + } + } + for (_, v) in associations.iter_mut() { + v.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); + } + + associations +} + +#[cfg(test)] +mod tests { + use crate::primitives::{BBoxMetricType, RBBox}; + use geo::Area; + + #[test] + fn test_calculate_union_area() { + let bb1 = RBBox::new(0.0, 0.0, 2.0, 2.0, Some(0.0)); + let bb2 = RBBox::new(2.0, 0.0, 4.0, 2.0, Some(0.0)); + + let empty_union = super::calculate_union_area(&[]); + assert_eq!(empty_union.unsigned_area(), 0.0); + + let union = super::calculate_union_area(&[&bb1]); + assert_eq!(union.unsigned_area(), 4.0); + + let union = super::calculate_union_area(&[&bb1, &bb2]); + assert_eq!(union.unsigned_area(), 10.0); + + let self_union = super::calculate_union_area(&[&bb1, &bb1]); + assert_eq!(self_union.unsigned_area(), 4.0); + } + + #[test] + fn test_solely_owned_areas() { + let bb1 = RBBox::new(0.0, 0.0, 2.0, 2.0, Some(0.0)); + let bb2 = RBBox::new(2.0, 0.0, 4.0, 2.0, Some(0.0)); + + let areas = super::sequential_solely_owned_areas(&[&bb1, &bb2]); + assert_eq!(areas, vec![2.0, 6.0]); + + let areas = super::rayon_solely_owned_areas(&[&bb1, &bb2]); + assert_eq!(areas, vec![2.0, 6.0]); + + let areas = super::solely_owned_areas(&[&bb1, &bb2], true); + assert_eq!(areas, vec![2.0, 6.0]); + + let areas = super::solely_owned_areas(&[&bb1, &bb2], false); + assert_eq!(areas, vec![2.0, 6.0]); + } + + #[test] + fn test_complex_owned_areas() { + let red = RBBox::ltrb(0.0, 2.0, 2.0, 4.0); + let green = RBBox::ltrb(1.0, 3.0, 5.0, 5.0); + let yellow = RBBox::ltrb(1.0, 1.0, 3.0, 6.0); + let purple = RBBox::ltrb(4.0, 0.0, 7.0, 2.0); + + for flavor in [true, false] { + let areas = super::solely_owned_areas(&[&red, &green, &yellow, &purple], flavor); + + let red_area = areas[0]; + let green_area = areas[1]; + let yellow_area = areas[2]; + let purple_area = areas[3]; + + assert_eq!(red_area, 2.0); + assert_eq!(green_area, 4.0); + assert_eq!(yellow_area, 5.0); + assert_eq!(purple_area, 6.0); + } + } + + #[test] + fn test_associate_boxes() { + let lp1 = RBBox::ltrb(0.0, 1.0, 2.0, 2.0); + let lp2 = RBBox::ltrb(5.0, 2.0, 8.0, 3.0); + let lp3 = RBBox::ltrb(100.0, 0.0, 6.0, 3.0); + let owner1 = RBBox::ltrb(1.0, 0.0, 6.0, 3.0); + let owner2 = RBBox::ltrb(6.0, 1.0, 9.0, 4.0); + let associations_iou = super::associate_bboxes( + &[&lp1, &lp2, &lp3], + &[&owner1, &owner2], + BBoxMetricType::IoU, + 0.01, + ); + let lp1_associations = associations_iou.get(&0).unwrap(); + let lp2_associations = associations_iou.get(&1).unwrap(); + let lp3_associations = associations_iou.get(&2).unwrap(); + assert!(matches!(lp1_associations.as_slice(), [(0, _)])); + assert!(matches!(lp2_associations.as_slice(), [(1, _), (0, _)])); + assert!(lp3_associations.is_empty()); + } +} diff --git a/savant_core/src/primitives/frame.rs b/savant_core/src/primitives/frame.rs index a0e64798..c540a740 100644 --- a/savant_core/src/primitives/frame.rs +++ b/savant_core/src/primitives/frame.rs @@ -190,6 +190,12 @@ impl ToSerdeJsonValue for VideoFrame { .map(|v| Uuid::from_u128(v).to_string()); let version = version(); + let mut objects = self.objects.values().collect::>(); + objects.sort_by(|a, b| a.id.cmp(&b.id)); + let objects = objects + .iter() + .map(|o| o.to_serde_json_value()) + .collect::>(); serde_json::json!( { "previous_frame_seq_id": self.previous_frame_seq_id, @@ -212,7 +218,7 @@ impl ToSerdeJsonValue for VideoFrame { "content": self.content.to_serde_json_value(), "transformations": self.transformations.iter().map(|t| t.to_serde_json_value()).collect::>(), "attributes": self.attributes.iter().filter_map(|v| if v.is_hidden { None } else { Some(v.to_serde_json_value()) }).collect::>(), - "objects": self.objects.values().map(|o| o.to_serde_json_value()).collect::>(), + "objects": objects, } ) } diff --git a/savant_core/src/telemetry.rs b/savant_core/src/telemetry.rs index 7fc32bb9..aaac072d 100644 --- a/savant_core/src/telemetry.rs +++ b/savant_core/src/telemetry.rs @@ -6,7 +6,6 @@ use opentelemetry_sdk::propagation::TraceContextPropagator; use opentelemetry_sdk::trace::{Config, TracerProvider}; use opentelemetry_sdk::{runtime, Resource}; use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_NAMESPACE}; -use opentelemetry_stdout::SpanExporter; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::cell::OnceCell; @@ -209,9 +208,14 @@ impl Configurator { .install_batch(runtime::Tokio) .expect("Failed to install OpenTelemetry tracer globally") } - None => TracerProvider::builder() - .with_simple_exporter(SpanExporter::default()) - .build(), + None => { + let exporter = opentelemetry_stdout::SpanExporter::builder() + .with_writer(std::io::sink()) + .build(); + TracerProvider::builder() + .with_simple_exporter(exporter) + .build() + } }; global::set_tracer_provider(tracer_provider); diff --git a/savant_core/src/transport/zeromq/writer.rs b/savant_core/src/transport/zeromq/writer.rs index b156af77..35a947bb 100644 --- a/savant_core/src/transport/zeromq/writer.rs +++ b/savant_core/src/transport/zeromq/writer.rs @@ -42,6 +42,7 @@ pub enum WriterResult { }, } +#[allow(dead_code)] #[derive(Default)] struct MockResponder; diff --git a/savant_core_py/Cargo.toml b/savant_core_py/Cargo.toml index 95ff1b0c..52e490f0 100644 --- a/savant_core_py/Cargo.toml +++ b/savant_core_py/Cargo.toml @@ -16,30 +16,29 @@ rust-version.workspace = true crate-type = ["dylib"] [dependencies] +anyhow = { workspace = true } +evalexpr = { workspace = true } +hashbrown = { workspace = true } +geo = { workspace = true } +lazy_static = { workspace = true } +log = { workspace = true } +opentelemetry = { workspace = true } +opentelemetry-otlp = { workspace = true } +parking_lot = { workspace = true } +pyo3 = { workspace = true } savant_core = { workspace = true } -serde_json = "1.0" -serde = { version = "1.0", features = ["derive"] } -anyhow = "1.0" -thiserror = "1.0" -geo = "0.26" -lazy_static = "1.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } -evalexpr = { version = "11", features = ["rand", "regex_support"] } -log = "0.4" -opentelemetry = "0.24.0" -opentelemetry-otlp = "0.17.0" -colored = "2" -hashbrown = "0.14" +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } -[dependencies.pyo3] -version = "0.21" +# unique to savant_core_py +colored = "2" [dev-dependencies] serial_test = "2.0" - [build-dependencies] -pyo3-build-config = "0.21" +pyo3-build-config = { workspace = true } cbindgen = "0.24" [package.metadata.maturin] diff --git a/savant_core_py/src/draw_spec.rs b/savant_core_py/src/draw_spec.rs index b1ff93d4..11a1671b 100644 --- a/savant_core_py/src/draw_spec.rs +++ b/savant_core_py/src/draw_spec.rs @@ -417,8 +417,8 @@ impl DotDraw { /// or get properties. There is no way to update properties inplace. Fields are /// not available in Python, use getters. /// -#[pyclass] -#[derive(Clone, Copy, Debug)] +#[pyclass(eq, eq_int)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum LabelPositionKind { /// Margin is relative to the **top** left corner of the text bounding box TopLeftInside, diff --git a/savant_core_py/src/logging.rs b/savant_core_py/src/logging.rs index b46ef2e2..ead3f8cd 100644 --- a/savant_core_py/src/logging.rs +++ b/savant_core_py/src/logging.rs @@ -13,8 +13,8 @@ use savant_core::otlp::with_current_context; /// :py:class:`LogLevel` /// The log level. By default, the log level is set to Info. /// -#[pyclass] -#[derive(Debug, Clone, Copy)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum LogLevel { Trace, Debug, diff --git a/savant_core_py/src/pipeline.rs b/savant_core_py/src/pipeline.rs index d9ca7e1c..7a9e472b 100644 --- a/savant_core_py/src/pipeline.rs +++ b/savant_core_py/src/pipeline.rs @@ -64,14 +64,15 @@ pub fn load_stage_function_plugin( /// Defines which type of payload a stage handles. /// -#[pyclass] +#[pyclass(eq, eq_int)] #[derive(Copy, Clone, Debug, PartialEq)] pub enum VideoPipelineStagePayloadType { Frame, Batch, } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, PartialEq)] pub enum FrameProcessingStatRecordType { Initial, Frame, diff --git a/savant_core_py/src/primitives/attribute_value.rs b/savant_core_py/src/primitives/attribute_value.rs index 383d5342..a08c8e0f 100644 --- a/savant_core_py/src/primitives/attribute_value.rs +++ b/savant_core_py/src/primitives/attribute_value.rs @@ -775,8 +775,8 @@ impl AttributeValue { /// Represents attribute value types for matching /// -#[pyclass] -#[derive(Debug, Clone, Hash)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, Hash, PartialEq)] pub enum AttributeValueType { Bytes, String, diff --git a/savant_core_py/src/primitives/bbox.rs b/savant_core_py/src/primitives/bbox.rs index 04a98b44..3b8521d0 100644 --- a/savant_core_py/src/primitives/bbox.rs +++ b/savant_core_py/src/primitives/bbox.rs @@ -1,3 +1,5 @@ +pub mod utils; + use crate::draw_spec::PaddingDraw; use crate::primitives::polygonal_area::PolygonalArea; use pyo3::exceptions::{PyNotImplementedError, PyValueError}; @@ -11,8 +13,8 @@ use savant_core::primitives::rust; /// IoSelf - Intersection over Self (Intersection / Area of Self) /// IoOther - Intersection over Other (Intersection / Area of Other) /// -#[pyclass] -#[derive(Debug, Clone)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, PartialEq)] pub enum BBoxMetricType { IoU, IoSelf, @@ -221,11 +223,13 @@ impl RBBox { /// new bbox /// #[staticmethod] + #[pyo3(signature = (xc, yc, width, height, angle=None))] fn constructor(xc: f32, yc: f32, width: f32, height: f32, angle: Option) -> Self { Self::new(xc, yc, width, height, angle) } #[new] + #[pyo3(signature = (xc, yc, width, height, angle=None))] pub fn new(xc: f32, yc: f32, width: f32, height: f32, angle: Option) -> Self { Self(rust::RBBox::new(xc, yc, width, height, angle)) } diff --git a/savant_core_py/src/primitives/bbox/utils.rs b/savant_core_py/src/primitives/bbox/utils.rs new file mode 100644 index 00000000..9deb72bb --- /dev/null +++ b/savant_core_py/src/primitives/bbox/utils.rs @@ -0,0 +1,29 @@ +use crate::primitives::bbox::{BBoxMetricType, RBBox}; +use crate::with_gil; +use pyo3::prelude::*; +use std::collections::HashMap; + +#[pyfunction] +pub fn solely_owned_areas(bboxes: Vec, parallel: bool) -> Vec { + let boxes = bboxes.iter().map(|b| &b.0).collect::>(); + with_gil!(|_| { savant_core::primitives::bbox::utils::solely_owned_areas(&boxes, parallel) }) +} + +#[pyfunction] +pub fn associate_bboxes( + candidates: Vec, + owners: Vec, + metric: BBoxMetricType, + threshold: f32, +) -> HashMap> { + let candidates = candidates.iter().map(|b| &b.0).collect::>(); + let owners = owners.iter().map(|b| &b.0).collect::>(); + with_gil!(|_| { + savant_core::primitives::bbox::utils::associate_bboxes( + &candidates, + &owners, + metric.into(), + threshold, + ) + }) +} diff --git a/savant_core_py/src/primitives/frame.rs b/savant_core_py/src/primitives/frame.rs index 3e8cde4b..35888a32 100644 --- a/savant_core_py/src/primitives/frame.rs +++ b/savant_core_py/src/primitives/frame.rs @@ -26,6 +26,7 @@ pub struct ExternalFrame(pub(crate) rust::ExternalFrame); #[pymethods] impl ExternalFrame { #[new] + #[pyo3(signature = (method, location=None))] pub fn new(method: &str, location: Option) -> Self { Self(rust::ExternalFrame::new(method, &location.as_deref())) } @@ -88,6 +89,7 @@ impl VideoFrameContent { } #[staticmethod] + #[pyo3(signature = (method, location=None))] pub fn external(method: String, location: Option) -> Self { Self(rust::VideoFrameContent::External(rust::ExternalFrame { method, @@ -195,8 +197,8 @@ impl VideoFrameContent { /// Represents the structure for accessing primary video content encoding information /// for the frame. -#[pyclass] -#[derive(Copy, Clone, Debug)] +#[pyclass(eq, eq_int)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum VideoFrameTranscodingMethod { Copy, Encoded, @@ -937,6 +939,7 @@ impl VideoFrame { } #[allow(clippy::too_many_arguments)] + #[pyo3(signature = (namespace, label, parent_id=None, confidence=None, detection_box=None, track_id=None, track_box=None, attributes=None))] pub fn create_object( &self, namespace: &str, diff --git a/savant_core_py/src/primitives/frame_update.rs b/savant_core_py/src/primitives/frame_update.rs index 307ee17d..cbcb60b5 100644 --- a/savant_core_py/src/primitives/frame_update.rs +++ b/savant_core_py/src/primitives/frame_update.rs @@ -14,8 +14,8 @@ use savant_core::protobuf::{from_pb, ToProtobuf}; /// * the one to error if labels collide; /// * the one to replace objects with the same label. /// -#[pyclass] -#[derive(Clone, Debug)] +#[pyclass(eq, eq_int)] +#[derive(Clone, Debug, PartialEq)] pub enum ObjectUpdatePolicy { AddForeignObjects, ErrorIfLabelsCollide, @@ -58,8 +58,8 @@ impl From for ObjectUpdatePolicy { /// * the one to error when duplicates are found; /// * the one to prefix duplicates with a given string. /// -#[pyclass] -#[derive(Clone, Debug)] +#[pyclass(eq, eq_int)] +#[derive(Clone, Debug, PartialEq)] pub enum AttributeUpdatePolicy { ReplaceWithForeignWhenDuplicate, KeepOwnWhenDuplicate, diff --git a/savant_core_py/src/primitives/object.rs b/savant_core_py/src/primitives/object.rs index 0e7ad454..76932e02 100644 --- a/savant_core_py/src/primitives/object.rs +++ b/savant_core_py/src/primitives/object.rs @@ -11,8 +11,8 @@ use savant_core::primitives::{rust, WithAttributes}; use savant_core::protobuf::{from_pb, ToProtobuf}; use serde_json::Value; -#[pyclass] -#[derive(Debug, Clone)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, PartialEq)] pub enum IdCollisionResolutionPolicy { GenerateNewId, Overwrite, @@ -39,6 +39,7 @@ pub struct VideoObject(pub(crate) rust::VideoObject); impl VideoObject { #[allow(clippy::too_many_arguments)] #[new] + #[pyo3(signature = (id, namespace, label, detection_box, attributes, confidence=None, track_id=None, track_box=None))] pub fn new( id: i64, namespace: &str, @@ -145,6 +146,7 @@ impl VideoObject { self.0.set_attribute(attribute.0.clone()).map(Attribute) } + #[pyo3(signature = (namespace, name, is_hidden, hint=None, values=None))] fn set_persistent_attribute( &mut self, namespace: &str, @@ -162,6 +164,7 @@ impl VideoObject { .set_persistent_attribute(namespace, name, &hint, is_hidden, values) } + #[pyo3(signature = (namespace, name, is_hidden, hint=None, values=None))] fn set_temporary_attribute( &mut self, namespace: &str, diff --git a/savant_core_py/src/primitives/objects_view.rs b/savant_core_py/src/primitives/objects_view.rs index 64dbbc64..b7cf25a0 100644 --- a/savant_core_py/src/primitives/objects_view.rs +++ b/savant_core_py/src/primitives/objects_view.rs @@ -11,8 +11,8 @@ pub type VideoObjectsViewBatch = HashMap; /// Determines which object bbox is a subject of the operation /// -#[pyclass] -#[derive(Clone, Debug, Copy)] +#[pyclass(eq, eq_int)] +#[derive(Clone, Debug, Copy, PartialEq)] #[repr(C)] pub enum VideoObjectBBoxType { Detection, diff --git a/savant_core_py/src/primitives/polygonal_area.rs b/savant_core_py/src/primitives/polygonal_area.rs index c5724803..63acc5bb 100644 --- a/savant_core_py/src/primitives/polygonal_area.rs +++ b/savant_core_py/src/primitives/polygonal_area.rs @@ -83,6 +83,7 @@ impl PolygonalArea { } #[new] + #[pyo3(signature = (vertices, tags=None))] pub fn new(vertices: Vec, tags: Option>>) -> Self { let vertices = unsafe { mem::transmute::, Vec>(vertices) }; Self(rust::PolygonalArea::new(vertices, tags)) diff --git a/savant_core_py/src/primitives/segment.rs b/savant_core_py/src/primitives/segment.rs index a8047406..0d0adc01 100644 --- a/savant_core_py/src/primitives/segment.rs +++ b/savant_core_py/src/primitives/segment.rs @@ -35,8 +35,8 @@ impl Segment { } } -#[pyclass] -#[derive(Debug, Clone)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, PartialEq)] pub enum IntersectionKind { Enter, Inside, diff --git a/savant_core_py/src/telemetry.rs b/savant_core_py/src/telemetry.rs index 6123bec6..5f5030a7 100644 --- a/savant_core_py/src/telemetry.rs +++ b/savant_core_py/src/telemetry.rs @@ -2,15 +2,15 @@ use pyo3::{pyclass, pyfunction, pymethods}; use savant_core::telemetry; use std::time::Duration; -#[pyclass] -#[derive(Clone)] +#[pyclass(eq, eq_int)] +#[derive(Clone, PartialEq)] pub enum ContextPropagationFormat { Jaeger, W3C, } -#[pyclass] -#[derive(Clone)] +#[pyclass(eq, eq_int)] +#[derive(Clone, PartialEq)] pub enum Protocol { Grpc, HttpBinary, diff --git a/savant_core_py/src/utils/byte_buffer.rs b/savant_core_py/src/utils/byte_buffer.rs index 6f695b59..ef20d0ba 100644 --- a/savant_core_py/src/utils/byte_buffer.rs +++ b/savant_core_py/src/utils/byte_buffer.rs @@ -27,6 +27,7 @@ pub struct ByteBuffer { #[pymethods] impl ByteBuffer { #[new] + #[pyo3(signature = (v, checksum=None))] fn create(v: &Bound<'_, PyBytes>, checksum: Option) -> PyResult { Ok(Self::new(v.as_bytes().to_vec(), checksum)) } diff --git a/savant_core_py/src/utils/otlp.rs b/savant_core_py/src/utils/otlp.rs index 0153367c..fe6eef42 100644 --- a/savant_core_py/src/utils/otlp.rs +++ b/savant_core_py/src/utils/otlp.rs @@ -172,6 +172,7 @@ impl TelemetrySpan { push_context(self.0.clone()); } + #[pyo3(signature = (exc_type=None, exc_value=None, traceback=None))] fn __exit__( &self, exc_type: Option<&Bound<'_, PyAny>>, @@ -189,13 +190,13 @@ impl TelemetrySpan { attrs.insert("python.exception.type".to_string(), format!("{:?}", e)); if let Some(v) = exc_value { - if let Ok(e) = PyAny::downcast::(v.as_gil_ref()) { + if let Ok(e) = v.downcast::() { attrs.insert("python.exception.value".to_string(), e.to_string()); } } if let Some(t) = traceback { - let traceback = PyAny::downcast::(t.as_gil_ref()).unwrap(); + let traceback = t.downcast::().unwrap(); if let Ok(formatted) = traceback.format() { attrs.insert("python.exception.traceback".to_string(), formatted); } @@ -383,6 +384,7 @@ pub struct MaybeTelemetrySpan { #[pymethods] impl MaybeTelemetrySpan { #[new] + #[pyo3(signature = (span=None))] fn new(span: Option) -> MaybeTelemetrySpan { MaybeTelemetrySpan { span } } @@ -414,6 +416,7 @@ impl MaybeTelemetrySpan { } } + #[pyo3(signature = (exc_type=None, exc_value=None, traceback=None))] fn __exit__( &self, exc_type: Option<&Bound<'_, PyAny>>, diff --git a/savant_core_py/src/utils/symbol_mapper.rs b/savant_core_py/src/utils/symbol_mapper.rs index 6457e798..826de1e6 100644 --- a/savant_core_py/src/utils/symbol_mapper.rs +++ b/savant_core_py/src/utils/symbol_mapper.rs @@ -19,8 +19,8 @@ lazy_static! { /// ErrorIfNonUnique /// The key will not be registered and a error will be triggered. /// -#[pyclass] -#[derive(Debug, Clone)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, PartialEq)] pub enum RegistrationPolicy { Override, ErrorIfNonUnique, diff --git a/savant_core_py/src/zmq/basic_types.rs b/savant_core_py/src/zmq/basic_types.rs index 8e898782..2cc65d6b 100644 --- a/savant_core_py/src/zmq/basic_types.rs +++ b/savant_core_py/src/zmq/basic_types.rs @@ -5,8 +5,8 @@ use std::hash::{Hash, Hasher}; /// Represents a socket type for a writer socket. /// -#[pyclass] -#[derive(Debug, Clone, Hash)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, Hash, PartialEq)] pub enum WriterSocketType { Pub, Dealer, @@ -52,8 +52,8 @@ impl From for zeromq::WriterSocketType { /// Represents a socket type for a reader socket. /// -#[pyclass] -#[derive(Debug, Clone, Hash)] +#[pyclass(eq, eq_int)] +#[derive(Debug, Clone, Hash, PartialEq)] pub enum ReaderSocketType { Sub, Router, diff --git a/savant_core_py/src/zmq/configs.rs b/savant_core_py/src/zmq/configs.rs index c8ae6e73..5f568763 100644 --- a/savant_core_py/src/zmq/configs.rs +++ b/savant_core_py/src/zmq/configs.rs @@ -299,6 +299,7 @@ impl WriterConfigBuilder { /// fix_ipc_permissions: int /// The access mask to set, defaults to ``0o777``. /// + #[pyo3(signature = (fix_ipc_permissions=None))] pub fn with_fix_ipc_permissions(&mut self, fix_ipc_permissions: Option) -> PyResult<()> { self.0 = Some( self.0 @@ -611,6 +612,7 @@ impl ReaderConfigBuilder { /// ValueError /// If the permissions are double set /// + #[pyo3(signature = (permissions=None))] pub fn with_fix_ipc_permissions(&mut self, permissions: Option) -> PyResult<()> { self.0 = Some( self.0 diff --git a/savant_plugins/savant_plugin_sample/Cargo.toml b/savant_plugins/savant_plugin_sample/Cargo.toml index 5e7f4b17..f609dd7a 100644 --- a/savant_plugins/savant_plugin_sample/Cargo.toml +++ b/savant_plugins/savant_plugin_sample/Cargo.toml @@ -18,15 +18,13 @@ rust-version.workspace = true crate-type = ["dylib"] [dependencies] +anyhow = { workspace = true } +pyo3 = { workspace = true } savant_core_py = { workspace = true } savant_core = { workspace = true } -anyhow = "1" - -[dependencies.pyo3] -version = "0.21" [build-dependencies] -pyo3-build-config = "0.21" +pyo3-build-config = { workspace = true } [package.metadata.maturin] python-source = "python" diff --git a/savant_python/Cargo.toml b/savant_python/Cargo.toml index 4c72eda4..f00ff871 100644 --- a/savant_python/Cargo.toml +++ b/savant_python/Cargo.toml @@ -18,13 +18,10 @@ crate-type = ["dylib"] [dependencies] savant_core_py = { workspace = true } pretty_env_logger = "0.5" - - -[dependencies.pyo3] -version = "0.21" +pyo3 = { workspace = true } [build-dependencies] -pyo3-build-config = "0.21" +pyo3-build-config = { workspace = true } [package.metadata.maturin] python-source = "python" diff --git a/savant_python/python/savant_rs/primitives/geometry/geometry.pyi b/savant_python/python/savant_rs/primitives/geometry/geometry.pyi index 5ac17fc5..fd2d06fe 100644 --- a/savant_python/python/savant_rs/primitives/geometry/geometry.pyi +++ b/savant_python/python/savant_rs/primitives/geometry/geometry.pyi @@ -8,17 +8,13 @@ class Point: def __init__(self, x: float, y: float): ... - class Segment: def __init__(self, begin: Point, end: Point): ... - @property def begin(self) -> Point: ... - @property def end(self) -> Point: ... - class IntersectionKind(Enum): Enter: ... Inside: ... @@ -26,51 +22,36 @@ class IntersectionKind(Enum): Cross: ... Outside: ... - class Intersection: - def __init__(self, kind: IntersectionKind, edges: List[Tuple[int, Optional[str]]]): ... - + def __init__( + self, kind: IntersectionKind, edges: List[Tuple[int, Optional[str]]] + ): ... @property def kind(self) -> IntersectionKind: ... - @property def edges(self) -> List[Tuple[int, Optional[str]]]: ... - class PolygonalArea: @classmethod def contains_many_points(cls, points: List[Point]) -> List[bool]: ... - @classmethod def crossed_by_segments(cls, segments: List[Segment]) -> List[Intersection]: ... - def is_self_intersecting(self) -> bool: ... - def crossed_by_segment(self, segment: Segment) -> Intersection: ... - def contains(self, point: Point) -> bool: ... - def build_polygon(self): ... - def get_tag(self, edge: int) -> Optional[str]: ... - @classmethod - def points_positions(cls, - polys: List[PolygonalArea], - points: List[Point], - no_gil: bool = False) -> List[List[int]]: ... - + def points_positions( + cls, polys: List[PolygonalArea], points: List[Point], no_gil: bool = False + ) -> List[List[int]]: ... @classmethod - def segments_intersections(cls, - polys: List[PolygonalArea], - segments: List[Segment], - no_gil: bool = False) -> List[List[Intersection]]: ... - - - def __init__(self, - vertices: List[Point], - tags: Optional[List[Optional[str]]] = None): ... - + def segments_intersections( + cls, polys: List[PolygonalArea], segments: List[Segment], no_gil: bool = False + ) -> List[List[Intersection]]: ... + def __init__( + self, vertices: List[Point], tags: Optional[List[Optional[str]]] = None + ): ... class RBBox: xc: float @@ -82,74 +63,51 @@ class RBBox: left: float @property def area(self) -> float: ... - def eq(self, other: RBBox) -> bool: ... - def almost_eq(self, other: RBBox, eps: float) -> bool: ... - def __richcmp__(self, other: RBBox, op: int) -> bool: ... - @property def width_to_height_ratio(self) -> float: ... - def is_modified(self) -> bool: ... - def set_modifications(self, value: bool): ... - - def __init__(self, xc: float, yc: float, width: float, height: float, angle: Optional[float] = None): ... - + def __init__( + self, + xc: float, + yc: float, + width: float, + height: float, + angle: Optional[float] = None, + ): ... def scale(self, scale_x: float, scale_y: float): ... - @property def vertices(self) -> List[Tuple[float, float]]: ... - @property def vertices_rounded(self) -> List[Tuple[float, float]]: ... - @property def vertices_int(self) -> List[Tuple[int, int]]: ... - def as_polygonal_area(self) -> PolygonalArea: ... - @property def wrapping_box(self) -> BBox: ... - def get_visual_box(self, padding: PaddingDraw, border_width: int) -> RBBox: ... - def new_padded(self, padding: PaddingDraw) -> RBBox: ... - def copy(self) -> RBBox: ... - def iou(self, other: RBBox) -> float: ... - def ios(self, other: RBBox) -> float: ... - def ioo(self, other: RBBox) -> float: ... - def shift(self, dx: float, dy: float) -> RBBox: ... - @classmethod def ltrb(cls, left: float, top: float, right: float, bottom: float) -> RBBox: ... - @classmethod def ltwh(cls, left: float, top: float, width: float, height: float) -> RBBox: ... - @property def right(self) -> float: ... - @property def bottom(self) -> float: ... - def as_ltrb(self) -> Tuple[float, float, float, float]: ... - def as_ltrb_int(self) -> Tuple[int, int, int, int]: ... - def as_ltwh(self) -> Tuple[float, float, float, float]: ... - def as_ltwh_int(self) -> Tuple[int, int, int, int]: ... - def as_xcycwh(self) -> Tuple[float, float, float, float]: ... - def as_xcycwh_int(self) -> Tuple[int, int, int, int]: ... class BBox: @@ -161,67 +119,43 @@ class BBox: left: float def __init__(self, xc: float, yc: float, width: float, height: float): ... def eq(self, other: BBox) -> bool: ... - def almost_eq(self, other: BBox, eps: float) -> bool: ... - def __richcmp__(self, other: BBox, op: int) -> bool: ... - def iou(self, other: BBox) -> float: ... - def ios(self, other: BBox) -> float: ... - def ioo(self, other: BBox) -> float: ... - def is_modified(self) -> bool: ... - @classmethod def ltrb(cls, left: float, top: float, right: float, bottom: float) -> BBox: ... - @classmethod def ltwh(cls, left: float, top: float, width: float, height: float) -> BBox: ... - @property def right(self) -> float: ... - @property def bottom(self) -> float: ... - @property def vertices(self) -> List[Tuple[float, float]]: ... - @property def vertices_rounded(self) -> List[Tuple[float, float]]: ... - @property def vertices_int(self) -> List[Tuple[int, int]]: ... - @property def wrapping_box(self) -> BBox: ... - def get_visual_box(self, padding: PaddingDraw, border_width: int) -> BBox: ... - def as_ltrb(self) -> Tuple[float, float, float, float]: ... - def as_ltrb_int(self) -> Tuple[int, int, int, int]: ... - def as_ltwh(self) -> Tuple[float, float, float, float]: ... - def as_ltwh_int(self) -> Tuple[int, int, int, int]: ... - def as_xcycwh(self) -> Tuple[float, float, float, float]: ... - def as_xcycwh_int(self) -> Tuple[int, int, int, int]: ... - def as_rbbox(self) -> RBBox: ... - def scale(self, scale_x: float, scale_y: float) -> BBox: ... - def shift(self, dx: float, dy: float) -> BBox: ... - def as_polygonal_area(self) -> PolygonalArea: ... - def copy(self) -> BBox: ... - def new_padded(self, padding: PaddingDraw) -> BBox: ... - +def solely_owned_areas(bboxes: List[RBBox], parallel: bool) -> List[float]: ... +def associate_bboxes( + candidates: List[RBBox], owners: List[RBBox], metric: str, threshold: float +) -> dict[int, list[tuple[int, float]]]: ... diff --git a/savant_python/src/lib.rs b/savant_python/src/lib.rs index 02d5055f..97a14e2c 100644 --- a/savant_python/src/lib.rs +++ b/savant_python/src/lib.rs @@ -14,6 +14,7 @@ use savant_core_py::primitives::attribute_value::{ AttributeValue, AttributeValueType, AttributeValuesView, }; use savant_core_py::primitives::batch::VideoFrameBatch; +use savant_core_py::primitives::bbox::utils::*; use savant_core_py::primitives::bbox::{ BBox, BBoxMetricType, RBBox, VideoObjectBBoxTransformation, }; @@ -154,6 +155,10 @@ pub fn geometry(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + + m.add_function(wrap_pyfunction!(solely_owned_areas, m)?)?; + m.add_function(wrap_pyfunction!(associate_bboxes, m)?)?; + Ok(()) } @@ -265,7 +270,9 @@ fn savant_rs(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { let log_env_var_name = "LOGLEVEL"; let log_env_var_level = "trace"; if std::env::var(log_env_var_name).is_err() { - std::env::set_var(log_env_var_name, log_env_var_level); + unsafe { + std::env::set_var(log_env_var_name, log_env_var_level); + } } pretty_env_logger::try_init_custom_env(log_env_var_name) .map_err(|_| PyRuntimeError::new_err("Failed to initialize logger"))?; @@ -286,7 +293,8 @@ fn savant_rs(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(self::telemetry))?; // PYI let sys = PyModule::import_bound(py, "sys")?; - let sys_modules: &PyDict = sys.as_gil_ref().getattr("modules")?.downcast()?; + let sys_modules_bind = sys.as_ref().getattr("modules")?; + let sys_modules = sys_modules_bind.downcast::()?; sys_modules.set_item("savant_rs.primitives", m.getattr("primitives")?)?; sys_modules.set_item("savant_rs.pipeline", m.getattr("pipeline")?)?;