fn smooth(y: &[f64], f: f64) -> Vec<f64> {
let nf: usize = ((f * y.len() as f64) / 2.0).floor().max(1.0) as usize * 2 + 1;
let half = nf / 2;
let mut padded = vec![0.0f64; y.len() + nf - 1];
for (i, v) in y.iter().enumerate() {
padded[i + half] = *v;
}
let n = padded.len();
for i in 0..half {
padded[i] = y[0];
padded[n - 1 - i] = y[y.len() - 1];
}
let kernel = vec![1.0 / nf as f64; nf];
let mut out = vec![0.0f64; y.len()];
for i in 0..y.len() {
let mut acc = 0.0;
for k in 0..nf {
acc += padded[i + k] * kernel[k];
}
out[i] = acc;
}
out
}
pub fn compute_ap(recall: &[f64], precision: &[f64]) -> f64 {
if recall.is_empty() {
return 0.0;
}
let mut mrec = Vec::with_capacity(recall.len() + 3);
mrec.push(0.0);
mrec.extend_from_slice(recall);
mrec.push(*recall.last().unwrap_or(&1.0));
mrec.push(1.0);
let mut mpre = Vec::with_capacity(precision.len() + 3);
mpre.push(1.0);
mpre.extend_from_slice(precision);
mpre.push(0.0);
mpre.push(0.0);
for i in (0..mpre.len() - 1).rev() {
mpre[i] = mpre[i].max(mpre[i + 1]);
}
let mut ap = 0.0;
let mut ys: Vec<f64> = Vec::with_capacity(101);
for k in 0..=100 {
let x = k as f64 / 100.0;
ys.push(interp1(&mrec, &mpre, x));
}
for k in 0..100 {
ap += 0.5 * (ys[k] + ys[k + 1]) * (1.0 / 100.0);
}
ap
}
fn interp1(xp: &[f64], fp: &[f64], x: f64) -> f64 {
interp1_left(xp, fp, x, fp[0])
}
fn interp1_left(xp: &[f64], fp: &[f64], x: f64, left: f64) -> f64 {
let n = xp.len();
let mut lo = 0usize;
let mut hi = n;
while lo < hi {
let mid = (lo + hi) / 2;
if xp[mid] <= x {
lo = mid + 1;
} else {
hi = mid;
}
}
if lo == 0 {
return left;
}
if lo == n {
return fp[n - 1];
}
let a = lo - 1;
let t = (x - xp[a]) / (xp[lo] - xp[a]);
fp[a] * (1.0 - t) + fp[lo] * t
}
#[derive(Debug, Clone, Default)]
pub struct ApPerClass {
pub tp: Vec<f64>,
pub fp: Vec<f64>,
pub p: Vec<f64>,
pub r: Vec<f64>,
pub f1: Vec<f64>,
pub ap: Vec<Vec<f64>>,
pub unique_classes: Vec<i64>,
}
pub fn ap_per_class(tp: &[Vec<bool>], conf: &[f32], pred_cls: &[i64], target_cls: &[i64]) -> ApPerClass {
let eps = 1e-16f64;
let n_iou = if tp.is_empty() { 10 } else { tp[0].len() };
let mut idx: Vec<usize> = (0..conf.len()).collect();
idx.sort_by(|a, b| conf[*b].partial_cmp(&conf[*a]).unwrap_or(std::cmp::Ordering::Equal));
let conf_s: Vec<f32> = idx.iter().map(|&i| conf[i]).collect();
let pred_cls_s: Vec<i64> = idx.iter().map(|&i| pred_cls[i]).collect();
let tp_s: Vec<Vec<bool>> = idx.iter().map(|&i| tp[i].clone()).collect();
let mut classes: Vec<i64> = target_cls.to_vec();
classes.sort();
classes.dedup();
let nt: Vec<i64> = classes
.iter()
.map(|c| target_cls.iter().filter(|&&x| x == *c).count() as i64)
.collect();
let nc = classes.len();
let mut out = ApPerClass {
unique_classes: classes.clone(),
..Default::default()
};
out.ap = vec![vec![0.0; n_iou]; nc];
let mut p_curve = vec![0.0f64; nc];
let mut r_curve = vec![0.0f64; nc];
let x: Vec<f64> = (0..1000).map(|i| i as f64 / 999.0).collect();
let mut p_curves = vec![vec![0.0f64; 1000]; nc];
let mut r_curves = vec![vec![0.0f64; 1000]; nc];
for (ci, &c) in classes.iter().enumerate() {
let sel: Vec<usize> = (0..pred_cls_s.len()).filter(|&i| pred_cls_s[i] == c).collect();
let n_l = nt[ci] as f64;
let n_p = sel.len();
if n_p == 0 || n_l == 0.0 {
continue;
}
let mut fpc = vec![0.0f64; n_p];
let mut tpc = vec![0.0f64; n_p];
let (mut fc, mut tc) = (0.0, 0.0);
for k in 0..n_p {
fc += if tp_s[sel[k]][0] { 0.0 } else { 1.0 };
tc += if tp_s[sel[k]][0] { 1.0 } else { 0.0 };
fpc[k] = fc;
tpc[k] = tc;
}
let recall: Vec<f64> = tpc.iter().map(|t| t / (n_l + eps)).collect();
let precision: Vec<f64> = tpc
.iter()
.zip(fpc.iter())
.map(|(t, f)| t / (t + f + eps))
.collect();
let conf_col: Vec<f64> = sel.iter().map(|&i| conf_s[i] as f64).collect();
let neg_x: Vec<f64> = x.iter().map(|v| -v).collect();
let mut neg_conf_c: Vec<f64> = conf_col.clone();
for v in neg_conf_c.iter_mut() {
*v = -*v;
}
for k in 0..1000 {
r_curves[ci][k] = interp1_left(&neg_conf_c, &recall, neg_x[k], 0.0);
p_curves[ci][k] = interp1_left(&neg_conf_c, &precision, neg_x[k], 1.0);
}
for j in 0..n_iou {
let (mut fc, mut tc) = (0.0, 0.0);
let mut rec_j = vec![0.0f64; n_p];
let mut pre_j = vec![0.0f64; n_p];
for k in 0..n_p {
fc += if tp_s[sel[k]][j] { 0.0 } else { 1.0 };
tc += if tp_s[sel[k]][j] { 1.0 } else { 0.0 };
rec_j[k] = tc / (n_l + eps);
pre_j[k] = tc / (tc + fc + eps);
}
out.ap[ci][j] = compute_ap(&rec_j, &pre_j);
}
let _ = (p_curve.as_mut_slice(), r_curve.as_mut_slice());
}
let mut f1_curves = vec![vec![0.0f64; 1000]; nc];
for ci in 0..nc {
for k in 0..1000 {
let (p, r) = (p_curves[ci][k], r_curves[ci][k]);
f1_curves[ci][k] = 2.0 * p * r / (p + r + eps);
}
}
let mut mean_f1 = vec![0.0f64; 1000];
for k in 0..1000 {
mean_f1[k] = f1_curves.iter().map(|f| f[k]).sum::<f64>() / nc.max(1) as f64;
}
let sm = smooth(&mean_f1, 0.1);
let best = sm
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
for ci in 0..nc {
let (p, r) = (p_curves[ci][best], r_curves[ci][best]);
let f1 = 2.0 * p * r / (p + r + eps);
let tpc = r * nt[ci] as f64;
let fpc = tpc / (p + eps) - tpc;
out.p.push(p);
out.r.push(r);
out.f1.push(f1);
out.tp.push(tpc.round());
out.fp.push(fpc.round());
}
out
}
fn interp_left(xp: &[f64], fp: &[f64], x: f64, left: f64) -> f64 {
if xp.is_empty() || x < xp[0] {
return left;
}
interp1(xp, fp, x)
}
pub fn match_predictions(pred_cls: &[i64], gt_cls: &[i64], iou: &[Vec<f32>], iouv: &[f32]) -> Vec<Vec<bool>> {
let n_p = pred_cls.len();
let mut correct = vec![vec![false; iouv.len()]; n_p];
for (ti, &thr) in iouv.iter().enumerate() {
let mut matches: Vec<(f32, usize, usize)> = Vec::new(); for p in 0..n_p {
for g in 0..gt_cls.len() {
let v = iou[p][g];
if v >= thr && pred_cls[p] == gt_cls[g] {
matches.push((v, p, g));
}
}
}
if matches.is_empty() {
continue;
}
matches.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let mut seen_gt = std::collections::HashSet::new();
let mut step1: Vec<(f32, usize, usize)> = Vec::new();
for m in matches {
if seen_gt.insert(m.2) {
step1.push(m);
}
}
let mut seen_pred = std::collections::HashSet::new();
for m in step1 {
if seen_pred.insert(m.1) {
correct[m.1][ti] = true;
}
}
}
correct
}
#[derive(Debug, Clone, Default)]
pub struct DetMetrics {
pub map50_95: f64,
pub map50: f64,
pub map75: f64,
pub mp: f64,
pub mr: f64,
pub nc_present: usize,
}
impl DetMetrics {
pub fn from_ap(ap: &ApPerClass) -> Self {
let n = ap.ap.len();
let mean10 = |row: &[f64]| if row.is_empty() { 0.0 } else { row.iter().sum::<f64>() / row.len() as f64 };
let map50_95 = if n == 0 {
0.0
} else {
ap.ap.iter().map(|row| mean10(row)).sum::<f64>() / n as f64
};
let map50 = if n == 0 {
0.0
} else {
ap.ap.iter().map(|row| row.first().copied().unwrap_or(0.0)).sum::<f64>() / n as f64
};
let map75 = if n == 0 {
0.0
} else {
ap.ap.iter().map(|row| row.get(5).copied().unwrap_or(0.0)).sum::<f64>() / n as f64
};
DetMetrics {
map50_95,
map50,
map75,
mp: if ap.p.is_empty() { 0.0 } else { ap.p.iter().sum::<f64>() / ap.p.len() as f64 },
mr: if ap.r.is_empty() { 0.0 } else { ap.r.iter().sum::<f64>() / ap.r.len() as f64 },
nc_present: n,
}
}
pub fn fitness(&self) -> f64 {
0.9 * self.map50_95 + 0.1 * self.map50
}
pub fn mean_results(&self) -> [f64; 4] {
[self.mp, self.mr, self.map50, self.map50_95]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn perfect_predictions() {
let tp = vec![vec![true; 10]];
let ap = ap_per_class(&tp, &[0.9], &[0], &[0]);
assert!((ap.ap[0][0] - 0.995).abs() < 1e-6);
let m = DetMetrics::from_ap(&ap);
assert!((m.map50_95 - 0.995).abs() < 1e-6);
}
#[test]
fn half_wrong() {
let tp = vec![vec![false; 10], vec![true; 10]];
let ap = ap_per_class(&tp, &[0.9, 0.8], &[0, 0], &[0]);
assert!((ap.ap[0][0] - 0.4975).abs() < 1e-6);
let tp = vec![vec![true; 10], vec![false; 10]];
let ap = ap_per_class(&tp, &[0.9, 0.8], &[0, 0], &[0]);
assert!((ap.ap[0][0] - 0.995).abs() < 1e-6);
}
#[test]
fn match_greedy() {
let pred_cls = [0, 0];
let gt_cls = [0];
let iou = vec![vec![0.9], vec![0.95]];
let c = match_predictions(&pred_cls, >_cls, &iou, &[0.5]);
assert!(c[1][0]);
assert!(!c[0][0]);
}
}