valor-lite 0.34.3__py3-none-any.whl → 0.35.0__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.

@@ -1,3 +1,6 @@
1
+ import warnings
2
+ from enum import IntFlag, auto
3
+
1
4
  import numpy as np
2
5
  import shapely
3
6
  from numpy.typing import NDArray
@@ -171,99 +174,119 @@ def compute_polygon_iou(
171
174
  return ious
172
175
 
173
176
 
174
- def _compute_ranked_pairs_for_datum(
175
- data: NDArray[np.float64],
176
- label_metadata: NDArray[np.int32],
177
- ) -> NDArray[np.float64]:
178
- """
179
- Computes ranked pairs for a datum.
177
+ def compute_label_metadata(
178
+ ids: NDArray[np.int32],
179
+ n_labels: int,
180
+ ) -> NDArray[np.int32]:
180
181
  """
182
+ Computes label metadata returning a count of annotations per label.
181
183
 
182
- # remove null predictions
183
- data = data[data[:, 2] >= 0.0]
184
+ Parameters
185
+ ----------
186
+ detailed_pairs : NDArray[np.int32]
187
+ Detailed annotation pairings with shape (N, 7).
188
+ Index 0 - Datum Index
189
+ Index 1 - GroundTruth Index
190
+ Index 2 - Prediction Index
191
+ Index 3 - GroundTruth Label Index
192
+ Index 4 - Prediction Label Index
193
+ n_labels : int
194
+ The total number of unique labels.
184
195
 
185
- # find best fits for prediction
186
- mask_label_match = data[:, 4] == data[:, 5]
187
- matched_predicitons = np.unique(data[mask_label_match, 2].astype(np.int32))
188
- mask_unmatched_predictions = ~np.isin(data[:, 2], matched_predicitons)
189
- data = data[mask_label_match | mask_unmatched_predictions]
196
+ Returns
197
+ -------
198
+ NDArray[np.int32]
199
+ The label metadata array with shape (n_labels, 2).
200
+ Index 0 - Ground truth label count
201
+ Index 1 - Prediction label count
202
+ """
203
+ label_metadata = np.zeros((n_labels, 2), dtype=np.int32)
190
204
 
191
- # sort by gt_id, iou, score
192
- indices = np.lexsort(
193
- (
194
- data[:, 1],
195
- -data[:, 3],
196
- -data[:, 6],
197
- )
205
+ ground_truth_pairs = ids[:, (0, 1, 3)]
206
+ ground_truth_pairs = ground_truth_pairs[ground_truth_pairs[:, 1] >= 0]
207
+ unique_pairs = np.unique(ground_truth_pairs, axis=0)
208
+ label_indices, unique_counts = np.unique(
209
+ unique_pairs[:, 2], return_counts=True
198
210
  )
199
- data = data[indices]
211
+ label_metadata[label_indices.astype(np.int32), 0] = unique_counts
200
212
 
201
- # remove ignored predictions
202
- for label_idx, count in enumerate(label_metadata[:, 0]):
203
- if count > 0:
204
- continue
205
- data = data[data[:, 5] != label_idx]
206
-
207
- # only keep the highest ranked pair
208
- _, indices = np.unique(data[:, [0, 2, 5]], axis=0, return_index=True)
209
-
210
- # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
211
- data = data[indices, :]
213
+ prediction_pairs = ids[:, (0, 2, 4)]
214
+ prediction_pairs = prediction_pairs[prediction_pairs[:, 1] >= 0]
215
+ unique_pairs = np.unique(prediction_pairs, axis=0)
216
+ label_indices, unique_counts = np.unique(
217
+ unique_pairs[:, 2], return_counts=True
218
+ )
219
+ label_metadata[label_indices.astype(np.int32), 1] = unique_counts
212
220
 
213
- return data
221
+ return label_metadata
214
222
 
215
223
 
216
- def compute_ranked_pairs(
217
- data: list[NDArray[np.float64]],
224
+ def rank_pairs(
225
+ detailed_pairs: NDArray[np.float64],
218
226
  label_metadata: NDArray[np.int32],
219
227
  ) -> NDArray[np.float64]:
220
228
  """
221
- Performs pair ranking on input data.
222
-
223
- Takes data with shape (N, 7):
224
-
225
- Index 0 - Datum Index
226
- Index 1 - GroundTruth Index
227
- Index 2 - Prediction Index
228
- Index 3 - IOU
229
- Index 4 - GroundTruth Label Index
230
- Index 5 - Prediction Label Index
231
- Index 6 - Score
229
+ Highly optimized pair ranking for computing precision and recall based metrics.
232
230
 
233
- Returns data with shape (N - M, 7)
231
+ Only ground truths and predictions that provide unique information are kept. The unkept
232
+ pairs are represented via the label metadata array.
234
233
 
235
234
  Parameters
236
235
  ----------
237
- data : NDArray[np.float64]
238
- A sorted array summarizing the IOU calculations of one or more pairs.
236
+ detailed_pairs : NDArray[np.float64]
237
+ Detailed annotation pairs with shape (n_pairs, 7).
238
+ Index 0 - Datum Index
239
+ Index 1 - GroundTruth Index
240
+ Index 2 - Prediction Index
241
+ Index 3 - GroundTruth Label Index
242
+ Index 4 - Prediction Label Index
243
+ Index 5 - IOU
244
+ Index 6 - Score
239
245
  label_metadata : NDArray[np.int32]
240
- An array containing metadata related to labels.
246
+ Array containing label counts with shape (n_labels, 2)
247
+ Index 0 - Ground truth label count
248
+ Index 1 - Prediction label count
241
249
 
242
250
  Returns
243
251
  -------
244
252
  NDArray[np.float64]
245
- A filtered array containing only ranked pairs.
253
+ Array of ranked pairs for precision-recall metric computation.
246
254
  """
255
+ pairs = detailed_pairs
247
256
 
248
- ranked_pairs_by_datum = [
249
- _compute_ranked_pairs_for_datum(
250
- data=datum,
251
- label_metadata=label_metadata,
252
- )
253
- for datum in data
254
- ]
255
- ranked_pairs = np.concatenate(ranked_pairs_by_datum, axis=0)
257
+ # remove null predictions
258
+ pairs = pairs[pairs[:, 2] >= 0.0]
259
+
260
+ # find best fits for prediction
261
+ mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
262
+ matched_predictions = np.unique(pairs[mask_label_match, 2])
263
+ mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
264
+ pairs = pairs[mask_label_match | mask_unmatched_predictions]
265
+
266
+ # remove predictions for labels that have no ground truths
267
+ for label_idx, count in enumerate(label_metadata[:, 0]):
268
+ if count > 0:
269
+ continue
270
+ pairs = pairs[pairs[:, 4] != label_idx]
271
+
272
+ # only keep the highest ranked pair
273
+ _, indices = np.unique(pairs[:, [0, 2, 4]], axis=0, return_index=True)
274
+ pairs = pairs[indices]
275
+
276
+ # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
256
277
  indices = np.lexsort(
257
278
  (
258
- -ranked_pairs[:, 3], # iou
259
- -ranked_pairs[:, 6], # score
279
+ -pairs[:, 5], # iou
280
+ -pairs[:, 6], # score
260
281
  )
261
282
  )
262
- return ranked_pairs[indices]
283
+ pairs = pairs[indices]
284
+
285
+ return pairs
263
286
 
264
287
 
265
288
  def compute_precion_recall(
266
- data: NDArray[np.float64],
289
+ ranked_pairs: NDArray[np.float64],
267
290
  label_metadata: NDArray[np.int32],
268
291
  iou_thresholds: NDArray[np.float64],
269
292
  score_thresholds: NDArray[np.float64],
@@ -271,14 +294,10 @@ def compute_precion_recall(
271
294
  tuple[
272
295
  NDArray[np.float64],
273
296
  NDArray[np.float64],
274
- NDArray[np.float64],
275
- float,
276
297
  ],
277
298
  tuple[
278
299
  NDArray[np.float64],
279
300
  NDArray[np.float64],
280
- NDArray[np.float64],
281
- float,
282
301
  ],
283
302
  NDArray[np.float64],
284
303
  NDArray[np.float64],
@@ -298,8 +317,8 @@ def compute_precion_recall(
298
317
 
299
318
  Parameters
300
319
  ----------
301
- data : NDArray[np.float64]
302
- A sorted array summarizing the IOU calculations of one or more pairs.
320
+ ranked_pairs : NDArray[np.float64]
321
+ A ranked array summarizing the IOU calculations of one or more pairs.
303
322
  label_metadata : NDArray[np.int32]
304
323
  An array containing metadata related to labels.
305
324
  iou_thresholds : NDArray[np.float64]
@@ -309,32 +328,45 @@ def compute_precion_recall(
309
328
 
310
329
  Returns
311
330
  -------
312
- tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], float]
313
- Average Precision results.
314
- tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], float]
315
- Average Recall results.
331
+ tuple[NDArray[np.float64], NDArray[np.float64]]
332
+ Average Precision results (AP, mAP).
333
+ tuple[NDArray[np.float64], NDArray[np.float64]]
334
+ Average Recall results (AR, mAR).
316
335
  NDArray[np.float64]
317
336
  Precision, Recall, TP, FP, FN, F1 Score.
318
337
  NDArray[np.float64]
319
338
  Interpolated Precision-Recall Curves.
320
339
  """
321
-
322
- n_rows = data.shape[0]
340
+ n_rows = ranked_pairs.shape[0]
323
341
  n_labels = label_metadata.shape[0]
324
342
  n_ious = iou_thresholds.shape[0]
325
343
  n_scores = score_thresholds.shape[0]
326
344
 
327
- if n_ious == 0:
328
- raise ValueError("At least one IOU threshold must be passed.")
329
- elif n_scores == 0:
330
- raise ValueError("At least one score threshold must be passed.")
331
-
345
+ # initialize result arrays
332
346
  average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
347
+ mAP = np.zeros(n_ious, dtype=np.float64)
333
348
  average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
349
+ mAR = np.zeros(n_scores, dtype=np.float64)
334
350
  counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
351
+ pr_curve = np.zeros((n_ious, n_labels, 101, 2))
352
+
353
+ if ranked_pairs.size == 0:
354
+ warnings.warn("no valid ranked pairs")
355
+ return (
356
+ (average_precision, mAP),
357
+ (average_recall, mAR),
358
+ counts,
359
+ pr_curve,
360
+ )
361
+
362
+ # start computation
363
+ ids = ranked_pairs[:, :5].astype(np.int32)
364
+ gt_ids = ids[:, 1]
365
+ gt_labels = ids[:, 3]
366
+ pd_labels = ids[:, 4]
367
+ ious = ranked_pairs[:, 5]
368
+ scores = ranked_pairs[:, 6]
335
369
 
336
- pd_labels = data[:, 5].astype(np.int32)
337
- scores = data[:, 6]
338
370
  unique_pd_labels, unique_pd_indices = np.unique(
339
371
  pd_labels, return_index=True
340
372
  )
@@ -346,9 +378,9 @@ def compute_precion_recall(
346
378
  running_tp_count = np.zeros_like(running_total_count)
347
379
  running_gt_count = np.zeros_like(running_total_count)
348
380
 
349
- mask_score_nonzero = data[:, 6] > 1e-9
350
- mask_gt_exists = data[:, 1] >= 0.0
351
- mask_labels_match = np.isclose(data[:, 4], data[:, 5])
381
+ mask_score_nonzero = scores > 1e-9
382
+ mask_gt_exists = gt_ids >= 0.0
383
+ mask_labels_match = np.isclose(gt_labels, pd_labels)
352
384
 
353
385
  mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
354
386
 
@@ -357,7 +389,7 @@ def compute_precion_recall(
357
389
  mask_fn = mask_gt_exists_labels_match
358
390
 
359
391
  for iou_idx in range(n_ious):
360
- mask_iou = data[:, 3] >= iou_thresholds[iou_idx]
392
+ mask_iou = ious >= iou_thresholds[iou_idx]
361
393
 
362
394
  mask_tp_outer = mask_tp & mask_iou
363
395
  mask_fp_outer = mask_fp & (
@@ -366,16 +398,16 @@ def compute_precion_recall(
366
398
  mask_fn_outer = mask_fn & mask_iou
367
399
 
368
400
  for score_idx in range(n_scores):
369
- mask_score_thresh = data[:, 6] >= score_thresholds[score_idx]
401
+ mask_score_thresh = scores >= score_thresholds[score_idx]
370
402
 
371
403
  mask_tp_inner = mask_tp_outer & mask_score_thresh
372
404
  mask_fp_inner = mask_fp_outer & mask_score_thresh
373
405
  mask_fn_inner = mask_fn_outer & ~mask_score_thresh
374
406
 
375
407
  # create true-positive mask score threshold
376
- tp_candidates = data[mask_tp_inner]
408
+ tp_candidates = ids[mask_tp_inner]
377
409
  _, indices_gt_unique = np.unique(
378
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
410
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
379
411
  )
380
412
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
381
413
  mask_gt_unique[indices_gt_unique] = True
@@ -437,9 +469,9 @@ def compute_precion_recall(
437
469
  average_recall[score_idx] += recall
438
470
 
439
471
  # create true-positive mask score threshold
440
- tp_candidates = data[mask_tp_outer]
472
+ tp_candidates = ids[mask_tp_outer]
441
473
  _, indices_gt_unique = np.unique(
442
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
474
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
443
475
  )
444
476
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
445
477
  mask_gt_unique[indices_gt_unique] = True
@@ -476,7 +508,6 @@ def compute_precion_recall(
476
508
  recall_index = np.floor(recall * 100.0).astype(np.int32)
477
509
 
478
510
  # bin precision-recall curve
479
- pr_curve = np.zeros((n_ious, n_labels, 101, 2))
480
511
  for iou_idx in range(n_ious):
481
512
  p = precision[iou_idx]
482
513
  r = recall_index[iou_idx]
@@ -523,80 +554,18 @@ def compute_precion_recall(
523
554
  mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
524
555
  axis=1
525
556
  )
526
- else:
527
- mAP = np.zeros(n_ious, dtype=np.float64)
528
- mAR = np.zeros(n_scores, dtype=np.float64)
529
-
530
- # calculate AR and AR averaged over thresholds
531
- APAveragedOverIOUs = average_precision.mean(axis=0)
532
- ARAveragedOverScores = average_recall.mean(axis=0)
533
-
534
- # calculate mAP and mAR averaged over thresholds
535
- mAPAveragedOverIOUs = mAP.mean(axis=0)
536
- mARAveragedOverScores = mAR.mean(axis=0)
537
-
538
- ap_results = (
539
- average_precision,
540
- mAP,
541
- APAveragedOverIOUs,
542
- mAPAveragedOverIOUs,
543
- )
544
- ar_results = (
545
- average_recall,
546
- mAR,
547
- ARAveragedOverScores,
548
- mARAveragedOverScores,
549
- )
550
557
 
551
558
  return (
552
- ap_results, # type: ignore[reportReturnType]
553
- ar_results,
559
+ (average_precision.astype(np.float64), mAP),
560
+ (average_recall, mAR),
554
561
  counts,
555
562
  pr_curve,
556
563
  )
557
564
 
558
565
 
559
- def _count_with_examples(
560
- data: NDArray[np.float64],
561
- unique_idx: int | list[int],
562
- label_idx: int | list[int],
563
- ) -> tuple[NDArray[np.float64], NDArray[np.int32], NDArray[np.intp]]:
564
- """
565
- Helper function for counting occurences of unique detailed pairs.
566
-
567
- Parameters
568
- ----------
569
- data : NDArray[np.float64]
570
- A masked portion of a detailed pairs array.
571
- unique_idx : int | list[int]
572
- The index or indices upon which uniqueness is constrained.
573
- label_idx : int | list[int]
574
- The index or indices within the unique index or indices that encode labels.
575
-
576
- Returns
577
- -------
578
- NDArray[np.float64]
579
- Examples drawn from the data input.
580
- NDArray[np.int32]
581
- Unique label indices.
582
- NDArray[np.intp]
583
- Counts for each unique label index.
584
- """
585
- unique_rows, indices = np.unique(
586
- data.astype(np.int32)[:, unique_idx],
587
- return_index=True,
588
- axis=0,
589
- )
590
- examples = data[indices]
591
- labels, counts = np.unique(
592
- unique_rows[:, label_idx], return_counts=True, axis=0
593
- )
594
- return examples, labels, counts
595
-
596
-
597
566
  def _isin(
598
- data: NDArray[np.int32],
599
- subset: NDArray[np.int32],
567
+ data: NDArray,
568
+ subset: NDArray,
600
569
  ) -> NDArray[np.bool_]:
601
570
  """
602
571
  Creates a mask of rows that exist within the subset.
@@ -614,22 +583,59 @@ def _isin(
614
583
  Returns a bool mask with shape (N,).
615
584
  """
616
585
  combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
617
- np.uint32
586
+ np.int32
618
587
  )
619
588
  combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
620
589
  :, 1
621
- ].astype(np.uint32)
590
+ ].astype(np.int32)
622
591
  mask = np.isin(combined_data, combined_subset, assume_unique=False)
623
592
  return mask
624
593
 
625
594
 
595
+ class PairClassification(IntFlag):
596
+ TP = auto()
597
+ FP_FN_MISCLF = auto()
598
+ FP_UNMATCHED = auto()
599
+ FN_UNMATCHED = auto()
600
+
601
+
602
+ def mask_pairs_greedily(
603
+ pairs: NDArray[np.float64],
604
+ ):
605
+ groundtruths = pairs[:, 1].astype(np.int32)
606
+ predictions = pairs[:, 2].astype(np.int32)
607
+
608
+ # Pre‑allocate "seen" flags for every possible x and y
609
+ max_gt = groundtruths.max()
610
+ max_pd = predictions.max()
611
+ used_gt = np.zeros(max_gt + 1, dtype=np.bool_)
612
+ used_pd = np.zeros(max_pd + 1, dtype=np.bool_)
613
+
614
+ # This mask will mark which pairs to keep
615
+ keep = np.zeros(pairs.shape[0], dtype=bool)
616
+
617
+ for idx in range(groundtruths.shape[0]):
618
+ gidx = groundtruths[idx]
619
+ pidx = predictions[idx]
620
+
621
+ if not (gidx < 0 or pidx < 0 or used_gt[gidx] or used_pd[pidx]):
622
+ keep[idx] = True
623
+ used_gt[gidx] = True
624
+ used_pd[pidx] = True
625
+
626
+ mask_matches = _isin(
627
+ data=pairs[:, (1, 2)],
628
+ subset=np.unique(pairs[np.ix_(keep, (1, 2))], axis=0), # type: ignore - np.ix_ typing
629
+ )
630
+
631
+ return mask_matches
632
+
633
+
626
634
  def compute_confusion_matrix(
627
- data: NDArray[np.float64],
628
- label_metadata: NDArray[np.int32],
635
+ detailed_pairs: NDArray[np.float64],
629
636
  iou_thresholds: NDArray[np.float64],
630
637
  score_thresholds: NDArray[np.float64],
631
- n_examples: int,
632
- ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32]]:
638
+ ) -> NDArray[np.uint8]:
633
639
  """
634
640
  Compute detailed counts.
635
641
 
@@ -638,265 +644,108 @@ def compute_confusion_matrix(
638
644
  Index 0 - Datum Index
639
645
  Index 1 - GroundTruth Index
640
646
  Index 2 - Prediction Index
641
- Index 3 - IOU
642
- Index 4 - GroundTruth Label Index
643
- Index 5 - Prediction Label Index
647
+ Index 3 - GroundTruth Label Index
648
+ Index 4 - Prediction Label Index
649
+ Index 5 - IOU
644
650
  Index 6 - Score
645
651
 
646
652
  Parameters
647
653
  ----------
648
- data : NDArray[np.float64]
649
- A sorted array summarizing the IOU calculations of one or more pairs.
654
+ detailed_pairs : NDArray[np.float64]
655
+ An unsorted array summarizing the IOU calculations of one or more pairs.
650
656
  label_metadata : NDArray[np.int32]
651
657
  An array containing metadata related to labels.
652
658
  iou_thresholds : NDArray[np.float64]
653
659
  A 1-D array containing IOU thresholds.
654
660
  score_thresholds : NDArray[np.float64]
655
661
  A 1-D array containing score thresholds.
656
- n_examples : int
657
- The maximum number of examples to return per count.
658
662
 
659
663
  Returns
660
664
  -------
661
- NDArray[np.float64]
665
+ NDArray[np.uint8]
662
666
  Confusion matrix.
663
- NDArray[np.float64]
664
- Unmatched Predictions.
665
- NDArray[np.int32]
666
- Unmatched Ground Truths.
667
667
  """
668
-
669
- n_labels = label_metadata.shape[0]
668
+ n_pairs = detailed_pairs.shape[0]
670
669
  n_ious = iou_thresholds.shape[0]
671
670
  n_scores = score_thresholds.shape[0]
672
671
 
673
- confusion_matrix = -1 * np.ones(
674
- # (datum idx, gt idx, pd idx, pd score) * n_examples + count
675
- (n_ious, n_scores, n_labels, n_labels, 4 * n_examples + 1),
676
- dtype=np.float32,
677
- )
678
- unmatched_predictions = -1 * np.ones(
679
- # (datum idx, pd idx, pd score) * n_examples + count
680
- (n_ious, n_scores, n_labels, 3 * n_examples + 1),
681
- dtype=np.float32,
682
- )
683
- unmatched_ground_truths = -1 * np.ones(
684
- # (datum idx, gt idx) * n_examples + count
685
- (n_ious, n_scores, n_labels, 2 * n_examples + 1),
686
- dtype=np.int32,
672
+ pair_classifications = np.zeros(
673
+ (n_ious, n_scores, n_pairs),
674
+ dtype=np.uint8,
687
675
  )
688
676
 
689
- mask_gt_exists = data[:, 1] > -0.5
690
- mask_pd_exists = data[:, 2] > -0.5
691
- mask_label_match = np.isclose(data[:, 4], data[:, 5])
692
- mask_score_nonzero = data[:, 6] > 1e-9
693
- mask_iou_nonzero = data[:, 3] > 1e-9
677
+ ids = detailed_pairs[:, :5].astype(np.int32)
678
+ groundtruths = ids[:, (0, 1)]
679
+ predictions = ids[:, (0, 2)]
680
+ gt_ids = ids[:, 1]
681
+ pd_ids = ids[:, 2]
682
+ gt_labels = ids[:, 3]
683
+ pd_labels = ids[:, 4]
684
+ ious = detailed_pairs[:, 5]
685
+ scores = detailed_pairs[:, 6]
686
+
687
+ mask_gt_exists = gt_ids > -0.5
688
+ mask_pd_exists = pd_ids > -0.5
689
+ mask_label_match = np.isclose(gt_labels, pd_labels)
690
+ mask_score_nonzero = scores > 1e-9
691
+ mask_iou_nonzero = ious > 1e-9
694
692
 
695
693
  mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
696
694
  mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
697
- mask_gt_pd_mismatch = mask_gt_pd_exists & ~mask_label_match
698
695
 
699
- groundtruths = data[:, [0, 1]].astype(np.int32)
700
- predictions = data[:, [0, 2]].astype(np.int32)
696
+ mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
697
+
701
698
  for iou_idx in range(n_ious):
702
- mask_iou_threshold = data[:, 3] >= iou_thresholds[iou_idx]
699
+ mask_iou_threshold = ious >= iou_thresholds[iou_idx]
703
700
  mask_iou = mask_iou_nonzero & mask_iou_threshold
704
-
705
- groundtruths_passing_ious = np.unique(groundtruths[mask_iou], axis=0)
706
- mask_groundtruths_with_passing_ious = _isin(
707
- data=groundtruths,
708
- subset=groundtruths_passing_ious,
709
- )
710
- mask_groundtruths_without_passing_ious = (
711
- ~mask_groundtruths_with_passing_ious & mask_gt_exists
712
- )
713
-
714
- predictions_with_passing_ious = np.unique(
715
- predictions[mask_iou], axis=0
716
- )
717
- mask_predictions_with_passing_ious = _isin(
718
- data=predictions,
719
- subset=predictions_with_passing_ious,
720
- )
721
- mask_predictions_without_passing_ious = (
722
- ~mask_predictions_with_passing_ious & mask_pd_exists
723
- )
724
-
725
701
  for score_idx in range(n_scores):
726
- mask_score_threshold = data[:, 6] >= score_thresholds[score_idx]
702
+ mask_score_threshold = scores >= score_thresholds[score_idx]
727
703
  mask_score = mask_score_nonzero & mask_score_threshold
728
704
 
729
- groundtruths_with_passing_score = np.unique(
730
- groundtruths[mask_iou & mask_score], axis=0
731
- )
732
- mask_groundtruths_with_passing_score = _isin(
733
- data=groundtruths,
734
- subset=groundtruths_with_passing_score,
735
- )
736
- mask_groundtruths_without_passing_score = (
737
- ~mask_groundtruths_with_passing_score & mask_gt_exists
705
+ mask_thresholded_matched_pairs = (
706
+ mask_matched_pairs & mask_iou & mask_score
738
707
  )
739
708
 
740
- # create category masks
741
- mask_tp = mask_score & mask_iou & mask_gt_pd_match
742
- mask_misclf = mask_iou & (
743
- (
744
- ~mask_score
745
- & mask_gt_pd_match
746
- & mask_groundtruths_with_passing_score
747
- )
748
- | (mask_score & mask_gt_pd_mismatch)
749
- )
750
- mask_halluc = mask_score & mask_predictions_without_passing_ious
751
- mask_misprd = (
752
- mask_groundtruths_without_passing_ious
753
- | mask_groundtruths_without_passing_score
709
+ mask_true_positives = (
710
+ mask_thresholded_matched_pairs & mask_gt_pd_match
754
711
  )
712
+ mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
755
713
 
756
- # filter out true-positives from misclf and misprd
757
- mask_gts_with_tp_override = _isin(
758
- data=groundtruths[mask_misclf],
759
- subset=groundtruths[mask_tp],
760
- )
761
- mask_pds_with_tp_override = _isin(
762
- data=predictions[mask_misclf],
763
- subset=predictions[mask_tp],
764
- )
765
- mask_misprd[mask_misclf] |= (
766
- ~mask_gts_with_tp_override & mask_pds_with_tp_override
767
- )
768
- mask_misclf[mask_misclf] &= (
769
- ~mask_gts_with_tp_override & ~mask_pds_with_tp_override
714
+ mask_groundtruths_in_thresholded_matched_pairs = _isin(
715
+ data=groundtruths,
716
+ subset=np.unique(
717
+ groundtruths[mask_thresholded_matched_pairs], axis=0
718
+ ),
770
719
  )
771
-
772
- # count true positives
773
- tp_examples, tp_labels, tp_counts = _count_with_examples(
774
- data[mask_tp],
775
- unique_idx=[0, 2, 5],
776
- label_idx=2,
720
+ mask_predictions_in_thresholded_matched_pairs = _isin(
721
+ data=predictions,
722
+ subset=np.unique(
723
+ predictions[mask_thresholded_matched_pairs], axis=0
724
+ ),
777
725
  )
778
726
 
779
- # count misclassifications
780
- (
781
- misclf_examples,
782
- misclf_labels,
783
- misclf_counts,
784
- ) = _count_with_examples(
785
- data[mask_misclf], unique_idx=[0, 1, 2, 4, 5], label_idx=[3, 4]
727
+ mask_unmatched_predictions = (
728
+ ~mask_predictions_in_thresholded_matched_pairs
729
+ & mask_pd_exists
730
+ & mask_score
786
731
  )
787
-
788
- # count unmatched predictions
789
- (
790
- halluc_examples,
791
- halluc_labels,
792
- halluc_counts,
793
- ) = _count_with_examples(
794
- data[mask_halluc], unique_idx=[0, 2, 5], label_idx=2
732
+ mask_unmatched_groundtruths = (
733
+ ~mask_groundtruths_in_thresholded_matched_pairs
734
+ & mask_gt_exists
795
735
  )
796
736
 
797
- # count unmatched ground truths
798
- (
799
- misprd_examples,
800
- misprd_labels,
801
- misprd_counts,
802
- ) = _count_with_examples(
803
- data[mask_misprd], unique_idx=[0, 1, 4], label_idx=2
737
+ # classify pairings
738
+ pair_classifications[
739
+ iou_idx, score_idx, mask_true_positives
740
+ ] |= np.uint8(PairClassification.TP)
741
+ pair_classifications[iou_idx, score_idx, mask_misclf] |= np.uint8(
742
+ PairClassification.FP_FN_MISCLF
804
743
  )
805
-
806
- # store the counts
807
- confusion_matrix[
808
- iou_idx, score_idx, tp_labels, tp_labels, 0
809
- ] = tp_counts
810
- confusion_matrix[
811
- iou_idx,
812
- score_idx,
813
- misclf_labels[:, 0],
814
- misclf_labels[:, 1],
815
- 0,
816
- ] = misclf_counts
817
- unmatched_predictions[
818
- iou_idx,
819
- score_idx,
820
- halluc_labels,
821
- 0,
822
- ] = halluc_counts
823
- unmatched_ground_truths[
824
- iou_idx,
825
- score_idx,
826
- misprd_labels,
827
- 0,
828
- ] = misprd_counts
829
-
830
- # store examples
831
- if n_examples > 0:
832
- for label_idx in range(n_labels):
833
-
834
- # true-positive examples
835
- mask_tp_label = tp_examples[:, 5] == label_idx
836
- if mask_tp_label.sum() > 0:
837
- tp_label_examples = tp_examples[mask_tp_label][
838
- :n_examples
839
- ]
840
- confusion_matrix[
841
- iou_idx,
842
- score_idx,
843
- label_idx,
844
- label_idx,
845
- 1 : 4 * tp_label_examples.shape[0] + 1,
846
- ] = tp_label_examples[:, [0, 1, 2, 6]].flatten()
847
-
848
- # misclassification examples
849
- mask_misclf_gt_label = misclf_examples[:, 4] == label_idx
850
- if mask_misclf_gt_label.sum() > 0:
851
- for pd_label_idx in range(n_labels):
852
- mask_misclf_pd_label = (
853
- misclf_examples[:, 5] == pd_label_idx
854
- )
855
- mask_misclf_label_combo = (
856
- mask_misclf_gt_label & mask_misclf_pd_label
857
- )
858
- if mask_misclf_label_combo.sum() > 0:
859
- misclf_label_examples = misclf_examples[
860
- mask_misclf_label_combo
861
- ][:n_examples]
862
- confusion_matrix[
863
- iou_idx,
864
- score_idx,
865
- label_idx,
866
- pd_label_idx,
867
- 1 : 4 * misclf_label_examples.shape[0] + 1,
868
- ] = misclf_label_examples[
869
- :, [0, 1, 2, 6]
870
- ].flatten()
871
-
872
- # unmatched prediction examples
873
- mask_halluc_label = halluc_examples[:, 5] == label_idx
874
- if mask_halluc_label.sum() > 0:
875
- halluc_label_examples = halluc_examples[
876
- mask_halluc_label
877
- ][:n_examples]
878
- unmatched_predictions[
879
- iou_idx,
880
- score_idx,
881
- label_idx,
882
- 1 : 3 * halluc_label_examples.shape[0] + 1,
883
- ] = halluc_label_examples[:, [0, 2, 6]].flatten()
884
-
885
- # unmatched ground truth examples
886
- mask_misprd_label = misprd_examples[:, 4] == label_idx
887
- if misprd_examples.size > 0:
888
- misprd_label_examples = misprd_examples[
889
- mask_misprd_label
890
- ][:n_examples]
891
- unmatched_ground_truths[
892
- iou_idx,
893
- score_idx,
894
- label_idx,
895
- 1 : 2 * misprd_label_examples.shape[0] + 1,
896
- ] = misprd_label_examples[:, [0, 1]].flatten()
897
-
898
- return (
899
- confusion_matrix,
900
- unmatched_predictions,
901
- unmatched_ground_truths,
902
- ) # type: ignore[reportReturnType]
744
+ pair_classifications[
745
+ iou_idx, score_idx, mask_unmatched_predictions
746
+ ] |= np.uint8(PairClassification.FP_UNMATCHED)
747
+ pair_classifications[
748
+ iou_idx, score_idx, mask_unmatched_groundtruths
749
+ ] |= np.uint8(PairClassification.FN_UNMATCHED)
750
+
751
+ return pair_classifications