valor-lite 0.34.3__py3-none-any.whl → 0.36.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,210 @@ 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]
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
206
220
 
207
- # only keep the highest ranked pair
208
- _, indices = np.unique(data[:, [0, 2, 5]], axis=0, return_index=True)
221
+ return label_metadata
209
222
 
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, :]
212
223
 
213
- return data
224
+ def filter_cache(
225
+ detailed_pairs: NDArray[np.float64],
226
+ mask_datums: NDArray[np.bool_],
227
+ mask_predictions: NDArray[np.bool_],
228
+ mask_ground_truths: NDArray[np.bool_],
229
+ n_labels: int,
230
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32],]:
231
+ """
232
+ Performs filtering on a detailed cache.
214
233
 
234
+ Parameters
235
+ ----------
236
+ detailed_pairs : NDArray[float64]
237
+ A list of sorted detailed pairs with size (N, 7).
238
+ mask_datums : NDArray[bool]
239
+ A boolean mask with size (N,).
240
+ mask_ground_truths : NDArray[bool]
241
+ A boolean mask with size (N,).
242
+ mask_predictions : NDArray[bool]
243
+ A boolean mask with size (N,).
244
+ n_labels : int
245
+ The total number of unique labels.
215
246
 
216
- def compute_ranked_pairs(
217
- data: list[NDArray[np.float64]],
218
- label_metadata: NDArray[np.int32],
219
- ) -> NDArray[np.float64]:
247
+ Returns
248
+ -------
249
+ NDArray[float64]
250
+ Filtered detailed pairs.
251
+ NDArray[float64]
252
+ Filtered ranked pairs.
253
+ NDArray[int32]
254
+ Label metadata.
220
255
  """
221
- Performs pair ranking on input data.
256
+ # filter datums
257
+ detailed_pairs = detailed_pairs[mask_datums].copy()
258
+
259
+ # filter ground truths
260
+ if mask_ground_truths.any():
261
+ invalid_groundtruth_indices = np.where(mask_ground_truths)[0]
262
+ detailed_pairs[
263
+ invalid_groundtruth_indices[:, None], (1, 3, 5)
264
+ ] = np.array([[-1, -1, 0]])
265
+
266
+ # filter predictions
267
+ if mask_predictions.any():
268
+ invalid_prediction_indices = np.where(mask_predictions)[0]
269
+ detailed_pairs[
270
+ invalid_prediction_indices[:, None], (2, 4, 5, 6)
271
+ ] = np.array([[-1, -1, 0, -1]])
272
+
273
+ # filter null pairs
274
+ mask_null_pairs = np.all(
275
+ np.isclose(
276
+ detailed_pairs[:, 1:5],
277
+ np.array([-1.0, -1.0, -1.0, -1.0]),
278
+ ),
279
+ axis=1,
280
+ )
281
+ detailed_pairs = detailed_pairs[~mask_null_pairs]
282
+
283
+ if detailed_pairs.size == 0:
284
+ warnings.warn("no valid filtered pairs")
285
+ return (
286
+ np.array([], dtype=np.float64),
287
+ np.array([], dtype=np.float64),
288
+ np.zeros((n_labels, 2), dtype=np.int32),
289
+ )
222
290
 
223
- Takes data with shape (N, 7):
291
+ # sorts by score, iou with ground truth id as a tie-breaker
292
+ indices = np.lexsort(
293
+ (
294
+ detailed_pairs[:, 1], # ground truth id
295
+ -detailed_pairs[:, 5], # iou
296
+ -detailed_pairs[:, 6], # score
297
+ )
298
+ )
299
+ detailed_pairs = detailed_pairs[indices]
300
+ label_metadata = compute_label_metadata(
301
+ ids=detailed_pairs[:, :5].astype(np.int32),
302
+ n_labels=n_labels,
303
+ )
304
+ ranked_pairs = rank_pairs(
305
+ detailed_pairs=detailed_pairs,
306
+ label_metadata=label_metadata,
307
+ )
308
+ return (
309
+ detailed_pairs,
310
+ ranked_pairs,
311
+ label_metadata,
312
+ )
224
313
 
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
232
314
 
