use std::fmt;
use std::str::FromStr;
use rayon::prelude::*;
use crate::geometry::{obb_intersection_area, obb_to_corners};
use crate::mask::{area as rle_area, intersection_area};
use crate::types::Rle;
const MIN_PARALLEL_WORK: usize = 1024;
#[inline]
fn iou_from_areas(inter: f64, dt_area: f64, gt_area: f64, gt_is_crowd: bool) -> f64 {
if gt_is_crowd {
if dt_area == 0.0 { 0.0 } else { inter / dt_area }
} else {
let union = dt_area + gt_area - inter;
if union == 0.0 { 0.0 } else { inter / union }
}
}
#[inline]
pub(crate) fn rows<F>(d: usize, g: usize, compute_row: F) -> Vec<Vec<f64>>
where
F: Fn(usize) -> Vec<f64> + Sync + Send,
{
if d * g >= MIN_PARALLEL_WORK {
(0..d).into_par_iter().map(compute_row).collect()
} else {
(0..d).map(compute_row).collect()
}
}
pub fn mask_iou(dt: &[Rle], gt: &[Rle], iscrowd: &[bool]) -> Vec<Vec<f64>> {
let d = dt.len();
let g = gt.len();
if d == 0 || g == 0 {
return vec![vec![]; d];
}
let dt_areas: Vec<u64> = dt.iter().map(rle_area).collect();
let gt_areas: Vec<u64> = gt.iter().map(rle_area).collect();
rows(d, g, |i| {
let dt_a = dt_areas[i] as f64;
(0..g)
.map(|j| {
let inter = intersection_area(&dt[i], >[j]) as f64;
iou_from_areas(inter, dt_a, gt_areas[j] as f64, iscrowd[j])
})
.collect()
})
}
#[inline]
pub fn bbox_iou_pair(a: [f64; 4], b: [f64; 4], b_is_crowd: bool) -> f64 {
let x1 = a[0].max(b[0]);
let y1 = a[1].max(b[1]);
let x2 = (a[0] + a[2]).min(b[0] + b[2]);
let y2 = (a[1] + a[3]).min(b[1] + b[3]);
let iw = (x2 - x1).max(0.0);
let ih = (y2 - y1).max(0.0);
iou_from_areas(iw * ih, a[2] * a[3], b[2] * b[3], b_is_crowd)
}
pub fn bbox_iou(dt: &[[f64; 4]], gt: &[[f64; 4]], iscrowd: &[bool]) -> Vec<Vec<f64>> {
let d = dt.len();
let g = gt.len();
if d == 0 || g == 0 {
return vec![vec![]; d];
}
rows(d, g, |i| {
(0..g)
.map(|j| bbox_iou_pair(dt[i], gt[j], iscrowd[j]))
.collect()
})
}
pub(crate) 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 inter_area = obb_intersection_area(corners_a, corners_b);
if inter_area <= 0.0 {
return 0.0;
}
iou_from_areas(inter_area, area_a, area_b, 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();
rows(d, g, |i| {
let corners_a = obb_to_corners(&dt[i]);
let area_a = dt[i][2] * dt[i][3];
(0..g)
.map(|j| obb_iou_pair(&corners_a, area_a, >_corners[j], gt_areas[j], iscrowd[j]))
.collect()
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SimKind {
Bbox,
Mask,
Obb,
Oks,
}
impl SimKind {
pub fn as_str(self) -> &'static str {
match self {
SimKind::Bbox => "bbox",
SimKind::Mask => "mask",
SimKind::Obb => "obb",
SimKind::Oks => "oks",
}
}
}
impl fmt::Display for SimKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for SimKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"bbox" => Ok(SimKind::Bbox),
"mask" | "segm" => Ok(SimKind::Mask),
"obb" => Ok(SimKind::Obb),
"oks" | "keypoints" => Ok(SimKind::Oks),
_ => Err(format!(
"Unknown sim kind: '{s}'. Expected 'bbox', 'mask' (alias 'segm'), \
'obb', or 'oks' (alias 'keypoints')"
)),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct GtPose<'a> {
pub keypoints: &'a [f64],
pub area: f64,
pub bbox: [f64; 4],
}
pub fn oks_matrix(dt_keypoints: &[&[f64]], gt: &[GtPose<'_>], sigmas: &[f64]) -> Vec<Vec<f64>> {
let num_kpts = sigmas.len();
let vars: Vec<f64> = sigmas.iter().map(|s| (2.0 * s).powi(2)).collect();
let d = dt_keypoints.len();
let g = gt.len();
if d == 0 || g == 0 {
return vec![vec![]; d];
}
struct GtPrep<'a> {
kpts: &'a [f64],
area: f64,
k1: usize,
x0: f64,
x1: f64,
y0: f64,
y1: f64,
}
let prep: Vec<GtPrep<'_>> = gt
.iter()
.map(|gt_pose| {
let gt_kpts = gt_pose.keypoints;
let bb = gt_pose.bbox;
let k1 = (0..num_kpts)
.filter(|&ki| gt_kpts.get(ki * 3 + 2).copied().unwrap_or(0.0) > 0.0)
.count();
GtPrep {
kpts: gt_kpts,
area: gt_pose.area + f64::EPSILON,
k1,
x0: bb[0] - bb[2],
x1: bb[0] + bb[2] * 2.0,
y0: bb[1] - bb[3],
y1: bb[1] + bb[3] * 2.0,
}
})
.collect();
rows(d, g, |i| {
let dt_kpts = dt_keypoints[i];
prep.iter()
.map(|p| {
if dt_kpts.is_empty() || p.kpts.is_empty() {
return 0.0;
}
let mut oks_sum = 0.0_f64;
let mut oks_count = 0_usize;
for (ki, &var_k) in vars.iter().enumerate() {
let visible = p.kpts.get(ki * 3 + 2).copied().unwrap_or(0.0) > 0.0;
if p.k1 > 0 && !visible {
continue;
}
let gx = p.kpts.get(ki * 3).copied().unwrap_or(0.0);
let gy = p.kpts.get(ki * 3 + 1).copied().unwrap_or(0.0);
let xd = dt_kpts.get(ki * 3).copied().unwrap_or(0.0);
let yd = dt_kpts.get(ki * 3 + 1).copied().unwrap_or(0.0);
let (dx, dy) = if p.k1 > 0 {
(xd - gx, yd - gy)
} else {
let dx = 0.0_f64.max(p.x0 - xd) + 0.0_f64.max(xd - p.x1);
let dy = 0.0_f64.max(p.y0 - yd) + 0.0_f64.max(yd - p.y1);
(dx, dy)
};
let e = (dx * dx + dy * dy) / var_k / p.area / 2.0;
oks_sum += (-e).exp();
oks_count += 1;
}
if oks_count > 0 {
oks_sum / oks_count as f64
} else {
0.0
}
})
.collect()
})
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
fn rand_box(rng: &mut StdRng) -> [f64; 4] {
[
rng.random_range(0.0..1000.0),
rng.random_range(0.0..1000.0),
rng.random_range(1.0..200.0),
rng.random_range(1.0..200.0),
]
}
#[test]
fn bbox_iou_algebraic_properties() {
let mut rng = StdRng::seed_from_u64(0x1005);
for case in 0..20000 {
let a = rand_box(&mut rng);
let b = rand_box(&mut rng);
let iou_fwd = bbox_iou_pair(a, b, false);
let iou_rev = bbox_iou_pair(b, a, false);
assert!(
(0.0..=1.0 + 1e-12).contains(&iou_fwd),
"case {case}: IoU {iou_fwd} outside [0,1] for {a:?} vs {b:?}"
);
assert!(
(iou_fwd - iou_rev).abs() < 1e-12,
"case {case}: asymmetric, {iou_fwd} vs {iou_rev} for {a:?} vs {b:?}"
);
let self_iou = bbox_iou_pair(a, a, false);
assert!(
(self_iou - 1.0).abs() < 1e-12,
"case {case}: self-IoU {self_iou} not within 1e-12 of 1.0 for {a:?}"
);
assert!(
self_iou >= crate::primitives::greedy::coco_match_floor(1.0),
"case {case}: self-IoU {self_iou} falls below the match floor for {a:?}"
);
}
}
#[test]
fn self_iou_degrades_for_subpixel_boxes() {
let thin = [
225.205_188_785_783_66,
691.079_072_122_579_8,
11.209,
2.431e-05,
];
let self_iou = bbox_iou_pair(thin, thin, false);
assert!(
(self_iou - 1.0).abs() > 1e-10,
"expected measurable drift for a sub-pixel box, got {self_iou}"
);
assert!(
self_iou < crate::primitives::greedy::coco_match_floor(1.0),
"expected the drift to fall below the match floor, got {self_iou}"
);
assert!((self_iou - 1.0).abs() < 1e-8);
}
#[test]
fn obb_iou_matches_shapely() {
#[derive(serde::Deserialize)]
struct Case {
kind: String,
a: [f64; 5],
b: [f64; 5],
iou: f64,
}
let data = include_str!("testdata/obb_iou_shapely.json");
let cases: Vec<Case> = serde_json::from_str(data).expect("parse fixture");
assert!(cases.len() > 500, "fixture looks truncated");
let mut worst = 0.0f64;
let mut worst_case = String::new();
for (i, c) in cases.iter().enumerate() {
let got = obb_iou(&[c.a], &[c.b], &[false])[0][0];
let diff = (got - c.iou).abs();
if diff > worst {
worst = diff;
worst_case = format!(
"case {i} ({}): a={:?} b={:?} shapely={} hotcoco={got}",
c.kind, c.a, c.b, c.iou
);
}
assert!(
diff < 1e-9,
"case {i} ({}): IoU {got} vs Shapely {} (diff {diff:.3e})\n a={:?}\n b={:?}",
c.kind,
c.iou,
c.a,
c.b
);
}
if worst > 0.0 {
println!("obb_iou worst deviation from Shapely: {worst:.3e} — {worst_case}");
}
}
#[test]
fn bbox_iou_parallel_and_sequential_agree() {
let mut rng = StdRng::seed_from_u64(0x9E37_79B9);
let n = (MIN_PARALLEL_WORK as f64).sqrt().ceil() as usize;
for &(d, g) in &[(2, 2), (n - 1, n - 1), (n, n - 1), (n, n), (n + 1, n)] {
let dt: Vec<[f64; 4]> = (0..d).map(|_| rand_box(&mut rng)).collect();
let gt: Vec<[f64; 4]> = (0..g).map(|_| rand_box(&mut rng)).collect();
let iscrowd: Vec<bool> = (0..g).map(|_| rng.random_bool(0.2)).collect();
let matrix = bbox_iou(&dt, >, &iscrowd);
assert_eq!(matrix.len(), d);
for (di, row) in matrix.iter().enumerate() {
assert_eq!(row.len(), g);
for (gi, &got) in row.iter().enumerate() {
let want = bbox_iou_pair(dt[di], gt[gi], iscrowd[gi]);
assert_eq!(
got,
want,
"d={d} g={g} (d*g={}) cell [{di}][{gi}] disagrees with the pair kernel",
d * g
);
}
}
}
}
#[test]
fn parse_roundtrips_and_aliases() {
assert_eq!("bbox".parse(), Ok(SimKind::Bbox));
assert_eq!("mask".parse(), Ok(SimKind::Mask));
assert_eq!("obb".parse(), Ok(SimKind::Obb));
assert_eq!("oks".parse(), Ok(SimKind::Oks));
assert_eq!("segm".parse(), Ok(SimKind::Mask));
assert_eq!("keypoints".parse(), Ok(SimKind::Oks));
for k in [SimKind::Bbox, SimKind::Mask, SimKind::Obb, SimKind::Oks] {
assert_eq!(k.as_str().parse(), Ok(k));
}
}
#[test]
fn parse_rejects_unknown() {
assert!("polygon".parse::<SimKind>().is_err());
}
fn oks_1kpt(dx: f64, dy: f64, s: f64, area: f64) -> f64 {
let e = (dx * dx + dy * dy) / (4.0 * s * s) / (area + f64::EPSILON) / 2.0;
(-e).exp()
}
fn gt(keypoints: &[f64], area: f64) -> GtPose<'_> {
GtPose {
keypoints,
area,
bbox: [0.0, 0.0, 30.0, 30.0],
}
}
#[test]
fn oks_identical_keypoints_is_one() {
let sigmas = [0.05, 0.07];
let kpts = [10.0, 10.0, 2.0, 20.0, 20.0, 2.0]; let dt = kpts; let m = oks_matrix(&[&dt], &[gt(&kpts, 1000.0)], &sigmas);
assert!((m[0][0] - 1.0).abs() < 1e-12);
}
#[test]
fn oks_single_displaced_keypoint_matches_formula() {
let sigmas = [0.05];
let area = 1000.0;
let kpts = [10.0, 10.0, 2.0]; let dt = [13.0, 14.0, 2.0]; let m = oks_matrix(&[&dt], &[gt(&kpts, area)], &sigmas);
assert!((m[0][0] - oks_1kpt(3.0, 4.0, 0.05, area)).abs() < 1e-12);
}
#[test]
fn oks_averages_only_visible_gt_keypoints() {
let sigmas = [0.05, 0.05];
let kpts = [10.0, 10.0, 2.0, 0.0, 0.0, 0.0];
let dt = [10.0, 10.0, 2.0, 999.0, 999.0, 2.0];
let m = oks_matrix(&[&dt], &[gt(&kpts, 1000.0)], &sigmas);
assert!((m[0][0] - 1.0).abs() < 1e-12);
}
#[test]
fn oks_no_visible_gt_uses_bbox_distance_branch() {
let sigmas = [0.05];
let kpts = [0.0, 0.0, 0.0]; let pose = GtPose {
keypoints: &kpts,
area: 1000.0,
bbox: [0.0, 0.0, 20.0, 20.0],
};
let inside = [10.0, 10.0, 2.0]; let m = oks_matrix(&[&inside], &[pose], &sigmas);
assert!((m[0][0] - 1.0).abs() < 1e-12);
let outside = [1000.0, 1000.0, 2.0];
let m2 = oks_matrix(&[&outside], &[pose], &sigmas);
assert!(m2[0][0] < 1.0);
}
#[test]
fn oks_empty_keypoints_leave_zero() {
let sigmas = [0.05];
let kpts = [10.0, 10.0, 2.0];
let empty: &[f64] = &[];
let m = oks_matrix(&[empty], &[gt(&kpts, 1000.0)], &sigmas);
assert_eq!(m[0][0], 0.0);
let m2 = oks_matrix(&[&kpts[..]], &[gt(empty, 1000.0)], &sigmas);
assert_eq!(m2[0][0], 0.0);
}
#[test]
fn oks_parallel_and_sequential_agree_with_pairwise() {
let mut rng = StdRng::seed_from_u64(0x0C50C5);
let sigmas = [0.05, 0.07, 0.09];
let n = (MIN_PARALLEL_WORK as f64).sqrt().ceil() as usize;
for &(d, g) in &[(2, 3), (n, n - 1), (n, n + 1)] {
let mut kpt_store: Vec<Vec<f64>> = Vec::new();
for _ in 0..d + g {
if rng.random_bool(0.05) {
kpt_store.push(Vec::new());
} else {
kpt_store.push(
(0..3)
.flat_map(|_| {
[
rng.random_range(0.0..100.0),
rng.random_range(0.0..100.0),
if rng.random_bool(0.7) { 2.0 } else { 0.0 },
]
})
.collect(),
);
}
}
let (dt_kpts, gt_kpts) = kpt_store.split_at(d);
let dt: Vec<&[f64]> = dt_kpts.iter().map(Vec::as_slice).collect();
let gts: Vec<GtPose<'_>> = gt_kpts
.iter()
.map(|k| GtPose {
keypoints: k,
area: rng.random_range(100.0..2000.0),
bbox: [10.0, 10.0, 30.0, 40.0],
})
.collect();
let matrix = oks_matrix(&dt, >s, &sigmas);
assert_eq!(matrix.len(), d);
for (di, row) in matrix.iter().enumerate() {
assert_eq!(row.len(), g);
for (gi, &got) in row.iter().enumerate() {
let want = oks_matrix(&[dt[di]], &[gts[gi]], &sigmas)[0][0];
assert_eq!(
got, want,
"d={d} g={g} cell [{di}][{gi}] disagrees with the 1x1 call"
);
}
}
}
}
#[test]
fn bbox_kernel_matches_and_obeys_contract() {
let dt = [[0.0, 0.0, 10.0, 10.0], [100.0, 100.0, 10.0, 10.0]];
let gt = [[0.0, 0.0, 10.0, 10.0]];
let iscrowd = [false];
let m = bbox_iou(&dt, >, &iscrowd);
assert_eq!(m, crate::mask::bbox_iou(&dt, >, &iscrowd));
assert!((m[0][0] - 1.0).abs() < 1e-12, "identical box => IoU 1");
assert_eq!(m[1][0], 0.0, "disjoint box => IoU 0");
for row in &m {
for &v in row {
assert!((0.0..=1.0).contains(&v), "similarity in [0,1]");
}
}
}
}