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();
let mut result = vec![vec![0.0f64; g]; d];
for (j, gt_pose) in gt.iter().enumerate() {
let gt_kpts = gt_pose.keypoints;
if gt_kpts.is_empty() {
continue;
}
let gt_area = gt_pose.area + f64::EPSILON;
let bb = gt_pose.bbox;
let k1: usize = (0..num_kpts)
.filter(|&ki| gt_kpts.get(ki * 3 + 2).copied().unwrap_or(0.0) > 0.0)
.count();
let x0 = bb[0] - bb[2];
let x1 = bb[0] + bb[2] * 2.0;
let y0 = bb[1] - bb[3];
let y1 = bb[1] + bb[3] * 2.0;
for (i, &dt_kpts) in dt_keypoints.iter().enumerate() {
if dt_kpts.is_empty() {
continue;
}
let mut oks_sum = 0.0_f64;
let mut oks_count = 0_usize;
for (ki, &var_k) in vars.iter().enumerate() {
let visible = gt_kpts.get(ki * 3 + 2).copied().unwrap_or(0.0) > 0.0;
if k1 > 0 && !visible {
continue;
}
let gx = gt_kpts.get(ki * 3).copied().unwrap_or(0.0);
let gy = gt_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 k1 > 0 {
(xd - gx, yd - gy)
} else {
let dx = 0.0_f64.max(x0 - xd) + 0.0_f64.max(xd - x1);
let dy = 0.0_f64.max(y0 - yd) + 0.0_f64.max(yd - y1);
(dx, dy)
};
let e = (dx * dx + dy * dy) / var_k / gt_area / 2.0;
oks_sum += (-e).exp();
oks_count += 1;
}
if oks_count > 0 {
result[i][j] = oks_sum / oks_count as f64;
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[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 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]");
}
}
}
}