use rayon::prelude::*;
const MIN_PARALLEL_WORK: usize = 1000;
pub fn obb_to_corners(obb: &[f64; 5]) -> [(f64, f64); 4] {
let [cx, cy, w, h, angle] = *obb;
let cos_a = angle.cos();
let sin_a = angle.sin();
let hw = w / 2.0;
let hh = h / 2.0;
let dx_w = hw * cos_a;
let dy_w = hw * sin_a;
let dx_h = hh * sin_a;
let dy_h = hh * cos_a;
[
(cx - dx_w + dx_h, cy - dy_w - dy_h), (cx + dx_w + dx_h, cy + dy_w - dy_h), (cx + dx_w - dx_h, cy + dy_w + dy_h), (cx - dx_w - dx_h, cy - dy_w + dy_h), ]
}
fn signed_polygon_area(vertices: &[(f64, f64)]) -> f64 {
let n = vertices.len();
if n < 3 {
return 0.0;
}
let mut area = 0.0;
for i in 0..n {
let j = (i + 1) % n;
area += vertices[i].0 * vertices[j].1;
area -= vertices[j].0 * vertices[i].1;
}
area * 0.5
}
fn polygon_area(vertices: &[(f64, f64)]) -> f64 {
signed_polygon_area(vertices).abs()
}
fn sutherland_hodgman_clip(subject: &[(f64, f64)], clip: &[(f64, f64)]) -> Vec<(f64, f64)> {
if subject.is_empty() || clip.is_empty() {
return Vec::new();
}
let mut output = subject.to_vec();
let clip_len = clip.len();
for i in 0..clip_len {
if output.is_empty() {
return output;
}
let edge_start = clip[i];
let edge_end = clip[(i + 1) % clip_len];
let input = std::mem::take(&mut output);
output.reserve(input.len() + 1);
let input_len = input.len();
for j in 0..input_len {
let current = input[j];
let previous = input[(j + input_len - 1) % input_len];
let curr_inside = is_inside(current, edge_start, edge_end);
let prev_inside = is_inside(previous, edge_start, edge_end);
if curr_inside {
if !prev_inside {
if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) {
output.push(pt);
}
}
output.push(current);
} else if prev_inside {
if let Some(pt) = line_intersection(previous, current, edge_start, edge_end) {
output.push(pt);
}
}
}
}
output
}
#[inline]
fn is_inside(point: (f64, f64), edge_start: (f64, f64), edge_end: (f64, f64)) -> bool {
let cross = (edge_end.0 - edge_start.0) * (point.1 - edge_start.1)
- (edge_end.1 - edge_start.1) * (point.0 - edge_start.0);
cross >= 0.0
}
#[inline]
fn line_intersection(
p1: (f64, f64),
p2: (f64, f64),
p3: (f64, f64),
p4: (f64, f64),
) -> Option<(f64, f64)> {
let d1x = p2.0 - p1.0;
let d1y = p2.1 - p1.1;
let d2x = p4.0 - p3.0;
let d2y = p4.1 - p3.1;
let denom = d1x * d2y - d1y * d2x;
if denom.abs() < 1e-12 {
return None;
}
let t = ((p3.0 - p1.0) * d2y - (p3.1 - p1.1) * d2x) / denom;
Some((p1.0 + t * d1x, p1.1 + t * d1y))
}
pub fn obb_to_aabb(obb: &[f64; 5]) -> [f64; 4] {
let corners = obb_to_corners(obb);
let mut min_x = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut min_y = f64::INFINITY;
let mut max_y = f64::NEG_INFINITY;
for (x, y) in corners {
min_x = min_x.min(x);
max_x = max_x.max(x);
min_y = min_y.min(y);
max_y = max_y.max(y);
}
[min_x, min_y, max_x - min_x, max_y - min_y]
}
pub fn corners_to_obb(coords: &[f64]) -> [f64; 5] {
let cx = (coords[0] + coords[2] + coords[4] + coords[6]) / 4.0;
let cy = (coords[1] + coords[3] + coords[5] + coords[7]) / 4.0;
let dx01 = coords[2] - coords[0];
let dy01 = coords[3] - coords[1];
let w = (dx01 * dx01 + dy01 * dy01).sqrt();
let dx12 = coords[4] - coords[2];
let dy12 = coords[5] - coords[3];
let h = (dx12 * dx12 + dy12 * dy12).sqrt();
let angle = dy01.atan2(dx01);
[cx, cy, w, h, angle]
}
fn obb_iou_pair(
corners_a: &[(f64, f64); 4],
area_a: f64,
corners_b: &[(f64, f64); 4],
area_b: f64,
b_is_crowd: bool,
) -> f64 {
if area_a <= 0.0 || area_b <= 0.0 {
return 0.0;
}
let intersection = sutherland_hodgman_clip(corners_a, corners_b);
let inter_area = polygon_area(&intersection);
if inter_area <= 0.0 {
return 0.0;
}
if b_is_crowd {
inter_area / area_a
} else {
let union_area = area_a + area_b - inter_area;
if union_area <= 0.0 {
0.0
} else {
inter_area / union_area
}
}
}
#[cfg(test)]
fn obb_iou_single(a: &[f64; 5], b: &[f64; 5], b_is_crowd: bool) -> f64 {
obb_iou_pair(
&obb_to_corners(a),
a[2] * a[3],
&obb_to_corners(b),
b[2] * b[3],
b_is_crowd,
)
}
pub fn obb_iou(dt: &[[f64; 5]], gt: &[[f64; 5]], iscrowd: &[bool]) -> Vec<Vec<f64>> {
let d = dt.len();
let g = gt.len();
if d == 0 || g == 0 {
return vec![vec![]; d];
}
let gt_corners: Vec<[(f64, f64); 4]> = gt.iter().map(obb_to_corners).collect();
let gt_areas: Vec<f64> = gt.iter().map(|b| b[2] * b[3]).collect();
let compute_row = |i: usize| {
let corners_a = obb_to_corners(&dt[i]);
let area_a = dt[i][2] * dt[i][3];
let mut row = vec![0.0f64; g];
for j in 0..g {
row[j] = obb_iou_pair(&corners_a, area_a, >_corners[j], gt_areas[j], iscrowd[j]);
}
row
};
if d * g >= MIN_PARALLEL_WORK {
(0..d).into_par_iter().map(compute_row).collect()
} else {
(0..d).map(compute_row).collect()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
const EPS: f64 = 1e-9;
#[test]
fn test_obb_to_corners_axis_aligned() {
let corners = obb_to_corners(&[5.0, 3.0, 10.0, 6.0, 0.0]);
let mut xs: Vec<f64> = corners.iter().map(|c| c.0).collect();
let mut ys: Vec<f64> = corners.iter().map(|c| c.1).collect();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
ys.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((xs[0] - 0.0).abs() < EPS);
assert!((xs[3] - 10.0).abs() < EPS);
assert!((ys[0] - 0.0).abs() < EPS);
assert!((ys[3] - 6.0).abs() < EPS);
}
#[test]
fn test_obb_to_corners_90deg() {
let corners = obb_to_corners(&[0.0, 0.0, 10.0, 4.0, FRAC_PI_2]);
let mut xs: Vec<f64> = corners.iter().map(|c| c.0).collect();
let mut ys: Vec<f64> = corners.iter().map(|c| c.1).collect();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
ys.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((xs[3] - xs[0] - 4.0).abs() < EPS);
assert!((ys[3] - ys[0] - 10.0).abs() < EPS);
}
#[test]
fn test_polygon_area_square() {
let square = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
assert!((polygon_area(&square) - 1.0).abs() < EPS);
}
#[test]
fn test_polygon_area_triangle() {
let tri = [(0.0, 0.0), (4.0, 0.0), (0.0, 3.0)];
assert!((polygon_area(&tri) - 6.0).abs() < EPS);
}
#[test]
fn test_identical_boxes_iou_1() {
let a = [10.0, 10.0, 20.0, 30.0, 0.5];
let iou = obb_iou_single(&a, &a, false);
assert!(
(iou - 1.0).abs() < EPS,
"identical boxes should have IoU=1.0, got {iou}"
);
}
#[test]
fn test_non_overlapping_iou_0() {
let a = [0.0, 0.0, 2.0, 2.0, 0.0];
let b = [100.0, 100.0, 2.0, 2.0, 0.0];
let iou = obb_iou_single(&a, &b, false);
assert!(
iou.abs() < EPS,
"non-overlapping boxes should have IoU=0.0, got {iou}"
);
}
#[test]
fn test_axis_aligned_overlap() {
let a = [2.0, 2.0, 4.0, 4.0, 0.0];
let b = [4.0, 2.0, 4.0, 4.0, 0.0];
let iou = obb_iou_single(&a, &b, false);
assert!(
(iou - 1.0 / 3.0).abs() < 1e-6,
"expected IoU ≈ 1/3, got {iou}"
);
}
#[test]
fn test_90deg_rotation_identical() {
let a = [5.0, 5.0, 4.0, 4.0, 0.0];
let b = [5.0, 5.0, 4.0, 4.0, FRAC_PI_2];
let iou = obb_iou_single(&a, &b, false);
assert!(
(iou - 1.0).abs() < 1e-6,
"square rotated 90° should have IoU=1.0, got {iou}"
);
}
#[test]
fn test_45deg_rotation_partial_overlap() {
let a = [0.0, 0.0, 2.0, 2.0, 0.0];
let b = [0.0, 0.0, 2.0, 2.0, FRAC_PI_4];
let iou = obb_iou_single(&a, &b, false);
assert!(
iou > 0.5,
"45° rotated squares should have significant overlap, got {iou}"
);
assert!(
iou < 1.0,
"45° rotated squares should not fully overlap, got {iou}"
);
}
#[test]
fn test_iscrowd() {
let a = [0.0, 0.0, 2.0, 2.0, 0.0]; let b = [0.0, 0.0, 4.0, 4.0, 0.0]; let iou = obb_iou_single(&a, &b, true);
assert!(
(iou - 1.0).abs() < EPS,
"small box inside crowd should have IoU=1.0, got {iou}"
);
let iou_normal = obb_iou_single(&a, &b, false);
assert!(
(iou_normal - 0.25).abs() < 1e-6,
"expected IoU=0.25, got {iou_normal}"
);
}
#[test]
fn test_zero_area_box() {
let a = [0.0, 0.0, 0.0, 2.0, 0.0]; let b = [0.0, 0.0, 2.0, 2.0, 0.0];
assert!(obb_iou_single(&a, &b, false).abs() < EPS);
}
#[test]
fn test_obb_iou_matrix() {
let dt = vec![[0.0, 0.0, 2.0, 2.0, 0.0], [100.0, 100.0, 2.0, 2.0, 0.0]];
let gt = vec![[0.0, 0.0, 2.0, 2.0, 0.0]];
let iscrowd = vec![false];
let result = obb_iou(&dt, >, &iscrowd);
assert_eq!(result.len(), 2);
assert_eq!(result[0].len(), 1);
assert!((result[0][0] - 1.0).abs() < EPS);
assert!(result[1][0].abs() < EPS);
}
#[test]
fn test_obb_iou_empty() {
let dt: Vec<[f64; 5]> = vec![];
let gt = vec![[0.0, 0.0, 2.0, 2.0, 0.0]];
let iscrowd = vec![false];
let result = obb_iou(&dt, >, &iscrowd);
assert!(result.is_empty());
}
#[test]
fn test_180deg_rotation() {
let a = [5.0, 5.0, 6.0, 4.0, 0.0];
let b = [5.0, 5.0, 6.0, 4.0, PI];
let iou = obb_iou_single(&a, &b, false);
assert!(
(iou - 1.0).abs() < 1e-6,
"180° rotation should give IoU=1.0, got {iou}"
);
}
#[test]
fn test_contained_box() {
let big = [0.0, 0.0, 10.0, 10.0, 0.3];
let small = [0.0, 0.0, 2.0, 2.0, 0.3]; let iou = obb_iou_single(&small, &big, false);
assert!((iou - 0.04).abs() < 1e-6, "expected IoU=0.04, got {iou}");
}
}