valor-lite 0.33.2__py3-none-any.whl → 0.33.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of valor-lite might be problematic. Click here for more details.

@@ -3,12 +3,20 @@ from dataclasses import dataclass
3
3
 
4
4
  import numpy as np
5
5
  from numpy.typing import NDArray
6
+ from shapely.geometry import Polygon as ShapelyPolygon
6
7
  from tqdm import tqdm
7
- from valor_lite.detection.annotation import Detection
8
+ from valor_lite.detection.annotation import (
9
+ Bitmask,
10
+ BoundingBox,
11
+ Detection,
12
+ Polygon,
13
+ )
8
14
  from valor_lite.detection.computation import (
15
+ compute_bbox_iou,
16
+ compute_bitmask_iou,
9
17
  compute_detailed_counts,
10
- compute_iou,
11
18
  compute_metrics,
19
+ compute_polygon_iou,
12
20
  compute_ranked_pairs,
13
21
  )
14
22
  from valor_lite.detection.metric import (
@@ -35,7 +43,7 @@ Usage
35
43
  -----
36
44
 
37
45
  loader = DataLoader()
38
- loader.add_data(
46
+ loader.add_bounding_boxes(
39
47
  groundtruths=groundtruths,
40
48
  predictions=predictions,
41
49
  )
@@ -51,6 +59,103 @@ filtered_metrics = evaluator.evaluate(iou_thresholds=[0.5], filter_mask=filter_m
51
59
  """
52
60
 
53
61
 
62
+ def _get_valor_dict_annotation_key(
63
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask],
64
+ ) -> str:
65
+ """Get the correct JSON key to extract a given annotation type."""
66
+
67
+ if issubclass(annotation_type, BoundingBox):
68
+ return "bounding_box"
69
+ if issubclass(annotation_type, Polygon):
70
+ return "polygon"
71
+ else:
72
+ return "raster"
73
+
74
+
75
+ def _get_annotation_representation(
76
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask],
77
+ ) -> str:
78
+ """Get the correct representation of an annotation object."""
79
+
80
+ representation = (
81
+ "extrema"
82
+ if issubclass(annotation_type, BoundingBox)
83
+ else ("mask" if issubclass(annotation_type, Bitmask) else "shape")
84
+ )
85
+
86
+ return representation
87
+
88
+
89
+ def _get_annotation_representation_from_valor_dict(
90
+ data: list,
91
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask],
92
+ ) -> tuple[float, float, float, float] | ShapelyPolygon | NDArray[np.bool_]:
93
+ """Get the correct representation of an annotation object from a valor dictionary."""
94
+
95
+ if issubclass(annotation_type, BoundingBox):
96
+ x = [point[0] for shape in data for point in shape]
97
+ y = [point[1] for shape in data for point in shape]
98
+ return (min(x), max(x), min(y), max(y))
99
+ if issubclass(annotation_type, Polygon):
100
+ return ShapelyPolygon(data)
101
+ else:
102
+ return np.array(data)
103
+
104
+
105
+ def _get_annotation_data(
106
+ keyed_groundtruths: dict,
107
+ keyed_predictions: dict,
108
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask] | None,
109
+ key=int,
110
+ ) -> np.ndarray:
111
+ """Create an array of annotation pairs for use when calculating IOU. Needed because we unpack bounding box representations, but not bitmask or polygon representations."""
112
+ if annotation_type == BoundingBox:
113
+ return np.array(
114
+ [
115
+ np.array([*gextrema, *pextrema])
116
+ for _, _, _, pextrema in keyed_predictions[key]
117
+ for _, _, gextrema in keyed_groundtruths[key]
118
+ ]
119
+ )
120
+ else:
121
+ return np.array(
122
+ [
123
+ np.array([groundtruth_obj, prediction_obj])
124
+ for _, _, _, prediction_obj in keyed_predictions[key]
125
+ for _, _, groundtruth_obj in keyed_groundtruths[key]
126
+ ]
127
+ )
128
+
129
+
130
+ def compute_iou(
131
+ data: NDArray[np.floating],
132
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask],
133
+ ) -> NDArray[np.floating]:
134
+ """
135
+ Computes intersection-over-union (IoU) calculations for various annotation types.
136
+
137
+ Parameters
138
+ ----------
139
+ data : NDArray[np.floating]
140
+ A sorted array of bounding box, bitmask, or polygon pairs.
141
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask]
142
+ The type of annotation contained in the data.
143
+
144
+
145
+ Returns
146
+ -------
147
+ NDArray[np.floating]
148
+ Computed IoU's.
149
+ """
150
+
151
+ if annotation_type == BoundingBox:
152
+ return compute_bbox_iou(data=data)
153
+ elif annotation_type == Bitmask:
154
+ return compute_bitmask_iou(data=data)
155
+ else:
156
+ return compute_polygon_iou(data=data)
157
+
158
+
54
159
  @dataclass
55
160
  class Filter:
56
161
  indices: NDArray[np.int32]
@@ -74,6 +179,10 @@ class Evaluator:
74
179
  self.uid_to_index: dict[str, int] = dict()
75
180
  self.index_to_uid: dict[int, str] = dict()
76
181
 
182
+ # annotation reference
183
+ self.groundtruth_examples: dict[int, NDArray[np.float16]] = dict()
184
+ self.prediction_examples: dict[int, NDArray[np.float16]] = dict()
185
+
77
186
  # label reference
78
187
  self.label_to_index: dict[tuple[str, str], int] = dict()
79
188
  self.index_to_label: dict[int, tuple[str, str]] = dict()
@@ -84,10 +193,10 @@ class Evaluator:
84
193
  self.label_index_to_label_key_index: dict[int, int] = dict()
85
194
 
86
195
  # computation caches
87
- self._detailed_pairs = np.array([])
88
- self._ranked_pairs = np.array([])
89
- self._label_metadata = np.array([])
90
- self._label_metadata_per_datum = np.array([])
196
+ self._detailed_pairs: NDArray[np.floating] = np.array([])
197
+ self._ranked_pairs: NDArray[np.floating] = np.array([])
198
+ self._label_metadata: NDArray[np.int32] = np.array([])
199
+ self._label_metadata_per_datum: NDArray[np.int32] = np.array([])
91
200
 
92
201
  @property
93
202
  def ignored_prediction_labels(self) -> list[tuple[str, str]]:
@@ -163,46 +272,35 @@ class Evaluator:
163
272
  [self.uid_to_index[uid] for uid in datum_uids],
164
273
  dtype=np.int32,
165
274
  )
166
- mask = np.zeros_like(mask_pairs, dtype=np.bool_)
167
- mask[
168
- np.isin(self._ranked_pairs[:, 0].astype(int), datum_uids)
169
- ] = True
170
- mask_pairs &= mask
171
-
172
- mask = np.zeros_like(mask_datums, dtype=np.bool_)
173
- mask[datum_uids] = True
174
- mask_datums &= mask
275
+ mask_pairs[
276
+ ~np.isin(self._ranked_pairs[:, 0].astype(int), datum_uids)
277
+ ] = False
278
+ mask_datums[~np.isin(np.arange(n_datums), datum_uids)] = False
175
279
 
176
280
  if labels is not None:
177
281
  if isinstance(labels, list):
178
282
  labels = np.array(
179
283
  [self.label_to_index[label] for label in labels]
180
284
  )
181
- mask = np.zeros_like(mask_pairs, dtype=np.bool_)
182
- mask[np.isin(self._ranked_pairs[:, 4].astype(int), labels)] = True
183
- mask_pairs &= mask
184
-
185
- mask = np.zeros_like(mask_labels, dtype=np.bool_)
186
- mask[labels] = True
187
- mask_labels &= mask
285
+ mask_pairs[
286
+ ~np.isin(self._ranked_pairs[:, 4].astype(int), labels)
287
+ ] = False
288
+ mask_labels[~np.isin(np.arange(n_labels), labels)] = False
188
289
 
189
290
  if label_keys is not None:
190
291
  if isinstance(label_keys, list):
191
292
  label_keys = np.array(
192
293
  [self.label_key_to_index[key] for key in label_keys]
193
294
  )
194
- label_indices = np.where(
195
- np.isclose(self._label_metadata[:, 2], label_keys)
196
- )[0]
197
- mask = np.zeros_like(mask_pairs, dtype=np.bool_)
198
- mask[
199
- np.isin(self._ranked_pairs[:, 4].astype(int), label_indices)
200
- ] = True
201
- mask_pairs &= mask
202
-
203
- mask = np.zeros_like(mask_labels, dtype=np.bool_)
204
- mask[label_indices] = True
205
- mask_labels &= mask
295
+ label_indices = (
296
+ np.where(np.isclose(self._label_metadata[:, 2], label_keys))[0]
297
+ if label_keys.size > 0
298
+ else np.array([])
299
+ )
300
+ mask_pairs[
301
+ ~np.isin(self._ranked_pairs[:, 4].astype(int), label_indices)
302
+ ] = False
303
+ mask_labels[~np.isin(np.arange(n_labels), label_indices)] = False
206
304
 
207
305
  mask = mask_datums[:, np.newaxis] & mask_labels[np.newaxis, :]
208
306
  label_metadata_per_datum = self._label_metadata_per_datum.copy()
@@ -224,8 +322,10 @@ class Evaluator:
224
322
 
225
323
  def evaluate(
226
324
  self,
325
+ metrics_to_return: list[MetricType] = MetricType.base_metrics(),
227
326
  iou_thresholds: list[float] = [0.5, 0.75, 0.9],
228
327
  score_thresholds: list[float] = [0.5],
328
+ number_of_examples: int = 0,
229
329
  filter_: Filter | None = None,
230
330
  ) -> dict[MetricType, list]:
231
331
  """
@@ -233,10 +333,14 @@ class Evaluator:
233
333
 
234
334
  Parameters
235
335
  ----------
336
+ metrics_to_return : list[MetricType]
337
+ A list of metrics to return in the results.
236
338
  iou_thresholds : list[float]
237
339
  A list of IoU thresholds to compute metrics over.
238
340
  score_thresholds : list[float]
239
341
  A list of score thresholds to compute metrics over.
342
+ number_of_examples : int, default=0
343
+ Number of annotation examples to return in DetailedCounts.
240
344
  filter_ : Filter, optional
241
345
  An optional filter object.
242
346
 
@@ -284,7 +388,7 @@ class Evaluator:
284
388
  )
285
389
  for iou_idx in range(average_precision.shape[0])
286
390
  for label_idx in range(average_precision.shape[1])
287
- if int(label_metadata[label_idx][0]) > 0
391
+ if int(label_metadata[label_idx, 0]) > 0
288
392
  ]