233
- Returns data with shape (N - M, 7)
315
+ def rank_pairs(
316
+ detailed_pairs: NDArray[np.float64],
317
+ label_metadata: NDArray[np.int32],
318
+ ) -> NDArray[np.float64]:
319
+ """
320
+ Highly optimized pair ranking for computing precision and recall based metrics.
321
+
322
+ Only ground truths and predictions that provide unique information are kept. The unkept
323
+ pairs are represented via the label metadata array.
234
324
 
235
325
  Parameters
236
326
  ----------
237
- data : NDArray[np.float64]
238
- A sorted array summarizing the IOU calculations of one or more pairs.
327
+ detailed_pairs : NDArray[np.float64]
328
+ Detailed annotation pairs with shape (n_pairs, 7).
329
+ Index 0 - Datum Index
330
+ Index 1 - GroundTruth Index
331
+ Index 2 - Prediction Index
332
+ Index 3 - GroundTruth Label Index
333
+ Index 4 - Prediction Label Index
334
+ Index 5 - IOU
335
+ Index 6 - Score
239
336
  label_metadata : NDArray[np.int32]
240
- An array containing metadata related to labels.
337
+ Array containing label counts with shape (n_labels, 2)
338
+ Index 0 - Ground truth label count
339
+ Index 1 - Prediction label count
241
340
 
242
341
  Returns
243
342
  -------
244
343
  NDArray[np.float64]
245
- A filtered array containing only ranked pairs.
344
+ Array of ranked pairs for precision-recall metric computation.
246
345
  """
346
+ pairs = detailed_pairs
247
347
 
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)
348
+ # remove null predictions
349
+ pairs = pairs[pairs[:, 2] >= 0.0]
350
+
351
+ # find best fits for prediction
352
+ mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
353
+ matched_predictions = np.unique(pairs[mask_label_match, 2])
354
+ mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
355
+ pairs = pairs[mask_label_match | mask_unmatched_predictions]
356
+
357
+ # remove predictions for labels that have no ground truths
358
+ for label_idx, count in enumerate(label_metadata[:, 0]):
359
+ if count > 0:
360
+ continue
361
+ pairs = pairs[pairs[:, 4] != label_idx]
362
+
363
+ # only keep the highest ranked pair
364
+ _, indices = np.unique(pairs[:, [0, 2, 4]], axis=0, return_index=True)
365
+ pairs = pairs[indices]
366
+
367
+ # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
256
368
  indices = np.lexsort(
257
369
  (
258
- -ranked_pairs[:, 3], # iou
259
- -ranked_pairs[:, 6], # score
370
+ -pairs[:, 5], # iou
371
+ -pairs[:, 6], # score
260
372
  )
261
373
  )
262
- return ranked_pairs[indices]
374
+ pairs = pairs[indices]
375
+
376
+ return pairs
263
377
 
264
378
 
