use serde::{Deserialize, Serialize};
use super::coord::Coord;
#[derive(Clone, Copy, PartialEq)]
pub struct BBoxXYXY<TSpace> {
pub min: Coord<TSpace>,
pub max: Coord<TSpace>,
}
impl<TSpace> BBoxXYXY<TSpace> {
#[inline]
pub fn new(min: Coord<TSpace>, max: Coord<TSpace>) -> Self {
Self { min, max }
}
#[inline]
pub fn from_xyxy(xmin: f64, ymin: f64, xmax: f64, ymax: f64) -> Self {
Self {
min: Coord::new(xmin, ymin),
max: Coord::new(xmax, ymax),
}
}
#[inline]
pub fn xmin(&self) -> f64 {
self.min.x
}
#[inline]
pub fn ymin(&self) -> f64 {
self.min.y
}
#[inline]
pub fn xmax(&self) -> f64 {
self.max.x
}
#[inline]
pub fn ymax(&self) -> f64 {
self.max.y
}
#[inline]
pub fn width(&self) -> f64 {
self.max.x - self.min.x
}
#[inline]
pub fn height(&self) -> f64 {
self.max.y - self.min.y
}
#[inline]
pub fn area(&self) -> f64 {
self.width() * self.height()
}
#[inline]
pub fn is_finite(&self) -> bool {
self.min.is_finite() && self.max.is_finite()
}
#[inline]
pub fn is_ordered(&self) -> bool {
self.min.x <= self.max.x && self.min.y <= self.max.y
}
pub fn iou(&self, other: &Self) -> f64 {
if !self.is_finite() || !other.is_finite() || !self.is_ordered() || !other.is_ordered() {
return 0.0;
}
let inter_xmin = self.xmin().max(other.xmin());
let inter_ymin = self.ymin().max(other.ymin());
let inter_xmax = self.xmax().min(other.xmax());
let inter_ymax = self.ymax().min(other.ymax());
let inter_w = (inter_xmax - inter_xmin).max(0.0);
let inter_h = (inter_ymax - inter_ymin).max(0.0);
let intersection = inter_w * inter_h;
let union = self.area() + other.area() - intersection;
if !union.is_finite() || union <= 0.0 {
0.0
} else {
intersection / union
}
}
}
impl<TSpace> std::fmt::Debug for BBoxXYXY<TSpace> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BBoxXYXY")
.field("xmin", &self.min.x)
.field("ymin", &self.min.y)
.field("xmax", &self.max.x)
.field("ymax", &self.max.y)
.finish()
}
}
impl<TSpace> Default for BBoxXYXY<TSpace> {
fn default() -> Self {
Self::from_xyxy(0.0, 0.0, 0.0, 0.0)
}
}
impl<TSpace> Serialize for BBoxXYXY<TSpace> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("BBoxXYXY", 4)?;
state.serialize_field("xmin", &self.min.x)?;
state.serialize_field("ymin", &self.min.y)?;
state.serialize_field("xmax", &self.max.x)?;
state.serialize_field("ymax", &self.max.y)?;
state.end()
}
}
impl<'de, TSpace> Deserialize<'de> for BBoxXYXY<TSpace> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
struct BBoxData {
xmin: f64,
ymin: f64,
xmax: f64,
ymax: f64,
}
let data = BBoxData::deserialize(deserializer)?;
Ok(BBoxXYXY::from_xyxy(
data.xmin, data.ymin, data.xmax, data.ymax,
))
}
}
impl<TSpace> BBoxXYXY<TSpace> {
#[inline]
pub fn from_xywh(x: f64, y: f64, width: f64, height: f64) -> Self {
Self::from_xyxy(x, y, x + width, y + height)
}
#[inline]
pub fn to_xywh(&self) -> (f64, f64, f64, f64) {
(self.xmin(), self.ymin(), self.width(), self.height())
}
#[inline]
pub fn from_cxcywh(cx: f64, cy: f64, w: f64, h: f64) -> Self {
let half_w = w / 2.0;
let half_h = h / 2.0;
Self::from_xyxy(cx - half_w, cy - half_h, cx + half_w, cy + half_h)
}
#[inline]
pub fn to_cxcywh(&self) -> (f64, f64, f64, f64) {
(
(self.xmin() + self.xmax()) / 2.0,
(self.ymin() + self.ymax()) / 2.0,
self.width(),
self.height(),
)
}
}
use super::{Normalized, Pixel};
impl BBoxXYXY<Pixel> {
pub fn to_normalized(&self, image_width: f64, image_height: f64) -> BBoxXYXY<Normalized> {
BBoxXYXY::from_xyxy(
self.min.x / image_width,
self.min.y / image_height,
self.max.x / image_width,
self.max.y / image_height,
)
}
}
impl BBoxXYXY<Normalized> {
pub fn to_pixel(&self, image_width: f64, image_height: f64) -> BBoxXYXY<Pixel> {
BBoxXYXY::from_xyxy(
self.min.x * image_width,
self.min.y * image_height,
self.max.x * image_width,
self.max.y * image_height,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::Pixel;
#[test]
fn test_bbox_from_xyxy() {
let bbox: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 20.0, 100.0, 80.0);
assert_eq!(bbox.xmin(), 10.0);
assert_eq!(bbox.ymin(), 20.0);
assert_eq!(bbox.xmax(), 100.0);
assert_eq!(bbox.ymax(), 80.0);
}
#[test]
fn test_bbox_from_xywh() {
let bbox: BBoxXYXY<Pixel> = BBoxXYXY::from_xywh(10.0, 20.0, 90.0, 60.0);
assert_eq!(bbox.xmin(), 10.0);
assert_eq!(bbox.ymin(), 20.0);
assert_eq!(bbox.xmax(), 100.0);
assert_eq!(bbox.ymax(), 80.0);
}
#[test]
fn test_bbox_dimensions() {
let bbox: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 20.0, 100.0, 80.0);
assert_eq!(bbox.width(), 90.0);
assert_eq!(bbox.height(), 60.0);
assert_eq!(bbox.area(), 5400.0);
}
#[test]
fn test_bbox_ordering() {
let ordered: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 20.0, 100.0, 80.0);
assert!(ordered.is_ordered());
let unordered: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(100.0, 80.0, 10.0, 20.0);
assert!(!unordered.is_ordered());
}
#[test]
fn test_bbox_to_xywh_roundtrip() {
let original: BBoxXYXY<Pixel> = BBoxXYXY::from_xywh(15.0, 25.0, 50.0, 30.0);
let (x, y, w, h) = original.to_xywh();
let restored: BBoxXYXY<Pixel> = BBoxXYXY::from_xywh(x, y, w, h);
assert_eq!(original, restored);
}
#[test]
fn test_bbox_from_cxcywh() {
let bbox: BBoxXYXY<Pixel> = BBoxXYXY::from_cxcywh(30.0, 40.0, 20.0, 10.0);
assert_eq!(bbox.xmin(), 20.0);
assert_eq!(bbox.ymin(), 35.0);
assert_eq!(bbox.xmax(), 40.0);
assert_eq!(bbox.ymax(), 45.0);
}
#[test]
fn test_bbox_to_cxcywh_roundtrip() {
let original: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(12.0, 18.0, 60.0, 54.0);
let (cx, cy, w, h) = original.to_cxcywh();
let restored: BBoxXYXY<Pixel> = BBoxXYXY::from_cxcywh(cx, cy, w, h);
assert_eq!(original, restored);
}
#[test]
fn test_bbox_to_cxcywh_preserves_negative_dimensions() {
let malformed: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(30.0, 30.0, 20.0, 10.0);
let (_, _, w, h) = malformed.to_cxcywh();
assert_eq!(w, -10.0);
assert_eq!(h, -20.0);
}
#[test]
fn test_iou_identical_boxes() {
let a: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 10.0, 20.0, 20.0);
let b: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 10.0, 20.0, 20.0);
assert!((a.iou(&b) - 1.0).abs() < 1e-12);
}
#[test]
fn test_iou_disjoint_boxes() {
let a: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(0.0, 0.0, 10.0, 10.0);
let b: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(20.0, 20.0, 30.0, 30.0);
assert_eq!(a.iou(&b), 0.0);
}
#[test]
fn test_iou_partial_overlap() {
let a: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(0.0, 0.0, 10.0, 10.0);
let b: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(5.0, 5.0, 15.0, 15.0);
assert!((a.iou(&b) - (25.0 / 175.0)).abs() < 1e-12);
}
#[test]
fn test_iou_invalid_boxes_return_zero() {
let unordered: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(10.0, 10.0, 5.0, 5.0);
let valid: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(0.0, 0.0, 10.0, 10.0);
assert_eq!(unordered.iou(&valid), 0.0);
let nan_box: BBoxXYXY<Pixel> = BBoxXYXY::from_xyxy(f64::NAN, 0.0, 1.0, 1.0);
assert_eq!(nan_box.iou(&valid), 0.0);
}
}