289
393
 
290
394
  metrics[MetricType.mAP] = [
@@ -304,7 +408,7 @@ class Evaluator:
304
408
  label=self.index_to_label[label_idx],
305
409
  )
306
410
  for label_idx in range(self.n_labels)
307
- if int(label_metadata[label_idx][0]) > 0
411
+ if int(label_metadata[label_idx, 0]) > 0
308
412
  ]
309
413
 
310
414
  metrics[MetricType.mAPAveragedOverIOUs] = [
@@ -327,7 +431,7 @@ class Evaluator:
327
431
  )
328
432
  for score_idx in range(average_recall.shape[0])
329
433
  for label_idx in range(average_recall.shape[1])
330
- if int(label_metadata[label_idx][0]) > 0
434
+ if int(label_metadata[label_idx, 0]) > 0
331
435
  ]
332
436
 
333
437
  metrics[MetricType.mAR] = [
@@ -349,7 +453,7 @@ class Evaluator:
349
453
  label=self.index_to_label[label_idx],
350
454
  )
351
455
  for label_idx in range(self.n_labels)
352
- if int(label_metadata[label_idx][0]) > 0
456
+ if int(label_metadata[label_idx, 0]) > 0
353
457
  ]
354
458
 
355
459
  metrics[MetricType.mARAveragedOverScores] = [
@@ -372,16 +476,17 @@ class Evaluator:
372
476
  )
