1use std::collections::{HashMap, HashSet};
44use std::sync::{Arc, OnceLock};
45
46use serde::{Deserialize, Serialize};
47
48use crate::error::EvalError;
49use crate::parity::ParityMode;
50use crate::segmentation::{Segmentation, SegmentationRleCounts};
51
52#[cfg(feature = "bench-timings")]
54pub(crate) mod dataset_timings {
55 use crate::bench_counters::BenchCounterSet;
56
57 pub(super) const GT_PARSE_NS: usize = 0;
58 pub(super) const GT_FROM_PARTS_NS: usize = 1;
59 pub(super) const DT_PARSE_NS: usize = 2;
60 pub(super) const DT_FROM_INPUTS_NS: usize = 3;
61
62 pub(super) static COUNTERS: BenchCounterSet<4> = BenchCounterSet::new();
63
64 pub(crate) fn read_and_reset() -> (u64, u64, u64, u64) {
65 let [a, b, c, d] = COUNTERS.read_and_reset();
66 (a, b, c, d)
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
74#[serde(transparent)]
75pub struct ImageId(pub i64);
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
79#[serde(transparent)]
80pub struct CategoryId(pub i64);
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
84#[serde(transparent)]
85pub struct AnnId(pub i64);
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct ImageMeta {
92 pub id: ImageId,
94 pub width: u32,
96 pub height: u32,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub file_name: Option<String>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106pub struct CategoryMeta {
107 pub id: CategoryId,
109 pub name: String,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub supercategory: Option<String>,
114}
115
116#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
120#[serde(from = "[f64; 4]", into = "[f64; 4]")]
121pub struct Bbox {
122 pub x: f64,
124 pub y: f64,
126 pub w: f64,
128 pub h: f64,
130}
131
132impl From<[f64; 4]> for Bbox {
133 fn from([x, y, w, h]: [f64; 4]) -> Self {
134 Self { x, y, w, h }
135 }
136}
137
138impl From<Bbox> for [f64; 4] {
139 fn from(b: Bbox) -> Self {
140 [b.x, b.y, b.w, b.h]
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
151pub struct CocoAnnotation {
152 pub id: AnnId,
154 pub image_id: ImageId,
156 pub category_id: CategoryId,
158 pub area: f64,
161 #[serde(rename = "iscrowd", default, deserialize_with = "deserialize_bool_int")]
164 pub is_crowd: bool,
165 #[serde(
172 rename = "ignore",
173 default,
174 deserialize_with = "deserialize_opt_bool_int"
175 )]
176 pub ignore_flag: Option<bool>,
177 pub bbox: Bbox,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub segmentation: Option<Segmentation>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub keypoints: Option<Vec<f64>>,
195 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub num_keypoints: Option<u32>,
201}
202
203impl CocoAnnotation {
204 pub fn effective_ignore(&self, mode: ParityMode) -> bool {
212 match mode {
213 ParityMode::Strict => self.is_crowd,
214 ParityMode::Corrected => self.ignore_flag.unwrap_or(self.is_crowd),
215 }
216 }
217}
218
219pub trait Annotation {
225 fn image_id(&self) -> ImageId;
227 fn category_id(&self) -> CategoryId;
229 fn area(&self) -> f64;
231 fn is_crowd(&self) -> bool;
233 fn effective_ignore(&self, mode: ParityMode) -> bool;
235}
236
237impl Annotation for CocoAnnotation {
238 fn image_id(&self) -> ImageId {
239 self.image_id
240 }
241 fn category_id(&self) -> CategoryId {
242 self.category_id
243 }
244 fn area(&self) -> f64 {
245 self.area
246 }
247 fn is_crowd(&self) -> bool {
248 self.is_crowd
249 }
250 fn effective_ignore(&self, mode: ParityMode) -> bool {
251 Self::effective_ignore(self, mode)
252 }
253}
254
255pub trait EvalDataset: Send + Sync {
261 type Annotation: Annotation;
265
266 fn images(&self) -> &[ImageMeta];
268
269 fn categories(&self) -> &[CategoryMeta];
271
272 fn annotations(&self) -> &[Self::Annotation];
274
275 fn ann_indices_for_image(&self, image_id: ImageId) -> &[usize];
278
279 fn ann_indices_for_category(&self, cat_id: CategoryId) -> &[usize];
282
283 fn ann_iter_for_image(&self, image_id: ImageId) -> AnnotationIter<'_, Self::Annotation> {
285 AnnotationIter {
286 anns: self.annotations(),
287 indices: self.ann_indices_for_image(image_id).iter(),
288 }
289 }
290
291 fn ann_iter_for_category(&self, cat_id: CategoryId) -> AnnotationIter<'_, Self::Annotation> {
293 AnnotationIter {
294 anns: self.annotations(),
295 indices: self.ann_indices_for_category(cat_id).iter(),
296 }
297 }
298}
299
300pub struct AnnotationIter<'a, A> {
304 anns: &'a [A],
305 indices: std::slice::Iter<'a, usize>,
306}
307
308impl<'a, A> Iterator for AnnotationIter<'a, A> {
309 type Item = &'a A;
310
311 fn next(&mut self) -> Option<Self::Item> {
312 let idx = *self.indices.next()?;
313 self.anns.get(idx)
314 }
315
316 fn size_hint(&self) -> (usize, Option<usize>) {
317 self.indices.size_hint()
318 }
319}
320
321impl<'a, A> ExactSizeIterator for AnnotationIter<'a, A> {}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct CocoJson {
332 pub images: Vec<ImageMeta>,
334 pub annotations: Vec<CocoAnnotation>,
336 pub categories: Vec<CategoryMeta>,
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
360pub enum Frequency {
361 #[serde(rename = "r")]
363 Rare,
364 #[serde(rename = "c")]
366 Common,
367 #[serde(rename = "f")]
369 Frequent,
370}
371
372impl Frequency {
373 pub const fn as_letter(self) -> &'static str {
378 match self {
379 Self::Rare => "r",
380 Self::Common => "c",
381 Self::Frequent => "f",
382 }
383 }
384}
385
386#[derive(Debug, Clone, Deserialize)]
391struct LvisImageRaw {
392 id: ImageId,
393 width: u32,
394 height: u32,
395 #[serde(default)]
396 file_name: Option<String>,
397 #[serde(default)]
401 neg_category_ids: Option<Vec<CategoryId>>,
402 #[serde(default)]
407 not_exhaustive_category_ids: Option<Vec<CategoryId>>,
408}
409
410#[derive(Debug, Clone, Deserialize)]
415struct LvisCategoryRaw {
416 id: CategoryId,
417 name: String,
418 #[serde(default)]
419 supercategory: Option<String>,
420 #[serde(default)]
424 frequency: Option<Frequency>,
425}
426
427#[derive(Debug, Clone, Deserialize)]
432struct LvisJson {
433 images: Vec<LvisImageRaw>,
434 annotations: Vec<CocoAnnotation>,
435 categories: Vec<LvisCategoryRaw>,
436}
437
438#[derive(Debug, Clone)]
446pub struct FederatedMetadata {
447 pub pos_category_ids: HashMap<ImageId, HashSet<CategoryId>>,
450 pub neg_category_ids: HashMap<ImageId, HashSet<CategoryId>>,
453 pub not_exhaustive_category_ids: HashMap<ImageId, HashSet<CategoryId>>,
456 pub category_frequency: HashMap<CategoryId, Frequency>,
461}
462
463#[derive(Debug, Clone)]
479pub struct CocoDataset {
480 images: Arc<Vec<ImageMeta>>,
481 categories: Arc<Vec<CategoryMeta>>,
482 annotations: Arc<Vec<CocoAnnotation>>,
483 by_image: HashMap<ImageId, Vec<usize>>,
484 by_category: HashMap<CategoryId, Vec<usize>>,
485 by_image_cat: HashMap<(ImageId, CategoryId), Vec<usize>>,
486 federated: Option<FederatedMetadata>,
487 cached_hash: Arc<OnceLock<[u8; 32]>>,
494}
495
496impl CocoDataset {
497 pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, EvalError> {
503 #[cfg(feature = "bench-timings")]
504 let t0 = std::time::Instant::now();
505 let raw: CocoJson = serde_json::from_slice(bytes)?;
506 #[cfg(feature = "bench-timings")]
507 let parse_ns = u64::try_from(t0.elapsed().as_nanos()).unwrap_or(u64::MAX);
508 #[cfg(feature = "bench-timings")]
509 let t1 = std::time::Instant::now();
510 let result = Self::from_parts(raw.images, raw.annotations, raw.categories);
511 #[cfg(feature = "bench-timings")]
512 {
513 let from_parts_ns = u64::try_from(t1.elapsed().as_nanos()).unwrap_or(u64::MAX);
514 dataset_timings::COUNTERS.add(dataset_timings::GT_PARSE_NS, parse_ns);
515 dataset_timings::COUNTERS.add(dataset_timings::GT_FROM_PARTS_NS, from_parts_ns);
516 }
517 result
518 }
519
520 pub fn from_parts(
522 images: Vec<ImageMeta>,
523 annotations: Vec<CocoAnnotation>,
524 categories: Vec<CategoryMeta>,
525 ) -> Result<Self, EvalError> {
526 let known_images: HashSet<ImageId> = images.iter().map(|i| i.id).collect();
527 let known_categories: HashSet<CategoryId> = categories.iter().map(|c| c.id).collect();
528
529 let mut by_image: HashMap<ImageId, Vec<usize>> = HashMap::with_capacity(images.len());
530 let mut by_category: HashMap<CategoryId, Vec<usize>> =
531 HashMap::with_capacity(categories.len());
532 let mut by_image_cat: HashMap<(ImageId, CategoryId), Vec<usize>> = HashMap::new();
533
534 for (idx, ann) in annotations.iter().enumerate() {
535 if !known_images.contains(&ann.image_id) {
536 return Err(EvalError::InvalidAnnotation {
537 detail: format!(
538 "annotation id={} references unknown image_id={}",
539 ann.id.0, ann.image_id.0
540 ),
541 });
542 }
543 if !known_categories.contains(&ann.category_id) {
544 return Err(EvalError::InvalidAnnotation {
545 detail: format!(
546 "annotation id={} references unknown category_id={}",
547 ann.id.0, ann.category_id.0
548 ),
549 });
550 }
551 by_image.entry(ann.image_id).or_default().push(idx);
552 by_category.entry(ann.category_id).or_default().push(idx);
553 by_image_cat
554 .entry((ann.image_id, ann.category_id))
555 .or_default()
556 .push(idx);
557 }
558
559 Ok(Self {
560 images: Arc::new(images),
561 categories: Arc::new(categories),
562 annotations: Arc::new(annotations),
563 by_image,
564 by_category,
565 by_image_cat,
566 federated: None,
567 cached_hash: Arc::new(OnceLock::new()),
568 })
569 }
570
571 pub fn from_lvis_json_bytes(bytes: &[u8]) -> Result<Self, EvalError> {
610 let raw: LvisJson = serde_json::from_slice(bytes)?;
611
612 let images: Vec<ImageMeta> = raw
613 .images
614 .iter()
615 .map(|im| ImageMeta {
616 id: im.id,
617 width: im.width,
618 height: im.height,
619 file_name: im.file_name.clone(),
620 })
621 .collect();
622 let categories: Vec<CategoryMeta> = raw
623 .categories
624 .iter()
625 .map(|c| CategoryMeta {
626 id: c.id,
627 name: c.name.clone(),
628 supercategory: c.supercategory.clone(),
629 })
630 .collect();
631
632 let mut missing_freq: Vec<i64> = raw
636 .categories
637 .iter()
638 .filter(|c| c.frequency.is_none())
639 .map(|c| c.id.0)
640 .collect();
641 if !missing_freq.is_empty() {
642 missing_freq.sort_unstable();
643 return Err(EvalError::MissingFrequency {
644 category_ids: missing_freq,
645 });
646 }
647 let category_frequency: HashMap<CategoryId, Frequency> = raw
648 .categories
649 .iter()
650 .filter_map(|c| c.frequency.map(|f| (c.id, f)))
651 .collect();
652
653 let mut dataset = Self::from_parts(images, raw.annotations, categories)?;
656
657 let mut pos: HashMap<ImageId, HashSet<CategoryId>> =
660 HashMap::with_capacity(raw.images.len());
661 for im in &raw.images {
662 pos.entry(im.id).or_default();
663 }
664 for ann in dataset.annotations.iter() {
665 pos.entry(ann.image_id).or_default().insert(ann.category_id);
666 }
667
668 let mut neg: HashMap<ImageId, HashSet<CategoryId>> =
671 HashMap::with_capacity(raw.images.len());
672 let mut nel: HashMap<ImageId, HashSet<CategoryId>> =
673 HashMap::with_capacity(raw.images.len());
674 for im in &raw.images {
675 let neg_set: HashSet<CategoryId> = im
676 .neg_category_ids
677 .as_deref()
678 .unwrap_or(&[])
679 .iter()
680 .copied()
681 .collect();
682 let nel_set: HashSet<CategoryId> = im
683 .not_exhaustive_category_ids
684 .as_deref()
685 .unwrap_or(&[])
686 .iter()
687 .copied()
688 .collect();
689 neg.insert(im.id, neg_set);
690 nel.insert(im.id, nel_set);
691 }
692
693 for im in &raw.images {
695 let image_id = im.id;
696 let pos_i = pos.get(&image_id).map_or_else(HashSet::new, Clone::clone);
697 let neg_i = &neg[&image_id];
698 let nel_i = &nel[&image_id];
699
700 if let Some(c) = pos_i.intersection(neg_i).next().copied() {
703 return Err(EvalError::LvisFederatedConflict {
704 image_id: image_id.0,
705 category_id: c.0,
706 detail: "category has GT on image but is also in neg_category_ids",
707 });
708 }
709 if let Some(c) = nel_i.difference(&pos_i).next().copied() {
711 return Err(EvalError::LvisFederatedConflict {
712 image_id: image_id.0,
713 category_id: c.0,
714 detail:
715 "category in not_exhaustive_category_ids but not in pos (no GT on image)",
716 });
717 }
718 if let Some(c) = nel_i.intersection(neg_i).next().copied() {
722 return Err(EvalError::LvisFederatedConflict {
723 image_id: image_id.0,
724 category_id: c.0,
725 detail: "category in both not_exhaustive_category_ids and neg_category_ids",
726 });
727 }
728 }
729
730 dataset.federated = Some(FederatedMetadata {
731 pos_category_ids: pos,
732 neg_category_ids: neg,
733 not_exhaustive_category_ids: nel,
734 category_frequency,
735 });
736 Ok(dataset)
737 }
738
739 pub fn federated(&self) -> Option<&FederatedMetadata> {
743 self.federated.as_ref()
744 }
745
746 pub fn pos_category_ids(&self) -> Option<&HashMap<ImageId, HashSet<CategoryId>>> {
749 self.federated.as_ref().map(|f| &f.pos_category_ids)
750 }
751
752 pub fn neg_category_ids(&self) -> Option<&HashMap<ImageId, HashSet<CategoryId>>> {
755 self.federated.as_ref().map(|f| &f.neg_category_ids)
756 }
757
758 pub fn not_exhaustive_category_ids(&self) -> Option<&HashMap<ImageId, HashSet<CategoryId>>> {
762 self.federated
763 .as_ref()
764 .map(|f| &f.not_exhaustive_category_ids)
765 }
766
767 pub fn category_frequency(&self) -> Option<&HashMap<CategoryId, Frequency>> {
772 self.federated.as_ref().map(|f| &f.category_frequency)
773 }
774
775 pub fn is_federated(&self) -> bool {
779 self.federated.is_some()
780 }
781
782 pub fn to_json_value(&self) -> CocoJson {
790 CocoJson {
791 images: (*self.images).clone(),
792 annotations: (*self.annotations).clone(),
793 categories: (*self.categories).clone(),
794 }
795 }
796}
797
798impl EvalDataset for CocoDataset {
799 type Annotation = CocoAnnotation;
800
801 fn images(&self) -> &[ImageMeta] {
802 &self.images
803 }
804
805 fn categories(&self) -> &[CategoryMeta] {
806 &self.categories
807 }
808
809 fn annotations(&self) -> &[CocoAnnotation] {
810 &self.annotations
811 }
812
813 fn ann_indices_for_image(&self, image_id: ImageId) -> &[usize] {
814 self.by_image.get(&image_id).map_or(&[][..], Vec::as_slice)
815 }
816
817 fn ann_indices_for_category(&self, cat_id: CategoryId) -> &[usize] {
818 self.by_category.get(&cat_id).map_or(&[][..], Vec::as_slice)
819 }
820}
821
822impl CocoDataset {
823 pub fn ann_indices_for(&self, image: ImageId, cat: CategoryId) -> &[usize] {
826 self.by_image_cat
827 .get(&(image, cat))
828 .map_or(&[][..], Vec::as_slice)
829 }
830}
831
832const HASH_TAG_DATASET: &[u8; 4] = b"DSET";
850const HASH_TAG_IMAGES: &[u8; 4] = b"IMGS";
851const HASH_TAG_CATEGORIES: &[u8; 4] = b"CATS";
852const HASH_TAG_ANNOTATIONS: &[u8; 4] = b"ANNS";
853const HASH_TAG_FEDERATED: &[u8; 4] = b"FEDM";
854
855const HASH_CANONICAL_VERSION: u8 = 1;
859
860#[inline]
861fn hash_u8(h: &mut blake3::Hasher, v: u8) {
862 h.update(&[v]);
863}
864#[inline]
865fn hash_u32(h: &mut blake3::Hasher, v: u32) {
866 h.update(&v.to_le_bytes());
867}
868#[inline]
869fn hash_i64(h: &mut blake3::Hasher, v: i64) {
870 h.update(&v.to_le_bytes());
871}
872#[inline]
873fn hash_u64(h: &mut blake3::Hasher, v: u64) {
874 h.update(&v.to_le_bytes());
875}
876#[inline]
877fn hash_f64(h: &mut blake3::Hasher, v: f64) {
878 h.update(&v.to_bits().to_le_bytes());
883}
884#[inline]
885fn hash_bool(h: &mut blake3::Hasher, v: bool) {
886 hash_u8(h, u8::from(v));
887}
888#[inline]
889fn hash_bytes(h: &mut blake3::Hasher, bytes: &[u8]) {
890 hash_u64(h, bytes.len() as u64);
891 h.update(bytes);
892}
893#[inline]
894fn hash_string(h: &mut blake3::Hasher, s: &str) {
895 hash_bytes(h, s.as_bytes());
896}
897#[inline]
898fn hash_option<T>(
899 h: &mut blake3::Hasher,
900 opt: Option<T>,
901 write: impl FnOnce(&mut blake3::Hasher, T),
902) {
903 match opt {
904 None => hash_u8(h, 0),
905 Some(v) => {
906 hash_u8(h, 1);
907 write(h, v);
908 }
909 }
910}
911
912fn hash_bbox(h: &mut blake3::Hasher, b: &Bbox) {
913 hash_f64(h, b.x);
914 hash_f64(h, b.y);
915 hash_f64(h, b.w);
916 hash_f64(h, b.h);
917}
918
919fn hash_segmentation(h: &mut blake3::Hasher, seg: Option<&Segmentation>) {
920 match seg {
921 None => hash_u8(h, 0),
922 Some(Segmentation::Polygons(polys)) => {
923 hash_u8(h, 1);
924 hash_u64(h, polys.len() as u64);
925 for poly in polys {
926 hash_u64(h, poly.len() as u64);
927 for &v in poly {
928 hash_f64(h, v);
929 }
930 }
931 }
932 Some(Segmentation::Rle(rle)) => {
933 let [rh, rw] = rle.size;
934 match &rle.counts {
935 SegmentationRleCounts::Compressed(s) => {
936 hash_u8(h, 2);
937 hash_u32(h, rh);
938 hash_u32(h, rw);
939 hash_string(h, s);
940 }
941 SegmentationRleCounts::Uncompressed(counts) => {
942 hash_u8(h, 3);
943 hash_u32(h, rh);
944 hash_u32(h, rw);
945 hash_u64(h, counts.len() as u64);
946 for &c in counts.iter() {
947 hash_u32(h, c);
948 }
949 }
950 }
951 }
952 }
953}
954
955fn hash_id_sorted<T>(
961 h: &mut blake3::Hasher,
962 tag: &[u8; 4],
963 items: &[T],
964 key: impl Fn(&T) -> i64,
965 write: impl Fn(&mut blake3::Hasher, &T),
966) {
967 h.update(tag);
968 let mut order: Vec<usize> = (0..items.len()).collect();
969 order.sort_unstable_by_key(|&i| key(&items[i]));
970 hash_u64(h, order.len() as u64);
971 for &i in &order {
972 write(h, &items[i]);
973 }
974}
975
976fn hash_image_meta(h: &mut blake3::Hasher, im: &ImageMeta) {
977 let ImageMeta {
978 id,
979 width,
980 height,
981 file_name,
982 } = im;
983 hash_i64(h, id.0);
984 hash_u32(h, *width);
985 hash_u32(h, *height);
986 hash_option(h, file_name.as_deref(), hash_string);
987}
988
989fn hash_category_meta(h: &mut blake3::Hasher, c: &CategoryMeta) {
990 let CategoryMeta {
991 id,
992 name,
993 supercategory,
994 } = c;
995 hash_i64(h, id.0);
996 hash_string(h, name);
997 hash_option(h, supercategory.as_deref(), hash_string);
998}
999
1000fn hash_coco_annotation(h: &mut blake3::Hasher, a: &CocoAnnotation) {
1001 let CocoAnnotation {
1004 id,
1005 image_id,
1006 category_id,
1007 area,
1008 is_crowd,
1009 ignore_flag,
1010 bbox,
1011 segmentation,
1012 keypoints,
1013 num_keypoints,
1014 } = a;
1015 hash_i64(h, id.0);
1016 hash_i64(h, image_id.0);
1017 hash_i64(h, category_id.0);
1018 hash_f64(h, *area);
1019 hash_bool(h, *is_crowd);
1020 hash_option(h, *ignore_flag, hash_bool);
1021 hash_bbox(h, bbox);
1022 hash_segmentation(h, segmentation.as_ref());
1023 hash_option(h, keypoints.as_deref(), |h, kps| {
1024 hash_u64(h, kps.len() as u64);
1025 for &v in kps {
1026 hash_f64(h, v);
1027 }
1028 });
1029 hash_option(h, *num_keypoints, hash_u32);
1030}
1031
1032fn hash_federated(h: &mut blake3::Hasher, fed: &FederatedMetadata) {
1033 h.update(HASH_TAG_FEDERATED);
1034
1035 let mut freq_pairs: Vec<(i64, &Frequency)> = fed
1037 .category_frequency
1038 .iter()
1039 .map(|(k, v)| (k.0, v))
1040 .collect();
1041 freq_pairs.sort_unstable_by_key(|(k, _)| *k);
1042 hash_u64(h, freq_pairs.len() as u64);
1043 for (cid, freq) in freq_pairs {
1044 hash_i64(h, cid);
1045 hash_u8(h, freq.as_letter().as_bytes()[0]);
1047 }
1048
1049 type FedSection<'a> = (&'a [u8; 3], &'a HashMap<ImageId, HashSet<CategoryId>>);
1053 let sections: [FedSection<'_>; 3] = [
1054 (b"POS", &fed.pos_category_ids),
1055 (b"NEG", &fed.neg_category_ids),
1056 (b"NEX", &fed.not_exhaustive_category_ids),
1057 ];
1058 for (tag, map) in sections {
1059 h.update(tag);
1060 let mut entries: Vec<(i64, Vec<i64>)> = map
1061 .iter()
1062 .map(|(image_id, cats)| {
1063 let mut cat_ids: Vec<i64> = cats.iter().map(|c| c.0).collect();
1064 cat_ids.sort_unstable();
1065 (image_id.0, cat_ids)
1066 })
1067 .collect();
1068 entries.sort_unstable_by_key(|(image_id, _)| *image_id);
1069 hash_u64(h, entries.len() as u64);
1070 for (image_id, cat_ids) in entries {
1071 hash_i64(h, image_id);
1072 hash_u64(h, cat_ids.len() as u64);
1073 for cid in cat_ids {
1074 hash_i64(h, cid);
1075 }
1076 }
1077 }
1078}
1079
1080impl CocoDataset {
1081 pub fn dataset_hash(&self) -> [u8; 32] {
1090 *self.cached_hash.get_or_init(|| self.compute_dataset_hash())
1091 }
1092
1093 fn compute_dataset_hash(&self) -> [u8; 32] {
1094 let mut h = blake3::Hasher::new();
1095 h.update(HASH_TAG_DATASET);
1096 hash_u8(&mut h, HASH_CANONICAL_VERSION);
1097
1098 hash_id_sorted(
1099 &mut h,
1100 HASH_TAG_IMAGES,
1101 &self.images,
1102 |im| im.id.0,
1103 hash_image_meta,
1104 );
1105 hash_id_sorted(
1106 &mut h,
1107 HASH_TAG_CATEGORIES,
1108 &self.categories,
1109 |c| c.id.0,
1110 hash_category_meta,
1111 );
1112 hash_id_sorted(
1113 &mut h,
1114 HASH_TAG_ANNOTATIONS,
1115 &self.annotations,
1116 |a| a.id.0,
1117 hash_coco_annotation,
1118 );
1119
1120 match self.federated.as_ref() {
1122 None => hash_u8(&mut h, 0),
1123 Some(fed) => {
1124 hash_u8(&mut h, 1);
1125 hash_federated(&mut h, fed);
1126 }
1127 }
1128
1129 *h.finalize().as_bytes()
1130 }
1131}
1132
1133#[derive(Debug, Clone, PartialEq)]
1148pub struct CocoDetection {
1149 pub id: AnnId,
1152 pub image_id: ImageId,
1154 pub category_id: CategoryId,
1156 pub score: f64,
1158 pub bbox: Bbox,
1160 pub area: f64,
1162 pub segmentation: Option<Segmentation>,
1166 pub keypoints: Option<Vec<f64>>,
1171 pub num_keypoints: Option<u32>,
1176}
1177
1178impl Annotation for CocoDetection {
1179 fn image_id(&self) -> ImageId {
1180 self.image_id
1181 }
1182 fn category_id(&self) -> CategoryId {
1183 self.category_id
1184 }
1185 fn area(&self) -> f64 {
1186 self.area
1187 }
1188 fn is_crowd(&self) -> bool {
1189 false
1190 }
1191 fn effective_ignore(&self, _: ParityMode) -> bool {
1192 false
1193 }
1194}
1195
1196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1199pub struct DetectionInput {
1200 #[serde(default)]
1202 pub id: Option<AnnId>,
1203 pub image_id: ImageId,
1205 pub category_id: CategoryId,
1207 pub score: f64,
1209 pub bbox: Bbox,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1215 pub segmentation: Option<Segmentation>,
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub keypoints: Option<Vec<f64>>,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1223 pub num_keypoints: Option<u32>,
1224}
1225
1226#[derive(Debug, Clone)]
1229pub struct CocoDetections {
1230 detections: Arc<Vec<CocoDetection>>,
1231 by_image_cat: HashMap<(ImageId, CategoryId), Vec<usize>>,
1232 by_image: HashMap<ImageId, Vec<usize>>,
1233}
1234
1235impl CocoDetections {
1236 pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, EvalError> {
1244 #[cfg(feature = "bench-timings")]
1245 let t0 = std::time::Instant::now();
1246 let raw: Vec<DetectionInput> = serde_json::from_slice(bytes)?;
1247 #[cfg(feature = "bench-timings")]
1248 let parse_ns = u64::try_from(t0.elapsed().as_nanos()).unwrap_or(u64::MAX);
1249 #[cfg(feature = "bench-timings")]
1250 let t1 = std::time::Instant::now();
1251 let result = Self::from_inputs(raw);
1252 #[cfg(feature = "bench-timings")]
1253 {
1254 let from_inputs_ns = u64::try_from(t1.elapsed().as_nanos()).unwrap_or(u64::MAX);
1255 dataset_timings::COUNTERS.add(dataset_timings::DT_PARSE_NS, parse_ns);
1256 dataset_timings::COUNTERS.add(dataset_timings::DT_FROM_INPUTS_NS, from_inputs_ns);
1257 }
1258 result
1259 }
1260
1261 pub fn from_inputs(inputs: Vec<DetectionInput>) -> Result<Self, EvalError> {
1265 let mut detections = Vec::with_capacity(inputs.len());
1266 let mut next_auto = 1i64;
1267 for input in inputs {
1268 if !input.score.is_finite() {
1269 return Err(EvalError::NonFinite {
1270 context: "detection score",
1271 });
1272 }
1273 let id = match input.id {
1274 Some(id) => id,
1275 None => {
1276 let id = AnnId(next_auto);
1277 next_auto += 1;
1278 id
1279 }
1280 };
1281 detections.push(CocoDetection {
1282 id,
1283 image_id: input.image_id,
1284 category_id: input.category_id,
1285 score: input.score,
1286 bbox: input.bbox,
1287 area: input.bbox.w * input.bbox.h,
1288 segmentation: input.segmentation,
1289 keypoints: input.keypoints,
1290 num_keypoints: input.num_keypoints,
1291 });
1292 }
1293
1294 let mut by_image_cat: HashMap<(ImageId, CategoryId), Vec<usize>> = HashMap::new();
1295 let mut by_image: HashMap<ImageId, Vec<usize>> = HashMap::new();
1296 for (idx, dt) in detections.iter().enumerate() {
1297 by_image_cat
1298 .entry((dt.image_id, dt.category_id))
1299 .or_default()
1300 .push(idx);
1301 by_image.entry(dt.image_id).or_default().push(idx);
1302 }
1303
1304 Ok(Self {
1305 detections: Arc::new(detections),
1306 by_image_cat,
1307 by_image,
1308 })
1309 }
1310
1311 pub fn from_records(records: Vec<CocoDetection>) -> Self {
1317 let mut by_image_cat: HashMap<(ImageId, CategoryId), Vec<usize>> = HashMap::new();
1318 let mut by_image: HashMap<ImageId, Vec<usize>> = HashMap::new();
1319 for (idx, dt) in records.iter().enumerate() {
1320 by_image_cat
1321 .entry((dt.image_id, dt.category_id))
1322 .or_default()
1323 .push(idx);
1324 by_image.entry(dt.image_id).or_default().push(idx);
1325 }
1326 Self {
1327 detections: Arc::new(records),
1328 by_image_cat,
1329 by_image,
1330 }
1331 }
1332
1333 pub fn detections(&self) -> &[CocoDetection] {
1335 &self.detections
1336 }
1337
1338 pub fn indices_for(&self, image: ImageId, cat: CategoryId) -> &[usize] {
1342 self.by_image_cat
1343 .get(&(image, cat))
1344 .map_or(&[][..], Vec::as_slice)
1345 }
1346
1347 pub fn indices_for_image(&self, image: ImageId) -> &[usize] {
1351 self.by_image.get(&image).map_or(&[][..], Vec::as_slice)
1352 }
1353
1354 pub fn lvis_trim(&self, max_dets: i64) -> CocoDetections {
1383 if max_dets < 0 {
1384 return self.clone();
1387 }
1388 let cap = max_dets as usize;
1389 let mut by_image_groups: HashMap<ImageId, Vec<usize>> = HashMap::new();
1390 for (idx, dt) in self.detections.iter().enumerate() {
1391 by_image_groups.entry(dt.image_id).or_default().push(idx);
1392 }
1393 let mut image_ids: Vec<ImageId> = by_image_groups.keys().copied().collect();
1401 image_ids.sort_unstable_by_key(|i| i.0);
1402
1403 let upper_bound = self
1410 .detections
1411 .len()
1412 .min(cap.saturating_mul(image_ids.len()));
1413 let mut out: Vec<CocoDetection> = Vec::with_capacity(upper_bound);
1414 for image_id in image_ids {
1415 let mut group = by_image_groups.remove(&image_id).unwrap_or_default();
1416 group.sort_by(|&a, &b| {
1421 self.detections[b]
1422 .score
1423 .partial_cmp(&self.detections[a].score)
1424 .unwrap_or(std::cmp::Ordering::Equal)
1425 });
1426 for &idx in group.iter().take(cap) {
1427 out.push(self.detections[idx].clone());
1428 }
1429 }
1430 CocoDetections::from_records(out)
1431 }
1432}
1433
1434#[derive(Deserialize)]
1442#[serde(untagged)]
1443enum BoolOrInt {
1444 Bool(bool),
1445 Int(i64),
1446}
1447
1448impl BoolOrInt {
1449 fn into_bool<E: serde::de::Error>(self) -> Result<bool, E> {
1450 match self {
1451 Self::Bool(b) => Ok(b),
1452 Self::Int(0) => Ok(false),
1453 Self::Int(1) => Ok(true),
1454 Self::Int(other) => Err(E::custom(format!(
1455 "expected 0 or 1 for COCO bool field, got {other}"
1456 ))),
1457 }
1458 }
1459}
1460
1461fn deserialize_bool_int<'de, D>(de: D) -> Result<bool, D::Error>
1462where
1463 D: serde::Deserializer<'de>,
1464{
1465 BoolOrInt::deserialize(de)?.into_bool()
1466}
1467
1468fn deserialize_opt_bool_int<'de, D>(de: D) -> Result<Option<bool>, D::Error>
1469where
1470 D: serde::Deserializer<'de>,
1471{
1472 Option::<BoolOrInt>::deserialize(de)?
1473 .map(BoolOrInt::into_bool)
1474 .transpose()
1475}
1476
1477#[cfg(test)]
1478mod tests {
1479 use super::*;
1480 use proptest::prelude::*;
1481
1482 const CROWD_REGION_GT: &str = r#"{
1483 "images": [
1484 {"id": 1, "width": 200, "height": 200, "file_name": "img1.png"}
1485 ],
1486 "annotations": [
1487 {"id": 1, "image_id": 1, "category_id": 1,
1488 "bbox": [100, 100, 50, 50], "area": 2500, "iscrowd": 0},
1489 {"id": 2, "image_id": 1, "category_id": 1,
1490 "bbox": [0, 0, 200, 200], "area": 40000, "iscrowd": 1}
1491 ],
1492 "categories": [
1493 {"id": 1, "name": "widget", "supercategory": "thing"}
1494 ]
1495 }"#;
1496
1497 fn load_crowd_region() -> CocoDataset {
1498 CocoDataset::from_json_bytes(CROWD_REGION_GT.as_bytes()).unwrap()
1499 }
1500
1501 #[test]
1502 fn loads_crowd_region_fixture() {
1503 let ds = load_crowd_region();
1504 assert_eq!(ds.images().len(), 1);
1505 assert_eq!(ds.categories().len(), 1);
1506 assert_eq!(ds.annotations().len(), 2);
1507 assert_eq!(ds.images()[0].file_name.as_deref(), Some("img1.png"));
1508 assert_eq!(ds.categories()[0].name, "widget");
1509 }
1510
1511 #[test]
1512 fn by_image_index_returns_both_anns() {
1513 let ds = load_crowd_region();
1514 let idxs = ds.ann_indices_for_image(ImageId(1));
1515 assert_eq!(idxs.len(), 2);
1516 let anns: Vec<_> = ds.ann_iter_for_image(ImageId(1)).collect();
1517 assert_eq!(anns.len(), 2);
1518 assert_eq!(anns[0].id, AnnId(1));
1519 assert_eq!(anns[1].id, AnnId(2));
1520 }
1521
1522 #[test]
1523 fn by_category_index_returns_both_anns() {
1524 let ds = load_crowd_region();
1525 let idxs = ds.ann_indices_for_category(CategoryId(1));
1526 assert_eq!(idxs.len(), 2);
1527 }
1528
1529 #[test]
1530 fn unknown_image_returns_empty_slice() {
1531 let ds = load_crowd_region();
1532 assert!(ds.ann_indices_for_image(ImageId(999)).is_empty());
1533 assert!(ds.ann_indices_for_category(CategoryId(999)).is_empty());
1534 }
1535
1536 #[test]
1537 fn empty_image_or_category_returns_empty_slice_not_missing() {
1538 const ONLY_EMPTY_IMG: &str = r#"{
1542 "images": [{"id": 7, "width": 1, "height": 1}],
1543 "annotations": [],
1544 "categories": [{"id": 3, "name": "thing"}]
1545 }"#;
1546 let ds = CocoDataset::from_json_bytes(ONLY_EMPTY_IMG.as_bytes()).unwrap();
1547 assert!(ds.ann_indices_for_image(ImageId(7)).is_empty());
1548 assert!(ds.ann_indices_for_category(CategoryId(3)).is_empty());
1549 }
1550
1551 #[test]
1552 fn rejects_annotation_referencing_unknown_image() {
1553 const BAD: &str = r#"{
1554 "images": [{"id": 1, "width": 10, "height": 10}],
1555 "annotations": [
1556 {"id": 1, "image_id": 99, "category_id": 1,
1557 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0}
1558 ],
1559 "categories": [{"id": 1, "name": "thing"}]
1560 }"#;
1561 let err = CocoDataset::from_json_bytes(BAD.as_bytes()).unwrap_err();
1562 match err {
1563 EvalError::InvalidAnnotation { detail } => {
1564 assert!(detail.contains("image_id=99"), "msg: {detail}");
1565 }
1566 other => panic!("expected InvalidAnnotation, got {other:?}"),
1567 }
1568 }
1569
1570 #[test]
1571 fn rejects_annotation_referencing_unknown_category() {
1572 const BAD: &str = r#"{
1573 "images": [{"id": 1, "width": 10, "height": 10}],
1574 "annotations": [
1575 {"id": 1, "image_id": 1, "category_id": 42,
1576 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0}
1577 ],
1578 "categories": [{"id": 1, "name": "thing"}]
1579 }"#;
1580 let err = CocoDataset::from_json_bytes(BAD.as_bytes()).unwrap_err();
1581 match err {
1582 EvalError::InvalidAnnotation { detail } => {
1583 assert!(detail.contains("category_id=42"), "msg: {detail}");
1584 }
1585 other => panic!("expected InvalidAnnotation, got {other:?}"),
1586 }
1587 }
1588
1589 #[test]
1590 fn round_trips_through_json() {
1591 let ds = load_crowd_region();
1592 let json = serde_json::to_string(&ds.to_json_value()).unwrap();
1593 let again = CocoDataset::from_json_bytes(json.as_bytes()).unwrap();
1594 assert_eq!(ds.images(), again.images());
1595 assert_eq!(ds.categories(), again.categories());
1596 assert_eq!(ds.annotations(), again.annotations());
1597 }
1598
1599 #[test]
1602 fn d1_strict_mode_drops_explicit_ignore_field() {
1603 const ANN_JSON: &str = r#"{
1607 "images": [{"id": 1, "width": 10, "height": 10}],
1608 "annotations": [
1609 {"id": 1, "image_id": 1, "category_id": 1,
1610 "bbox": [0, 0, 1, 1], "area": 1,
1611 "iscrowd": 0, "ignore": 1}
1612 ],
1613 "categories": [{"id": 1, "name": "thing"}]
1614 }"#;
1615 let ds = CocoDataset::from_json_bytes(ANN_JSON.as_bytes()).unwrap();
1616 let ann = &ds.annotations()[0];
1617 assert!(!ann.effective_ignore(ParityMode::Strict));
1618 assert!(ann.effective_ignore(ParityMode::Corrected));
1619 }
1620
1621 #[test]
1622 fn d1_strict_mode_uses_iscrowd_when_ignore_absent() {
1623 const ANN_JSON: &str = r#"{
1626 "images": [{"id": 1, "width": 10, "height": 10}],
1627 "annotations": [
1628 {"id": 1, "image_id": 1, "category_id": 1,
1629 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 1}
1630 ],
1631 "categories": [{"id": 1, "name": "thing"}]
1632 }"#;
1633 let ds = CocoDataset::from_json_bytes(ANN_JSON.as_bytes()).unwrap();
1634 let ann = &ds.annotations()[0];
1635 assert!(ann.effective_ignore(ParityMode::Strict));
1636 assert!(ann.effective_ignore(ParityMode::Corrected));
1637 }
1638
1639 #[test]
1642 fn ann_indices_for_image_cat_returns_correct_subset() {
1643 const TWO_CATS: &str = r#"{
1644 "images": [{"id": 1, "width": 10, "height": 10}],
1645 "annotations": [
1646 {"id": 1, "image_id": 1, "category_id": 1,
1647 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0},
1648 {"id": 2, "image_id": 1, "category_id": 2,
1649 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0},
1650 {"id": 3, "image_id": 1, "category_id": 1,
1651 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0}
1652 ],
1653 "categories": [
1654 {"id": 1, "name": "a"}, {"id": 2, "name": "b"}
1655 ]
1656 }"#;
1657 let ds = CocoDataset::from_json_bytes(TWO_CATS.as_bytes()).unwrap();
1658 let cat1: Vec<AnnId> = ds
1659 .ann_indices_for(ImageId(1), CategoryId(1))
1660 .iter()
1661 .map(|&i| ds.annotations()[i].id)
1662 .collect();
1663 assert_eq!(cat1, vec![AnnId(1), AnnId(3)]);
1664 let cat2: Vec<AnnId> = ds
1665 .ann_indices_for(ImageId(1), CategoryId(2))
1666 .iter()
1667 .map(|&i| ds.annotations()[i].id)
1668 .collect();
1669 assert_eq!(cat2, vec![AnnId(2)]);
1670 assert!(ds.ann_indices_for(ImageId(1), CategoryId(99)).is_empty());
1671 assert!(ds.ann_indices_for(ImageId(99), CategoryId(1)).is_empty());
1672 }
1673
1674 fn dt_input(image: i64, cat: i64, score: f64, bbox: (f64, f64, f64, f64)) -> DetectionInput {
1677 DetectionInput {
1678 id: None,
1679 image_id: ImageId(image),
1680 category_id: CategoryId(cat),
1681 score,
1682 bbox: Bbox {
1683 x: bbox.0,
1684 y: bbox.1,
1685 w: bbox.2,
1686 h: bbox.3,
1687 },
1688 segmentation: None,
1689 keypoints: None,
1690 num_keypoints: None,
1691 }
1692 }
1693
1694 #[test]
1695 fn j1_auto_assigns_ids_when_absent() {
1696 let dts = CocoDetections::from_inputs(vec![
1697 dt_input(1, 1, 0.9, (0.0, 0.0, 1.0, 1.0)),
1698 dt_input(1, 1, 0.8, (0.0, 0.0, 1.0, 1.0)),
1699 ])
1700 .unwrap();
1701 let ids: Vec<AnnId> = dts.detections().iter().map(|d| d.id).collect();
1702 assert_eq!(ids, vec![AnnId(1), AnnId(2)]);
1703 }
1704
1705 #[test]
1706 fn j1_preserves_user_supplied_ids() {
1707 let mut a = dt_input(1, 1, 0.9, (0.0, 0.0, 1.0, 1.0));
1708 a.id = Some(AnnId(42));
1709 let mut b = dt_input(1, 1, 0.8, (0.0, 0.0, 1.0, 1.0));
1710 b.id = Some(AnnId(7));
1711 let dts = CocoDetections::from_inputs(vec![a, b]).unwrap();
1712 let ids: Vec<AnnId> = dts.detections().iter().map(|d| d.id).collect();
1713 assert_eq!(ids, vec![AnnId(42), AnnId(7)]);
1714 }
1715
1716 #[test]
1717 fn j3_derives_area_from_bbox() {
1718 let dts =
1719 CocoDetections::from_inputs(vec![dt_input(1, 1, 0.5, (10.0, 10.0, 4.0, 5.0))]).unwrap();
1720 assert_eq!(dts.detections()[0].area, 20.0);
1721 }
1722
1723 #[test]
1724 fn rejects_non_finite_score() {
1725 let err = CocoDetections::from_inputs(vec![dt_input(1, 1, f64::NAN, (0.0, 0.0, 1.0, 1.0))])
1726 .unwrap_err();
1727 assert!(matches!(
1728 err,
1729 EvalError::NonFinite {
1730 context: "detection score"
1731 }
1732 ));
1733 }
1734
1735 #[test]
1736 fn detections_indices_per_image_cat() {
1737 let dts = CocoDetections::from_inputs(vec![
1738 dt_input(1, 1, 0.9, (0.0, 0.0, 1.0, 1.0)),
1739 dt_input(1, 2, 0.8, (0.0, 0.0, 1.0, 1.0)),
1740 dt_input(2, 1, 0.7, (0.0, 0.0, 1.0, 1.0)),
1741 ])
1742 .unwrap();
1743 assert_eq!(dts.indices_for(ImageId(1), CategoryId(1)), &[0]);
1744 assert_eq!(dts.indices_for(ImageId(1), CategoryId(2)), &[1]);
1745 assert_eq!(dts.indices_for(ImageId(2), CategoryId(1)), &[2]);
1746 assert!(dts.indices_for(ImageId(99), CategoryId(1)).is_empty());
1747 let img1: Vec<usize> = dts.indices_for_image(ImageId(1)).to_vec();
1749 assert_eq!(img1, vec![0, 1]);
1750 }
1751
1752 #[test]
1753 fn loads_detections_from_json_array() {
1754 const JSON: &str = r#"[
1755 {"image_id": 1, "category_id": 1, "score": 0.9,
1756 "bbox": [0, 0, 2, 3]},
1757 {"id": 7, "image_id": 1, "category_id": 1, "score": 0.5,
1758 "bbox": [1, 1, 1, 1]}
1759 ]"#;
1760 let dts = CocoDetections::from_json_bytes(JSON.as_bytes()).unwrap();
1761 let ds = dts.detections();
1762 assert_eq!(ds[0].id, AnnId(1)); assert_eq!(ds[0].area, 6.0); assert_eq!(ds[1].id, AnnId(7)); assert!(!ds[0].is_crowd()); assert!(ds[0].segmentation.is_none());
1767 }
1768
1769 #[test]
1772 fn gt_loads_polygon_segmentation() {
1773 const JSON: &str = r#"{
1774 "images": [{"id": 1, "width": 10, "height": 10}],
1775 "annotations": [
1776 {"id": 1, "image_id": 1, "category_id": 1,
1777 "bbox": [0, 0, 4, 4], "area": 16, "iscrowd": 0,
1778 "segmentation": [[0, 0, 4, 0, 4, 4, 0, 4]]}
1779 ],
1780 "categories": [{"id": 1, "name": "thing"}]
1781 }"#;
1782 let ds = CocoDataset::from_json_bytes(JSON.as_bytes()).unwrap();
1783 let seg = ds.annotations()[0].segmentation.as_ref().unwrap();
1784 let rle = seg.to_rle(10, 10).unwrap();
1785 assert_eq!(rle.area(), 16);
1786 }
1787
1788 #[test]
1789 fn gt_loads_compressed_rle_segmentation() {
1790 let counts_str = String::from_utf8(vernier_mask::encode_counts(&[0, 16])).unwrap();
1791 let json = format!(
1792 r#"{{
1793 "images": [{{"id": 1, "width": 4, "height": 4}}],
1794 "annotations": [
1795 {{"id": 1, "image_id": 1, "category_id": 1,
1796 "bbox": [0, 0, 4, 4], "area": 16, "iscrowd": 1,
1797 "segmentation": {{"size": [4, 4], "counts": "{counts_str}"}}}}
1798 ],
1799 "categories": [{{"id": 1, "name": "thing"}}]
1800 }}"#
1801 );
1802 let ds = CocoDataset::from_json_bytes(json.as_bytes()).unwrap();
1803 let seg = ds.annotations()[0].segmentation.as_ref().unwrap();
1804 let rle = seg.to_rle(4, 4).unwrap();
1805 assert_eq!((rle.h, rle.w), (4, 4));
1806 assert_eq!(rle.area(), 16);
1807 }
1808
1809 #[test]
1810 fn gt_segmentation_round_trips_through_to_json_value() {
1811 const JSON: &str = r#"{
1812 "images": [{"id": 1, "width": 10, "height": 10}],
1813 "annotations": [
1814 {"id": 1, "image_id": 1, "category_id": 1,
1815 "bbox": [0, 0, 4, 4], "area": 16, "iscrowd": 0,
1816 "segmentation": [[0, 0, 4, 0, 4, 4, 0, 4]]}
1817 ],
1818 "categories": [{"id": 1, "name": "thing"}]
1819 }"#;
1820 let ds = CocoDataset::from_json_bytes(JSON.as_bytes()).unwrap();
1821 let serialized = serde_json::to_string(&ds.to_json_value()).unwrap();
1822 let again = CocoDataset::from_json_bytes(serialized.as_bytes()).unwrap();
1823 assert_eq!(ds.annotations(), again.annotations());
1824 }
1825
1826 #[test]
1827 fn gt_without_segmentation_field_loads_as_none() {
1828 let ds = load_crowd_region();
1829 assert!(ds.annotations().iter().all(|a| a.segmentation.is_none()));
1830 }
1831
1832 #[test]
1833 fn dt_loads_compressed_rle_segmentation() {
1834 const JSON: &str = r#"[
1835 {"image_id": 1, "category_id": 1, "score": 0.9,
1836 "bbox": [0, 0, 4, 4],
1837 "segmentation": {"size": [4, 4], "counts": "04L4"}}
1838 ]"#;
1839 let dts = CocoDetections::from_json_bytes(JSON.as_bytes()).unwrap();
1840 assert!(dts.detections()[0].segmentation.is_some());
1841 }
1842
1843 #[test]
1844 fn dt_without_segmentation_loads_as_none() {
1845 const JSON: &str = r#"[
1846 {"image_id": 1, "category_id": 1, "score": 0.9, "bbox": [0, 0, 1, 1]}
1847 ]"#;
1848 let dts = CocoDetections::from_json_bytes(JSON.as_bytes()).unwrap();
1849 assert!(dts.detections()[0].segmentation.is_none());
1850 }
1851
1852 fn arb_image() -> impl Strategy<Value = ImageMeta> {
1855 (1i64..1000, 1u32..2048, 1u32..2048).prop_map(|(id, w, h)| ImageMeta {
1856 id: ImageId(id),
1857 width: w,
1858 height: h,
1859 file_name: None,
1860 })
1861 }
1862
1863 fn arb_category() -> impl Strategy<Value = CategoryMeta> {
1864 (1i64..100, "[a-z]{1,8}").prop_map(|(id, name)| CategoryMeta {
1865 id: CategoryId(id),
1866 name,
1867 supercategory: None,
1868 })
1869 }
1870
1871 fn make_min_annotation(
1875 id: AnnId,
1876 image_id: ImageId,
1877 category_id: CategoryId,
1878 ) -> CocoAnnotation {
1879 CocoAnnotation {
1880 id,
1881 image_id,
1882 category_id,
1883 area: 25.0,
1884 is_crowd: false,
1885 ignore_flag: None,
1886 bbox: Bbox {
1887 x: 0.0,
1888 y: 0.0,
1889 w: 5.0,
1890 h: 5.0,
1891 },
1892 segmentation: None,
1893 keypoints: None,
1894 num_keypoints: None,
1895 }
1896 }
1897
1898 proptest! {
1899 #![proptest_config(ProptestConfig::with_cases(64))]
1900
1901 #[test]
1902 fn index_invariants_hold(
1903 images in proptest::collection::vec(arb_image(), 1..6),
1910 categories in proptest::collection::vec(arb_category(), 1..6),
1911 n_anns in 0usize..40,
1912 ann_seed in any::<u64>(),
1913 ) {
1914 let mut images = images;
1918 images.sort_by_key(|i| i.id);
1919 images.dedup_by_key(|i| i.id);
1920 let mut categories = categories;
1921 categories.sort_by_key(|c| c.id);
1922 categories.dedup_by_key(|c| c.id);
1923
1924 let mut state = ann_seed.wrapping_add(1);
1927 let mut next = || {
1928 state = state.wrapping_mul(6364136223846793005)
1929 .wrapping_add(1442695040888963407);
1930 state
1931 };
1932
1933 let mut annotations = Vec::with_capacity(n_anns);
1934 for ann_idx in 0..n_anns {
1935 let img = &images[(next() as usize) % images.len()];
1936 let cat = &categories[(next() as usize) % categories.len()];
1937 annotations.push(CocoAnnotation {
1938 id: AnnId(ann_idx as i64 + 1),
1939 image_id: img.id,
1940 category_id: cat.id,
1941 area: 1.0,
1942 is_crowd: false,
1943 ignore_flag: None,
1944 bbox: Bbox { x: 0.0, y: 0.0, w: 1.0, h: 1.0 },
1945 segmentation: None,
1946 keypoints: None,
1947 num_keypoints: None,
1948 });
1949 }
1950
1951 let ds = CocoDataset::from_parts(
1952 images.clone(), annotations.clone(), categories.clone()
1953 ).unwrap();
1954
1955 let mut seen_img: Vec<usize> = images.iter()
1959 .flat_map(|i| ds.ann_indices_for_image(i.id).iter().copied())
1960 .collect();
1961 seen_img.sort_unstable();
1962 let expected: Vec<usize> = (0..annotations.len()).collect();
1963 prop_assert_eq!(&seen_img, &expected);
1964
1965 let mut seen_cat: Vec<usize> = categories.iter()
1966 .flat_map(|c| ds.ann_indices_for_category(c.id).iter().copied())
1967 .collect();
1968 seen_cat.sort_unstable();
1969 prop_assert_eq!(&seen_cat, &expected);
1970
1971 for img in &images {
1973 for &idx in ds.ann_indices_for_image(img.id) {
1974 prop_assert_eq!(ds.annotations()[idx].image_id, img.id);
1975 }
1976 }
1977 for cat in &categories {
1978 for &idx in ds.ann_indices_for_category(cat.id) {
1979 prop_assert_eq!(ds.annotations()[idx].category_id, cat.id);
1980 }
1981 }
1982 }
1983 }
1984
1985 const LVIS_MIN_VALID: &str = r#"{
1993 "images": [
1994 {"id": 1, "width": 100, "height": 100,
1995 "neg_category_ids": [2], "not_exhaustive_category_ids": []},
1996 {"id": 2, "width": 100, "height": 100,
1997 "neg_category_ids": [], "not_exhaustive_category_ids": [2]}
1998 ],
1999 "annotations": [
2000 {"id": 1, "image_id": 1, "category_id": 1,
2001 "bbox": [0, 0, 10, 10], "area": 100, "iscrowd": 0},
2002 {"id": 2, "image_id": 2, "category_id": 2,
2003 "bbox": [0, 0, 20, 20], "area": 400, "iscrowd": 0}
2004 ],
2005 "categories": [
2006 {"id": 1, "name": "a", "frequency": "f"},
2007 {"id": 2, "name": "b", "frequency": "r"}
2008 ]
2009 }"#;
2010
2011 #[test]
2012 fn lvis_loads_minimal_valid_dataset() {
2013 let ds = CocoDataset::from_lvis_json_bytes(LVIS_MIN_VALID.as_bytes()).unwrap();
2014 assert_eq!(ds.images().len(), 2);
2016 assert_eq!(ds.categories().len(), 2);
2017 assert_eq!(ds.annotations().len(), 2);
2018 assert!(ds.is_federated());
2020 let pos = ds.pos_category_ids().unwrap();
2021 let neg = ds.neg_category_ids().unwrap();
2022 let nel = ds.not_exhaustive_category_ids().unwrap();
2023 let freq = ds.category_frequency().unwrap();
2024 assert_eq!(pos[&ImageId(1)], HashSet::from([CategoryId(1)]));
2026 assert_eq!(pos[&ImageId(2)], HashSet::from([CategoryId(2)]));
2027 assert_eq!(neg[&ImageId(1)], HashSet::from([CategoryId(2)]));
2029 assert_eq!(neg[&ImageId(2)], HashSet::new());
2030 assert_eq!(nel[&ImageId(1)], HashSet::new());
2032 assert_eq!(nel[&ImageId(2)], HashSet::from([CategoryId(2)]));
2033 assert_eq!(freq[&CategoryId(1)], Frequency::Frequent);
2035 assert_eq!(freq[&CategoryId(2)], Frequency::Rare);
2036 }
2037
2038 #[test]
2039 fn aa1_pos_derived_from_gts_does_not_include_zero_ann_categories() {
2040 let ds = CocoDataset::from_lvis_json_bytes(LVIS_MIN_VALID.as_bytes()).unwrap();
2043 let pos = ds.pos_category_ids().unwrap();
2044 assert!(!pos[&ImageId(1)].contains(&CategoryId(2)));
2045 assert!(!pos[&ImageId(2)].contains(&CategoryId(1)));
2046 }
2047
2048 #[test]
2049 fn from_json_bytes_leaves_federated_metadata_none() {
2050 let ds = CocoDataset::from_json_bytes(LVIS_MIN_VALID.as_bytes()).unwrap();
2054 assert!(!ds.is_federated());
2055 assert!(ds.pos_category_ids().is_none());
2056 assert!(ds.neg_category_ids().is_none());
2057 assert!(ds.not_exhaustive_category_ids().is_none());
2058 assert!(ds.category_frequency().is_none());
2059 }
2060
2061 #[test]
2062 fn aa7_pos_intersect_neg_rejected() {
2063 const BAD: &str = r#"{
2066 "images": [
2067 {"id": 1, "width": 10, "height": 10,
2068 "neg_category_ids": [1], "not_exhaustive_category_ids": []}
2069 ],
2070 "annotations": [
2071 {"id": 1, "image_id": 1, "category_id": 1,
2072 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0}
2073 ],
2074 "categories": [{"id": 1, "name": "a", "frequency": "f"}]
2075 }"#;
2076 let err = CocoDataset::from_lvis_json_bytes(BAD.as_bytes()).unwrap_err();
2077 match err {
2078 EvalError::LvisFederatedConflict {
2079 image_id,
2080 category_id,
2081 detail,
2082 } => {
2083 assert_eq!(image_id, 1);
2084 assert_eq!(category_id, 1);
2085 assert!(detail.contains("GT"));
2086 }
2087 other => panic!("expected LvisFederatedConflict, got {other:?}"),
2088 }
2089 }
2090
2091 #[test]
2092 fn aa7_not_exhaustive_outside_pos_rejected() {
2093 const BAD: &str = r#"{
2096 "images": [
2097 {"id": 1, "width": 10, "height": 10,
2098 "neg_category_ids": [], "not_exhaustive_category_ids": [2]}
2099 ],
2100 "annotations": [
2101 {"id": 1, "image_id": 1, "category_id": 1,
2102 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0}
2103 ],
2104 "categories": [
2105 {"id": 1, "name": "a", "frequency": "f"},
2106 {"id": 2, "name": "b", "frequency": "r"}
2107 ]
2108 }"#;
2109 let err = CocoDataset::from_lvis_json_bytes(BAD.as_bytes()).unwrap_err();
2110 match err {
2111 EvalError::LvisFederatedConflict {
2112 image_id,
2113 category_id,
2114 detail,
2115 } => {
2116 assert_eq!(image_id, 1);
2117 assert_eq!(category_id, 2);
2118 assert!(detail.contains("not_exhaustive"));
2119 }
2120 other => panic!("expected LvisFederatedConflict, got {other:?}"),
2121 }
2122 }
2123
2124 #[test]
2125 fn ab6_missing_frequency_collects_all_offenders() {
2126 const BAD: &str = r#"{
2129 "images": [
2130 {"id": 1, "width": 10, "height": 10,
2131 "neg_category_ids": [], "not_exhaustive_category_ids": []}
2132 ],
2133 "annotations": [],
2134 "categories": [
2135 {"id": 7, "name": "g"},
2136 {"id": 3, "name": "c"}
2137 ]
2138 }"#;
2139 let err = CocoDataset::from_lvis_json_bytes(BAD.as_bytes()).unwrap_err();
2140 match err {
2141 EvalError::MissingFrequency { category_ids } => {
2142 assert_eq!(category_ids, vec![3, 7]);
2143 }
2144 other => panic!("expected MissingFrequency, got {other:?}"),
2145 }
2146 }
2147
2148 #[test]
2149 fn lvis_loader_treats_absent_neg_field_as_empty() {
2150 const TOLERANT: &str = r#"{
2154 "images": [{"id": 1, "width": 10, "height": 10}],
2155 "annotations": [],
2156 "categories": [{"id": 1, "name": "a", "frequency": "c"}]
2157 }"#;
2158 let ds = CocoDataset::from_lvis_json_bytes(TOLERANT.as_bytes()).unwrap();
2159 let neg = ds.neg_category_ids().unwrap();
2160 let nel = ds.not_exhaustive_category_ids().unwrap();
2161 assert!(neg[&ImageId(1)].is_empty());
2162 assert!(nel[&ImageId(1)].is_empty());
2163 }
2164
2165 #[test]
2166 fn frequency_round_trips_serde() {
2167 for f in [Frequency::Rare, Frequency::Common, Frequency::Frequent] {
2168 let s = serde_json::to_string(&f).unwrap();
2169 let back: Frequency = serde_json::from_str(&s).unwrap();
2170 assert_eq!(f, back);
2171 }
2172 assert_eq!(serde_json::to_string(&Frequency::Rare).unwrap(), "\"r\"");
2174 assert_eq!(serde_json::to_string(&Frequency::Common).unwrap(), "\"c\"");
2175 assert_eq!(
2176 serde_json::to_string(&Frequency::Frequent).unwrap(),
2177 "\"f\""
2178 );
2179 }
2180
2181 #[test]
2184 fn ac2_q1_trims_500_single_category_to_300() {
2185 let dts = CocoDetections::from_inputs(
2189 (0..500)
2190 .map(|i| {
2191 let score = 1.0 - (i as f64) / 1000.0; dt_input(1, 1, score, (0.0, 0.0, 1.0, 1.0))
2193 })
2194 .collect(),
2195 )
2196 .unwrap();
2197 let trimmed = dts.lvis_trim(300);
2198 assert_eq!(trimmed.detections().len(), 300);
2199 let scores: Vec<f64> = trimmed.detections().iter().map(|d| d.score).collect();
2201 for w in scores.windows(2) {
2202 assert!(
2203 w[0] >= w[1],
2204 "lvis_trim must preserve score-descending order"
2205 );
2206 }
2207 assert!((scores[0] - 1.0).abs() < 1e-12);
2208 assert!((scores[299] - 0.701).abs() < 1e-12);
2211 }
2212
2213 #[test]
2214 fn ac3_q2_cross_class_crowding_keeps_300_total_across_classes() {
2215 let mut inputs = Vec::with_capacity(600);
2222 for i in 0..250 {
2223 let score = 0.5 - (i as f64) * 0.002;
2225 inputs.push(dt_input(1, 1, score, (0.0, 0.0, 1.0, 1.0)));
2226 }
2227 for i in 0..350 {
2228 let score = 1.0 - (i as f64) * 0.002;
2235 inputs.push(dt_input(1, 2, score, (0.0, 0.0, 1.0, 1.0)));
2236 }
2237 let dts = CocoDetections::from_inputs(inputs).unwrap();
2238 let trimmed = dts.lvis_trim(300);
2239 assert_eq!(trimmed.detections().len(), 300);
2241 let n_cat1 = trimmed
2245 .detections()
2246 .iter()
2247 .filter(|d| d.category_id == CategoryId(1))
2248 .count();
2249 let n_cat2 = trimmed
2250 .detections()
2251 .iter()
2252 .filter(|d| d.category_id == CategoryId(2))
2253 .count();
2254 assert_eq!(n_cat1 + n_cat2, 300);
2258 assert!(n_cat1 > 0, "cat 1 must keep at least its top-score entries");
2261 assert!(n_cat2 > 0, "cat 2 must keep its high-score entries");
2262 }
2263
2264 #[test]
2265 fn ac5_negative_max_dets_disables_trim() {
2266 let dts = CocoDetections::from_inputs(
2270 (0..50)
2271 .map(|i| dt_input(1, 1, i as f64 / 100.0, (0.0, 0.0, 1.0, 1.0)))
2272 .collect(),
2273 )
2274 .unwrap();
2275 let trimmed = dts.lvis_trim(-1);
2276 assert_eq!(trimmed.detections().len(), 50);
2277 for (i, dt) in trimmed.detections().iter().enumerate() {
2279 assert!((dt.score - (i as f64 / 100.0)).abs() < 1e-12);
2280 }
2281 }
2282
2283 #[test]
2284 fn ac5_max_dets_at_capacity_is_no_op() {
2285 let dts = CocoDetections::from_inputs(
2290 (0..10)
2291 .map(|i| dt_input(1, 1, i as f64 / 10.0, (0.0, 0.0, 1.0, 1.0)))
2292 .collect(),
2293 )
2294 .unwrap();
2295 let trimmed = dts.lvis_trim(100);
2296 assert_eq!(trimmed.detections().len(), 10);
2297 }
2298
2299 #[test]
2300 fn ac4_stable_sort_preserves_input_order_for_score_ties() {
2301 let mut a = dt_input(1, 1, 0.5, (0.0, 0.0, 1.0, 1.0));
2307 a.id = Some(AnnId(100));
2308 let mut b = dt_input(1, 1, 0.5, (1.0, 0.0, 1.0, 1.0));
2309 b.id = Some(AnnId(200));
2310 let dts = CocoDetections::from_inputs(vec![a, b]).unwrap();
2311 let trimmed = dts.lvis_trim(2);
2312 let ids: Vec<AnnId> = trimmed.detections().iter().map(|d| d.id).collect();
2313 assert_eq!(
2314 ids,
2315 vec![AnnId(100), AnnId(200)],
2316 "AC4: stable sort must preserve input order on score ties"
2317 );
2318 }
2319
2320 #[test]
2321 fn lvis_trim_groups_by_image_id() {
2322 let mut inputs = Vec::with_capacity(15);
2327 for img in 1..=3i64 {
2328 for i in 0..5 {
2329 let score = 1.0 - (img as f64) * 0.01 - (i as f64) * 0.001;
2330 inputs.push(dt_input(img, img, score, (0.0, 0.0, 1.0, 1.0)));
2331 }
2332 }
2333 let dts = CocoDetections::from_inputs(inputs).unwrap();
2334 let trimmed = dts.lvis_trim(2);
2335 assert_eq!(trimmed.detections().len(), 6);
2336 for img in 1..=3i64 {
2338 let n = trimmed
2339 .detections()
2340 .iter()
2341 .filter(|d| d.image_id == ImageId(img))
2342 .count();
2343 assert_eq!(n, 2, "image {img} must trim to 2");
2344 }
2345 }
2346
2347 #[test]
2348 fn lvis_trim_zero_max_dets_keeps_nothing() {
2349 let dts = CocoDetections::from_inputs(vec![
2350 dt_input(1, 1, 0.9, (0.0, 0.0, 1.0, 1.0)),
2351 dt_input(1, 1, 0.5, (0.0, 0.0, 1.0, 1.0)),
2352 ])
2353 .unwrap();
2354 let trimmed = dts.lvis_trim(0);
2355 assert!(trimmed.detections().is_empty());
2356 }
2357
2358 #[test]
2359 fn lvis_loader_inherits_invalid_annotation_validation() {
2360 const BAD: &str = r#"{
2363 "images": [
2364 {"id": 1, "width": 10, "height": 10,
2365 "neg_category_ids": [], "not_exhaustive_category_ids": []}
2366 ],
2367 "annotations": [
2368 {"id": 1, "image_id": 99, "category_id": 1,
2369 "bbox": [0, 0, 1, 1], "area": 1, "iscrowd": 0}
2370 ],
2371 "categories": [{"id": 1, "name": "a", "frequency": "f"}]
2372 }"#;
2373 let err = CocoDataset::from_lvis_json_bytes(BAD.as_bytes()).unwrap_err();
2374 assert!(matches!(err, EvalError::InvalidAnnotation { .. }));
2375 }
2376
2377 #[test]
2382 fn dataset_hash_is_stable_for_equal_inputs() {
2383 let a = load_crowd_region();
2384 let b = load_crowd_region();
2385 assert_eq!(a.dataset_hash(), b.dataset_hash());
2386 }
2387
2388 #[test]
2389 fn dataset_hash_caches_via_arc_clone() {
2390 let a = load_crowd_region();
2394 let b = a.clone();
2395 let h1 = a.dataset_hash();
2396 let h2 = b.dataset_hash();
2397 assert_eq!(h1, h2);
2398 }
2399
2400 #[test]
2401 fn dataset_hash_invariant_to_image_order() {
2402 let order_a = r#"{
2405 "images": [
2406 {"id": 1, "width": 10, "height": 10},
2407 {"id": 2, "width": 20, "height": 20}
2408 ],
2409 "annotations": [
2410 {"id": 1, "image_id": 1, "category_id": 1,
2411 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0}
2412 ],
2413 "categories": [{"id": 1, "name": "x"}]
2414 }"#;
2415 let order_b = r#"{
2416 "images": [
2417 {"id": 2, "width": 20, "height": 20},
2418 {"id": 1, "width": 10, "height": 10}
2419 ],
2420 "annotations": [
2421 {"id": 1, "image_id": 1, "category_id": 1,
2422 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0}
2423 ],
2424 "categories": [{"id": 1, "name": "x"}]
2425 }"#;
2426 let a = CocoDataset::from_json_bytes(order_a.as_bytes()).unwrap();
2427 let b = CocoDataset::from_json_bytes(order_b.as_bytes()).unwrap();
2428 assert_eq!(a.dataset_hash(), b.dataset_hash());
2429 }
2430
2431 #[test]
2432 fn dataset_hash_invariant_to_annotation_order() {
2433 let order_a = r#"{
2434 "images": [{"id": 1, "width": 200, "height": 200}],
2435 "annotations": [
2436 {"id": 1, "image_id": 1, "category_id": 1,
2437 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0},
2438 {"id": 2, "image_id": 1, "category_id": 1,
2439 "bbox": [10, 10, 5, 5], "area": 25, "iscrowd": 0}
2440 ],
2441 "categories": [{"id": 1, "name": "x"}]
2442 }"#;
2443 let order_b = r#"{
2444 "images": [{"id": 1, "width": 200, "height": 200}],
2445 "annotations": [
2446 {"id": 2, "image_id": 1, "category_id": 1,
2447 "bbox": [10, 10, 5, 5], "area": 25, "iscrowd": 0},
2448 {"id": 1, "image_id": 1, "category_id": 1,
2449 "bbox": [0, 0, 5, 5], "area": 25, "iscrowd": 0}
2450 ],
2451 "categories": [{"id": 1, "name": "x"}]
2452 }"#;
2453 let a = CocoDataset::from_json_bytes(order_a.as_bytes()).unwrap();
2454 let b = CocoDataset::from_json_bytes(order_b.as_bytes()).unwrap();
2455 assert_eq!(a.dataset_hash(), b.dataset_hash());
2456 }
2457
2458 #[test]
2459 fn dataset_hash_changes_when_bbox_changes_by_one_pixel() {
2460 let base = r#"{
2461 "images": [{"id": 1, "width": 200, "height": 200}],
2462 "annotations": [
2463 {"id": 1, "image_id": 1, "category_id": 1,
2464 "bbox": [10, 10, 5, 5], "area": 25, "iscrowd": 0}
2465 ],
2466 "categories": [{"id": 1, "name": "x"}]
2467 }"#;
2468 let shifted = r#"{
2469 "images": [{"id": 1, "width": 200, "height": 200}],
2470 "annotations": [
2471 {"id": 1, "image_id": 1, "category_id": 1,
2472 "bbox": [11, 10, 5, 5], "area": 25, "iscrowd": 0}
2473 ],
2474 "categories": [{"id": 1, "name": "x"}]
2475 }"#;
2476 let a = CocoDataset::from_json_bytes(base.as_bytes()).unwrap();
2477 let b = CocoDataset::from_json_bytes(shifted.as_bytes()).unwrap();
2478 assert_ne!(a.dataset_hash(), b.dataset_hash());
2479 }
2480
2481 proptest! {
2482 #[test]
2483 fn dataset_hash_invariant_under_id_shuffle(
2484 mut images in proptest::collection::vec(arb_image(), 1..16),
2485 categories in proptest::collection::vec(arb_category(), 1..4),
2486 ) {
2487 images.sort_by_key(|im| im.id.0);
2491 images.dedup_by_key(|im| im.id.0);
2492 let mut unique_categories = categories;
2493 unique_categories.sort_by_key(|c| c.id.0);
2494 unique_categories.dedup_by_key(|c| c.id.0);
2495 prop_assume!(!images.is_empty());
2496 prop_assume!(!unique_categories.is_empty());
2497
2498 let cat_id = unique_categories[0].id;
2502 let annotations: Vec<CocoAnnotation> = images
2503 .iter()
2504 .enumerate()
2505 .map(|(i, im)| make_min_annotation(AnnId((i as i64) + 1), im.id, cat_id))
2506 .collect();
2507 let mut shuffled = images.clone();
2508 shuffled.reverse();
2509
2510 let a = CocoDataset::from_parts(
2511 images,
2512 annotations.clone(),
2513 unique_categories.clone(),
2514 ).unwrap();
2515 let b = CocoDataset::from_parts(
2516 shuffled,
2517 annotations,
2518 unique_categories,
2519 ).unwrap();
2520 prop_assert_eq!(a.dataset_hash(), b.dataset_hash());
2521 }
2522 }
2523
2524 #[test]
2529 fn params_hash_is_stable_for_equal_inputs() {
2530 use crate::evaluate::OwnedEvaluateParams;
2531 let a = OwnedEvaluateParams {
2532 iou_thresholds: vec![0.5, 0.55, 0.6],
2533 area_ranges: vec![],
2534 max_dets_per_image: 100,
2535 use_cats: true,
2536 retain_iou: false,
2537 };
2538 let b = a.clone();
2539 assert_eq!(a.params_hash().unwrap(), b.params_hash().unwrap());
2540 }
2541
2542 #[test]
2543 fn params_hash_changes_when_thresholds_change() {
2544 use crate::evaluate::OwnedEvaluateParams;
2545 let a = OwnedEvaluateParams {
2546 iou_thresholds: vec![0.5, 0.55, 0.6],
2547 area_ranges: vec![],
2548 max_dets_per_image: 100,
2549 use_cats: true,
2550 retain_iou: false,
2551 };
2552 let mut b = a.clone();
2553 b.iou_thresholds.push(0.65);
2554 assert_ne!(a.params_hash().unwrap(), b.params_hash().unwrap());
2555 }
2556
2557 #[test]
2558 fn params_hash_changes_when_use_cats_toggles() {
2559 use crate::evaluate::OwnedEvaluateParams;
2560 let a = OwnedEvaluateParams {
2561 iou_thresholds: vec![0.5],
2562 area_ranges: vec![],
2563 max_dets_per_image: 100,
2564 use_cats: true,
2565 retain_iou: false,
2566 };
2567 let mut b = a.clone();
2568 b.use_cats = false;
2569 assert_ne!(a.params_hash().unwrap(), b.params_hash().unwrap());
2570 }
2571}