From 4b4d23d1d453bfdae5215e2be36ed49eee714804 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Fri, 22 Aug 2025 15:27:47 +0530 Subject: [PATCH] feat(bbox): add bounding box implementation with serialization Add initial implementation of the `BBox` struct in the `bbox` module, including basic operations and serialization/deserialization support with Serde. --- bbox/Cargo.toml | 13 + bbox/src/lib.rs | 708 +++++++++++++++++++++++++++++++++++++++++ bbox/src/traits.rs | 2 + bbox/src/traits/max.rs | 27 ++ bbox/src/traits/min.rs | 27 ++ 5 files changed, 777 insertions(+) create mode 100644 bbox/Cargo.toml create mode 100644 bbox/src/lib.rs create mode 100644 bbox/src/traits.rs create mode 100644 bbox/src/traits/max.rs create mode 100644 bbox/src/traits/min.rs diff --git a/bbox/Cargo.toml b/bbox/Cargo.toml new file mode 100644 index 0000000..a0515b4 --- /dev/null +++ b/bbox/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bbox" +version.workspace = true +edition.workspace = true + +[dependencies] +ndarray.workspace = true +num = "0.4.3" +serde = { workspace = true, features = ["derive"], optional = true } + +[features] +serde = ["dep:serde"] +default = ["serde"] diff --git a/bbox/src/lib.rs b/bbox/src/lib.rs new file mode 100644 index 0000000..0bba68f --- /dev/null +++ b/bbox/src/lib.rs @@ -0,0 +1,708 @@ +pub mod traits; + +/// A bounding box of co-ordinates whose origin is at the top-left corner. +#[derive( + Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Hash, serde::Serialize, serde::Deserialize, +)] +#[non_exhaustive] +pub struct BBox { + pub x: T, + pub y: T, + pub width: T, + pub height: T, +} + +impl From<[T; 4]> for BBox { + fn from([x, y, width, height]: [T; 4]) -> Self { + Self { + x, + y, + width, + height, + } + } +} + +impl BBox { + pub fn new(x: T, y: T, width: T, height: T) -> Self { + Self { + x, + y, + width, + height, + } + } + + /// Casts the internal values to another type using [as] keyword + pub fn cast(self) -> BBox + where + T: num::cast::AsPrimitive, + T2: Copy + 'static, + { + BBox { + x: self.x.as_(), + y: self.y.as_(), + width: self.width.as_(), + height: self.height.as_(), + } + } + + /// Clamps all the internal values to the given min and max. + pub fn clamp(&self, min: T, max: T) -> Self + where + T: std::cmp::PartialOrd, + { + Self { + x: num::clamp(self.x, min, max), + y: num::clamp(self.y, min, max), + width: num::clamp(self.width, min, max), + height: num::clamp(self.height, min, max), + } + } + + pub fn clamp_box(&self, bbox: BBox) -> Self + where + T: std::cmp::PartialOrd, + T: num::Zero, + T: core::ops::Add, + T: core::ops::Sub, + { + let x1 = num::clamp(self.x1(), bbox.x1(), bbox.x2()); + let y1 = num::clamp(self.y1(), bbox.y1(), bbox.y2()); + let x2 = num::clamp(self.x2(), bbox.x1(), bbox.x2()); + let y2 = num::clamp(self.y2(), bbox.y1(), bbox.y2()); + Self::new_xyxy(x1, y1, x2, y2) + } + + pub fn normalize(&self, width: T, height: T) -> Self + where + T: core::ops::Div + Copy, + { + Self { + x: self.x / width, + y: self.y / height, + width: self.width / width, + height: self.height / height, + } + } + + /// Normalize after casting to float + pub fn normalize_f64(&self, width: T, height: T) -> BBox + where + T: core::ops::Div + Copy, + T: num::cast::AsPrimitive, + { + BBox { + x: self.x.as_() / width.as_(), + y: self.y.as_() / height.as_(), + width: self.width.as_() / width.as_(), + height: self.height.as_() / height.as_(), + } + } + + pub fn denormalize(&self, width: T, height: T) -> Self + where + T: core::ops::Mul + Copy, + { + Self { + x: self.x * width, + y: self.y * height, + width: self.width * width, + height: self.height * height, + } + } + + pub fn height(&self) -> T { + self.height + } + + pub fn width(&self) -> T { + self.width + } + + pub fn padding(&self, padding: T) -> Self + where + T: core::ops::Add + core::ops::Sub + Copy, + { + Self { + x: self.x - padding, + y: self.y - padding, + width: self.width + padding + padding, + height: self.height + padding + padding, + } + } + + pub fn padding_height(&self, padding: T) -> Self + where + T: core::ops::Add + core::ops::Sub + Copy, + { + Self { + x: self.x, + y: self.y - padding, + width: self.width, + height: self.height + padding + padding, + } + } + + pub fn padding_width(&self, padding: T) -> Self + where + T: core::ops::Add + core::ops::Sub + Copy, + { + Self { + x: self.x - padding, + y: self.y, + width: self.width + padding + padding, + height: self.height, + } + } + + // Enlarge / shrink the bounding box by a factor while + // keeping the center point and the aspect ratio fixed + pub fn scale(&self, factor: T) -> Self + where + T: core::ops::Mul, + T: core::ops::Sub, + T: core::ops::Add, + T: core::ops::Div, + T: num::One + Copy, + { + let two = num::one::() + num::one::(); + let width = self.width * factor; + let height = self.height * factor; + let width_inc = width - self.width; + let height_inc = height - self.height; + Self { + x: self.x - width_inc / two, + y: self.y - height_inc / two, + width, + height, + } + } + + pub fn scale_x(&self, factor: T) -> Self + where + T: core::ops::Mul + + core::ops::Sub + + core::ops::Add + + core::ops::Div + + num::One + + Copy, + { + let two = num::one::() + num::one::(); + let width = self.width * factor; + let width_inc = width - self.width; + Self { + x: self.x - width_inc / two, + y: self.y, + width, + height: self.height, + } + } + + pub fn scale_y(&self, factor: T) -> Self + where + T: core::ops::Mul + + core::ops::Sub + + core::ops::Add + + core::ops::Div + + num::One + + Copy, + { + let two = num::one::() + num::one::(); + let height = self.height * factor; + let height_inc = height - self.height; + Self { + x: self.x, + y: self.y - height_inc / two, + width: self.width, + height, + } + } + + pub fn offset(&self, offset: Point) -> Self + where + T: core::ops::Add + Copy, + { + Self { + x: self.x + offset.x, + y: self.y + offset.y, + width: self.width, + height: self.height, + } + } + + /// Translate the bounding box by the given offset + /// if they are in the same scale + pub fn translate(&self, bbox: Self) -> Self + where + T: core::ops::Add + Copy, + { + Self { + x: self.x + bbox.x, + y: self.y + bbox.y, + width: self.width, + height: self.height, + } + } + + pub fn with_top_left(&self, top_left: Point) -> Self { + Self { + x: top_left.x, + y: top_left.y, + width: self.width, + height: self.height, + } + } + + pub fn center(&self) -> Point + where + T: core::ops::Add + core::ops::Div + Copy, + T: num::One, + { + let two = T::one() + T::one(); + Point::new(self.x + self.width / two, self.y + self.height / two) + } + + pub fn area(&self) -> T + where + T: core::ops::Mul + Copy, + { + self.width * self.height + } + + // Corresponds to self.x1() and self.y1() + pub fn top_left(&self) -> Point { + Point::new(self.x, self.y) + } + + pub fn top_right(&self) -> Point + where + T: core::ops::Add + Copy, + { + Point::new(self.x + self.width, self.y) + } + + pub fn bottom_left(&self) -> Point + where + T: core::ops::Add + Copy, + { + Point::new(self.x, self.y + self.height) + } + + // Corresponds to self.x2() and self.y2() + pub fn bottom_right(&self) -> Point + where + T: core::ops::Add + Copy, + { + Point::new(self.x + self.width, self.y + self.height) + } + + pub const fn x1(&self) -> T { + self.x + } + + pub const fn y1(&self) -> T { + self.y + } + + pub fn x2(&self) -> T + where + T: core::ops::Add + Copy, + { + self.x + self.width + } + + pub fn y2(&self) -> T + where + T: core::ops::Add + Copy, + { + self.y + self.height + } + + pub fn overlap(&self, other: &Self) -> T + where + T: std::cmp::PartialOrd + + traits::min::Min + + traits::max::Max + + num::Zero + + core::ops::Add + + core::ops::Sub + + core::ops::Mul + + Copy, + { + let x1 = self.x.max(other.x); + let y1 = self.y.max(other.y); + let x2 = (self.x + self.width).min(other.x + other.width); + let y2 = (self.y + self.height).min(other.y + other.height); + let width = (x2 - x1).max(T::zero()); + let height = (y2 - y1).max(T::zero()); + width * height + } + + pub fn iou(&self, other: &Self) -> T + where + T: std::cmp::Ord + + num::Zero + + traits::min::Min + + traits::max::Max + + core::ops::Add + + core::ops::Sub + + core::ops::Mul + + core::ops::Div + + Copy, + { + let overlap = self.overlap(other); + let union = self.area() + other.area() - overlap; + overlap / union + } + + pub fn contains(&self, point: Point) -> bool + where + T: std::cmp::PartialOrd + core::ops::Add + Copy, + { + point.x >= self.x + && point.x <= self.x + self.width + && point.y >= self.y + && point.y <= self.y + self.height + } + + pub fn contains_bbox(&self, other: Self) -> bool + where + T: std::cmp::PartialOrd + Copy, + T: core::ops::Add, + { + self.contains(other.top_left()) + && self.contains(other.top_right()) + && self.contains(other.bottom_left()) + && self.contains(other.bottom_right()) + } + + pub fn new_xywh(x: T, y: T, width: T, height: T) -> Self { + Self { + x, + y, + width, + height, + } + } + pub fn new_xyxy(x1: T, y1: T, x2: T, y2: T) -> Self + where + T: core::ops::Sub + Copy, + { + Self { + x: x1, + y: y1, + width: x2 - x1, + height: y2 - y1, + } + } + + pub fn containing(box1: Self, box2: Self) -> Self + where + T: traits::min::Min + traits::max::Max + Copy, + T: core::ops::Sub, + T: core::ops::Add, + { + let x1 = box1.x.min(box2.x); + let y1 = box1.y.min(box2.y); + let x2 = box1.x2().max(box2.x2()); + let y2 = box1.y2().max(box2.y2()); + Self::new_xyxy(x1, y1, x2, y2) + } +} + +impl + Copy> core::ops::Sub for BBox { + type Output = BBox; + fn sub(self, rhs: T) -> Self::Output { + BBox { + x: self.x - rhs, + y: self.y - rhs, + width: self.width - rhs, + height: self.height - rhs, + } + } +} + +impl + Copy> core::ops::Add for BBox { + type Output = BBox; + fn add(self, rhs: T) -> Self::Output { + BBox { + x: self.x + rhs, + y: self.y + rhs, + width: self.width + rhs, + height: self.height + rhs, + } + } +} +impl + Copy> core::ops::Mul for BBox { + type Output = BBox; + fn mul(self, rhs: T) -> Self::Output { + BBox { + x: self.x * rhs, + y: self.y * rhs, + width: self.width * rhs, + height: self.height * rhs, + } + } +} +impl + Copy> core::ops::Div for BBox { + type Output = BBox; + fn div(self, rhs: T) -> Self::Output { + BBox { + x: self.x / rhs, + y: self.y / rhs, + width: self.width / rhs, + height: self.height / rhs, + } + } +} + +impl core::ops::Add> for BBox +where + T: core::ops::Sub + + core::ops::Add + + traits::min::Min + + traits::max::Max + + Copy, +{ + type Output = BBox; + fn add(self, rhs: BBox) -> Self::Output { + let x1 = self.x1().min(rhs.x1()); + let y1 = self.y1().min(rhs.y1()); + let x2 = self.x2().max(rhs.x2()); + let y2 = self.y2().max(rhs.y2()); + BBox::new_xyxy(x1, y1, x2, y2) + } +} + +#[test] +fn test_bbox_add() { + let bbox1: BBox = BBox::new_xyxy(0, 0, 10, 10); + let bbox2: BBox = BBox::new_xyxy(5, 5, 15, 15); + let bbox3: BBox = bbox1 + bbox2; + assert_eq!(bbox3, BBox::new_xyxy(0, 0, 15, 15).cast()); +} + +#[derive( + Debug, Copy, Clone, serde::Serialize, serde::Deserialize, PartialEq, PartialOrd, Eq, Ord, Hash, +)] +pub struct Point { + x: T, + y: T, +} + +impl Point { + pub const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + pub const fn x(&self) -> T + where + T: Copy, + { + self.x + } + + pub const fn y(&self) -> T + where + T: Copy, + { + self.y + } + + pub fn cast(&self) -> Point + where + T: num::cast::AsPrimitive, + T2: Copy + 'static, + { + Point { + x: self.x.as_(), + y: self.y.as_(), + } + } +} + +impl + Copy> core::ops::Sub> for Point { + type Output = Point; + fn sub(self, rhs: Point) -> Self::Output { + Point { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl + Copy> core::ops::Add> for Point { + type Output = Point; + fn add(self, rhs: Point) -> Self::Output { + Point { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl + Copy> Point { + /// If both the boxes are in the same scale then make the translation of the origin to the + /// other box + pub fn with_origin(&self, origin: Self) -> Self { + *self - origin + } +} + +impl + Copy> Point { + pub fn translate(&self, point: Point) -> Self { + *self + point + } +} + +impl BBox +where + I: num::cast::AsPrimitive, +{ + pub fn zeros_ndarray_2d(&self) -> ndarray::Array2 { + ndarray::Array2::::zeros((self.height.as_(), self.width.as_())) + } + pub fn zeros_ndarray_3d(&self, channels: usize) -> ndarray::Array3 { + ndarray::Array3::::zeros((self.height.as_(), self.width.as_(), channels)) + } + pub fn ones_ndarray_2d(&self) -> ndarray::Array2 { + ndarray::Array2::::ones((self.height.as_(), self.width.as_())) + } +} + +impl BBox { + pub fn round(&self) -> Self { + Self { + x: self.x.round(), + y: self.y.round(), + width: self.width.round(), + height: self.height.round(), + } + } +} + +#[cfg(test)] +mod bbox_clamp_tests { + use super::*; + #[test] + pub fn bbox_test_clamp_box() { + let large_box = BBox::new(0, 0, 100, 100); + let small_box = BBox::new(10, 10, 20, 20); + let clamped = large_box.clamp_box(small_box); + assert_eq!(clamped, small_box); + } + + #[test] + pub fn bbox_test_clamp_box_offset() { + let box_a = BBox::new(0, 0, 100, 100); + let box_b = BBox::new(-10, -10, 20, 20); + let clamped = box_b.clamp_box(box_a); + let expected = BBox::new(0, 0, 10, 10); + assert_eq!(expected, clamped); + } +} + +#[cfg(test)] +mod bbox_padding_tests { + use super::*; + #[test] + pub fn bbox_test_padding() { + let bbox = BBox::new(0, 0, 10, 10); + let padded = bbox.padding(2); + assert_eq!(padded, BBox::new(-2, -2, 14, 14)); + } + + #[test] + pub fn bbox_test_padding_height() { + let bbox = BBox::new(0, 0, 10, 10); + let padded = bbox.padding_height(2); + assert_eq!(padded, BBox::new(0, -2, 10, 14)); + } + + #[test] + pub fn bbox_test_padding_width() { + let bbox = BBox::new(0, 0, 10, 10); + let padded = bbox.padding_width(2); + assert_eq!(padded, BBox::new(-2, 0, 14, 10)); + } + + #[test] + pub fn bbox_test_clamped_padding() { + let bbox = BBox::new(0, 0, 10, 10); + let padded = bbox.padding(2); + let clamp = BBox::new(0, 0, 12, 12); + let clamped = padded.clamp_box(clamp); + assert_eq!(clamped, clamp); + } + + #[test] + pub fn bbox_clamp_failure() { + let og = BBox::new(475.0, 79.625, 37.0, 282.15); + let padded = BBox { + x: 471.3, + y: 51.412499999999994, + width: 40.69999999999999, + height: 338.54999999999995, + }; + let clamp = BBox::new(0.0, 0.0, 512.0, 512.0); + let sus = padded.clamp_box(clamp); + assert!(clamp.contains_bbox(sus)); + } +} + +#[cfg(test)] +mod bbox_scale_tests { + use super::*; + #[test] + pub fn bbox_test_scale_int() { + let bbox = BBox::new(0, 0, 10, 10); + let scaled = bbox.scale(2); + assert_eq!(scaled, BBox::new(-5, -5, 20, 20)); + } + + #[test] + pub fn bbox_test_scale_float() { + let bbox = BBox::new(0, 0, 10, 10).cast(); + let scaled = bbox.scale(1.05); // 5% increase + let l = 10.0 * 0.05; + assert_eq!(scaled, BBox::new(-l / 2.0, -l / 2.0, 10.0 + l, 10.0 + l)); + } + + #[test] + pub fn bbox_test_scale_float_negative() { + let bbox = BBox::new(0, 0, 10, 10).cast(); + let scaled = bbox.scale(0.95); // 5% decrease + let l = -10.0 * 0.05; + assert_eq!(scaled, BBox::new(-l / 2.0, -l / 2.0, 10.0 + l, 10.0 + l)); + } + + #[test] + pub fn bbox_scale_float() { + let bbox = BBox::new_xywh(0, 0, 200, 200); + let scaled = bbox.cast::().scale(1.1).cast::().clamp(0, 1000); + let expected = BBox::new(0, 0, 220, 220); + assert_eq!(scaled, expected); + } + #[test] + pub fn add_padding_bbox_example() { + // let result = add_padding_bbox( + // vec![Rect::new(100, 200, 300, 400)], + // (0.1, 0.1), + // (1000, 1000), + // ); + // assert_eq!(result[0], Rect::new(70, 160, 360, 480)); + let bbox = BBox::new(100, 200, 300, 400); + let scaled = bbox.cast::().scale(1.2).cast::().clamp(0, 1000); + assert_eq!(bbox, BBox::new(100, 200, 300, 400)); + assert_eq!(scaled, BBox::new(70, 160, 360, 480)); + } + #[test] + pub fn scale_bboxes() { + // let result = scale_bboxes(Rect::new(100, 200, 300, 400), (1000, 1000), (500, 500)); + // assert_eq!(result[0], Rect::new(200, 400, 600, 800)); + let bbox = BBox::new(100, 200, 300, 400); + let scaled = bbox.scale(2); + assert_eq!(scaled, BBox::new(200, 400, 600, 800)); + } +} diff --git a/bbox/src/traits.rs b/bbox/src/traits.rs new file mode 100644 index 0000000..8c95292 --- /dev/null +++ b/bbox/src/traits.rs @@ -0,0 +1,2 @@ +pub mod max; +pub mod min; diff --git a/bbox/src/traits/max.rs b/bbox/src/traits/max.rs new file mode 100644 index 0000000..94b501d --- /dev/null +++ b/bbox/src/traits/max.rs @@ -0,0 +1,27 @@ +pub trait Max: Sized + Copy { + fn max(self, other: Self) -> Self; +} + +macro_rules! impl_max { + ($($t:ty),*) => { + $( + impl Max for $t { + fn max(self, other: Self) -> Self { + Ord::max(self, other) + } + } + )* + }; + (float $($t:ty),*) => { + $( + impl Max for $t { + fn max(self, other: Self) -> Self { + Self::max(self, other) + } + } + )* + }; +} + +impl_max!(usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128); +impl_max!(float f32, f64); diff --git a/bbox/src/traits/min.rs b/bbox/src/traits/min.rs new file mode 100644 index 0000000..e4d24ae --- /dev/null +++ b/bbox/src/traits/min.rs @@ -0,0 +1,27 @@ +pub trait Min: Sized + Copy { + fn min(self, other: Self) -> Self; +} + +macro_rules! impl_min { + ($($t:ty),*) => { + $( + impl Min for $t { + fn min(self, other: Self) -> Self { + Ord::min(self, other) + } + } + )* + }; + (float $($t:ty),*) => { + $( + impl Min for $t { + fn min(self, other: Self) -> Self { + Self::min(self, other) + } + } + )* + }; +} + +impl_min!(usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128); +impl_min!(float f32, f64);