373
477
  for iou_idx, iou_threshold in enumerate(iou_thresholds)
374
478
  for label_idx, label in self.index_to_label.items()
375
- if int(label_metadata[label_idx][0]) > 0
479
+ if int(label_metadata[label_idx, 0]) > 0
376
480
  ]
377
481
 
378
482
  for label_idx, label in self.index_to_label.items():
483
+
484
+ if label_metadata[label_idx, 0] == 0:
485
+ continue
486
+
379
487
  for score_idx, score_threshold in enumerate(score_thresholds):
380
488
  for iou_idx, iou_threshold in enumerate(iou_thresholds):
381
489
 
382
- if label_metadata[label_idx, 0] == 0:
383
- continue
384
-
385
490
  row = precision_recall[iou_idx][score_idx][label_idx]
386
491
  kwargs = {
387
492
  "label": label,
@@ -422,16 +527,27 @@ class Evaluator:
422
527
  )
423
528
  )
424
529
 
530
+ if MetricType.DetailedCounts in metrics_to_return:
531
+ metrics[MetricType.DetailedCounts] = self._compute_detailed_counts(
532
+ iou_thresholds=iou_thresholds,
533
+ score_thresholds=score_thresholds,
534
+ n_samples=number_of_examples,
535
+ )
536
+
537
+ for metric in set(metrics.keys()):
538
+ if metric not in metrics_to_return:
539
+ del metrics[metric]
540
+
425
541
  return metrics
426
542
 