265
379
  def compute_precion_recall(
266
- data: NDArray[np.float64],
380
+ ranked_pairs: NDArray[np.float64],
267
381
  label_metadata: NDArray[np.int32],
268
382
  iou_thresholds: NDArray[np.float64],
269
383
  score_thresholds: NDArray[np.float64],
@@ -271,14 +385,10 @@ def compute_precion_recall(
271
385
  tuple[
272
386
  NDArray[np.float64],
273
387
  NDArray[np.float64],
274
- NDArray[np.float64],
275
- float,
276
388
  ],
277
389
  tuple[
278
390
  NDArray[np.float64],
279
391
  NDArray[np.float64],
280
- NDArray[np.float64],
281
- float,
282
392
  ],
283
393
  NDArray[np.float64],
284
394
  NDArray[np.float64],
@@ -298,8 +408,8 @@ def compute_precion_recall(
298
408
 
299
409
  Parameters
300
410
  ----------
301
- data : NDArray[np.float64]
302
- A sorted array summarizing the IOU calculations of one or more pairs.
411
+ ranked_pairs : NDArray[np.float64]
412
+ A ranked array summarizing the IOU calculations of one or more pairs.
303
413
  label_metadata : NDArray[np.int32]
304
414
  An array containing metadata related to labels.
305
415
  iou_thresholds : NDArray[np.float64]
@@ -309,32 +419,45 @@ def compute_precion_recall(
309
419
 
310
420
  Returns
311
421
  -------
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.
422
+ tuple[NDArray[np.float64], NDArray[np.float64]]
423
+ Average Precision results (AP, mAP).
424
+ tuple[NDArray[np.float64], NDArray[np.float64]]
425
+ Average Recall results (AR, mAR).
316
426
  NDArray[np.float64]
317
427
  Precision, Recall, TP, FP, FN, F1 Score.
318
428
  NDArray[np.float64]
319
429
  Interpolated Precision-Recall Curves.
320
430
  """
321
-
322
- n_rows = data.shape[0]
431
+ n_rows = ranked_pairs.shape[0]
323
432
  n_labels = label_metadata.shape[0]
324
433
  n_ious = iou_thresholds.shape[0]
325
434
  n_scores = score_thresholds.shape[0]
326
435
 
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
-
436
+ # initialize result arrays
332
437
  average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
438
+ mAP = np.zeros(n_ious, dtype=np.float64)
333
439
  average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
440
+ mAR = np.zeros(n_scores, dtype=np.float64)
334
441
  counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
442
+ pr_curve = np.zeros((n_ious, n_labels, 101, 2))
443
+
444
+ if ranked_pairs.size == 0:
445
+ warnings.warn("no valid ranked pairs")
446
+ return (
447
+ (average_precision, mAP),
448
+ (average_recall, mAR),
449
+ counts,
450
+ pr_curve,
451
+ )
452
+
453
+ # start computation
454
+ ids = ranked_pairs[:, :5].astype(np.int32)
455
+ gt_ids = ids[:, 1]
456
+ gt_labels = ids[:, 3]
457
+ pd_labels = ids[:, 4]
458
+ ious = ranked_pairs[:, 5]
459
+ scores = ranked_pairs[:, 6]
335
460
 
336
- pd_labels = data[:, 5].astype(np.int32)
337
- scores = data[:, 6]
338
461
  unique_pd_labels, unique_pd_indices = np.unique(
339
462
  pd_labels, return_index=True
340
463
  )
@@ -346,9 +469,9 @@ def compute_precion_recall(
346
469
  running_tp_count = np.zeros_like(running_total_count)
347
470
  running_gt_count = np.zeros_like(running_total_count)
348
471
 
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])
472
+ mask_score_nonzero = scores > 1e-9
473
+ mask_gt_exists = gt_ids >= 0.0
474
+ mask_labels_match = np.isclose(gt_labels, pd_labels)
352
475
 
353
476
  mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
354
477
 
@@ -357,7 +480,7 @@ def compute_precion_recall(
357
480
  mask_fn = mask_gt_exists_labels_match
358
481
 
359
482
  for iou_idx in range(n_ious):
360
- mask_iou = data[:, 3] >= iou_thresholds[iou_idx]
483
+ mask_iou = ious >= iou_thresholds[iou_idx]
361
484
 
362
485
  mask_tp_outer = mask_tp & mask_iou
363
486
  mask_fp_outer = mask_fp & (
@@ -366,16 +489,16 @@ def compute_precion_recall(
366
489
  mask_fn_outer = mask_fn & mask_iou
367
490
 
368
491
  for score_idx in range(n_scores):
369
- mask_score_thresh = data[:, 6] >= score_thresholds[score_idx]
492
+ mask_score_thresh = scores >= score_thresholds[score_idx]
370
493
 
371
494
  mask_tp_inner = mask_tp_outer & mask_score_thresh
372
495
  mask_fp_inner = mask_fp_outer & mask_score_thresh
373
496
  mask_fn_inner = mask_fn_outer & ~mask_score_thresh
374
497
 
375
498
  # create true-positive mask score threshold
376
- tp_candidates = data[mask_tp_inner]
499
+ tp_candidates = ids[mask_tp_inner]
377
500
  _, indices_gt_unique = np.unique(
378
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
501
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
379
502
  )
380
503
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
381
504
  mask_gt_unique[indices_gt_unique] = True
@@ -437,9 +560,9 @@ def compute_precion_recall(
437
560
  average_recall[score_idx] += recall
438
561
 
439
562
  # create true-positive mask score threshold
440
- tp_candidates = data[mask_tp_outer]
563
+ tp_candidates = ids[mask_tp_outer]
441
564
  _, indices_gt_unique = np.unique(
442
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
565
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
443
566
  )
444
567
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
445
568
  mask_gt_unique[indices_gt_unique] = True
@@ -476,7 +599,6 @@ def compute_precion_recall(
476
599
  recall_index = np.floor(recall * 100.0).astype(np.int32)
477
600
 
478
601
  # bin precision-recall curve
479
- pr_curve = np.zeros((n_ious, n_labels, 101, 2))
480
602
  for iou_idx in range(n_ious):
481
603
  p = precision[iou_idx]
482
604
  r = recall_index[iou_idx]
@@ -523,80 +645,18 @@ def compute_precion_recall(
523
645
  mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
524
646
  axis=1
525
647
  )
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
648
 
551
649
  return (
552
- ap_results, # type: ignore[reportReturnType]
553
- ar_results,
650
+ (average_precision.astype(np.float64), mAP),
651
+ (average_recall, mAR),
554
652
  counts,
555
653
  pr_curve,
556
654
  )
557
655
 
558
656
 
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
657
  def _isin(
598
- data: NDArray[np.int32],
599
- subset: NDArray[np.int32],
658
+ data: NDArray,
659
+ subset: NDArray,
600
660
  ) -> NDArray[np.bool_]:
601
661
  """
602
662
  Creates a mask of rows that exist within the subset.
@@ -614,22 +674,59 @@ def _isin(
614
674
  Returns a bool mask with shape (N,).
615
675
  """
616
676
  combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
617
- np.uint32
677
+ np.int32
618
678
  )
619
679
  combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
620
680
  :, 1
621
- ].astype(np.uint32)
681
+ ].astype(np.int32)
622
682
  mask = np.isin(combined_data, combined_subset, assume_unique=False)
623
683
  return mask
624
684
 
625
685
 
686
+ class PairClassification(IntFlag):
687
+ TP = auto()
688
+ FP_FN_MISCLF = auto()
689
+ FP_UNMATCHED = auto()
690
+ FN_UNMATCHED = auto()
691
+
692
+
693
+ def mask_pairs_greedily(
694
+ pairs: NDArray[np.float64],
695
+ ):
696
+ groundtruths = pairs[:, 1].astype(np.int32)
697
+ predictions = pairs[:, 2].astype(np.int32)
698
+
699
+ # Pre‑allocate "seen" flags for every possible x and y
700
+ max_gt = groundtruths.max()
701
+ max_pd = predictions.max()
702
+ used_gt = np.zeros(max_gt + 1, dtype=np.bool_)
703
+ used_pd = np.zeros(max_pd + 1, dtype=np.bool_)
704
+
705
+ # This mask will mark which pairs to keep
706
+ keep = np.zeros(pairs.shape[0], dtype=bool)
707
+
708
+ for idx in range(groundtruths.shape[0]):
709
+ gidx = groundtruths[idx]
710
+ pidx = predictions[idx]
711
+
712
+ if not (gidx < 0 or pidx < 0 or used_gt[gidx] or used_pd[pidx]):
713
+ keep[idx] = True
714
+ used_gt[gidx] = True
715
+ used_pd[pidx] = True
716
+
717
+ mask_matches = _isin(
718
+ data=pairs[:, (1, 2)],
719
+ subset=np.unique(pairs[np.ix_(keep, (1, 2))], axis=0), # type: ignore - np.ix_ typing
720
+ )
721
+
722
+ return mask_matches
723
+
724
+
626
725
  def compute_confusion_matrix(
627
- data: NDArray[np.float64],
628
- label_metadata: NDArray[np.int32],
726
+ detailed_pairs: NDArray[np.float64],
629
727
  iou_thresholds: NDArray[np.float64],
630
728
  score_thresholds: NDArray[np.float64],
631
- n_examples: int,
632
- ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32]]:
729
+ ) -> NDArray[np.uint8]:
633
730
  """
634
731
  Compute detailed counts.
635
732
 
@@ -638,265 +735,108 @@ def compute_confusion_matrix(
638
735
  Index 0 - Datum Index
639
736
  Index 1 - GroundTruth Index
640
737
  Index 2 - Prediction Index
641
- Index 3 - IOU
642
- Index 4 - GroundTruth Label Index
643
- Index 5 - Prediction Label Index
738
+ Index 3 - GroundTruth Label Index
739
+ Index 4 - Prediction Label Index
740
+ Index 5 - IOU
644
741
  Index 6 - Score
645
742
 
646
743
  Parameters
647
744
  ----------
648
- data : NDArray[np.float64]
649
- A sorted array summarizing the IOU calculations of one or more pairs.
745
+ detailed_pairs : NDArray[np.float64]
746
+ An unsorted array summarizing the IOU calculations of one or more pairs.
650
747
  label_metadata : NDArray[np.int32]
651
748
  An array containing metadata related to labels.
652
749
  iou_thresholds : NDArray[np.float64]
653
750
  A 1-D array containing IOU thresholds.
654
751
  score_thresholds : NDArray[np.float64]
655
752
  A 1-D array containing score thresholds.
656
- n_examples : int
657
- The maximum number of examples to return per count.
658
753
 
659
754
  Returns
660
755
  -------
661
- NDArray[np.float64]
756
+ NDArray[np.uint8]
662
757
  Confusion matrix.
663
- NDArray[np.float64]
664
- Unmatched Predictions.
665
- NDArray[np.int32]
666
- Unmatched Ground Truths.
667
758
  """
668
-
669
- n_labels = label_metadata.shape[0]
759
+ n_pairs = detailed_pairs.shape[0]
670
760
  n_ious = iou_thresholds.shape[0]
671
761
  n_scores = score_thresholds.shape[0]
672
762
 
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,
763
+ pair_classifications = np.zeros(
764
+ (n_ious, n_scores, n_pairs),
765
+ dtype=np.uint8,
687
766
  )
688
767
 
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
768
+ ids = detailed_pairs[:, :5].astype(np.int32)
769
+ groundtruths = ids[:, (0, 1)]
770
+ predictions = ids[:, (0, 2)]
771
+ gt_ids = ids[:, 1]
772
+ pd_ids = ids[:, 2]
773
+ gt_labels = ids[:, 3]
774
+ pd_labels = ids[:, 4]
775
+ ious = detailed_pairs[:, 5]
776
+ scores = detailed_pairs[:, 6]
777
+
778
+ mask_gt_exists = gt_ids > -0.5
779
+ mask_pd_exists = pd_ids > -0.5
780
+ mask_label_match = np.isclose(gt_labels, pd_labels)
781
+ mask_score_nonzero = scores > 1e-9
782
+ mask_iou_nonzero = ious > 1e-9
694
783
 
695
784
  mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
696
785
  mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
697
- mask_gt_pd_mismatch = mask_gt_pd_exists & ~mask_label_match
698
786
 
699
- groundtruths = data[:, [0, 1]].astype(np.int32)
700
- predictions = data[:, [0, 2]].astype(np.int32)
787
+ mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
788
+
701
789
  for iou_idx in range(n_ious):
702
- mask_iou_threshold = data[:, 3] >= iou_thresholds[iou_idx]
790
+ mask_iou_threshold = ious >= iou_thresholds[iou_idx]
703
791
  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
792
  for score_idx in range(n_scores):
726
- mask_score_threshold = data[:, 6] >= score_thresholds[score_idx]
793
+ mask_score_threshold = scores >= score_thresholds[score_idx]
727
794
  mask_score = mask_score_nonzero & mask_score_threshold
728
795
 
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
796
+ mask_thresholded_matched_pairs = (
797
+ mask_matched_pairs & mask_iou & mask_score
738
798
  )
739
799
 
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
800
+ mask_true_positives = (
801
+ mask_thresholded_matched_pairs & mask_gt_pd_match
754
802
  )
803
+ mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
755
804
 
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
805
+ mask_groundtruths_in_thresholded_matched_pairs = _isin(
806
+ data=groundtruths,
807
+ subset=np.unique(
808
+ groundtruths[mask_thresholded_matched_pairs], axis=0
809
+ ),
767
810
  )
768
- mask_misclf[mask_misclf] &= (
769
- ~mask_gts_with_tp_override & ~mask_pds_with_tp_override
811
+ mask_predictions_in_thresholded_matched_pairs = _isin(
812
+ data=predictions,
813
+ subset=np.unique(
814
+ predictions[mask_thresholded_matched_pairs], axis=0
815
+ ),
770
816
  )
771
817
 
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,
818
+ mask_unmatched_predictions = (
819
+ ~mask_predictions_in_thresholded_matched_pairs
820
+ & mask_pd_exists
821
+ & mask_score
777
822
  )
778
-
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]
823
+ mask_unmatched_groundtruths = (
824
+ ~mask_groundtruths_in_thresholded_matched_pairs
825
+ & mask_gt_exists
786
826
  )
787
827
 
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
828
+ # classify pairings
829
+ pair_classifications[
830
+ iou_idx, score_idx, mask_true_positives
831
+ ] |= np.uint8(PairClassification.TP)
832
+ pair_classifications[iou_idx, score_idx, mask_misclf] |= np.uint8(
833
+ PairClassification.FP_FN_MISCLF
795
834
  )
796
-
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
804
- )
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]
835
+ pair_classifications[
836
+ iou_idx, score_idx, mask_unmatched_predictions
837
+ ] |= np.uint8(PairClassification.FP_UNMATCHED)
838
+ pair_classifications[
839
+ iou_idx, score_idx, mask_unmatched_groundtruths
840
+ ] |= np.uint8(PairClassification.FN_UNMATCHED)
841
+
842
+ return pair_classifications