use std::collections::HashMap;
use crate::core::Detections;
use crate::metrics::matching::match_counts;
#[derive(Debug, Clone)]
pub struct MeanAverageRecallResult {
pub mar: f32,
pub recall_per_class: HashMap<usize, f32>,
}
#[derive(Debug, Clone, Default)]
struct ClassCounts {
per_threshold: Vec<(usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct MeanAverageRecall {
iou_thresholds: Vec<f32>,
classes: HashMap<usize, ClassCounts>,
}
impl MeanAverageRecall {
pub fn new(iou_thresholds: Vec<f32>) -> Self {
Self {
iou_thresholds,
classes: HashMap::new(),
}
}
pub fn coco() -> Self {
Self::new((0..10).map(|i| 0.5 + 0.05 * i as f32).collect())
}
pub fn update(&mut self, predictions: &Detections, targets: &Detections) -> &mut Self {
let mut class_ids: Vec<usize> = predictions
.iter()
.chain(targets.iter())
.map(|d| d.class_id)
.collect();
class_ids.sort_unstable();
class_ids.dedup();
let num_thresholds = self.iou_thresholds.len();
for class_id in class_ids {
let class_predictions = predictions.filter_by_class(&[class_id]);
let class_targets = targets.filter_by_class(&[class_id]);
let counts = self.classes.entry(class_id).or_insert_with(|| ClassCounts {
per_threshold: vec![(0, 0); num_thresholds],
});
for (threshold_index, &threshold) in self.iou_thresholds.iter().enumerate() {
let (tp, _fp, fn_) = match_counts(&class_predictions, &class_targets, threshold);
counts.per_threshold[threshold_index].0 += tp;
counts.per_threshold[threshold_index].1 += fn_;
}
}
self
}
pub fn compute(&self) -> MeanAverageRecallResult {
let mut recall_per_class = HashMap::new();
for (&class_id, counts) in &self.classes {
let has_targets = counts.per_threshold.iter().any(|&(tp, fn_)| tp + fn_ > 0);
if !has_targets {
continue;
}
let sum: f32 = counts
.per_threshold
.iter()
.map(|&(tp, fn_)| {
if tp + fn_ == 0 {
0.0
} else {
tp as f32 / (tp + fn_) as f32
}
})
.sum();
recall_per_class.insert(class_id, sum / counts.per_threshold.len() as f32);
}
let mar = if recall_per_class.is_empty() {
0.0
} else {
recall_per_class.values().sum::<f32>() / recall_per_class.len() as f32
};
MeanAverageRecallResult {
mar,
recall_per_class,
}
}
}
impl Default for MeanAverageRecall {
fn default() -> Self {
Self::coco()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Detection;
fn det(bbox: [f32; 4], confidence: f32, class_id: usize) -> Detection {
Detection::new(bbox, confidence, class_id)
}
#[test]
fn perfect_predictions_score_recall_of_one() {
let mut metric = MeanAverageRecall::new(vec![0.5]);
let targets = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 1.0, 0)]);
let predictions = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
metric.update(&predictions, &targets);
assert!((metric.compute().mar - 1.0).abs() < 1e-4);
}
#[test]
fn missed_detection_scores_recall_of_zero() {
let mut metric = MeanAverageRecall::new(vec![0.5]);
let targets = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 1.0, 0)]);
metric.update(&Detections::empty(), &targets);
assert_eq!(metric.compute().mar, 0.0);
}
#[test]
fn accumulates_across_multiple_update_calls() {
let mut metric = MeanAverageRecall::new(vec![0.5]);
let targets1 = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 1.0, 0)]);
let predictions1 = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 0.9, 0)]);
metric.update(&predictions1, &targets1);
let targets2 = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 1.0, 0)]);
metric.update(&Detections::empty(), &targets2);
assert!((metric.compute().mar - 0.5).abs() < 1e-4);
}
#[test]
fn extra_iou_thresholds_average_in_zero_when_unmet() {
let mut metric = MeanAverageRecall::new(vec![0.5, 0.9]);
let targets = Detections::new(vec![det([0.0, 0.0, 10.0, 10.0], 1.0, 0)]);
let predictions = Detections::new(vec![det([1.0, 1.0, 11.0, 11.0], 0.9, 0)]);
metric.update(&predictions, &targets);
let mar = metric.compute().mar;
assert!(mar > 0.0 && mar < 1.0);
}
}