427
- def compute_detailed_counts(
543
+ def _compute_detailed_counts(
428
544
  self,
429
545
  iou_thresholds: list[float] = [0.5],
430
546
  score_thresholds: list[float] = [
431
547
  score / 10.0 for score in range(1, 11)
432
548
  ],
433
549
  n_samples: int = 0,
434
- ) -> list[list[DetailedCounts]]:
550
+ ) -> list[DetailedCounts]:
435
551
  """
436
552
  Computes detailed counting metrics.
437
553
 
@@ -454,7 +570,7 @@ class Evaluator:
454
570
  return list()
455
571
 
456
572
  metrics = compute_detailed_counts(
457
- self._detailed_pairs,
573
+ data=self._detailed_pairs,
458
574
  label_metadata=self._label_metadata,
459
575
  iou_thresholds=np.array(iou_thresholds),
460
576
  score_thresholds=np.array(score_thresholds),
@@ -462,95 +578,111 @@ class Evaluator:
462
578
  )
463
579
 
464
580
  tp_idx = 0
465
- fp_misclf_idx = tp_idx + n_samples + 1
466
- fp_halluc_idx = fp_misclf_idx + n_samples + 1
467
- fn_misclf_idx = fp_halluc_idx + n_samples + 1
468
- fn_misprd_idx = fn_misclf_idx + n_samples + 1
581
+ fp_misclf_idx = 2 * n_samples + 1
582
+ fp_halluc_idx = 4 * n_samples + 2
583
+ fn_misclf_idx = 6 * n_samples + 3
584
+ fn_misprd_idx = 8 * n_samples + 4
585
+
586
+ def _unpack_examples(
587
+ iou_idx: int,
588
+ label_idx: int,
589
+ type_idx: int,
590
+ example_source: dict[int, NDArray[np.float16]],
591
+ ) -> list[list[tuple[str, tuple[float, float, float, float]]]]:
592
+ """
593
+ Unpacks metric examples from computation.
594
+ """
595
+ type_idx += 1
596
+
597
+ results = list()
598
+ for score_idx in range(n_scores):
599
+ examples = list()
600
+ for example_idx in range(n_samples):
601
+ datum_idx = metrics[
602
+ iou_idx,
603
+ score_idx,
604
+ label_idx,
605
+ type_idx + example_idx * 2,
606
+ ]
607
+ annotation_idx = metrics[
608
+ iou_idx,
609
+ score_idx,
610
+ label_idx,
611
+ type_idx + example_idx * 2 + 1,
612
+ ]
613
+ if datum_idx >= 0:
614
+ examples.append(
615
+ (
616
+ self.index_to_uid[datum_idx],
617
+ tuple(
618
+ example_source[datum_idx][
619
+ annotation_idx
620
+ ].tolist()
621
+ ),
622
+ )
623
+ )
624
+ results.append(examples)
625
+
626
+ return results
469
627
 
470
628
  n_ious, n_scores, n_labels, _ = metrics.shape
471
629
  return [
472
- [
473
- DetailedCounts(
474
- iou_threshold=iou_thresholds[iou_idx],
475
- label=self.index_to_label[label_idx],
476
- score_thresholds=score_thresholds,
477
- tp=metrics[iou_idx, :, label_idx, tp_idx]
478
- .astype(int)
479
- .tolist(),
480
- tp_examples=[
481
- [
482
- self.index_to_uid[int(datum_idx)]
483
- for datum_idx in metrics[iou_idx][score_idx][
484
- label_idx
485
- ][tp_idx + 1 : fp_misclf_idx]
486
- if int(datum_idx) >= 0
487
- ]
488
- for score_idx in range(n_scores)
489
- ],
490
- fp_misclassification=metrics[
491
- iou_idx, :, label_idx, fp_misclf_idx
492
- ]
493
- .astype(int)
494
- .tolist(),
495
- fp_misclassification_examples=[
496
- [
497
- self.index_to_uid[int(datum_idx)]
498
- for datum_idx in metrics[iou_idx][score_idx][
499
- label_idx
500
- ][fp_misclf_idx + 1 : fp_halluc_idx]
501
- if int(datum_idx) >= 0
502
- ]
503
- for score_idx in range(n_scores)
504
- ],
505
- fp_hallucination=metrics[
506
- iou_idx, :, label_idx, fp_halluc_idx
507
- ]
508
- .astype(int)
509
- .tolist(),
510
- fp_hallucination_examples=[
511
- [
512
- self.index_to_uid[int(datum_idx)]
513
- for datum_idx in metrics[iou_idx][score_idx][
514
- label_idx
515
- ][fp_halluc_idx + 1 : fn_misclf_idx]
516
- if int(datum_idx) >= 0
517
- ]
518
- for score_idx in range(n_scores)
519
- ],
520
- fn_misclassification=metrics[
521
- iou_idx, :, label_idx, fn_misclf_idx
522
- ]
523
- .astype(int)
524
- .tolist(),
525
- fn_misclassification_examples=[
526
- [
527
- self.index_to_uid[int(datum_idx)]
528
- for datum_idx in metrics[iou_idx][score_idx][
529
- label_idx
530
- ][fn_misclf_idx + 1 : fn_misprd_idx]
531
- if int(datum_idx) >= 0
532
- ]
533
- for score_idx in range(n_scores)
534
- ],
535
- fn_missing_prediction=metrics[
536
- iou_idx, :, label_idx, fn_misprd_idx
537
- ]
538
- .astype(int)
539
- .tolist(),
540
- fn_missing_prediction_examples=[
541
- [
542
- self.index_to_uid[int(datum_idx)]
543
- for datum_idx in metrics[iou_idx][score_idx][
544
- label_idx
545
- ][fn_misprd_idx + 1 :]
546
- if int(datum_idx) >= 0
547
- ]
548
- for score_idx in range(n_scores)
549
- ],
550
- )
551
- for iou_idx in range(n_ious)
552
- ]
630
+ DetailedCounts(
631
+ iou_threshold=iou_thresholds[iou_idx],
632
+ label=self.index_to_label[label_idx],
633
+ score_thresholds=score_thresholds,
634
+ tp=metrics[iou_idx, :, label_idx, tp_idx].astype(int).tolist(),
635
+ fp_misclassification=metrics[
636
+ iou_idx, :, label_idx, fp_misclf_idx
637
+ ]
638
+ .astype(int)
639
+ .tolist(),
640
+ fp_hallucination=metrics[iou_idx, :, label_idx, fp_halluc_idx]
641
+ .astype(int)
642
+ .tolist(),
643
+ fn_misclassification=metrics[
644
+ iou_idx, :, label_idx, fn_misclf_idx
645
+ ]
646
+ .astype(int)
647
+ .tolist(),
648
+ fn_missing_prediction=metrics[
649
+ iou_idx, :, label_idx, fn_misprd_idx
650
+ ]
651
+ .astype(int)
652
+ .tolist(),
653
+ tp_examples=_unpack_examples(
654
+ iou_idx=iou_idx,
655
+ label_idx=label_idx,
656
+ type_idx=tp_idx,
657
+ example_source=self.prediction_examples,
658
+ ),
659
+ fp_misclassification_examples=_unpack_examples(
660
+ iou_idx=iou_idx,
661
+ label_idx=label_idx,
662
+ type_idx=fp_misclf_idx,
663
+ example_source=self.prediction_examples,
664
+ ),
665
+ fp_hallucination_examples=_unpack_examples(
666
+ iou_idx=iou_idx,
667
+ label_idx=label_idx,
668
+ type_idx=fp_halluc_idx,
669
+ example_source=self.prediction_examples,
670
+ ),
671
+ fn_misclassification_examples=_unpack_examples(
672
+ iou_idx=iou_idx,
673
+ label_idx=label_idx,
674
+ type_idx=fn_misclf_idx,
675
+ example_source=self.groundtruth_examples,
676
+ ),
677
+ fn_missing_prediction_examples=_unpack_examples(
678
+ iou_idx=iou_idx,
679
+ label_idx=label_idx,
680
+ type_idx=fn_misprd_idx,
681
+ example_source=self.groundtruth_examples,
682
+ ),
683
+ )
553
684
  for label_idx in range(n_labels)
685
+ for iou_idx in range(n_ious)
554
686
  ]
555
687
 
556
688
 
@@ -561,7 +693,7 @@ class DataLoader:
561
693
 
562
694
  def __init__(self):
563
695
  self._evaluator = Evaluator()
564
- self.pairs = list()
696
+ self.pairs: list[NDArray[np.floating]] = list()
565
697
  self.groundtruth_count = defaultdict(lambda: defaultdict(int))
566
698
  self.prediction_count = defaultdict(lambda: defaultdict(int))
567
699
 
@@ -624,9 +756,143 @@ class DataLoader:
624
756
  self._evaluator.label_key_to_index[label[0]],
625
757
  )
626
758
 
627
- def add_data(
759
+ def _compute_ious_and_cache_pairs(
760
+ self,
761
+ uid_index: int,
762
+ keyed_groundtruths: dict,
763
+ keyed_predictions: dict,
764
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask],
765
+ ) -> None:
766
+ """
767
+ Compute IOUs between groundtruths and preditions before storing as pairs.
768
+
769
+ Parameters
770
+ ----------
771
+ uid_index: int
772
+ The index of the detection.
773
+ keyed_groundtruths: dict
774
+ A dictionary of groundtruths.
775
+ keyed_predictions: dict
776
+ A dictionary of predictions.
777
+ annotation_type: type[BoundingBox] | type[Polygon] | type[Bitmask]
778
+ The type of annotation to compute IOUs for.
779
+ """
780
+ gt_keys = set(keyed_groundtruths.keys())
781
+ pd_keys = set(keyed_predictions.keys())
782
+ joint_keys = gt_keys.intersection(pd_keys)
783
+ gt_unique_keys = gt_keys - pd_keys
784
+ pd_unique_keys = pd_keys - gt_keys
785
+
786
+ pairs = list()
787
+ for key in joint_keys:
788
+ n_predictions = len(keyed_predictions[key])
789
+ n_groundtruths = len(keyed_groundtruths[key])
790
+ data = _get_annotation_data(
791
+ keyed_groundtruths=keyed_groundtruths,
792
+ keyed_predictions=keyed_predictions,
793
+ key=key,
794
+ annotation_type=annotation_type,
795
+ )
796
+ ious = compute_iou(data=data, annotation_type=annotation_type)
797
+ mask_nonzero_iou = (ious > 1e-9).reshape(
798
+ (n_predictions, n_groundtruths)
799
+ )
800
+ mask_ious_halluc = ~(mask_nonzero_iou.any(axis=1))
801
+ mask_ious_misprd = ~(mask_nonzero_iou.any(axis=0))
802
+
803
+ pairs.extend(
804
+ [
805
+ np.array(
806
+ [
807
+ float(uid_index),
808
+ float(gidx),
809
+ float(pidx),
810
+ ious[pidx * len(keyed_groundtruths[key]) + gidx],
811
+ float(glabel),
812
+ float(plabel),
813
+ float(score),
814
+ ]
815
+ )
816
+ for pidx, plabel, score, _ in keyed_predictions[key]
817
+ for gidx, glabel, _ in keyed_groundtruths[key]
818
+ if ious[pidx * len(keyed_groundtruths[key]) + gidx] > 1e-9
819
+ ]
820
+ )
821
+ pairs.extend(
822
+ [
823
+ np.array(
824
+ [
825
+ float(uid_index),
826
+ -1.0,
827
+ float(pidx),
828
+ 0.0,
829
+ -1.0,
830
+ float(plabel),
831
+ float(score),
832
+ ]
833
+ )
834
+ for pidx, plabel, score, _ in keyed_predictions[key]
835
+ if mask_ious_halluc[pidx]
836
+ ]
837
+ )
838
+ pairs.extend(
839
+ [
840
+ np.array(
841
+ [
842
+ float(uid_index),
843
+ float(gidx),
844
+ -1.0,
845
+ 0.0,
846
+ float(glabel),
847
+ -1.0,
848
+ -1.0,
849
+ ]
850
+ )
851
+ for gidx, glabel, _ in keyed_groundtruths[key]
852
+ if mask_ious_misprd[gidx]
853
+ ]
854
+ )
855
+ for key in gt_unique_keys:
856
+ pairs.extend(
857
+ [
858
+ np.array(
859
+ [
860
+ float(uid_index),
861
+ float(gidx),
862
+ -1.0,
863
+ 0.0,
864
+ float(glabel),
865
+ -1.0,
866
+ -1.0,
867
+ ]
868
+ )
869
+ for gidx, glabel, _ in keyed_groundtruths[key]
870
+ ]
871
+ )
872
+ for key in pd_unique_keys:
873
+ pairs.extend(
874
+ [
875
+ np.array(
876
+ [
877
+ float(uid_index),
878
+ -1.0,
879
+ float(pidx),
880
+ 0.0,
881
+ -1.0,
882
+ float(plabel),
883
+ float(score),
884
+ ]
885
+ )
886
+ for pidx, plabel, score, _ in keyed_predictions[key]
887
+ ]
888
+ )
889
+
890
+ self.pairs.append(np.array(pairs))
891
+
892
+ def _add_data(
628
893
  self,
629
894
  detections: list[Detection],
895
+ annotation_type: type[Bitmask] | type[BoundingBox] | type[Polygon],
630
896
  show_progress: bool = False,
631
897
  ):
632
898
  """
