1use ndarray::{Array2, ArrayView2, ArrayViewMut2};
87
88use crate::accumulate::PerImageEval;
89use crate::dataset::{
90 Bbox, CategoryId, CocoAnnotation, CocoDataset, CocoDetection, CocoDetections, EvalDataset,
91 ImageId, ImageMeta,
92};
93use crate::error::EvalError;
94use crate::matching::{match_image, MatchResult};
95use crate::parity::ParityMode;
96use crate::segmentation::Segmentation;
97use crate::similarity::{
98 boundary_iou_compute, segm_iou_compute, BboxAnn, BboxIou, BoundaryComputeScratch,
99 BoundaryGtCache, BoundaryIou, OksAnn, OksSimilarity, SegmAnn, SegmComputeScratch, SegmGtCache,
100 SegmIou, Similarity,
101};
102use std::cell::RefCell;
103use std::collections::{HashMap, HashSet};
104use std::sync::Arc;
105use thread_local::ThreadLocal;
106use vernier_mask::Rle;
107
108#[derive(Clone)]
118pub enum GtCacheRef<'a, T: ?Sized> {
119 Borrowed(&'a T),
122 Owned(Arc<T>),
126}
127
128impl<T: ?Sized> GtCacheRef<'_, T> {
129 pub fn get(&self) -> &T {
131 match self {
132 GtCacheRef::Borrowed(r) => r,
133 GtCacheRef::Owned(a) => a.as_ref(),
134 }
135 }
136}
137
138pub const COLLAPSED_CATEGORY_SENTINEL: i64 = -1;
141
142pub const AREA_UNBOUNDED: f64 = 1e10;
145
146#[derive(Debug, Clone, Copy, PartialEq, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
154#[rkyv(derive(Debug))]
155pub struct AreaRange {
156 pub index: usize,
159 pub lo: f64,
161 pub hi: f64,
164}
165
166impl AreaRange {
167 pub fn coco_default() -> [Self; 4] {
171 [
172 Self {
173 index: 0,
174 lo: 0.0,
175 hi: AREA_UNBOUNDED,
176 },
177 Self {
178 index: 1,
179 lo: 0.0,
180 hi: 32.0 * 32.0,
181 },
182 Self {
183 index: 2,
184 lo: 32.0 * 32.0,
185 hi: 96.0 * 96.0,
186 },
187 Self {
188 index: 3,
189 lo: 96.0 * 96.0,
190 hi: AREA_UNBOUNDED,
191 },
192 ]
193 }
194
195 pub fn keypoints_default() -> [Self; 3] {
202 [
203 Self {
204 index: 0,
205 lo: 0.0,
206 hi: AREA_UNBOUNDED,
207 },
208 Self {
209 index: 1,
210 lo: 32.0 * 32.0,
211 hi: 96.0 * 96.0,
212 },
213 Self {
214 index: 2,
215 lo: 96.0 * 96.0,
216 hi: AREA_UNBOUNDED,
217 },
218 ]
219 }
220
221 fn contains(&self, area: f64) -> bool {
222 area >= self.lo && area <= self.hi
227 }
228}
229
230#[derive(Debug, Clone, Copy)]
234pub struct EvaluateParams<'p> {
235 pub iou_thresholds: &'p [f64],
238 pub area_ranges: &'p [AreaRange],
243 pub max_dets_per_image: usize,
248 pub use_cats: bool,
252 pub retain_iou: bool,
258}
259
260#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
268#[rkyv(derive(Debug))]
269pub struct OwnedEvaluateParams {
270 pub iou_thresholds: Vec<f64>,
272 pub area_ranges: Vec<AreaRange>,
274 pub max_dets_per_image: usize,
276 pub use_cats: bool,
278 pub retain_iou: bool,
280}
281
282impl OwnedEvaluateParams {
283 pub fn borrow(&self) -> EvaluateParams<'_> {
285 EvaluateParams {
286 iou_thresholds: &self.iou_thresholds,
287 area_ranges: &self.area_ranges,
288 max_dets_per_image: self.max_dets_per_image,
289 use_cats: self.use_cats,
290 retain_iou: self.retain_iou,
291 }
292 }
293
294 pub fn params_hash(&self) -> Result<[u8; 32], EvalError> {
310 let bytes =
311 rkyv::to_bytes::<rkyv::rancor::Error>(self).map_err(|e| EvalError::InvalidConfig {
312 detail: format!("rkyv serialization of OwnedEvaluateParams failed: {e}"),
313 })?;
314 Ok(*blake3::hash(&bytes).as_bytes())
315 }
316}
317
318#[derive(
328 Debug, Clone, Copy, PartialEq, Eq, Hash, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize,
329)]
330#[rkyv(derive(Debug, PartialEq, Eq))]
331pub enum KernelKind {
332 Bbox,
334 Segm,
336 Boundary,
338 Keypoints,
340}
341
342impl KernelKind {
343 pub const fn discriminator(self) -> u32 {
348 match self {
349 Self::Bbox => 0,
350 Self::Segm => 1,
351 Self::Boundary => 2,
352 Self::Keypoints => 3,
353 }
354 }
355}
356
357#[cfg(feature = "bench-timings")]
359pub(crate) mod build_anns_count {
360 use crate::bench_counters::BenchCounterSet;
361
362 static COUNTERS: BenchCounterSet<1> = BenchCounterSet::new();
363
364 #[inline]
365 pub(crate) fn bump() {
366 COUNTERS.bump(0);
367 }
368
369 pub(crate) fn read_and_reset() -> u64 {
370 COUNTERS.read_and_reset()[0]
371 }
372}
373
374pub trait EvalKernel: Similarity {
385 fn kind(&self) -> KernelKind;
389
390 fn build_gt_anns(
394 &self,
395 gt_anns: &[CocoAnnotation],
396 indices: &[usize],
397 image: &ImageMeta,
398 ) -> Result<Vec<Self::Annotation>, EvalError>;
399
400 fn build_dt_anns(
408 &self,
409 dt_anns: &[CocoDetection],
410 indices: &[usize],
411 image: &ImageMeta,
412 parity_mode: ParityMode,
413 ) -> Result<Vec<Self::Annotation>, EvalError>;
414
415 fn build_gt_anns_into(
427 &self,
428 buf: &mut Vec<Self::Annotation>,
429 gt_anns: &[CocoAnnotation],
430 indices: &[usize],
431 image: &ImageMeta,
432 ) -> Result<(), EvalError> {
433 *buf = self.build_gt_anns(gt_anns, indices, image)?;
434 Ok(())
435 }
436
437 fn build_dt_anns_into(
443 &self,
444 buf: &mut Vec<Self::Annotation>,
445 dt_anns: &[CocoDetection],
446 indices: &[usize],
447 image: &ImageMeta,
448 parity_mode: ParityMode,
449 ) -> Result<(), EvalError> {
450 *buf = self.build_dt_anns(dt_anns, indices, image, parity_mode)?;
451 Ok(())
452 }
453
454 fn extra_gt_ignore(&self, _ann: &CocoAnnotation) -> bool {
465 false
466 }
467
468 fn is_keypoints(&self) -> bool {
477 false
478 }
479}
480
481impl EvalKernel for BboxIou {
482 fn kind(&self) -> KernelKind {
483 KernelKind::Bbox
484 }
485
486 fn build_gt_anns(
487 &self,
488 gt_anns: &[CocoAnnotation],
489 indices: &[usize],
490 _image: &ImageMeta,
491 ) -> Result<Vec<BboxAnn>, EvalError> {
492 Ok(indices
493 .iter()
494 .map(|&j| BboxAnn {
495 bbox: gt_anns[j].bbox,
496 is_crowd: gt_anns[j].is_crowd,
497 })
498 .collect())
499 }
500
501 fn build_dt_anns(
502 &self,
503 dt_anns: &[CocoDetection],
504 indices: &[usize],
505 _image: &ImageMeta,
506 _parity_mode: ParityMode,
507 ) -> Result<Vec<BboxAnn>, EvalError> {
508 Ok(indices
510 .iter()
511 .map(|&j| BboxAnn {
512 bbox: dt_anns[j].bbox,
513 is_crowd: false,
514 })
515 .collect())
516 }
517
518 fn build_gt_anns_into(
519 &self,
520 buf: &mut Vec<BboxAnn>,
521 gt_anns: &[CocoAnnotation],
522 indices: &[usize],
523 _image: &ImageMeta,
524 ) -> Result<(), EvalError> {
525 buf.clear();
526 buf.extend(indices.iter().map(|&j| BboxAnn {
527 bbox: gt_anns[j].bbox,
528 is_crowd: gt_anns[j].is_crowd,
529 }));
530 Ok(())
531 }
532
533 fn build_dt_anns_into(
534 &self,
535 buf: &mut Vec<BboxAnn>,
536 dt_anns: &[CocoDetection],
537 indices: &[usize],
538 _image: &ImageMeta,
539 _parity_mode: ParityMode,
540 ) -> Result<(), EvalError> {
541 buf.clear();
543 buf.extend(indices.iter().map(|&j| BboxAnn {
544 bbox: dt_anns[j].bbox,
545 is_crowd: false,
546 }));
547 Ok(())
548 }
549}
550
551impl EvalKernel for SegmIou {
552 fn kind(&self) -> KernelKind {
553 KernelKind::Segm
554 }
555
556 fn build_gt_anns(
557 &self,
558 gt_anns: &[CocoAnnotation],
559 indices: &[usize],
560 image: &ImageMeta,
561 ) -> Result<Vec<SegmAnn>, EvalError> {
562 build_segm_gt_anns(gt_anns, indices, image)
563 }
564
565 fn build_dt_anns(
566 &self,
567 dt_anns: &[CocoDetection],
568 indices: &[usize],
569 image: &ImageMeta,
570 parity_mode: ParityMode,
571 ) -> Result<Vec<SegmAnn>, EvalError> {
572 build_segm_dt_anns(dt_anns, indices, image, parity_mode)
573 }
574}
575
576impl EvalKernel for BoundaryIou {
577 fn kind(&self) -> KernelKind {
578 KernelKind::Boundary
579 }
580
581 fn build_gt_anns(
582 &self,
583 gt_anns: &[CocoAnnotation],
584 indices: &[usize],
585 image: &ImageMeta,
586 ) -> Result<Vec<SegmAnn>, EvalError> {
587 build_segm_gt_anns(gt_anns, indices, image)
588 }
589
590 fn build_dt_anns(
591 &self,
592 dt_anns: &[CocoDetection],
593 indices: &[usize],
594 image: &ImageMeta,
595 parity_mode: ParityMode,
596 ) -> Result<Vec<SegmAnn>, EvalError> {
597 build_segm_dt_anns(dt_anns, indices, image, parity_mode)
598 }
599}
600
601impl EvalKernel for OksSimilarity {
602 fn kind(&self) -> KernelKind {
603 KernelKind::Keypoints
604 }
605
606 fn build_gt_anns(
607 &self,
608 gt_anns: &[CocoAnnotation],
609 indices: &[usize],
610 _image: &ImageMeta,
611 ) -> Result<Vec<OksAnn>, EvalError> {
612 indices
613 .iter()
614 .map(|&j| {
615 let ann = >_anns[j];
616 let kps = ann
617 .keypoints
618 .as_deref()
619 .ok_or_else(|| missing_keypoints_err("GT", ann.id.0, ann.image_id.0))?;
620 let num_keypoints = ann
621 .num_keypoints
622 .unwrap_or_else(|| count_visible_keypoints(kps));
623 Ok(OksAnn {
624 category_id: ann.category_id.0,
625 keypoints: kps.to_vec(),
626 num_keypoints,
627 bbox: ann.bbox.into(),
628 area: ann.area,
629 })
630 })
631 .collect()
632 }
633
634 fn build_dt_anns(
635 &self,
636 dt_anns: &[CocoDetection],
637 indices: &[usize],
638 _image: &ImageMeta,
639 _parity_mode: ParityMode,
640 ) -> Result<Vec<OksAnn>, EvalError> {
641 indices
646 .iter()
647 .map(|&j| {
648 let dt = &dt_anns[j];
649 let kps = dt
650 .keypoints
651 .as_deref()
652 .ok_or_else(|| missing_keypoints_err("DT", dt.id.0, dt.image_id.0))?;
653 let num_keypoints = dt
654 .num_keypoints
655 .unwrap_or_else(|| count_visible_keypoints(kps));
656 Ok(OksAnn {
657 category_id: dt.category_id.0,
658 keypoints: kps.to_vec(),
659 num_keypoints,
660 bbox: dt.bbox.into(),
661 area: dt.area,
662 })
663 })
664 .collect()
665 }
666
667 fn extra_gt_ignore(&self, ann: &CocoAnnotation) -> bool {
668 let visible = ann
674 .num_keypoints
675 .or_else(|| ann.keypoints.as_deref().map(count_visible_keypoints))
676 .unwrap_or(0);
677 visible == 0
678 }
679
680 fn is_keypoints(&self) -> bool {
681 true
682 }
683}
684
685fn count_visible_keypoints(kps: &[f64]) -> u32 {
689 kps.chunks_exact(3).filter(|t| t[2] > 0.0).count() as u32
690}
691
692fn missing_keypoints_err(kind: &str, ann_id: i64, image_id: i64) -> EvalError {
696 EvalError::InvalidAnnotation {
697 detail: format!(
698 "{kind} id={ann_id} on image {image_id} has no `keypoints` field; \
699 OKS eval requires keypoints on every entry. There is no \
700 pycocotools-equivalent bbox-synthesis fallback for keypoints \
701 (unlike segm quirk J2)."
702 ),
703 }
704}
705
706fn build_segm_gt_anns(
707 gt_anns: &[CocoAnnotation],
708 indices: &[usize],
709 image: &ImageMeta,
710) -> Result<Vec<SegmAnn>, EvalError> {
711 indices
712 .iter()
713 .map(|&j| {
714 let ann = >_anns[j];
715 let seg = ann
716 .segmentation
717 .as_ref()
718 .ok_or_else(|| missing_segmentation_err("GT", ann.id.0, image.id.0))?;
719 Ok(SegmAnn {
720 rle: seg.to_rle(image.height, image.width)?,
721 is_crowd: ann.is_crowd,
722 ann_id: ann.id.0,
723 })
724 })
725 .collect()
726}
727
728fn build_segm_dt_anns(
729 dt_anns: &[CocoDetection],
730 indices: &[usize],
731 image: &ImageMeta,
732 parity_mode: ParityMode,
733) -> Result<Vec<SegmAnn>, EvalError> {
734 indices
735 .iter()
736 .map(|&j| {
737 let dt = &dt_anns[j];
738 let rle = match (&dt.segmentation, parity_mode) {
739 (Some(seg), _) => seg.to_rle(image.height, image.width)?,
740 (None, ParityMode::Strict) => {
747 synthesize_dt_segm_from_bbox(&dt.bbox, image.height, image.width)?
748 }
749 (None, ParityMode::Corrected) => {
757 return Err(missing_segmentation_err("DT", dt.id.0, image.id.0));
758 }
759 };
760 Ok(SegmAnn {
761 rle,
762 is_crowd: false,
763 ann_id: dt.id.0,
764 })
765 })
766 .collect()
767}
768
769fn synthesize_dt_segm_from_bbox(bbox: &Bbox, h: u32, w: u32) -> Result<Rle, EvalError> {
775 let x1 = bbox.x;
776 let y1 = bbox.y;
777 let x2 = bbox.x + bbox.w;
778 let y2 = bbox.y + bbox.h;
779 let polygon = vec![x1, y1, x1, y2, x2, y2, x2, y1];
780 let segm = Segmentation::Polygons(vec![polygon]);
781 segm.to_rle(h, w)
782}
783
784fn missing_segmentation_err(kind: &str, ann_id: i64, image_id: i64) -> EvalError {
790 EvalError::InvalidAnnotation {
791 detail: format!(
792 "{kind} id={ann_id} on image {image_id} has no `segmentation` field; \
793 segm eval in corrected mode requires one on every entry. \
794 pycocotools synthesizes a bbox-rectangle polygon here \
795 (quirks J2/J6); pass `ParityMode::Strict` to opt into that \
796 behavior."
797 ),
798 }
799}
800
801#[derive(Debug, Clone)]
812pub struct EvalImageMeta {
813 pub image_id: i64,
815 pub category_id: i64,
818 pub area_rng: [f64; 2],
820 pub max_det: usize,
822 pub dt_ids: Vec<i64>,
824 pub gt_ids: Vec<i64>,
826 pub dt_matches: Array2<i64>,
830 pub gt_matches: Array2<i64>,
833}
834
835#[derive(Debug, Clone)]
840pub struct EvalGrid {
841 pub eval_imgs: Vec<Option<Box<PerImageEval>>>,
854 pub eval_imgs_meta: Vec<Option<Box<EvalImageMeta>>>,
858 pub n_categories: usize,
861 pub n_area_ranges: usize,
863 pub n_images: usize,
866 pub retained_ious: Option<crate::tables::RetainedIous>,
870}
871
872impl EvalGrid {
873 pub fn cell(&self, k: usize, a: usize, i: usize) -> Option<&PerImageEval> {
878 let idx = self.flat_index(k, a, i)?;
879 self.eval_imgs.get(idx).and_then(Option::as_deref)
880 }
881
882 pub fn cell_meta(&self, k: usize, a: usize, i: usize) -> Option<&EvalImageMeta> {
885 let idx = self.flat_index(k, a, i)?;
886 self.eval_imgs_meta.get(idx).and_then(Option::as_deref)
887 }
888
889 fn flat_index(&self, k: usize, a: usize, i: usize) -> Option<usize> {
890 if k >= self.n_categories || a >= self.n_area_ranges || i >= self.n_images {
891 return None;
892 }
893 Some(k * self.n_area_ranges * self.n_images + a * self.n_images + i)
894 }
895}
896
897pub fn evaluate_with<K: EvalKernel>(
914 gt: &CocoDataset,
915 dt: &CocoDetections,
916 params: EvaluateParams<'_>,
917 parity_mode: ParityMode,
918 kernel: &K,
919) -> Result<EvalGrid, EvalError> {
920 let mut images: Vec<&ImageMeta> = gt.images().iter().collect();
922 images.sort_unstable_by_key(|im| im.id.0);
923 let n_i = images.len();
924 let n_a = params.area_ranges.len();
925
926 let category_buckets: Vec<Option<CategoryId>> = if params.use_cats {
928 let mut cats: Vec<_> = gt.categories().iter().map(|c| c.id).collect();
929 cats.sort_unstable_by_key(|id| id.0);
930 cats.into_iter().map(Some).collect()
931 } else {
932 vec![None]
933 };
934 let n_k = category_buckets.len();
935
936 let mut eval_imgs: Vec<Option<Box<PerImageEval>>> = vec![None; n_k * n_a * n_i];
937 let mut eval_imgs_meta: Vec<Option<Box<EvalImageMeta>>> = vec![None; n_k * n_a * n_i];
938 let mut retained_ious_map: Option<std::collections::HashMap<(usize, usize), Array2<f64>>> =
941 if params.retain_iou {
942 Some(std::collections::HashMap::new())
943 } else {
944 None
945 };
946
947 let federated_per_image: Vec<Option<(&HashSet<CategoryId>, &HashSet<CategoryId>)>> =
957 match (params.use_cats, gt.federated()) {
958 (true, Some(fed)) => images
959 .iter()
960 .map(|im| {
961 let neg = fed.neg_category_ids.get(&im.id)?;
962 let nel = fed.not_exhaustive_category_ids.get(&im.id)?;
963 Some((neg, nel))
964 })
965 .collect(),
966 _ => Vec::new(),
967 };
968
969 let mut scratch = CellScratch::new();
973 let mut kernel_scratch = KernelScratch::<K::Annotation>::default();
977 let gt_anns = gt.annotations();
978 let dt_anns = dt.detections();
979
980 let strict_lvis_zero_area_filter =
992 matches!(parity_mode, ParityMode::Strict) && gt.federated().is_some();
993
994 for (k, cat) in category_buckets.iter().enumerate() {
995 let nk = k * n_a * n_i;
996 let category_id = cat.map_or(COLLAPSED_CATEGORY_SENTINEL, |c| c.0);
997 for (i, image) in images.iter().enumerate() {
998 let image_id = image.id;
999 let gt_indices_raw = gt_indices_for_cell(gt, image_id, *cat);
1000 let gt_indices_buf: Vec<usize>;
1001 let gt_indices: &[usize] = if strict_lvis_zero_area_filter
1002 && gt_indices_raw.iter().any(|&j| gt_anns[j].area <= 0.0)
1003 {
1004 gt_indices_buf = gt_indices_raw
1005 .iter()
1006 .copied()
1007 .filter(|&j| gt_anns[j].area > 0.0)
1008 .collect();
1009 >_indices_buf
1010 } else {
1011 gt_indices_raw
1012 };
1013 let raw_dt_indices = raw_dt_indices_for_cell(dt, image_id, *cat);
1014 if gt_indices.is_empty() && raw_dt_indices.is_empty() {
1015 continue;
1016 }
1017
1018 let mut not_exhaustive_for_cell = false;
1023 if let (Some(c), Some(Some((neg_set, nel_set)))) = (cat, federated_per_image.get(i)) {
1024 if gt_indices.is_empty() && !neg_set.contains(c) {
1028 continue;
1029 }
1030 not_exhaustive_for_cell = nel_set.contains(c);
1031 }
1032
1033 dt_top_indices_for_cell_into(
1036 &mut scratch.dt_indices,
1037 &mut scratch.dt_score_buf,
1038 &mut scratch.dt_perm_buf,
1039 dt_anns,
1040 raw_dt_indices,
1041 params.max_dets_per_image,
1042 );
1043
1044 scratch.gt_areas.clear();
1049 scratch
1050 .gt_areas
1051 .extend(gt_indices.iter().map(|&j| gt_anns[j].area));
1052 scratch.gt_iscrowd.clear();
1053 scratch
1054 .gt_iscrowd
1055 .extend(gt_indices.iter().map(|&j| gt_anns[j].is_crowd));
1056 scratch.gt_base_ignore.clear();
1060 scratch.gt_base_ignore.extend(gt_indices.iter().map(|&j| {
1061 gt_anns[j].effective_ignore(parity_mode) || kernel.extra_gt_ignore(>_anns[j])
1062 }));
1063 scratch.gt_ids.clear();
1064 scratch
1065 .gt_ids
1066 .extend(gt_indices.iter().map(|&j| gt_anns[j].id.0));
1067 scratch.dt_areas.clear();
1068 scratch
1069 .dt_areas
1070 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].area));
1071 scratch.dt_scores.clear();
1072 scratch
1073 .dt_scores
1074 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].score));
1075 scratch.dt_ids.clear();
1076 scratch
1077 .dt_ids
1078 .extend(scratch.dt_indices.iter().map(|&j| dt_anns[j].id.0));
1079
1080 #[cfg(feature = "bench-timings")]
1081 {
1082 build_anns_count::bump();
1083 build_anns_count::bump();
1084 }
1085 kernel.build_gt_anns_into(&mut kernel_scratch.gt, gt_anns, gt_indices, image)?;
1086 kernel.build_dt_anns_into(
1087 &mut kernel_scratch.dt,
1088 dt_anns,
1089 &scratch.dt_indices,
1090 image,
1091 parity_mode,
1092 )?;
1093
1094 let g = kernel_scratch.gt.len();
1098 let d = kernel_scratch.dt.len();
1099 scratch.iou_buf.clear();
1100 scratch.iou_buf.resize(g * d, 0.0);
1101 if g > 0 && d > 0 {
1102 let mut iou_view = ArrayViewMut2::from_shape((g, d), &mut scratch.iou_buf[..])
1103 .map_err(|e| EvalError::DimensionMismatch {
1104 detail: format!("iou scratch view: {e}"),
1105 })?;
1106 kernel.compute(&kernel_scratch.gt, &kernel_scratch.dt, &mut iou_view)?;
1107 }
1108
1109 let iou_view = ArrayView2::from_shape((g, d), &scratch.iou_buf[..]).map_err(|e| {
1110 EvalError::DimensionMismatch {
1111 detail: format!("iou scratch view: {e}"),
1112 }
1113 })?;
1114 let buffers = CellBuffers {
1115 image_id: image_id.0,
1116 category_id,
1117 max_det: params.max_dets_per_image,
1118 gt_areas: &scratch.gt_areas,
1119 gt_iscrowd: &scratch.gt_iscrowd,
1120 gt_base_ignore: &scratch.gt_base_ignore,
1121 gt_ids: &scratch.gt_ids,
1122 dt_areas: &scratch.dt_areas,
1123 dt_scores: &scratch.dt_scores,
1124 dt_ids: &scratch.dt_ids,
1125 iou: iou_view,
1126 not_exhaustive: not_exhaustive_for_cell,
1127 };
1128 for (a, area) in params.area_ranges.iter().enumerate() {
1129 let (cell, meta) = evaluate_cell(
1130 &mut scratch.gt_ignore_buf,
1131 &buffers,
1132 area,
1133 params.iou_thresholds,
1134 parity_mode,
1135 )?;
1136 let flat = nk + a * n_i + i;
1137 eval_imgs[flat] = Some(Box::new(cell));
1138 eval_imgs_meta[flat] = Some(Box::new(meta));
1139 }
1140
1141 if let Some(map) = retained_ious_map.as_mut() {
1146 let cloned =
1147 Array2::from_shape_vec((g, d), scratch.iou_buf.clone()).map_err(|e| {
1148 EvalError::DimensionMismatch {
1149 detail: format!("retained iou clone: {e}"),
1150 }
1151 })?;
1152 map.insert((k, i), cloned);
1153 }
1154 }
1155 }
1156
1157 Ok(EvalGrid {
1158 eval_imgs,
1159 eval_imgs_meta,
1160 n_categories: n_k,
1161 n_area_ranges: n_a,
1162 n_images: n_i,
1163 retained_ious: retained_ious_map.map(crate::tables::RetainedIous::from_map),
1164 })
1165}
1166
1167pub(crate) fn evaluate_with_retention<K: EvalKernel>(
1186 gt: &CocoDataset,
1187 dt: &CocoDetections,
1188 params: EvaluateParams<'_>,
1189 parity_mode: ParityMode,
1190 kernel: &K,
1191) -> Result<(EvalGrid, crate::tables::CrossClassIous), EvalError> {
1192 let grid = evaluate_with(gt, dt, params, parity_mode, kernel)?;
1193 let cross_class = crate::tide::compute_cross_class_ious(
1194 gt,
1195 dt,
1196 kernel,
1197 parity_mode,
1198 params.max_dets_per_image,
1199 )?;
1200 Ok((grid, cross_class))
1201}
1202
1203pub fn evaluate_bbox(
1211 gt: &CocoDataset,
1212 dt: &CocoDetections,
1213 params: EvaluateParams<'_>,
1214 parity_mode: ParityMode,
1215) -> Result<EvalGrid, EvalError> {
1216 evaluate_with(gt, dt, params, parity_mode, &BboxIou)
1217}
1218
1219pub fn evaluate_segm(
1239 gt: &CocoDataset,
1240 dt: &CocoDetections,
1241 params: EvaluateParams<'_>,
1242 parity_mode: ParityMode,
1243) -> Result<EvalGrid, EvalError> {
1244 evaluate_with(gt, dt, params, parity_mode, &segm_kernel(None))
1245}
1246
1247pub fn evaluate_segm_cached(
1263 gt: &CocoDataset,
1264 dt: &CocoDetections,
1265 params: EvaluateParams<'_>,
1266 parity_mode: ParityMode,
1267 cache: &SegmGtCache,
1268) -> Result<EvalGrid, EvalError> {
1269 evaluate_with(gt, dt, params, parity_mode, &segm_kernel(Some(cache)))
1270}
1271
1272pub(crate) fn segm_kernel(gt_cache: Option<&SegmGtCache>) -> SegmIouCached<'_> {
1273 SegmIouCached {
1274 scratch: ThreadLocal::new(),
1275 gt_cache: gt_cache.map(GtCacheRef::Borrowed),
1276 }
1277}
1278
1279pub struct SegmIouCached<'a> {
1298 scratch: ThreadLocal<RefCell<SegmComputeScratch>>,
1299 gt_cache: Option<GtCacheRef<'a, SegmGtCache>>,
1300}
1301
1302impl SegmIouCached<'static> {
1303 pub fn with_arc_cache(cache: Arc<SegmGtCache>) -> Self {
1310 Self {
1311 scratch: ThreadLocal::new(),
1312 gt_cache: Some(GtCacheRef::Owned(cache)),
1313 }
1314 }
1315}
1316
1317impl Similarity for SegmIouCached<'_> {
1318 type Annotation = SegmAnn;
1319
1320 fn compute(
1321 &self,
1322 gts: &[SegmAnn],
1323 dts: &[SegmAnn],
1324 out: &mut ArrayViewMut2<'_, f64>,
1325 ) -> Result<(), EvalError> {
1326 let cell = self.scratch.get_or_default();
1327 let mut scratch = cell.borrow_mut();
1328 segm_iou_compute(
1329 gts,
1330 dts,
1331 out,
1332 &mut scratch,
1333 self.gt_cache.as_ref().map(GtCacheRef::get),
1334 )
1335 }
1336}
1337
1338impl EvalKernel for SegmIouCached<'_> {
1339 fn kind(&self) -> KernelKind {
1340 KernelKind::Segm
1341 }
1342
1343 fn build_gt_anns(
1344 &self,
1345 gt_anns: &[CocoAnnotation],
1346 indices: &[usize],
1347 image: &ImageMeta,
1348 ) -> Result<Vec<SegmAnn>, EvalError> {
1349 build_segm_gt_anns(gt_anns, indices, image)
1350 }
1351
1352 fn build_dt_anns(
1353 &self,
1354 dt_anns: &[CocoDetection],
1355 indices: &[usize],
1356 image: &ImageMeta,
1357 parity_mode: ParityMode,
1358 ) -> Result<Vec<SegmAnn>, EvalError> {
1359 build_segm_dt_anns(dt_anns, indices, image, parity_mode)
1360 }
1361}
1362
1363pub fn evaluate_boundary(
1378 gt: &CocoDataset,
1379 dt: &CocoDetections,
1380 params: EvaluateParams<'_>,
1381 parity_mode: ParityMode,
1382 dilation_ratio: f64,
1383) -> Result<EvalGrid, EvalError> {
1384 evaluate_with(
1385 gt,
1386 dt,
1387 params,
1388 parity_mode,
1389 &boundary_kernel(dilation_ratio, None),
1390 )
1391}
1392
1393pub fn evaluate_boundary_cached(
1411 gt: &CocoDataset,
1412 dt: &CocoDetections,
1413 params: EvaluateParams<'_>,
1414 parity_mode: ParityMode,
1415 dilation_ratio: f64,
1416 cache: &BoundaryGtCache,
1417) -> Result<EvalGrid, EvalError> {
1418 cache.align_ratio(dilation_ratio);
1419 evaluate_with(
1420 gt,
1421 dt,
1422 params,
1423 parity_mode,
1424 &boundary_kernel(dilation_ratio, Some(cache)),
1425 )
1426}
1427
1428pub(crate) fn boundary_kernel(
1429 dilation_ratio: f64,
1430 gt_cache: Option<&BoundaryGtCache>,
1431) -> BoundaryIouCached<'_> {
1432 BoundaryIouCached {
1433 dilation_ratio,
1434 scratch: ThreadLocal::new(),
1435 gt_cache: gt_cache.map(GtCacheRef::Borrowed),
1436 }
1437}
1438
1439pub struct BoundaryIouCached<'a> {
1458 dilation_ratio: f64,
1459 scratch: ThreadLocal<RefCell<BoundaryComputeScratch>>,
1460 gt_cache: Option<GtCacheRef<'a, BoundaryGtCache>>,
1461}
1462
1463impl BoundaryIouCached<'static> {
1464 pub fn with_arc_cache(dilation_ratio: f64, cache: Arc<BoundaryGtCache>) -> Self {
1475 cache.align_ratio(dilation_ratio);
1476 Self {
1477 dilation_ratio,
1478 scratch: ThreadLocal::new(),
1479 gt_cache: Some(GtCacheRef::Owned(cache)),
1480 }
1481 }
1482}
1483
1484impl Similarity for BoundaryIouCached<'_> {
1485 type Annotation = SegmAnn;
1486
1487 fn compute(
1488 &self,
1489 gts: &[SegmAnn],
1490 dts: &[SegmAnn],
1491 out: &mut ArrayViewMut2<'_, f64>,
1492 ) -> Result<(), EvalError> {
1493 let cell = self.scratch.get_or_default();
1494 let mut scratch = cell.borrow_mut();
1495 boundary_iou_compute(
1496 self.dilation_ratio,
1497 gts,
1498 dts,
1499 out,
1500 &mut scratch,
1501 self.gt_cache.as_ref().map(GtCacheRef::get),
1502 )
1503 }
1504}
1505
1506impl EvalKernel for BoundaryIouCached<'_> {
1507 fn kind(&self) -> KernelKind {
1508 KernelKind::Boundary
1509 }
1510
1511 fn build_gt_anns(
1512 &self,
1513 gt_anns: &[CocoAnnotation],
1514 indices: &[usize],
1515 image: &ImageMeta,
1516 ) -> Result<Vec<SegmAnn>, EvalError> {
1517 build_segm_gt_anns(gt_anns, indices, image)
1518 }
1519
1520 fn build_dt_anns(
1521 &self,
1522 dt_anns: &[CocoDetection],
1523 indices: &[usize],
1524 image: &ImageMeta,
1525 parity_mode: ParityMode,
1526 ) -> Result<Vec<SegmAnn>, EvalError> {
1527 build_segm_dt_anns(dt_anns, indices, image, parity_mode)
1528 }
1529}
1530
1531pub fn evaluate_keypoints(
1572 gt: &CocoDataset,
1573 dt: &CocoDetections,
1574 params: EvaluateParams<'_>,
1575 parity_mode: ParityMode,
1576 sigmas: HashMap<i64, Vec<f64>>,
1577) -> Result<EvalGrid, EvalError> {
1578 evaluate_with(gt, dt, params, parity_mode, &OksSimilarity::new(sigmas))
1579}
1580
1581pub(crate) fn gt_indices_for_cell(
1582 gt: &CocoDataset,
1583 image: ImageId,
1584 cat: Option<CategoryId>,
1585) -> &[usize] {
1586 match cat {
1587 Some(c) => gt.ann_indices_for(image, c),
1588 None => gt.ann_indices_for_image(image),
1589 }
1590}
1591
1592pub(crate) fn raw_dt_indices_for_cell(
1597 dt: &CocoDetections,
1598 image: ImageId,
1599 cat: Option<CategoryId>,
1600) -> &[usize] {
1601 match cat {
1602 Some(c) => dt.indices_for(image, c),
1603 None => dt.indices_for_image(image),
1604 }
1605}
1606
1607pub(crate) fn dt_top_indices_for_cell(
1608 dt: &CocoDetections,
1609 image: ImageId,
1610 cat: Option<CategoryId>,
1611 max_dets: usize,
1612) -> Vec<usize> {
1613 let raw_indices = raw_dt_indices_for_cell(dt, image, cat);
1614 let mut out = Vec::new();
1615 let mut score_buf = Vec::new();
1616 let mut perm_buf = Vec::new();
1617 dt_top_indices_for_cell_into(
1618 &mut out,
1619 &mut score_buf,
1620 &mut perm_buf,
1621 dt.detections(),
1622 raw_indices,
1623 max_dets,
1624 );
1625 out
1626}
1627
1628pub(crate) fn dt_top_indices_for_cell_into(
1636 out: &mut Vec<usize>,
1637 score_buf: &mut Vec<f64>,
1638 perm_buf: &mut Vec<usize>,
1639 dts: &[CocoDetection],
1640 raw_indices: &[usize],
1641 max_dets: usize,
1642) {
1643 score_buf.clear();
1644 score_buf.extend(raw_indices.iter().map(|&i| dts[i].score));
1645 perm_buf.clear();
1646 perm_buf.extend(0..score_buf.len());
1647 perm_buf.sort_by(|&a, &b| {
1650 score_buf[b]
1651 .partial_cmp(&score_buf[a])
1652 .unwrap_or(std::cmp::Ordering::Equal)
1653 });
1654 out.clear();
1655 out.extend(perm_buf.iter().take(max_dets).map(|&k| raw_indices[k]));
1656}
1657
1658#[derive(Default)]
1665pub(crate) struct CellScratch {
1666 pub(crate) gt_areas: Vec<f64>,
1668 pub(crate) gt_iscrowd: Vec<bool>,
1669 pub(crate) gt_base_ignore: Vec<bool>,
1670 pub(crate) gt_ids: Vec<i64>,
1671 pub(crate) dt_indices: Vec<usize>,
1674 pub(crate) dt_areas: Vec<f64>,
1676 pub(crate) dt_scores: Vec<f64>,
1677 pub(crate) dt_ids: Vec<i64>,
1678 pub(crate) iou_buf: Vec<f64>,
1682 pub(crate) dt_score_buf: Vec<f64>,
1684 pub(crate) dt_perm_buf: Vec<usize>,
1686 pub(crate) gt_ignore_buf: Vec<bool>,
1691}
1692
1693impl CellScratch {
1694 pub(crate) fn new() -> Self {
1695 Self::default()
1696 }
1697}
1698
1699pub(crate) struct KernelScratch<A> {
1705 pub(crate) gt: Vec<A>,
1706 pub(crate) dt: Vec<A>,
1707}
1708
1709impl<A> Default for KernelScratch<A> {
1710 fn default() -> Self {
1711 Self {
1712 gt: Vec::new(),
1713 dt: Vec::new(),
1714 }
1715 }
1716}
1717
1718pub(crate) struct CellBuffers<'a> {
1720 pub(crate) image_id: i64,
1721 pub(crate) category_id: i64,
1722 pub(crate) max_det: usize,
1723 pub(crate) gt_areas: &'a [f64],
1724 pub(crate) gt_iscrowd: &'a [bool],
1725 pub(crate) gt_base_ignore: &'a [bool],
1726 pub(crate) gt_ids: &'a [i64],
1727 pub(crate) dt_areas: &'a [f64],
1728 pub(crate) dt_scores: &'a [f64],
1729 pub(crate) dt_ids: &'a [i64],
1730 pub(crate) iou: ArrayView2<'a, f64>,
1731 pub(crate) not_exhaustive: bool,
1736}
1737
1738pub(crate) fn evaluate_cell(
1739 gt_ignore_buf: &mut Vec<bool>,
1740 buf: &CellBuffers<'_>,
1741 area: &AreaRange,
1742 iou_thresholds: &[f64],
1743 parity_mode: ParityMode,
1744) -> Result<(PerImageEval, EvalImageMeta), EvalError> {
1745 gt_ignore_buf.clear();
1751 gt_ignore_buf.extend(
1752 buf.gt_base_ignore
1753 .iter()
1754 .zip(buf.gt_areas)
1755 .map(|(&base, &a)| base || !area.contains(a)),
1756 );
1757 let gt_ignore: &[bool] = gt_ignore_buf.as_slice();
1758
1759 let MatchResult {
1760 dt_perm,
1761 gt_perm,
1762 dt_matches: dt_matches_pos,
1763 gt_matches: gt_matches_pos,
1764 mut dt_ignore,
1765 } = match_image(
1766 buf.iou,
1767 gt_ignore,
1768 buf.gt_iscrowd,
1769 buf.dt_scores,
1770 iou_thresholds,
1771 parity_mode,
1772 )?;
1773
1774 let n_t = iou_thresholds.len();
1775 let n_d = buf.dt_scores.len();
1776 let n_g = gt_ignore.len();
1777
1778 let dt_scores_sorted: Vec<f64> = dt_perm.iter().map(|&k| buf.dt_scores[k]).collect();
1779 let gt_ignore_sorted: Vec<bool> = gt_perm.iter().map(|&k| gt_ignore[k]).collect();
1780 let dt_ids_sorted: Vec<i64> = dt_perm.iter().map(|&k| buf.dt_ids[k]).collect();
1781 let gt_ids_sorted: Vec<i64> = gt_perm.iter().map(|&k| buf.gt_ids[k]).collect();
1782
1783 let mut dt_matched = Array2::<bool>::default((n_t, n_d));
1784 let mut dt_matches_id = Array2::<i64>::zeros((n_t, n_d));
1785 let mut gt_matches_id = Array2::<i64>::zeros((n_t, n_g));
1786 for d in 0..n_d {
1793 let in_range = area.contains(buf.dt_areas[dt_perm[d]]);
1794 for t in 0..n_t {
1795 let m = dt_matches_pos[(t, d)];
1796 let matched = m >= 0;
1797 dt_matched[(t, d)] = matched;
1798 if matched {
1799 dt_matches_id[(t, d)] = gt_ids_sorted[m as usize];
1800 }
1801 if !matched && (!in_range || buf.not_exhaustive) {
1806 dt_ignore[(t, d)] = true;
1807 }
1808 }
1809 }
1810 for t in 0..n_t {
1811 for g in 0..n_g {
1812 let p = gt_matches_pos[(t, g)];
1813 if p >= 0 {
1814 gt_matches_id[(t, g)] = dt_ids_sorted[p as usize];
1815 }
1816 }
1817 }
1818
1819 let cell = PerImageEval {
1820 dt_scores: dt_scores_sorted,
1821 dt_matched,
1822 dt_ignore,
1823 gt_ignore: gt_ignore_sorted,
1824 };
1825 let meta = EvalImageMeta {
1826 image_id: buf.image_id,
1827 category_id: buf.category_id,
1828 area_rng: [area.lo, area.hi],
1829 max_det: buf.max_det,
1830 dt_ids: dt_ids_sorted,
1831 gt_ids: gt_ids_sorted,
1832 dt_matches: dt_matches_id,
1833 gt_matches: gt_matches_id,
1834 };
1835 Ok((cell, meta))
1836}
1837
1838#[cfg(test)]
1839mod tests {
1840 use super::*;
1841 use crate::accumulate::{accumulate, AccumulateParams};
1842 use crate::dataset::{AnnId, Bbox, CategoryMeta, CocoAnnotation, DetectionInput, ImageMeta};
1843 use crate::parity::{iou_thresholds, recall_thresholds};
1844 use crate::summarize::summarize_detection;
1845
1846 fn img(id: i64, w: u32, h: u32) -> ImageMeta {
1847 ImageMeta {
1848 id: ImageId(id),
1849 width: w,
1850 height: h,
1851 file_name: None,
1852 }
1853 }
1854
1855 fn cat(id: i64, name: &str) -> CategoryMeta {
1856 CategoryMeta {
1857 id: CategoryId(id),
1858 name: name.into(),
1859 supercategory: None,
1860 }
1861 }
1862
1863 fn ann(id: i64, image: i64, cat: i64, bbox: (f64, f64, f64, f64)) -> CocoAnnotation {
1864 CocoAnnotation {
1865 id: AnnId(id),
1866 image_id: ImageId(image),
1867 category_id: CategoryId(cat),
1868 area: bbox.2 * bbox.3,
1869 is_crowd: false,
1870 ignore_flag: None,
1871 bbox: Bbox {
1872 x: bbox.0,
1873 y: bbox.1,
1874 w: bbox.2,
1875 h: bbox.3,
1876 },
1877 segmentation: None,
1878 keypoints: None,
1879 num_keypoints: None,
1880 }
1881 }
1882
1883 fn dt_input(image: i64, cat: i64, score: f64, bbox: (f64, f64, f64, f64)) -> DetectionInput {
1884 DetectionInput {
1885 id: None,
1886 image_id: ImageId(image),
1887 category_id: CategoryId(cat),
1888 score,
1889 bbox: Bbox {
1890 x: bbox.0,
1891 y: bbox.1,
1892 w: bbox.2,
1893 h: bbox.3,
1894 },
1895 segmentation: None,
1896 keypoints: None,
1897 num_keypoints: None,
1898 }
1899 }
1900
1901 fn perfect_match_grid() -> EvalGrid {
1902 let images = vec![img(1, 100, 100)];
1903 let cats = vec![cat(1, "thing")];
1904 let anns = vec![
1905 ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0)),
1906 ann(2, 1, 1, (50.0, 50.0, 10.0, 10.0)),
1907 ];
1908 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
1909 let dts = CocoDetections::from_inputs(vec![
1910 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
1911 dt_input(1, 1, 0.8, (50.0, 50.0, 10.0, 10.0)),
1912 ])
1913 .unwrap();
1914 let area = AreaRange::coco_default();
1915 let params = EvaluateParams {
1916 iou_thresholds: iou_thresholds(),
1917 area_ranges: &area,
1918 max_dets_per_image: 100,
1919 use_cats: true,
1920 retain_iou: false,
1921 };
1922 evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap()
1923 }
1924
1925 #[test]
1926 fn d4_coco_default_area_ranges_pin_literal_values() {
1927 let ranges = AreaRange::coco_default();
1934 assert_eq!(ranges.len(), 4);
1935 assert_eq!(
1936 (ranges[0].lo, ranges[0].hi),
1937 (0.0, 1e10),
1938 "all bucket bounds"
1939 );
1940 assert_eq!(
1941 (ranges[1].lo, ranges[1].hi),
1942 (0.0, 1024.0),
1943 "small bucket bounds"
1944 );
1945 assert_eq!(
1946 (ranges[2].lo, ranges[2].hi),
1947 (1024.0, 9216.0),
1948 "medium bucket bounds"
1949 );
1950 assert_eq!(
1951 (ranges[3].lo, ranges[3].hi),
1952 (9216.0, 1e10),
1953 "large bucket bounds"
1954 );
1955
1956 use crate::summarize::AreaRng;
1960 assert_eq!(ranges[0].index, AreaRng::ALL.index);
1961 assert_eq!(AreaRng::ALL.label.as_ref(), "all");
1962 assert_eq!(ranges[1].index, AreaRng::SMALL.index);
1963 assert_eq!(AreaRng::SMALL.label.as_ref(), "small");
1964 assert_eq!(ranges[2].index, AreaRng::MEDIUM.index);
1965 assert_eq!(AreaRng::MEDIUM.label.as_ref(), "medium");
1966 assert_eq!(ranges[3].index, AreaRng::LARGE.index);
1967 assert_eq!(AreaRng::LARGE.label.as_ref(), "large");
1968
1969 let pyco_unbounded: f64 = 1e5_f64.powi(2);
1973 assert_eq!(pyco_unbounded.to_bits(), 1e10_f64.to_bits());
1974 assert_eq!(ranges[0].hi.to_bits(), 1e10_f64.to_bits());
1975 assert_eq!(ranges[3].hi.to_bits(), 1e10_f64.to_bits());
1976 }
1977
1978 #[test]
1979 fn perfect_match_produces_one_cell_per_area_range() {
1980 let grid = perfect_match_grid();
1981 assert_eq!(grid.n_categories, 1);
1982 assert_eq!(grid.n_area_ranges, 4);
1983 assert_eq!(grid.n_images, 1);
1984 let cells: Vec<_> = grid.eval_imgs.iter().filter(|c| c.is_some()).collect();
1986 assert_eq!(cells.len(), 4);
1987 let all_cell = grid.cell(0, 0, 0).unwrap();
1989 assert_eq!(all_cell.dt_scores.len(), 2);
1990 assert!(all_cell.dt_matched.iter().all(|&m| m));
1991 assert!(all_cell.dt_ignore.iter().all(|&ig| !ig));
1992 }
1993
1994 #[test]
1995 fn perfect_match_summarizes_to_one() {
1996 let grid = perfect_match_grid();
1997 let max_dets = vec![1usize, 10, 100];
1998 let acc = accumulate(
1999 &grid.eval_imgs,
2000 AccumulateParams {
2001 iou_thresholds: iou_thresholds(),
2002 recall_thresholds: recall_thresholds(),
2003 max_dets: &max_dets,
2004 n_categories: grid.n_categories,
2005 n_area_ranges: grid.n_area_ranges,
2006 n_images: grid.n_images,
2007 },
2008 ParityMode::Strict,
2009 )
2010 .unwrap();
2011 let summary = summarize_detection(&acc, iou_thresholds(), &max_dets).unwrap();
2012 let stats = summary.stats();
2013 assert!((stats[0] - 1.0).abs() < 1e-12, "AP={}", stats[0]);
2017 assert!((stats[3] - 1.0).abs() < 1e-12, "AP_S={}", stats[3]);
2018 assert_eq!(stats[4], -1.0, "AP_M should be -1 with no medium GTs");
2019 assert_eq!(stats[5], -1.0, "AP_L should be -1 with no large GTs");
2020 assert!((stats[8] - 1.0).abs() < 1e-12, "AR@100={}", stats[8]);
2021 }
2022
2023 #[test]
2024 fn b7_unmatched_dt_outside_area_range_is_ignored() {
2025 let images = vec![img(1, 300, 300)];
2029 let cats = vec![cat(1, "thing")];
2030 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 200.0, 200.0))];
2031 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2032 let dts =
2033 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.5, (200.0, 200.0, 50.0, 50.0))])
2034 .unwrap();
2035 let area = AreaRange::coco_default();
2036 let params = EvaluateParams {
2037 iou_thresholds: iou_thresholds(),
2038 area_ranges: &area,
2039 max_dets_per_image: 100,
2040 use_cats: true,
2041 retain_iou: false,
2042 };
2043 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2044 let small = grid.cell(0, 1, 0).unwrap();
2045 assert_eq!(small.gt_ignore, vec![true]);
2047 assert!(small.dt_ignore.iter().all(|&ig| ig));
2049 assert!(small.dt_matched.iter().all(|&m| !m));
2050 }
2051
2052 #[test]
2053 fn d6_boundary_area_lands_in_both_buckets() {
2054 let images = vec![img(1, 100, 100)];
2058 let cats = vec![cat(1, "thing")];
2059 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 32.0, 32.0))];
2061 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2062 let dts =
2063 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.5, (0.0, 0.0, 32.0, 32.0))]).unwrap();
2064 let area = AreaRange::coco_default();
2065 let params = EvaluateParams {
2066 iou_thresholds: iou_thresholds(),
2067 area_ranges: &area,
2068 max_dets_per_image: 100,
2069 use_cats: true,
2070 retain_iou: false,
2071 };
2072 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2073 let small = grid.cell(0, 1, 0).unwrap();
2075 assert_eq!(small.gt_ignore, vec![false]);
2076 let medium = grid.cell(0, 2, 0).unwrap();
2078 assert_eq!(medium.gt_ignore, vec![false]);
2079 let all = grid.cell(0, 0, 0).unwrap();
2081 assert_eq!(all.gt_ignore, vec![false]);
2082 let large = grid.cell(0, 3, 0).unwrap();
2084 assert_eq!(large.gt_ignore, vec![true]);
2085 }
2086
2087 #[test]
2088 fn l4_use_cats_false_collapses_categories() {
2089 let images = vec![img(1, 100, 100)];
2090 let cats = vec![cat(1, "a"), cat(2, "b")];
2091 let anns = vec![
2092 ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0)),
2093 ann(2, 1, 2, (50.0, 50.0, 10.0, 10.0)),
2094 ];
2095 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2096 let dts = CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (50.0, 50.0, 10.0, 10.0))])
2099 .unwrap();
2100 let area = AreaRange::coco_default();
2101 let params = EvaluateParams {
2102 iou_thresholds: iou_thresholds(),
2103 area_ranges: &area,
2104 max_dets_per_image: 100,
2105 use_cats: false,
2106 retain_iou: false,
2107 };
2108 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2109 assert_eq!(grid.n_categories, 1);
2110 let all = grid.cell(0, 0, 0).unwrap();
2111 assert_eq!(all.gt_ignore.len(), 2);
2113 assert_eq!(all.dt_scores.len(), 1);
2114 assert!(all.dt_matched.iter().all(|&m| m));
2115 }
2116
2117 #[test]
2118 fn max_dets_per_image_caps_top_n_by_score() {
2119 let images = vec![img(1, 100, 100)];
2120 let cats = vec![cat(1, "thing")];
2121 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
2122 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2123 let dts = CocoDetections::from_inputs(vec![
2124 dt_input(1, 1, 0.1, (50.0, 50.0, 5.0, 5.0)),
2125 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
2126 dt_input(1, 1, 0.5, (50.0, 50.0, 5.0, 5.0)),
2127 ])
2128 .unwrap();
2129 let area = AreaRange::coco_default();
2130 let params = EvaluateParams {
2131 iou_thresholds: iou_thresholds(),
2132 area_ranges: &area,
2133 max_dets_per_image: 2,
2134 use_cats: true,
2135 retain_iou: false,
2136 };
2137 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2138 let all = grid.cell(0, 0, 0).unwrap();
2139 assert_eq!(all.dt_scores.len(), 2);
2141 assert_eq!(all.dt_scores[0], 0.9);
2142 assert_eq!(all.dt_scores[1], 0.5);
2143 }
2144
2145 #[test]
2146 fn d1_parity_mode_propagates_to_base_ignore() {
2147 const ANN_JSON: &str = r#"{
2153 "images": [{"id": 1, "width": 100, "height": 100}],
2154 "annotations": [
2155 {"id": 1, "image_id": 1, "category_id": 1,
2156 "bbox": [0, 0, 10, 10], "area": 100,
2157 "iscrowd": 0, "ignore": 1}
2158 ],
2159 "categories": [{"id": 1, "name": "thing"}]
2160 }"#;
2161 let gt = CocoDataset::from_json_bytes(ANN_JSON.as_bytes()).unwrap();
2162 let dts =
2163 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0))]).unwrap();
2164 let area = AreaRange::coco_default();
2165 let params = EvaluateParams {
2166 iou_thresholds: iou_thresholds(),
2167 area_ranges: &area,
2168 max_dets_per_image: 100,
2169 use_cats: true,
2170 retain_iou: false,
2171 };
2172
2173 let strict = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2174 let strict_all = strict.cell(0, 0, 0).unwrap();
2175 assert_eq!(strict_all.gt_ignore, vec![false]);
2176 assert!(strict_all.dt_ignore.iter().all(|&ig| !ig));
2177
2178 let corrected = evaluate_bbox(>, &dts, params, ParityMode::Corrected).unwrap();
2179 let corrected_all = corrected.cell(0, 0, 0).unwrap();
2180 assert_eq!(corrected_all.gt_ignore, vec![true]);
2181 assert!(corrected_all.dt_ignore.iter().all(|&ig| ig));
2183 }
2184
2185 #[test]
2186 fn cell_meta_carries_pycocotools_shape() {
2187 let grid = perfect_match_grid();
2188 let meta = grid.cell_meta(0, 0, 0).unwrap();
2190 assert_eq!(meta.image_id, 1);
2191 assert_eq!(meta.category_id, 1);
2192 assert_eq!(meta.area_rng, [0.0, AREA_UNBOUNDED]);
2193 assert_eq!(meta.max_det, 100);
2194 assert_eq!(meta.dt_ids, vec![1, 2]);
2196 assert_eq!(meta.gt_ids, vec![1, 2]);
2198 let n_t = iou_thresholds().len();
2199 assert_eq!(meta.dt_matches.shape(), &[n_t, 2]);
2200 assert_eq!(meta.gt_matches.shape(), &[n_t, 2]);
2201 for t in 0..n_t {
2204 assert_eq!(meta.dt_matches[(t, 0)], 1, "dt[0] -> gt[1] at t={t}");
2205 assert_eq!(meta.dt_matches[(t, 1)], 2, "dt[1] -> gt[2] at t={t}");
2206 assert_eq!(meta.gt_matches[(t, 0)], 1, "gt[1] -> dt[1] at t={t}");
2207 assert_eq!(meta.gt_matches[(t, 1)], 2, "gt[2] -> dt[2] at t={t}");
2208 }
2209 }
2210
2211 #[test]
2212 fn cell_meta_unmatched_dt_uses_zero_sentinel() {
2213 let images = vec![img(1, 100, 100)];
2215 let cats = vec![cat(1, "thing")];
2216 let anns = vec![ann(7, 1, 1, (0.0, 0.0, 10.0, 10.0))];
2217 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2218 let dts = CocoDetections::from_inputs(vec![dt_input(1, 1, 0.5, (50.0, 50.0, 10.0, 10.0))])
2219 .unwrap();
2220 let area = AreaRange::coco_default();
2221 let params = EvaluateParams {
2222 iou_thresholds: iou_thresholds(),
2223 area_ranges: &area,
2224 max_dets_per_image: 100,
2225 use_cats: true,
2226 retain_iou: false,
2227 };
2228 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2229 let meta = grid.cell_meta(0, 0, 0).unwrap();
2230 assert_eq!(meta.gt_ids, vec![7]);
2231 assert_eq!(meta.dt_ids.len(), 1);
2233 assert!(meta.dt_matches.iter().all(|&x| x == 0));
2234 assert!(meta.gt_matches.iter().all(|&x| x == 0));
2235 }
2236
2237 #[test]
2238 fn cell_meta_use_cats_false_emits_sentinel_category() {
2239 let images = vec![img(1, 100, 100)];
2240 let cats = vec![cat(1, "a"), cat(2, "b")];
2241 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
2242 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2243 let dts =
2244 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0))]).unwrap();
2245 let area = AreaRange::coco_default();
2246 let params = EvaluateParams {
2247 iou_thresholds: iou_thresholds(),
2248 area_ranges: &area,
2249 max_dets_per_image: 100,
2250 use_cats: false,
2251 retain_iou: false,
2252 };
2253 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2254 let meta = grid.cell_meta(0, 0, 0).unwrap();
2255 assert_eq!(meta.category_id, COLLAPSED_CATEGORY_SENTINEL);
2256 }
2257
2258 #[test]
2259 fn missing_dt_image_yields_none_cells() {
2260 let images = vec![img(1, 100, 100), img(2, 100, 100)];
2263 let cats = vec![cat(1, "thing")];
2264 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
2265 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2266 let dts = CocoDetections::from_inputs(vec![]).unwrap();
2267 let area = AreaRange::coco_default();
2268 let params = EvaluateParams {
2269 iou_thresholds: iou_thresholds(),
2270 area_ranges: &area,
2271 max_dets_per_image: 100,
2272 use_cats: true,
2273 retain_iou: false,
2274 };
2275 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
2276 for a in 0..4 {
2277 assert!(grid.cell(0, a, 0).is_some(), "image 1 area {a}");
2278 assert!(grid.cell(0, a, 1).is_none(), "image 2 area {a}");
2279 }
2280 }
2281
2282 fn square_polygon(x: f64, y: f64, side: f64) -> Segmentation {
2283 Segmentation::Polygons(vec![vec![
2284 x,
2285 y,
2286 x + side,
2287 y,
2288 x + side,
2289 y + side,
2290 x,
2291 y + side,
2292 ]])
2293 }
2294
2295 fn ann_with_segm(
2296 id: i64,
2297 image: i64,
2298 cat: i64,
2299 bbox: (f64, f64, f64, f64),
2300 segm: Segmentation,
2301 ) -> CocoAnnotation {
2302 CocoAnnotation {
2303 id: AnnId(id),
2304 image_id: ImageId(image),
2305 category_id: CategoryId(cat),
2306 area: bbox.2 * bbox.3,
2307 is_crowd: false,
2308 ignore_flag: None,
2309 bbox: Bbox {
2310 x: bbox.0,
2311 y: bbox.1,
2312 w: bbox.2,
2313 h: bbox.3,
2314 },
2315 segmentation: Some(segm),
2316 keypoints: None,
2317 num_keypoints: None,
2318 }
2319 }
2320
2321 fn dt_input_with_segm(
2322 image: i64,
2323 cat: i64,
2324 score: f64,
2325 bbox: (f64, f64, f64, f64),
2326 segm: Segmentation,
2327 ) -> DetectionInput {
2328 DetectionInput {
2329 id: None,
2330 image_id: ImageId(image),
2331 category_id: CategoryId(cat),
2332 score,
2333 bbox: Bbox {
2334 x: bbox.0,
2335 y: bbox.1,
2336 w: bbox.2,
2337 h: bbox.3,
2338 },
2339 segmentation: Some(segm),
2340 keypoints: None,
2341 num_keypoints: None,
2342 }
2343 }
2344
2345 #[test]
2346 fn segm_perfect_overlap_summarizes_to_one() {
2347 let images = vec![img(1, 100, 100)];
2348 let cats = vec![cat(1, "thing")];
2349 let anns = vec![ann_with_segm(
2350 1,
2351 1,
2352 1,
2353 (10.0, 10.0, 20.0, 20.0),
2354 square_polygon(10.0, 10.0, 20.0),
2355 )];
2356 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2357 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
2358 1,
2359 1,
2360 0.9,
2361 (10.0, 10.0, 20.0, 20.0),
2362 square_polygon(10.0, 10.0, 20.0),
2363 )])
2364 .unwrap();
2365 let area = AreaRange::coco_default();
2366 let params = EvaluateParams {
2367 iou_thresholds: iou_thresholds(),
2368 area_ranges: &area,
2369 max_dets_per_image: 100,
2370 use_cats: true,
2371 retain_iou: false,
2372 };
2373 let grid = evaluate_segm(>, &dts, params, ParityMode::Strict).unwrap();
2374 let max_dets = vec![1usize, 10, 100];
2375 let acc = accumulate(
2376 &grid.eval_imgs,
2377 AccumulateParams {
2378 iou_thresholds: iou_thresholds(),
2379 recall_thresholds: recall_thresholds(),
2380 max_dets: &max_dets,
2381 n_categories: grid.n_categories,
2382 n_area_ranges: grid.n_area_ranges,
2383 n_images: grid.n_images,
2384 },
2385 ParityMode::Strict,
2386 )
2387 .unwrap();
2388 let summary = summarize_detection(&acc, iou_thresholds(), &max_dets).unwrap();
2389 let stats = summary.stats();
2390 assert!((stats[0] - 1.0).abs() < 1e-12, "AP={}", stats[0]);
2391 }
2392
2393 #[test]
2394 fn segm_disjoint_masks_summarize_to_zero() {
2395 let images = vec![img(1, 100, 100)];
2396 let cats = vec![cat(1, "thing")];
2397 let anns = vec![ann_with_segm(
2398 1,
2399 1,
2400 1,
2401 (0.0, 0.0, 10.0, 10.0),
2402 square_polygon(0.0, 0.0, 10.0),
2403 )];
2404 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2405 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
2406 1,
2407 1,
2408 0.9,
2409 (50.0, 50.0, 10.0, 10.0),
2410 square_polygon(50.0, 50.0, 10.0),
2411 )])
2412 .unwrap();
2413 let area = AreaRange::coco_default();
2414 let params = EvaluateParams {
2415 iou_thresholds: iou_thresholds(),
2416 area_ranges: &area,
2417 max_dets_per_image: 100,
2418 use_cats: true,
2419 retain_iou: false,
2420 };
2421 let grid = evaluate_segm(>, &dts, params, ParityMode::Strict).unwrap();
2422 let all = grid.cell(0, 0, 0).unwrap();
2423 assert!(all.dt_matched.iter().all(|&m| !m));
2425 }
2426
2427 #[test]
2428 fn segm_missing_gt_segmentation_surfaces_typed_error() {
2429 let images = vec![img(1, 100, 100)];
2432 let cats = vec![cat(1, "thing")];
2433 let anns = vec![ann(7, 1, 1, (0.0, 0.0, 10.0, 10.0))];
2434 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2435 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
2436 1,
2437 1,
2438 0.9,
2439 (0.0, 0.0, 10.0, 10.0),
2440 square_polygon(0.0, 0.0, 10.0),
2441 )])
2442 .unwrap();
2443 let area = AreaRange::coco_default();
2444 let params = EvaluateParams {
2445 iou_thresholds: iou_thresholds(),
2446 area_ranges: &area,
2447 max_dets_per_image: 100,
2448 use_cats: true,
2449 retain_iou: false,
2450 };
2451 let err = evaluate_segm(>, &dts, params, ParityMode::Strict).unwrap_err();
2452 match err {
2453 EvalError::InvalidAnnotation { detail } => {
2454 assert!(detail.contains("GT id=7"), "msg: {detail}");
2455 }
2456 other => panic!("expected InvalidAnnotation, got {other:?}"),
2457 }
2458 }
2459
2460 #[test]
2461 fn j2_bbox_only_dt_under_segm_iou_type_raises_in_corrected_mode() {
2462 let images = vec![img(1, 100, 100)];
2466 let cats = vec![cat(1, "thing")];
2467 let anns = vec![ann_with_segm(
2468 1,
2469 1,
2470 1,
2471 (0.0, 0.0, 10.0, 10.0),
2472 square_polygon(0.0, 0.0, 10.0),
2473 )];
2474 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2475 let dts =
2477 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0))]).unwrap();
2478 let area = AreaRange::coco_default();
2479 let params = EvaluateParams {
2480 iou_thresholds: iou_thresholds(),
2481 area_ranges: &area,
2482 max_dets_per_image: 100,
2483 use_cats: true,
2484 retain_iou: false,
2485 };
2486 let err = evaluate_segm(>, &dts, params, ParityMode::Corrected).unwrap_err();
2487 match err {
2488 EvalError::InvalidAnnotation { detail } => {
2489 assert!(detail.contains("DT"), "expected DT in msg: {detail}");
2490 assert!(detail.contains("J2"), "expected J2 cite in msg: {detail}");
2491 }
2492 other => panic!("expected InvalidAnnotation, got {other:?}"),
2493 }
2494 }
2495
2496 #[test]
2497 fn j2_bbox_only_dt_under_segm_iou_type_synthesizes_in_strict_mode() {
2498 let images = vec![img(1, 100, 100)];
2504 let cats = vec![cat(1, "thing")];
2505 let anns = vec![ann_with_segm(
2507 1,
2508 1,
2509 1,
2510 (0.0, 0.0, 10.0, 10.0),
2511 square_polygon(0.0, 0.0, 10.0),
2512 )];
2513 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2514 let dts =
2516 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0))]).unwrap();
2517 let area = AreaRange::coco_default();
2518 let params = EvaluateParams {
2519 iou_thresholds: iou_thresholds(),
2520 area_ranges: &area,
2521 max_dets_per_image: 100,
2522 use_cats: true,
2523 retain_iou: false,
2524 };
2525 let grid = evaluate_segm(>, &dts, params, ParityMode::Strict).unwrap();
2526 let all = grid.cell(0, 0, 0).unwrap();
2527 assert!(all.dt_matched.iter().all(|&m| m), "expected matches");
2530 }
2531
2532 #[test]
2533 fn j6_heterogeneous_dt_list_first_with_segm_second_without_raises_in_corrected_mode() {
2534 let images = vec![img(1, 100, 100)];
2541 let cats = vec![cat(1, "thing")];
2542 let anns = vec![ann_with_segm(
2543 1,
2544 1,
2545 1,
2546 (0.0, 0.0, 10.0, 10.0),
2547 square_polygon(0.0, 0.0, 10.0),
2548 )];
2549 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2550 let dts = CocoDetections::from_inputs(vec![
2555 dt_input_with_segm(
2556 1,
2557 1,
2558 0.9,
2559 (0.0, 0.0, 10.0, 10.0),
2560 square_polygon(0.0, 0.0, 10.0),
2561 ),
2562 dt_input(1, 1, 0.8, (50.0, 50.0, 10.0, 10.0)),
2563 ])
2564 .unwrap();
2565 let area = AreaRange::coco_default();
2566 let params = EvaluateParams {
2567 iou_thresholds: iou_thresholds(),
2568 area_ranges: &area,
2569 max_dets_per_image: 100,
2570 use_cats: true,
2571 retain_iou: false,
2572 };
2573 let err = evaluate_segm(>, &dts, params, ParityMode::Corrected).unwrap_err();
2574 assert!(matches!(err, EvalError::InvalidAnnotation { .. }));
2575 }
2576
2577 #[test]
2578 fn j6_heterogeneous_dt_list_first_without_segm_second_with_raises_in_corrected_mode() {
2579 let images = vec![img(1, 100, 100)];
2586 let cats = vec![cat(1, "thing")];
2587 let anns = vec![ann_with_segm(
2588 1,
2589 1,
2590 1,
2591 (0.0, 0.0, 10.0, 10.0),
2592 square_polygon(0.0, 0.0, 10.0),
2593 )];
2594 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2595 let dts = CocoDetections::from_inputs(vec![
2596 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
2597 dt_input_with_segm(
2598 1,
2599 1,
2600 0.8,
2601 (50.0, 50.0, 10.0, 10.0),
2602 square_polygon(50.0, 50.0, 10.0),
2603 ),
2604 ])
2605 .unwrap();
2606 let area = AreaRange::coco_default();
2607 let params = EvaluateParams {
2608 iou_thresholds: iou_thresholds(),
2609 area_ranges: &area,
2610 max_dets_per_image: 100,
2611 use_cats: true,
2612 retain_iou: false,
2613 };
2614 let err = evaluate_segm(>, &dts, params, ParityMode::Corrected).unwrap_err();
2615 assert!(matches!(err, EvalError::InvalidAnnotation { .. }));
2616 }
2617
2618 #[test]
2619 fn j6_heterogeneous_dt_list_in_strict_mode_synthesizes_per_entry() {
2620 let images = vec![img(1, 100, 100)];
2626 let cats = vec![cat(1, "thing")];
2627 let anns = vec![
2628 ann_with_segm(
2629 1,
2630 1,
2631 1,
2632 (0.0, 0.0, 10.0, 10.0),
2633 square_polygon(0.0, 0.0, 10.0),
2634 ),
2635 ann_with_segm(
2636 2,
2637 1,
2638 1,
2639 (50.0, 50.0, 10.0, 10.0),
2640 square_polygon(50.0, 50.0, 10.0),
2641 ),
2642 ];
2643 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2644 let dts = CocoDetections::from_inputs(vec![
2646 dt_input_with_segm(
2647 1,
2648 1,
2649 0.9,
2650 (0.0, 0.0, 10.0, 10.0),
2651 square_polygon(0.0, 0.0, 10.0),
2652 ),
2653 dt_input(1, 1, 0.8, (50.0, 50.0, 10.0, 10.0)),
2654 ])
2655 .unwrap();
2656 let area = AreaRange::coco_default();
2657 let params = EvaluateParams {
2658 iou_thresholds: iou_thresholds(),
2659 area_ranges: &area,
2660 max_dets_per_image: 100,
2661 use_cats: true,
2662 retain_iou: false,
2663 };
2664 let grid = evaluate_segm(>, &dts, params, ParityMode::Strict).unwrap();
2665 let all = grid.cell(0, 0, 0).unwrap();
2666 assert_eq!(all.dt_matched.shape(), &[iou_thresholds().len(), 2]);
2669 assert!(all.dt_matched.iter().all(|&m| m));
2670 }
2671
2672 #[test]
2673 fn boundary_perfect_overlap_summarizes_to_one() {
2674 let images = vec![img(1, 100, 100)];
2677 let cats = vec![cat(1, "thing")];
2678 let anns = vec![ann_with_segm(
2679 1,
2680 1,
2681 1,
2682 (10.0, 10.0, 20.0, 20.0),
2683 square_polygon(10.0, 10.0, 20.0),
2684 )];
2685 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2686 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
2687 1,
2688 1,
2689 0.9,
2690 (10.0, 10.0, 20.0, 20.0),
2691 square_polygon(10.0, 10.0, 20.0),
2692 )])
2693 .unwrap();
2694 let area = AreaRange::coco_default();
2695 let params = EvaluateParams {
2696 iou_thresholds: iou_thresholds(),
2697 area_ranges: &area,
2698 max_dets_per_image: 100,
2699 use_cats: true,
2700 retain_iou: false,
2701 };
2702 let grid = evaluate_boundary(>, &dts, params, ParityMode::Strict, 0.02).unwrap();
2703 let max_dets = vec![1usize, 10, 100];
2704 let acc = accumulate(
2705 &grid.eval_imgs,
2706 AccumulateParams {
2707 iou_thresholds: iou_thresholds(),
2708 recall_thresholds: recall_thresholds(),
2709 max_dets: &max_dets,
2710 n_categories: grid.n_categories,
2711 n_area_ranges: grid.n_area_ranges,
2712 n_images: grid.n_images,
2713 },
2714 ParityMode::Strict,
2715 )
2716 .unwrap();
2717 let summary = summarize_detection(&acc, iou_thresholds(), &max_dets).unwrap();
2718 let stats = summary.stats();
2719 assert!((stats[0] - 1.0).abs() < 1e-12, "AP={}", stats[0]);
2720 }
2721
2722 #[test]
2723 fn boundary_disjoint_masks_summarize_to_zero() {
2724 let images = vec![img(1, 100, 100)];
2727 let cats = vec![cat(1, "thing")];
2728 let anns = vec![ann_with_segm(
2729 1,
2730 1,
2731 1,
2732 (0.0, 0.0, 10.0, 10.0),
2733 square_polygon(0.0, 0.0, 10.0),
2734 )];
2735 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2736 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
2737 1,
2738 1,
2739 0.9,
2740 (50.0, 50.0, 10.0, 10.0),
2741 square_polygon(50.0, 50.0, 10.0),
2742 )])
2743 .unwrap();
2744 let area = AreaRange::coco_default();
2745 let params = EvaluateParams {
2746 iou_thresholds: iou_thresholds(),
2747 area_ranges: &area,
2748 max_dets_per_image: 100,
2749 use_cats: true,
2750 retain_iou: false,
2751 };
2752 let grid = evaluate_boundary(>, &dts, params, ParityMode::Strict, 0.02).unwrap();
2753 let all = grid.cell(0, 0, 0).unwrap();
2754 assert!(all.dt_matched.iter().all(|&m| !m));
2755 }
2756
2757 fn boundary_cache_fixture() -> (
2762 CocoDataset,
2763 CocoDetections,
2764 CocoDetections,
2765 OwnedEvaluateParams,
2766 ) {
2767 let images = vec![img(1, 100, 100), img(2, 100, 100)];
2768 let cats = vec![cat(1, "thing"), cat(2, "other")];
2769 let anns = vec![
2770 ann_with_segm(
2771 10,
2772 1,
2773 1,
2774 (10.0, 10.0, 20.0, 20.0),
2775 square_polygon(10.0, 10.0, 20.0),
2776 ),
2777 ann_with_segm(
2778 11,
2779 1,
2780 2,
2781 (50.0, 50.0, 15.0, 15.0),
2782 square_polygon(50.0, 50.0, 15.0),
2783 ),
2784 ann_with_segm(
2785 12,
2786 2,
2787 1,
2788 (5.0, 5.0, 25.0, 25.0),
2789 square_polygon(5.0, 5.0, 25.0),
2790 ),
2791 ];
2792 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
2793 let dts_a = CocoDetections::from_inputs(vec![
2794 dt_input_with_segm(
2795 1,
2796 1,
2797 0.9,
2798 (10.0, 10.0, 20.0, 20.0),
2799 square_polygon(10.0, 10.0, 20.0),
2800 ),
2801 dt_input_with_segm(
2802 2,
2803 1,
2804 0.8,
2805 (5.0, 5.0, 25.0, 25.0),
2806 square_polygon(5.0, 5.0, 25.0),
2807 ),
2808 ])
2809 .unwrap();
2810 let dts_b = CocoDetections::from_inputs(vec![
2813 dt_input_with_segm(
2814 1,
2815 1,
2816 0.7,
2817 (12.0, 12.0, 20.0, 20.0),
2818 square_polygon(12.0, 12.0, 20.0),
2819 ),
2820 dt_input_with_segm(
2821 2,
2822 1,
2823 0.6,
2824 (8.0, 8.0, 25.0, 25.0),
2825 square_polygon(8.0, 8.0, 25.0),
2826 ),
2827 ])
2828 .unwrap();
2829 let params = OwnedEvaluateParams {
2830 iou_thresholds: iou_thresholds().to_vec(),
2831 area_ranges: AreaRange::coco_default().to_vec(),
2832 max_dets_per_image: 100,
2833 use_cats: true,
2834 retain_iou: false,
2835 };
2836 (gt, dts_a, dts_b, params)
2837 }
2838
2839 fn boundary_grid_cells(grid: &EvalGrid) -> Vec<f64> {
2840 grid.eval_imgs
2841 .iter()
2842 .filter_map(|c| c.as_ref())
2843 .flat_map(|c| c.dt_scores.iter().copied())
2844 .collect()
2845 }
2846
2847 #[test]
2848 fn boundary_cached_matches_uncached_bit_exact() {
2849 let (gt, dts, _, params) = boundary_cache_fixture();
2853 let p = params.borrow();
2854 let baseline = evaluate_boundary(>, &dts, p, ParityMode::Strict, 0.02).unwrap();
2855 let cache = BoundaryGtCache::new();
2856 let cached_first =
2857 evaluate_boundary_cached(>, &dts, p, ParityMode::Strict, 0.02, &cache).unwrap();
2858 let cached_second =
2859 evaluate_boundary_cached(>, &dts, p, ParityMode::Strict, 0.02, &cache).unwrap();
2860
2861 let baseline_scores = boundary_grid_cells(&baseline);
2862 let first_scores = boundary_grid_cells(&cached_first);
2863 let second_scores = boundary_grid_cells(&cached_second);
2864 assert_eq!(baseline_scores.len(), first_scores.len());
2865 for (b, c) in baseline_scores.iter().zip(first_scores.iter()) {
2866 assert_eq!(b.to_bits(), c.to_bits());
2867 }
2868 for (b, c) in baseline_scores.iter().zip(second_scores.iter()) {
2869 assert_eq!(b.to_bits(), c.to_bits());
2870 }
2871 }
2872
2873 #[test]
2874 fn boundary_cache_populates_lazily_per_evaluated_cell() {
2875 let (gt, dts, _, params) = boundary_cache_fixture();
2883 let cache = BoundaryGtCache::new();
2884 assert!(cache.is_empty());
2885 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.02, &cache)
2886 .unwrap();
2887 assert_eq!(cache.len(), 2);
2888 }
2889
2890 #[test]
2891 fn boundary_cache_invalidates_on_ratio_change() {
2892 let (gt, dts, _, params) = boundary_cache_fixture();
2896 let cache = BoundaryGtCache::new();
2897 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.02, &cache)
2898 .unwrap();
2899 let after_first = cache.len();
2900 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.05, &cache)
2901 .unwrap();
2902 assert_eq!(cache.len(), after_first);
2906 let fresh =
2907 evaluate_boundary(>, &dts, params.borrow(), ParityMode::Strict, 0.05).unwrap();
2908 let cached =
2909 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.05, &cache)
2910 .unwrap();
2911 let fresh_scores = boundary_grid_cells(&fresh);
2912 let cached_scores = boundary_grid_cells(&cached);
2913 for (f, c) in fresh_scores.iter().zip(cached_scores.iter()) {
2914 assert_eq!(f.to_bits(), c.to_bits());
2915 }
2916 }
2917
2918 #[test]
2919 fn boundary_cache_clear_resets_state() {
2920 let (gt, dts, _, params) = boundary_cache_fixture();
2921 let cache = BoundaryGtCache::new();
2922 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.02, &cache)
2923 .unwrap();
2924 assert!(!cache.is_empty());
2925 cache.clear();
2926 assert!(cache.is_empty());
2927 let after =
2930 evaluate_boundary_cached(>, &dts, params.borrow(), ParityMode::Strict, 0.02, &cache)
2931 .unwrap();
2932 let baseline =
2933 evaluate_boundary(>, &dts, params.borrow(), ParityMode::Strict, 0.02).unwrap();
2934 let after_scores = boundary_grid_cells(&after);
2935 let baseline_scores = boundary_grid_cells(&baseline);
2936 for (a, b) in after_scores.iter().zip(baseline_scores.iter()) {
2937 assert_eq!(a.to_bits(), b.to_bits());
2938 }
2939 }
2940
2941 #[test]
2942 fn boundary_cache_survives_changing_dt() {
2943 let (gt, dts_a, dts_b, params) = boundary_cache_fixture();
2948 let cache = BoundaryGtCache::new();
2949 let cached_a = evaluate_boundary_cached(
2950 >,
2951 &dts_a,
2952 params.borrow(),
2953 ParityMode::Strict,
2954 0.02,
2955 &cache,
2956 )
2957 .unwrap();
2958 let len_after_a = cache.len();
2959 let cached_b = evaluate_boundary_cached(
2960 >,
2961 &dts_b,
2962 params.borrow(),
2963 ParityMode::Strict,
2964 0.02,
2965 &cache,
2966 )
2967 .unwrap();
2968 assert_eq!(cache.len(), len_after_a);
2969
2970 let baseline_a =
2971 evaluate_boundary(>, &dts_a, params.borrow(), ParityMode::Strict, 0.02).unwrap();
2972 let baseline_b =
2973 evaluate_boundary(>, &dts_b, params.borrow(), ParityMode::Strict, 0.02).unwrap();
2974 for (lhs, rhs) in boundary_grid_cells(&cached_a)
2975 .iter()
2976 .zip(boundary_grid_cells(&baseline_a).iter())
2977 {
2978 assert_eq!(lhs.to_bits(), rhs.to_bits());
2979 }
2980 for (lhs, rhs) in boundary_grid_cells(&cached_b)
2981 .iter()
2982 .zip(boundary_grid_cells(&baseline_b).iter())
2983 {
2984 assert_eq!(lhs.to_bits(), rhs.to_bits());
2985 }
2986 }
2987
2988 #[test]
2995 fn segm_cached_matches_uncached_bit_exact() {
2996 let (gt, dts, _, params) = boundary_cache_fixture();
2997 let p = params.borrow();
2998 let baseline = evaluate_segm(>, &dts, p, ParityMode::Strict).unwrap();
2999 let cache = SegmGtCache::new();
3000 let cached_first = evaluate_segm_cached(>, &dts, p, ParityMode::Strict, &cache).unwrap();
3001 let cached_second = evaluate_segm_cached(>, &dts, p, ParityMode::Strict, &cache).unwrap();
3002
3003 let baseline_scores = boundary_grid_cells(&baseline);
3004 let first_scores = boundary_grid_cells(&cached_first);
3005 let second_scores = boundary_grid_cells(&cached_second);
3006 assert_eq!(baseline_scores.len(), first_scores.len());
3007 for (b, c) in baseline_scores.iter().zip(first_scores.iter()) {
3008 assert_eq!(b.to_bits(), c.to_bits());
3009 }
3010 for (b, c) in baseline_scores.iter().zip(second_scores.iter()) {
3011 assert_eq!(b.to_bits(), c.to_bits());
3012 }
3013 }
3014
3015 #[test]
3016 fn segm_cache_populates_lazily_per_evaluated_cell() {
3017 let (gt, dts, _, params) = boundary_cache_fixture();
3023 let cache = SegmGtCache::new();
3024 assert!(cache.is_empty());
3025 evaluate_segm_cached(>, &dts, params.borrow(), ParityMode::Strict, &cache).unwrap();
3026 assert_eq!(cache.len(), 2);
3027 }
3028
3029 #[test]
3030 fn segm_cache_clear_resets_state() {
3031 let (gt, dts, _, params) = boundary_cache_fixture();
3032 let cache = SegmGtCache::new();
3033 evaluate_segm_cached(>, &dts, params.borrow(), ParityMode::Strict, &cache).unwrap();
3034 assert!(!cache.is_empty());
3035 cache.clear();
3036 assert!(cache.is_empty());
3037 let after =
3038 evaluate_segm_cached(>, &dts, params.borrow(), ParityMode::Strict, &cache).unwrap();
3039 let baseline = evaluate_segm(>, &dts, params.borrow(), ParityMode::Strict).unwrap();
3040 for (a, b) in boundary_grid_cells(&after)
3041 .iter()
3042 .zip(boundary_grid_cells(&baseline).iter())
3043 {
3044 assert_eq!(a.to_bits(), b.to_bits());
3045 }
3046 }
3047
3048 #[test]
3049 fn segm_cache_survives_changing_dt() {
3050 let (gt, dts_a, dts_b, params) = boundary_cache_fixture();
3055 let cache = SegmGtCache::new();
3056 let cached_a =
3057 evaluate_segm_cached(>, &dts_a, params.borrow(), ParityMode::Strict, &cache).unwrap();
3058 let len_after_a = cache.len();
3059 let cached_b =
3060 evaluate_segm_cached(>, &dts_b, params.borrow(), ParityMode::Strict, &cache).unwrap();
3061 assert_eq!(cache.len(), len_after_a);
3062
3063 let baseline_a = evaluate_segm(>, &dts_a, params.borrow(), ParityMode::Strict).unwrap();
3064 let baseline_b = evaluate_segm(>, &dts_b, params.borrow(), ParityMode::Strict).unwrap();
3065 for (lhs, rhs) in boundary_grid_cells(&cached_a)
3066 .iter()
3067 .zip(boundary_grid_cells(&baseline_a).iter())
3068 {
3069 assert_eq!(lhs.to_bits(), rhs.to_bits());
3070 }
3071 for (lhs, rhs) in boundary_grid_cells(&cached_b)
3072 .iter()
3073 .zip(boundary_grid_cells(&baseline_b).iter())
3074 {
3075 assert_eq!(lhs.to_bits(), rhs.to_bits());
3076 }
3077 }
3078
3079 fn const_kps_vec(x: f64, y: f64, v: u32, len: usize) -> Vec<f64> {
3087 let mut out = Vec::with_capacity(3 * len);
3088 for _ in 0..len {
3089 out.push(x);
3090 out.push(y);
3091 out.push(f64::from(v));
3092 }
3093 out
3094 }
3095
3096 fn ann_with_kps(
3097 id: i64,
3098 image: i64,
3099 cat: i64,
3100 bbox: (f64, f64, f64, f64),
3101 keypoints: Vec<f64>,
3102 num_keypoints: Option<u32>,
3103 ) -> CocoAnnotation {
3104 CocoAnnotation {
3105 id: AnnId(id),
3106 image_id: ImageId(image),
3107 category_id: CategoryId(cat),
3108 area: bbox.2 * bbox.3,
3109 is_crowd: false,
3110 ignore_flag: None,
3111 bbox: Bbox {
3112 x: bbox.0,
3113 y: bbox.1,
3114 w: bbox.2,
3115 h: bbox.3,
3116 },
3117 segmentation: None,
3118 keypoints: Some(keypoints),
3119 num_keypoints,
3120 }
3121 }
3122
3123 fn dt_input_with_kps(
3124 image: i64,
3125 cat: i64,
3126 score: f64,
3127 bbox: (f64, f64, f64, f64),
3128 keypoints: Vec<f64>,
3129 ) -> DetectionInput {
3130 DetectionInput {
3131 id: None,
3132 image_id: ImageId(image),
3133 category_id: CategoryId(cat),
3134 score,
3135 bbox: Bbox {
3136 x: bbox.0,
3137 y: bbox.1,
3138 w: bbox.2,
3139 h: bbox.3,
3140 },
3141 segmentation: None,
3142 keypoints: Some(keypoints),
3143 num_keypoints: None,
3144 }
3145 }
3146
3147 #[test]
3148 fn test_evaluate_keypoints_perfect_match() {
3149 let images = vec![img(1, 100, 100)];
3153 let cats = vec![cat(1, "person")];
3154 let kps = const_kps_vec(50.0, 50.0, 2, 17);
3155 let anns = vec![ann_with_kps(
3156 1,
3157 1,
3158 1,
3159 (40.0, 40.0, 20.0, 20.0),
3160 kps.clone(),
3161 None,
3162 )];
3163 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3164 let dts = CocoDetections::from_inputs(vec![dt_input_with_kps(
3165 1,
3166 1,
3167 0.9,
3168 (40.0, 40.0, 20.0, 20.0),
3169 kps,
3170 )])
3171 .unwrap();
3172 let area = AreaRange::coco_default();
3173 let params = EvaluateParams {
3174 iou_thresholds: iou_thresholds(),
3175 area_ranges: &area,
3176 max_dets_per_image: 100,
3177 use_cats: true,
3178 retain_iou: false,
3179 };
3180 let grid =
3181 evaluate_keypoints(>, &dts, params, ParityMode::Strict, HashMap::new()).unwrap();
3182 let cell = grid.cell(0, 0, 0).unwrap();
3183 assert_eq!(cell.gt_ignore, vec![false]);
3185 assert!(cell.dt_matched.iter().all(|&m| m));
3187 let meta = grid.cell_meta(0, 0, 0).unwrap();
3189 assert!(
3190 meta.gt_matches.iter().all(|&id| id > 0),
3191 "every threshold should match the DT id (>0)",
3192 );
3193 }
3194
3195 #[test]
3196 fn test_evaluate_keypoints_zero_overlap() {
3197 let images = vec![img(1, 2000, 2000)];
3201 let cats = vec![cat(1, "person")];
3202 let gt_kps = const_kps_vec(50.0, 50.0, 2, 17);
3203 let dt_kps = const_kps_vec(1500.0, 1500.0, 2, 17);
3204 let anns = vec![ann_with_kps(
3205 1,
3206 1,
3207 1,
3208 (40.0, 40.0, 20.0, 20.0),
3209 gt_kps,
3210 None,
3211 )];
3212 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3213 let dts = CocoDetections::from_inputs(vec![dt_input_with_kps(
3214 1,
3215 1,
3216 0.9,
3217 (1490.0, 1490.0, 20.0, 20.0),
3218 dt_kps,
3219 )])
3220 .unwrap();
3221 let area = AreaRange::coco_default();
3222 let params = EvaluateParams {
3223 iou_thresholds: iou_thresholds(),
3224 area_ranges: &area,
3225 max_dets_per_image: 100,
3226 use_cats: true,
3227 retain_iou: false,
3228 };
3229 let grid =
3230 evaluate_keypoints(>, &dts, params, ParityMode::Strict, HashMap::new()).unwrap();
3231 let cell = grid.cell(0, 0, 0).unwrap();
3232 assert!(
3233 cell.dt_matched.iter().all(|&m| !m),
3234 "DTs far from GT should not match at any IoU threshold",
3235 );
3236 }
3237
3238 #[test]
3239 fn test_evaluate_keypoints_d2_implicit_ignore() {
3240 let images = vec![img(1, 100, 100)];
3245 let cats = vec![cat(1, "person")];
3246 let gt_kps = const_kps_vec(50.0, 50.0, 0, 17);
3247 let dt_kps = const_kps_vec(50.0, 50.0, 2, 17);
3248 let anns = vec![ann_with_kps(
3249 1,
3250 1,
3251 1,
3252 (40.0, 40.0, 20.0, 20.0),
3253 gt_kps,
3254 Some(0),
3258 )];
3259 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3260 let dts = CocoDetections::from_inputs(vec![dt_input_with_kps(
3261 1,
3262 1,
3263 0.9,
3264 (40.0, 40.0, 20.0, 20.0),
3265 dt_kps,
3266 )])
3267 .unwrap();
3268 let area = AreaRange::coco_default();
3269 let params = EvaluateParams {
3270 iou_thresholds: iou_thresholds(),
3271 area_ranges: &area,
3272 max_dets_per_image: 100,
3273 use_cats: true,
3274 retain_iou: false,
3275 };
3276 let grid =
3277 evaluate_keypoints(>, &dts, params, ParityMode::Strict, HashMap::new()).unwrap();
3278 let cell = grid.cell(0, 0, 0).unwrap();
3279 assert_eq!(
3280 cell.gt_ignore,
3281 vec![true],
3282 "D2: zero-visible-keypoints GT must be ignored",
3283 );
3284 }
3285
3286 #[test]
3287 fn test_evaluate_keypoints_per_category_sigmas() {
3288 let images = vec![img(1, 200, 200)];
3295 let cats = vec![cat(1, "person"), cat(2, "dog")];
3296 let gt_kps = const_kps_vec(50.0, 50.0, 2, 17);
3297 let anns = vec![
3298 ann_with_kps(1, 1, 1, (40.0, 40.0, 20.0, 20.0), gt_kps, None),
3299 ann_with_kps(
3300 2,
3301 1,
3302 2,
3303 (140.0, 140.0, 20.0, 20.0),
3304 const_kps_vec(150.0, 150.0, 2, 17),
3305 None,
3306 ),
3307 ];
3308 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3309 let dts = CocoDetections::from_inputs(vec![
3312 dt_input_with_kps(
3313 1,
3314 1,
3315 0.9,
3316 (40.0, 40.0, 20.0, 20.0),
3317 const_kps_vec(51.0, 50.0, 2, 17),
3318 ),
3319 dt_input_with_kps(
3320 1,
3321 2,
3322 0.8,
3323 (140.0, 140.0, 20.0, 20.0),
3324 const_kps_vec(151.0, 150.0, 2, 17),
3325 ),
3326 ])
3327 .unwrap();
3328 let mut sigmas: HashMap<i64, Vec<f64>> = HashMap::new();
3329 sigmas.insert(1, vec![0.5_f64; 17]);
3330 sigmas.insert(2, vec![0.5_f64; 17]);
3331 let area = AreaRange::coco_default();
3332 let params = EvaluateParams {
3333 iou_thresholds: iou_thresholds(),
3334 area_ranges: &area,
3335 max_dets_per_image: 100,
3336 use_cats: true,
3337 retain_iou: false,
3338 };
3339 let grid = evaluate_keypoints(>, &dts, params, ParityMode::Strict, sigmas).unwrap();
3340 let cell_cat1 = grid.cell(0, 0, 0).unwrap();
3342 let cell_cat2 = grid.cell(1, 0, 0).unwrap();
3343 assert!(
3344 cell_cat1.dt_matched.iter().all(|&m| m),
3345 "cat-1 DT should match cat-1 GT under override sigmas",
3346 );
3347 assert!(
3348 cell_cat2.dt_matched.iter().all(|&m| m),
3349 "cat-2 DT should match cat-2 GT under override sigmas",
3350 );
3351 }
3352
3353 #[test]
3354 fn test_evaluate_keypoints_missing_dt_kps_rejected() {
3355 let images = vec![img(1, 100, 100)];
3359 let cats = vec![cat(1, "person")];
3360 let gt_kps = const_kps_vec(50.0, 50.0, 2, 17);
3361 let anns = vec![ann_with_kps(
3362 1,
3363 1,
3364 1,
3365 (40.0, 40.0, 20.0, 20.0),
3366 gt_kps,
3367 None,
3368 )];
3369 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3370 let dts = CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (40.0, 40.0, 20.0, 20.0))])
3373 .unwrap();
3374 let area = AreaRange::coco_default();
3375 let params = EvaluateParams {
3376 iou_thresholds: iou_thresholds(),
3377 area_ranges: &area,
3378 max_dets_per_image: 100,
3379 use_cats: true,
3380 retain_iou: false,
3381 };
3382 let err =
3383 evaluate_keypoints(>, &dts, params, ParityMode::Strict, HashMap::new()).unwrap_err();
3384 match err {
3385 EvalError::InvalidAnnotation { detail } => {
3386 assert!(detail.contains("DT"), "expected DT in msg: {detail}");
3387 assert!(
3388 detail.contains("keypoints"),
3389 "expected keypoints in msg: {detail}",
3390 );
3391 }
3392 other => panic!("expected InvalidAnnotation, got {other:?}"),
3393 }
3394 }
3395
3396 #[test]
3397 fn test_keypoints_default_ignore_for_other_kernels() {
3398 let ann_zero_kps = ann_with_kps(
3403 1,
3404 1,
3405 1,
3406 (0.0, 0.0, 10.0, 10.0),
3407 const_kps_vec(0.0, 0.0, 0, 17),
3408 Some(0),
3409 );
3410 assert!(
3411 !BboxIou.extra_gt_ignore(&ann_zero_kps),
3412 "BboxIou must keep the default `false` ignore",
3413 );
3414 assert!(
3415 !SegmIou.extra_gt_ignore(&ann_zero_kps),
3416 "SegmIou must keep the default `false` ignore",
3417 );
3418 assert!(
3419 !BoundaryIou {
3420 dilation_ratio: 0.02,
3421 }
3422 .extra_gt_ignore(&ann_zero_kps),
3423 "BoundaryIou must keep the default `false` ignore",
3424 );
3425 assert!(
3427 OksSimilarity::default().extra_gt_ignore(&ann_zero_kps),
3428 "OksSimilarity must flip D2 to true on zero-visible-keypoints GT",
3429 );
3430 }
3431
3432 #[test]
3433 fn boundary_missing_gt_segmentation_surfaces_typed_error() {
3434 let images = vec![img(1, 100, 100)];
3438 let cats = vec![cat(1, "thing")];
3439 let anns = vec![ann(7, 1, 1, (0.0, 0.0, 10.0, 10.0))];
3440 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3441 let dts = CocoDetections::from_inputs(vec![dt_input_with_segm(
3442 1,
3443 1,
3444 0.9,
3445 (0.0, 0.0, 10.0, 10.0),
3446 square_polygon(0.0, 0.0, 10.0),
3447 )])
3448 .unwrap();
3449 let area = AreaRange::coco_default();
3450 let params = EvaluateParams {
3451 iou_thresholds: iou_thresholds(),
3452 area_ranges: &area,
3453 max_dets_per_image: 100,
3454 use_cats: true,
3455 retain_iou: false,
3456 };
3457 let err = evaluate_boundary(>, &dts, params, ParityMode::Strict, 0.02).unwrap_err();
3458 match err {
3459 EvalError::InvalidAnnotation { detail } => {
3460 assert!(detail.contains("GT id=7"), "msg: {detail}");
3461 }
3462 other => panic!("expected InvalidAnnotation, got {other:?}"),
3463 }
3464 }
3465
3466 fn lvis_dataset(
3473 images: &[ImageMeta],
3474 annotations: &[CocoAnnotation],
3475 categories: &[CategoryMeta],
3476 neg: &[(i64, Vec<i64>)],
3477 nel: &[(i64, Vec<i64>)],
3478 freq: &[(i64, crate::dataset::Frequency)],
3479 ) -> CocoDataset {
3480 let images_json: Vec<serde_json::Value> = images
3485 .iter()
3486 .map(|im| {
3487 let neg_for: Vec<i64> = neg
3488 .iter()
3489 .find(|(id, _)| *id == im.id.0)
3490 .map(|(_, v)| v.clone())
3491 .unwrap_or_default();
3492 let nel_for: Vec<i64> = nel
3493 .iter()
3494 .find(|(id, _)| *id == im.id.0)
3495 .map(|(_, v)| v.clone())
3496 .unwrap_or_default();
3497 serde_json::json!({
3498 "id": im.id.0,
3499 "width": im.width,
3500 "height": im.height,
3501 "neg_category_ids": neg_for,
3502 "not_exhaustive_category_ids": nel_for,
3503 })
3504 })
3505 .collect();
3506 let cats_json: Vec<serde_json::Value> = categories
3507 .iter()
3508 .map(|c| {
3509 let f = freq
3510 .iter()
3511 .find(|(id, _)| *id == c.id.0)
3512 .map(|(_, f)| match f {
3513 crate::dataset::Frequency::Rare => "r",
3514 crate::dataset::Frequency::Common => "c",
3515 crate::dataset::Frequency::Frequent => "f",
3516 })
3517 .expect("test fixture must include frequency for every category");
3518 serde_json::json!({
3519 "id": c.id.0,
3520 "name": c.name,
3521 "frequency": f,
3522 })
3523 })
3524 .collect();
3525 let anns_json = serde_json::to_value(annotations).unwrap();
3526 let payload = serde_json::json!({
3527 "images": images_json,
3528 "annotations": anns_json,
3529 "categories": cats_json,
3530 });
3531 let bytes = serde_json::to_vec(&payload).unwrap();
3532 CocoDataset::from_lvis_json_bytes(&bytes).unwrap()
3533 }
3534
3535 #[test]
3536 fn aa4_skips_cells_outside_pos_union_neg() {
3537 let images = vec![img(1, 100, 100), img(2, 100, 100)];
3545 let cats = vec![cat(1, "a"), cat(2, "b")];
3546 let anns = vec![
3547 ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0)),
3548 ann(2, 2, 2, (0.0, 0.0, 10.0, 10.0)),
3549 ];
3550 let gt_lvis = lvis_dataset(
3551 &images,
3552 &anns,
3553 &cats,
3554 &[(1, vec![]), (2, vec![])],
3555 &[(1, vec![]), (2, vec![])],
3556 &[
3557 (1, crate::dataset::Frequency::Frequent),
3558 (2, crate::dataset::Frequency::Frequent),
3559 ],
3560 );
3561 let gt_coco = CocoDataset::from_parts(images, anns, cats).unwrap();
3562 let dts = CocoDetections::from_inputs(vec![
3565 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
3566 dt_input(1, 2, 0.7, (50.0, 50.0, 10.0, 10.0)),
3567 dt_input(2, 2, 0.9, (0.0, 0.0, 10.0, 10.0)),
3568 ])
3569 .unwrap();
3570 let area = AreaRange::coco_default();
3571 let params = EvaluateParams {
3572 iou_thresholds: iou_thresholds(),
3573 area_ranges: &area,
3574 max_dets_per_image: 100,
3575 use_cats: true,
3576 retain_iou: false,
3577 };
3578 let grid_lvis = evaluate_bbox(>_lvis, &dts, params, ParityMode::Strict).unwrap();
3579 let grid_coco = evaluate_bbox(>_coco, &dts, params, ParityMode::Strict).unwrap();
3580
3581 let lvis_cell = grid_lvis.cell(1, 0, 0);
3585 let coco_cell = grid_coco.cell(1, 0, 0);
3586 assert!(lvis_cell.is_none(), "AA4: federated cell must be skipped");
3587 assert!(
3588 coco_cell.is_some(),
3589 "control: COCO dataset must evaluate the same cell"
3590 );
3591 assert_eq!(
3594 grid_lvis.cell(0, 0, 0).map(|c| c.dt_scores.len()),
3595 grid_coco.cell(0, 0, 0).map(|c| c.dt_scores.len()),
3596 );
3597 }
3598
3599 #[test]
3600 fn aa4_keeps_neg_cells_with_no_gts() {
3601 let images = vec![img(1, 100, 100)];
3605 let cats = vec![cat(1, "a"), cat(2, "b")];
3606 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
3607 let gt = lvis_dataset(
3608 &images,
3609 &anns,
3610 &cats,
3611 &[(1, vec![2])], &[(1, vec![])],
3613 &[
3614 (1, crate::dataset::Frequency::Frequent),
3615 (2, crate::dataset::Frequency::Frequent),
3616 ],
3617 );
3618 let dts = CocoDetections::from_inputs(vec![
3619 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
3620 dt_input(1, 2, 0.7, (50.0, 50.0, 10.0, 10.0)),
3621 ])
3622 .unwrap();
3623 let area = AreaRange::coco_default();
3624 let params = EvaluateParams {
3625 iou_thresholds: iou_thresholds(),
3626 area_ranges: &area,
3627 max_dets_per_image: 100,
3628 use_cats: true,
3629 retain_iou: false,
3630 };
3631 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3632 let cell = grid
3633 .cell(1, 0, 0)
3634 .expect("cat 2 ∈ neg[1] must produce an evaluated cell");
3635 assert_eq!(cell.dt_scores.len(), 1);
3638 assert!(cell.dt_ignore.iter().all(|&ig| !ig));
3639 }
3640
3641 #[test]
3642 fn aa3_dt_ignore_extension_in_not_exhaustive_cell() {
3643 let images = vec![img(1, 100, 100)];
3650 let cats = vec![cat(1, "a")];
3651 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
3652 let gt = lvis_dataset(
3653 &images,
3654 &anns,
3655 &cats,
3656 &[(1, vec![])],
3657 &[(1, vec![1])], &[(1, crate::dataset::Frequency::Frequent)],
3659 );
3660 let dts = CocoDetections::from_inputs(vec![
3661 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)), dt_input(1, 1, 0.7, (50.0, 50.0, 10.0, 10.0)), ])
3664 .unwrap();
3665 let area = AreaRange::coco_default();
3666 let params = EvaluateParams {
3667 iou_thresholds: iou_thresholds(),
3668 area_ranges: &area,
3669 max_dets_per_image: 100,
3670 use_cats: true,
3671 retain_iou: false,
3672 };
3673 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3674 let cell = grid.cell(0, 0, 0).expect("cell must evaluate");
3675 let n_t = cell.dt_ignore.shape()[0];
3676 for t in 0..n_t {
3681 assert!(!cell.dt_ignore[(t, 0)], "TP should not be dt_ignore");
3682 assert!(
3683 cell.dt_ignore[(t, 1)],
3684 "AA3: unmatched DT in not_exhaustive cell must be dt_ignore"
3685 );
3686 }
3687 }
3688
3689 #[test]
3690 fn aa3_dt_ignore_only_unmatched() {
3691 let images = vec![img(1, 100, 100)];
3695 let cats = vec![cat(1, "a")];
3696 let anns = vec![ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0))];
3697 let gt = lvis_dataset(
3698 &images,
3699 &anns,
3700 &cats,
3701 &[(1, vec![])],
3702 &[(1, vec![])],
3703 &[(1, crate::dataset::Frequency::Frequent)],
3704 );
3705 let dts = CocoDetections::from_inputs(vec![
3706 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
3707 dt_input(1, 1, 0.7, (50.0, 50.0, 10.0, 10.0)),
3708 ])
3709 .unwrap();
3710 let area = AreaRange::coco_default();
3711 let params = EvaluateParams {
3712 iou_thresholds: iou_thresholds(),
3713 area_ranges: &area,
3714 max_dets_per_image: 100,
3715 use_cats: true,
3716 retain_iou: false,
3717 };
3718 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3719 let cell = grid.cell(0, 0, 0).expect("cell must evaluate");
3720 assert!(cell.dt_ignore.iter().all(|&ig| !ig));
3721 }
3722
3723 #[test]
3724 fn federated_dataset_with_use_cats_false_falls_back_to_coco() {
3725 let images = vec![img(1, 100, 100), img(2, 100, 100)];
3730 let cats = vec![cat(1, "a"), cat(2, "b")];
3731 let anns = vec![
3732 ann(1, 1, 1, (0.0, 0.0, 10.0, 10.0)),
3733 ann(2, 2, 2, (0.0, 0.0, 10.0, 10.0)),
3734 ];
3735 let gt = lvis_dataset(
3736 &images,
3737 &anns,
3738 &cats,
3739 &[(1, vec![]), (2, vec![])],
3740 &[(1, vec![]), (2, vec![])],
3741 &[
3742 (1, crate::dataset::Frequency::Frequent),
3743 (2, crate::dataset::Frequency::Frequent),
3744 ],
3745 );
3746 let dts = CocoDetections::from_inputs(vec![
3747 dt_input(1, 1, 0.9, (0.0, 0.0, 10.0, 10.0)),
3748 dt_input(1, 2, 0.7, (50.0, 50.0, 10.0, 10.0)),
3749 ])
3750 .unwrap();
3751 let area = AreaRange::coco_default();
3752 let params = EvaluateParams {
3753 iou_thresholds: iou_thresholds(),
3754 area_ranges: &area,
3755 max_dets_per_image: 100,
3756 use_cats: false,
3757 retain_iou: false,
3758 };
3759 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3762 assert_eq!(grid.n_categories, 1);
3763 let cell = grid.cell(0, 0, 0).expect("collapsed cell must evaluate");
3766 assert_eq!(cell.dt_scores.len(), 2);
3767 }
3768
3769 #[test]
3770 fn coco_dataset_unaffected_by_federated_machinery() {
3771 let g = perfect_match_grid();
3777 let cell = g.cell(0, 0, 0).expect("perfect_match cell must exist");
3780 assert_eq!(cell.dt_scores.len(), 2);
3781 assert!(cell.dt_ignore.iter().all(|&ig| !ig));
3782 }
3783
3784 fn ann_with_area(
3791 id: i64,
3792 image: i64,
3793 cat: i64,
3794 bbox: (f64, f64, f64, f64),
3795 area: f64,
3796 ) -> CocoAnnotation {
3797 let mut a = ann(id, image, cat, bbox);
3798 a.area = area;
3799 a
3800 }
3801
3802 #[test]
3803 fn ag6_mixed_cell_drops_zero_area_gt_in_strict_mode() {
3804 let images = vec![img(1, 100, 100)];
3811 let cats = vec![cat(1, "a")];
3812 let anns = vec![
3813 ann(1, 1, 1, (10.0, 10.0, 20.0, 20.0)),
3814 ann_with_area(2, 1, 1, (50.0, 50.0, 0.1, 0.1), 0.0),
3815 ];
3816 let gt = lvis_dataset(
3817 &images,
3818 &anns,
3819 &cats,
3820 &[(1, vec![])],
3821 &[(1, vec![])],
3822 &[(1, crate::dataset::Frequency::Frequent)],
3823 );
3824 let dts = CocoDetections::from_inputs(vec![
3825 dt_input(1, 1, 0.9, (10.0, 10.0, 20.0, 20.0)),
3826 dt_input(1, 1, 0.8, (50.0, 50.0, 0.1, 0.1)),
3827 ])
3828 .unwrap();
3829 let area = AreaRange::coco_default();
3830 let params = EvaluateParams {
3831 iou_thresholds: iou_thresholds(),
3832 area_ranges: &area,
3833 max_dets_per_image: 100,
3834 use_cats: true,
3835 retain_iou: false,
3836 };
3837
3838 let strict = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3839 let cell = strict
3840 .cell(0, 0, 0)
3841 .expect("mixed cell must still evaluate in strict mode");
3842 assert_eq!(cell.dt_scores.len(), 2);
3843 let strict_meta = strict.cell_meta(0, 0, 0).unwrap();
3847 assert_eq!(strict_meta.dt_matches[(0, 0)], 1, "DT_real → GT id=1");
3848 assert_eq!(
3849 strict_meta.dt_matches[(0, 1)],
3850 0,
3851 "DT_zero must be unmatched after strict filter drops GT id=2"
3852 );
3853
3854 let corrected = evaluate_bbox(>, &dts, params, ParityMode::Corrected).unwrap();
3855 let cor_meta = corrected.cell_meta(0, 0, 0).unwrap();
3856 assert_eq!(cor_meta.dt_matches[(0, 0)], 1, "DT_real → GT id=1");
3857 assert_eq!(
3858 cor_meta.dt_matches[(0, 1)],
3859 2,
3860 "Corrected mode keeps the zero-area GT and matches DT_zero → GT id=2"
3861 );
3862 }
3863
3864 #[test]
3865 fn ag6_all_zero_area_cell_skipped_via_aa4_in_strict_mode() {
3866 let images = vec![img(1, 100, 100)];
3873 let cats = vec![cat(1, "a")];
3874 let anns = vec![ann_with_area(1, 1, 1, (50.0, 50.0, 0.1, 0.1), 0.0)];
3875 let gt = lvis_dataset(
3876 &images,
3877 &anns,
3878 &cats,
3879 &[(1, vec![])],
3880 &[(1, vec![])],
3881 &[(1, crate::dataset::Frequency::Frequent)],
3882 );
3883 let dts =
3884 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.9, (50.0, 50.0, 0.1, 0.1))]).unwrap();
3885 let area = AreaRange::coco_default();
3886 let params = EvaluateParams {
3887 iou_thresholds: iou_thresholds(),
3888 area_ranges: &area,
3889 max_dets_per_image: 100,
3890 use_cats: true,
3891 retain_iou: false,
3892 };
3893
3894 let strict = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3895 assert!(
3896 strict.cell(0, 0, 0).is_none(),
3897 "AG6: all-zero-area cell must be skipped via AA4 in strict mode"
3898 );
3899
3900 let corrected = evaluate_bbox(>, &dts, params, ParityMode::Corrected).unwrap();
3901 let cell = corrected
3902 .cell(0, 0, 0)
3903 .expect("Corrected mode must keep the zero-area GT");
3904 assert_eq!(cell.dt_scores.len(), 1);
3905 }
3906
3907 #[test]
3908 fn ag6_strict_filter_is_noop_on_coco_dataset() {
3909 let images = vec![img(1, 100, 100)];
3914 let cats = vec![cat(1, "a")];
3915 let anns = vec![
3916 ann(1, 1, 1, (10.0, 10.0, 20.0, 20.0)),
3917 ann_with_area(2, 1, 1, (50.0, 50.0, 0.1, 0.1), 0.0),
3918 ];
3919 let gt = CocoDataset::from_parts(images, anns, cats).unwrap();
3920 let dts = CocoDetections::from_inputs(vec![
3921 dt_input(1, 1, 0.9, (10.0, 10.0, 20.0, 20.0)),
3922 dt_input(1, 1, 0.8, (50.0, 50.0, 0.1, 0.1)),
3923 ])
3924 .unwrap();
3925 let area = AreaRange::coco_default();
3926 let params = EvaluateParams {
3927 iou_thresholds: iou_thresholds(),
3928 area_ranges: &area,
3929 max_dets_per_image: 100,
3930 use_cats: true,
3931 retain_iou: false,
3932 };
3933 let grid = evaluate_bbox(>, &dts, params, ParityMode::Strict).unwrap();
3934 let meta = grid.cell_meta(0, 0, 0).unwrap();
3935 assert_eq!(meta.dt_matches[(0, 0)], 1);
3936 assert_eq!(
3937 meta.dt_matches[(0, 1)],
3938 2,
3939 "COCO strict mode must NOT drop the zero-area GT — AG6 is LVIS-only"
3940 );
3941 }
3942}