@@ -636,6 +902,8 @@ class DataLoader:
636
902
  ----------
637
903
  detections : list[Detection]
638
904
  A list of Detection objects.
905
+ annotation_type : type[Bitmask] | type[BoundingBox] | type[Polygon]
906
+ The annotation type to process.
639
907
  show_progress : bool, default=False
640
908
  Toggle for tqdm progress bar.
641
909
  """
@@ -650,108 +918,157 @@ class DataLoader:
650
918
  # update datum uid index
651
919
  uid_index = self._add_datum(uid=detection.uid)
652
920
 
921
+ # initialize bounding box examples
922
+ self._evaluator.groundtruth_examples[uid_index] = np.zeros(
923
+ (len(detection.groundtruths), 4), dtype=np.float16
924
+ )
925
+ self._evaluator.prediction_examples[uid_index] = np.zeros(
926
+ (len(detection.predictions), 4), dtype=np.float16
927
+ )
928
+
653
929
  # cache labels and annotations
654
930
  keyed_groundtruths = defaultdict(list)
655
931
  keyed_predictions = defaultdict(list)
932
+
933
+ representation_property = _get_annotation_representation(
934
+ annotation_type=annotation_type
935
+ )
936
+
656
937
  for gidx, gann in enumerate(detection.groundtruths):
938
+ if not isinstance(gann, annotation_type):
939
+ raise ValueError(
940
+ f"Expected {annotation_type}, but annotation is of type {type(gann)}."
941
+ )
942
+
943
+ if isinstance(gann, BoundingBox):
944
+ self._evaluator.groundtruth_examples[uid_index][
945
+ gidx
946
+ ] = getattr(gann, representation_property)
947
+ else:
948
+ converted_box = gann.to_box()
949
+ self._evaluator.groundtruth_examples[uid_index][gidx] = (
950
+ getattr(converted_box, "extrema")
951
+ if converted_box is not None
952
+ else None
953
+ )
657
954
  for glabel in gann.labels:
658
955
  label_idx, label_key_idx = self._add_label(glabel)
659
956
  self.groundtruth_count[label_idx][uid_index] += 1
957
+ representation = getattr(gann, representation_property)
660
958
  keyed_groundtruths[label_key_idx].append(
661
959
  (
662
960
  gidx,
663
961
  label_idx,
664
- gann.extrema,
962
+ representation,
665
963
  )
666
964
  )
965
+
667
966
  for pidx, pann in enumerate(detection.predictions):
967
+ if not isinstance(pann, annotation_type):
968
+ raise ValueError(
969
+ f"Expected {annotation_type}, but annotation is of type {type(pann)}."
970
+ )
971
+
972
+ if isinstance(pann, BoundingBox):
973
+ self._evaluator.prediction_examples[uid_index][
974
+ pidx
975
+ ] = getattr(pann, representation_property)
976
+ else:
977
+ converted_box = pann.to_box()
978
+ self._evaluator.prediction_examples[uid_index][pidx] = (
979
+ getattr(converted_box, "extrema")
980
+ if converted_box is not None
981
+ else None
982
+ )
668
983
  for plabel, pscore in zip(pann.labels, pann.scores):
669
984
  label_idx, label_key_idx = self._add_label(plabel)
670
985
  self.prediction_count[label_idx][uid_index] += 1
986
+ representation = representation = getattr(
987
+ pann, representation_property
988
+ )
671
989
  keyed_predictions[label_key_idx].append(
672
990
  (
673
991
  pidx,
674
992
  label_idx,
675
993
  pscore,
676
- pann.extrema,
994
+ representation,
677
995
  )
678
996
  )
679
997
 
680
- gt_keys = set(keyed_groundtruths.keys())
681
- pd_keys = set(keyed_predictions.keys())
682
- joint_keys = gt_keys.intersection(pd_keys)
683
- gt_unique_keys = gt_keys - pd_keys
684
- pd_unique_keys = pd_keys - gt_keys
685
-
686
- pairs = list()
687
- for key in joint_keys:
688
- boxes = np.array(
689
- [
690
- np.array([*gextrema, *pextrema])
691
- for _, _, _, pextrema in keyed_predictions[key]
692
- for _, _, gextrema in keyed_groundtruths[key]
693
- ]
694
- )
695
- ious = compute_iou(boxes)
696
- pairs.extend(
697
- [
698
- np.array(
699
- [
700
- float(uid_index),
701
- float(gidx),
702
- float(pidx),
703
- ious[
704
- pidx * len(keyed_groundtruths[key]) + gidx
705
- ],
706
- float(glabel),
707
- float(plabel),
708
- float(score),
709
- ]
710
- )
711
- for pidx, plabel, score, _ in keyed_predictions[key]
712
- for gidx, glabel, _ in keyed_groundtruths[key]
713
- ]
714
- )
715
- for key in gt_unique_keys:
716
- pairs.extend(
717
- [
718
- np.array(
719
- [
720
- float(uid_index),
721
- float(gidx),
722
- -1.0,
723
- 0.0,
724
- float(glabel),
725
- -1.0,
726
- -1.0,
727
- ]
728
- )
729
- for gidx, glabel, _ in keyed_groundtruths[key]
730
- ]
731
- )
732
- for key in pd_unique_keys:
733
- pairs.extend(
734
- [
735
- np.array(
736
- [
737
- float(uid_index),
738
- -1.0,
739
- float(pidx),
740
- 0.0,
741
- -1.0,
742
- float(plabel),
743
- float(score),
744
- ]
745
- )
746
- for pidx, plabel, score, _ in keyed_predictions[key]
747
- ]
748
- )
998
+ self._compute_ious_and_cache_pairs(
999
+ uid_index=uid_index,
1000
+ keyed_groundtruths=keyed_groundtruths,
1001
+ keyed_predictions=keyed_predictions,
1002
+ annotation_type=annotation_type,
1003
+ )
1004
+
1005
+ def add_bounding_boxes(
1006
+ self,
1007
+ detections: list[Detection],
1008
+ show_progress: bool = False,
1009
+ ):
1010
+ """
1011
+ Adds bounding box detections to the cache.
1012
+
1013
+ Parameters
1014
+ ----------
1015
+ detections : list[Detection]
1016
+ A list of Detection objects.
1017
+ show_progress : bool, default=False
1018
+ Toggle for tqdm progress bar.
1019
+ """
1020
+ return self._add_data(
1021
+ detections=detections,
1022
+ show_progress=show_progress,
1023
+ annotation_type=BoundingBox,
1024
+ )
1025
+
1026
+ def add_polygons(
1027
+ self,
1028
+ detections: list[Detection],
1029
+ show_progress: bool = False,
1030
+ ):
1031
+ """
1032
+ Adds polygon detections to the cache.
1033
+
1034
+ Parameters
1035
+ ----------
1036
+ detections : list[Detection]
1037
+ A list of Detection objects.
1038
+ show_progress : bool, default=False
1039
+ Toggle for tqdm progress bar.
1040
+ """
1041
+ return self._add_data(
1042
+ detections=detections,
1043
+ show_progress=show_progress,
1044
+ annotation_type=Polygon,
1045
+ )
1046
+
1047
+ def add_bitmasks(
1048
+ self,
1049
+ detections: list[Detection],
1050
+ show_progress: bool = False,
1051
+ ):
1052
+ """
1053
+ Adds bitmask detections to the cache.
749
1054
 
750
- self.pairs.append(np.array(pairs))
1055
+ Parameters
1056
+ ----------
1057
+ detections : list[Detection]
1058
+ A list of Detection objects.
1059
+ show_progress : bool, default=False
1060
+ Toggle for tqdm progress bar.
1061
+ """
1062
+ return self._add_data(
1063
+ detections=detections,
1064
+ show_progress=show_progress,
1065
+ annotation_type=Bitmask,
1066
+ )
751
1067
 
752
- def add_data_from_valor_dict(
1068
+ def _add_data_from_valor_dict(
753
1069
  self,
754
1070
  detections: list[tuple[dict, dict]],
1071
+ annotation_type: type[Bitmask] | type[BoundingBox] | type[Polygon],
755
1072
  show_progress: bool = False,
756
1073
  ):
757
1074
  """
@@ -761,20 +1078,14 @@ class DataLoader:
761
1078
  ----------
762
1079
  detections : list[tuple[dict, dict]]
763
1080
  A list of groundtruth, prediction pairs in Valor-format dictionaries.
1081
+ annotation_type : type[Bitmask] | type[BoundingBox] | type[Polygon]
1082
+ The annotation type to process.
764
1083
  show_progress : bool, default=False
765
1084
  Toggle for tqdm progress bar.
766
1085
  """
767
1086
 
768
- def _get_bbox_extrema(
769
- data: list[list[list[float]]],
770
- ) -> tuple[float, float, float, float]:
771
- x = [point[0] for shape in data for point in shape]
772
- y = [point[1] for shape in data for point in shape]
773
- return (min(x), max(x), min(y), max(y))
774
-
775
1087
  disable_tqdm = not show_progress
776
1088
  for groundtruth, prediction in tqdm(detections, disable=disable_tqdm):
777
-
778
1089
  # update metadata
779
1090
  self._evaluator.n_datums += 1
780
1091
  self._evaluator.n_groundtruths += len(groundtruth["annotations"])
@@ -783,10 +1094,45 @@ class DataLoader:
783
1094
  # update datum uid index
784
1095
  uid_index = self._add_datum(uid=groundtruth["datum"]["uid"])
785
1096
 
1097
+ # initialize bounding box examples
1098
+ self._evaluator.groundtruth_examples[uid_index] = np.zeros(
1099
+ (len(groundtruth["annotations"]), 4), dtype=np.float16
1100
+ )
1101
+ self._evaluator.prediction_examples[uid_index] = np.zeros(
1102
+ (len(prediction["annotations"]), 4), dtype=np.float16
1103
+ )
1104
+
786
1105
  # cache labels and annotations
787
1106
  keyed_groundtruths = defaultdict(list)
788
1107
  keyed_predictions = defaultdict(list)
1108
+
1109
+ annotation_key = _get_valor_dict_annotation_key(
1110
+ annotation_type=annotation_type
1111
+ )
1112
+ invalid_keys = list(
1113
+ filter(
1114
+ lambda x: x != annotation_key,
1115
+ ["bounding_box", "raster", "polygon"],
1116
+ )
1117
+ )
1118
+
789
1119
  for gidx, gann in enumerate(groundtruth["annotations"]):
1120
+ if (gann[annotation_key] is None) or any(
1121
+ [gann[k] is not None for k in invalid_keys]
1122
+ ):
1123
+ raise ValueError(
1124
+ f"Input JSON doesn't contain {annotation_type} data, or contains data for multiple annotation types."
1125
+ )
1126
+ if annotation_type == BoundingBox:
1127
+ self._evaluator.groundtruth_examples[uid_index][
1128
+ gidx
1129
+ ] = np.array(
1130
+ _get_annotation_representation_from_valor_dict(
1131
+ gann[annotation_key],
1132
+ annotation_type=annotation_type,
1133
+ ),
1134
+ )
1135
+
790
1136
  for valor_label in gann["labels"]:
791
1137
  glabel = (valor_label["key"], valor_label["value"])
792
1138
  label_idx, label_key_idx = self._add_label(glabel)
@@ -795,10 +1141,29 @@ class DataLoader:
795
1141
  (
796
1142
  gidx,
797
1143
  label_idx,
798
- _get_bbox_extrema(gann["bounding_box"]),
1144
+ _get_annotation_representation_from_valor_dict(
1145
+ gann[annotation_key],
1146
+ annotation_type=annotation_type,
1147
+ ),
799
1148
  )
800
1149
  )
801
1150
  for pidx, pann in enumerate(prediction["annotations"]):
1151
+ if (pann[annotation_key] is None) or any(
1152
+ [pann[k] is not None for k in invalid_keys]
1153
+ ):
1154
+ raise ValueError(
1155
+ f"Input JSON doesn't contain {annotation_type} data, or contains data for multiple annotation types."
1156
+ )
1157
+
1158
+ if annotation_type == BoundingBox:
1159
+ self._evaluator.prediction_examples[uid_index][
1160
+ pidx
1161
+ ] = np.array(
1162
+ _get_annotation_representation_from_valor_dict(
1163
+ pann[annotation_key],
1164
+ annotation_type=annotation_type,
1165
+ )
1166
+ )
802
1167
  for valor_label in pann["labels"]:
803
1168
  plabel = (valor_label["key"], valor_label["value"])
804
1169
  pscore = valor_label["score"]
@@ -809,81 +1174,40 @@ class DataLoader:
809
1174
  pidx,
810
1175
  label_idx,
811
1176
  pscore,
812
- _get_bbox_extrema(pann["bounding_box"]),
1177
+ _get_annotation_representation_from_valor_dict(
1178
+ pann[annotation_key],
1179
+ annotation_type=annotation_type,
1180
+ ),
813
1181
  )
814
1182
  )
815
1183
 
816
- gt_keys = set(keyed_groundtruths.keys())
817
- pd_keys = set(keyed_predictions.keys())
818
- joint_keys = gt_keys.intersection(pd_keys)
819
- gt_unique_keys = gt_keys - pd_keys
820
- pd_unique_keys = pd_keys - gt_keys
821
-
822
- pairs = list()
823
- for key in joint_keys:
824
- boxes = np.array(
825
- [
826
- np.array([*gextrema, *pextrema])
827
- for _, _, _, pextrema in keyed_predictions[key]
828
- for _, _, gextrema in keyed_groundtruths[key]
829
- ]
830
- )
831
- ious = compute_iou(boxes)
832
- pairs.extend(
833
- [
834
- np.array(
835
- [
836
- float(uid_index),
837
- float(gidx),
838
- float(pidx),
839
- ious[
840
- pidx * len(keyed_groundtruths[key]) + gidx
841
- ],
842
- float(glabel),
843
- float(plabel),
844
- float(score),
845
- ]
846
- )
847
- for pidx, plabel, score, _ in keyed_predictions[key]
848
- for gidx, glabel, _ in keyed_groundtruths[key]
849
- ]
850
- )
851
- for key in gt_unique_keys:
852
- pairs.extend(
853
- [
854
- np.array(
855
- [
856
- float(uid_index),
857
- float(gidx),
858
- -1.0,
859
- 0.0,
860
- float(glabel),
861
- -1.0,
862
- -1.0,
863
- ]
864
- )
865
- for gidx, glabel, _ in keyed_groundtruths[key]
866
- ]
867
- )
868
- for key in pd_unique_keys:
869
- pairs.extend(
870
- [
871
- np.array(
872
- [
873
- float(uid_index),
874
- -1.0,
875
- float(pidx),
876
- 0.0,
877
- -1.0,
878
- float(plabel),
879
- float(score),
880
- ]
881
- )
882
- for pidx, plabel, score, _ in keyed_predictions[key]
883
- ]
884
- )
1184
+ self._compute_ious_and_cache_pairs(
1185
+ uid_index=uid_index,
1186
+ keyed_groundtruths=keyed_groundtruths,
1187
+ keyed_predictions=keyed_predictions,
1188
+ annotation_type=annotation_type,
1189
+ )
885
1190
 
886
- self.pairs.append(np.array(pairs))
1191
+ def add_bounding_boxes_from_valor_dict(
1192
+ self,
1193
+ detections: list[tuple[dict, dict]],
1194
+ show_progress: bool = False,
1195
+ ):
1196
+ """
1197
+ Adds Valor-format bounding box detections to the cache.
1198
+
1199
+ Parameters
1200
+ ----------
1201
+ detections : list[tuple[dict, dict]]
1202
+ A list of groundtruth, prediction pairs in Valor-format dictionaries.
1203
+ show_progress : bool, default=False
1204
+ Toggle for tqdm progress bar.
1205
+ """
1206
+ return self._add_data_from_valor_dict(
1207
+ detections=detections,
1208
+ show_progress=show_progress,
1209
+ annotation_type=BoundingBox,
1210
+ )
887
1211
 
888
1212
  def finalize(self) -> Evaluator:
889
1213
  """