valor-lite 0.34.2__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]
200
-
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]
211
+ label_metadata[label_indices.astype(np.int32), 0] = unique_counts
206
212
 
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,18 +294,13 @@ 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],
285
- NDArray[np.float64],
286
304
  ]:
287
305
  """
288
306
  Computes Object Detection metrics.
@@ -299,8 +317,8 @@ def compute_precion_recall(
299
317
 
300
318
  Parameters
301
319
  ----------
302
- data : NDArray[np.float64]
303
- 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.
304
322
  label_metadata : NDArray[np.int32]
305
323
  An array containing metadata related to labels.
306
324
  iou_thresholds : NDArray[np.float64]
@@ -310,35 +328,45 @@ def compute_precion_recall(
310
328
 
311
329
  Returns
312
330
  -------
313
- tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], float]
314
- Average Precision results.
315
- tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], float]
316
- Average Recall results.
317
- NDArray[np.float64]
318
- Accuracy.
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).
319
335
  NDArray[np.float64]
320
336
  Precision, Recall, TP, FP, FN, F1 Score.
321
337
  NDArray[np.float64]
322
338
  Interpolated Precision-Recall Curves.
323
339
  """
324
-
325
- n_rows = data.shape[0]
340
+ n_rows = ranked_pairs.shape[0]
326
341
  n_labels = label_metadata.shape[0]
327
342
  n_ious = iou_thresholds.shape[0]
328
343
  n_scores = score_thresholds.shape[0]
329
344
 
330
- if n_ious == 0:
331
- raise ValueError("At least one IOU threshold must be passed.")
332
- elif n_scores == 0:
333
- raise ValueError("At least one score threshold must be passed.")
334
-
345
+ # initialize result arrays
335
346
  average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
347
+ mAP = np.zeros(n_ious, dtype=np.float64)
336
348
  average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
337
- accuracy = np.zeros((n_ious, n_scores), dtype=np.float64)
349
+ mAR = np.zeros(n_scores, dtype=np.float64)
338
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]
339
369
 
340
- pd_labels = data[:, 5].astype(np.int32)
341
- scores = data[:, 6]
342
370
  unique_pd_labels, unique_pd_indices = np.unique(
343
371
  pd_labels, return_index=True
344
372
  )
@@ -350,9 +378,9 @@ def compute_precion_recall(
350
378
  running_tp_count = np.zeros_like(running_total_count)
351
379
  running_gt_count = np.zeros_like(running_total_count)
352
380
 
353
- mask_score_nonzero = data[:, 6] > 1e-9
354
- mask_gt_exists = data[:, 1] >= 0.0
355
- 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)
356
384
 
357
385
  mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
358
386
 
@@ -361,7 +389,7 @@ def compute_precion_recall(
361
389
  mask_fn = mask_gt_exists_labels_match
362
390
 
363
391
  for iou_idx in range(n_ious):
364
- mask_iou = data[:, 3] >= iou_thresholds[iou_idx]
392
+ mask_iou = ious >= iou_thresholds[iou_idx]
365
393
 
366
394
  mask_tp_outer = mask_tp & mask_iou
367
395
  mask_fp_outer = mask_fp & (
@@ -370,50 +398,51 @@ def compute_precion_recall(
370
398
  mask_fn_outer = mask_fn & mask_iou
371
399
 
372
400
  for score_idx in range(n_scores):
373
- mask_score_thresh = data[:, 6] >= score_thresholds[score_idx]
401
+ mask_score_thresh = scores >= score_thresholds[score_idx]
374
402
 
375
403
  mask_tp_inner = mask_tp_outer & mask_score_thresh
376
404
  mask_fp_inner = mask_fp_outer & mask_score_thresh
377
405
  mask_fn_inner = mask_fn_outer & ~mask_score_thresh
378
406
 
379
407
  # create true-positive mask score threshold
380
- tp_candidates = data[mask_tp_inner]
408
+ tp_candidates = ids[mask_tp_inner]
381
409
  _, indices_gt_unique = np.unique(
382
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
410
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
383
411
  )
384
412
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
385
413
  mask_gt_unique[indices_gt_unique] = True
414
+
386
415
  true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
387
416
  true_positives_mask[mask_tp_inner] = mask_gt_unique
388
417
 
418
+ mask_fp_inner |= mask_tp_inner & ~true_positives_mask
419
+
389
420
  # calculate intermediates
390
- pd_count = np.bincount(pd_labels, minlength=n_labels).astype(
391
- np.float64
392
- )
393
421
  tp_count = np.bincount(
394
422
  pd_labels,
395
423
  weights=true_positives_mask,
396
424
  minlength=n_labels,
397
425
  ).astype(np.float64)
398
-
399
426
  fp_count = np.bincount(
400
427
  pd_labels[mask_fp_inner],
401
428
  minlength=n_labels,
402
429
  ).astype(np.float64)
403
-
404
430
  fn_count = np.bincount(
405
431
  pd_labels[mask_fn_inner],
406
432
  minlength=n_labels,
407
433
  )
408
434
 
435
+ fn_count = gt_count - tp_count
436
+ tp_fp_count = tp_count + fp_count
437
+
409
438
  # calculate component metrics
410
439
  recall = np.zeros_like(tp_count)
411
440
  np.divide(tp_count, gt_count, where=gt_count > 1e-9, out=recall)
412
441
 
413
442
  precision = np.zeros_like(tp_count)
414
- np.divide(tp_count, pd_count, where=pd_count > 1e-9, out=precision)
415
-
416
- fn_count = gt_count - tp_count
443
+ np.divide(
444
+ tp_count, tp_fp_count, where=tp_fp_count > 1e-9, out=precision
445
+ )
417
446
 
418
447
  f1_score = np.zeros_like(precision)
419
448
  np.divide(
@@ -436,21 +465,13 @@ def compute_precion_recall(
436
465
  axis=1,
437
466
  )
438
467
 
439
- # caluculate accuracy
440
- total_pd_count = label_metadata[:, 1].sum()
441
- accuracy[iou_idx, score_idx] = (
442
- (tp_count.sum() / total_pd_count)
443
- if total_pd_count > 1e-9
444
- else 0.0
445
- )
446
-
447
468
  # calculate recall for AR
448
469
  average_recall[score_idx] += recall
449
470
 
450
471
  # create true-positive mask score threshold
451
- tp_candidates = data[mask_tp_outer]
472
+ tp_candidates = ids[mask_tp_outer]
452
473
  _, indices_gt_unique = np.unique(
453
- tp_candidates[:, [0, 1, 4]], axis=0, return_index=True
474
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
454
475
  )
455
476
  mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
456
477
  mask_gt_unique[indices_gt_unique] = True
@@ -487,7 +508,6 @@ def compute_precion_recall(
487
508
  recall_index = np.floor(recall * 100.0).astype(np.int32)
488
509
 
489
510
  # bin precision-recall curve
490
- pr_curve = np.zeros((n_ious, n_labels, 101, 2))
491
511
  for iou_idx in range(n_ious):
492
512
  p = precision[iou_idx]
493
513
  r = recall_index[iou_idx]
@@ -534,81 +554,18 @@ def compute_precion_recall(
534
554
  mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
535
555
  axis=1
536
556
  )
537
- else:
538
- mAP = np.zeros(n_ious, dtype=np.float64)
539
- mAR = np.zeros(n_scores, dtype=np.float64)
540
-
541
- # calculate AR and AR averaged over thresholds
542
- APAveragedOverIOUs = average_precision.mean(axis=0)
543
- ARAveragedOverScores = average_recall.mean(axis=0)
544
-
545
- # calculate mAP and mAR averaged over thresholds
546
- mAPAveragedOverIOUs = mAP.mean(axis=0)
547
- mARAveragedOverScores = mAR.mean(axis=0)
548
-
549
- ap_results = (
550
- average_precision,
551
- mAP,
552
- APAveragedOverIOUs,
553
- mAPAveragedOverIOUs,
554
- )
555
- ar_results = (
556
- average_recall,
557
- mAR,
558
- ARAveragedOverScores,
559
- mARAveragedOverScores,
560
- )
561
557
 
562
558
  return (
563
- ap_results, # type: ignore[reportReturnType]
564
- ar_results,
565
- accuracy,
559
+ (average_precision.astype(np.float64), mAP),
560
+ (average_recall, mAR),
566
561
  counts,
567
562
  pr_curve,
568
563
  )
569
564
 
570
565
 
571
- def _count_with_examples(
572
- data: NDArray[np.float64],
573
- unique_idx: int | list[int],
574
- label_idx: int | list[int],
575
- ) -> tuple[NDArray[np.float64], NDArray[np.int32], NDArray[np.intp]]:
576
- """
577
- Helper function for counting occurences of unique detailed pairs.
578
-
579
- Parameters
580
- ----------
581
- data : NDArray[np.float64]
582
- A masked portion of a detailed pairs array.
583
- unique_idx : int | list[int]
584
- The index or indices upon which uniqueness is constrained.
585
- label_idx : int | list[int]
586
- The index or indices within the unique index or indices that encode labels.
587
-
588
- Returns
589
- -------
590
- NDArray[np.float64]
591
- Examples drawn from the data input.
592
- NDArray[np.int32]
593
- Unique label indices.
594
- NDArray[np.intp]
595
- Counts for each unique label index.
596
- """
597
- unique_rows, indices = np.unique(
598
- data.astype(np.int32)[:, unique_idx],
599
- return_index=True,
600
- axis=0,
601
- )
602
- examples = data[indices]
603
- labels, counts = np.unique(
604
- unique_rows[:, label_idx], return_counts=True, axis=0
605
- )
606
- return examples, labels, counts
607
-
608
-
609
566
  def _isin(
610
- data: NDArray[np.int32],
611
- subset: NDArray[np.int32],
567
+ data: NDArray,
568
+ subset: NDArray,
612
569
  ) -> NDArray[np.bool_]:
613
570
  """
614
571
  Creates a mask of rows that exist within the subset.
@@ -626,22 +583,59 @@ def _isin(
626
583
  Returns a bool mask with shape (N,).
627
584
  """
628
585
  combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
629
- np.uint32
586
+ np.int32
630
587
  )
631
588
  combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
632
589
  :, 1
633
- ].astype(np.uint32)
590
+ ].astype(np.int32)
634
591
  mask = np.isin(combined_data, combined_subset, assume_unique=False)
635
592
  return mask
636
593
 
637
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
+
638
634
  def compute_confusion_matrix(
639
- data: NDArray[np.float64],
640
- label_metadata: NDArray[np.int32],
635
+ detailed_pairs: NDArray[np.float64],
641
636
  iou_thresholds: NDArray[np.float64],
642
637
  score_thresholds: NDArray[np.float64],
643
- n_examples: int,
644
- ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32]]:
638
+ ) -> NDArray[np.uint8]:
645
639
  """
646
640
  Compute detailed counts.
647
641
 
@@ -650,265 +644,108 @@ def compute_confusion_matrix(
650
644
  Index 0 - Datum Index
651
645
  Index 1 - GroundTruth Index
652
646
  Index 2 - Prediction Index
653
- Index 3 - IOU
654
- Index 4 - GroundTruth Label Index
655
- Index 5 - Prediction Label Index
647
+ Index 3 - GroundTruth Label Index
648
+ Index 4 - Prediction Label Index
649
+ Index 5 - IOU
656
650
  Index 6 - Score
657
651
 
658
652
  Parameters
659
653
  ----------
660
- data : NDArray[np.float64]
661
- 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.
662
656
  label_metadata : NDArray[np.int32]
663
657
  An array containing metadata related to labels.
664
658
  iou_thresholds : NDArray[np.float64]
665
659
  A 1-D array containing IOU thresholds.
666
660
  score_thresholds : NDArray[np.float64]
667
661
  A 1-D array containing score thresholds.
668
- n_examples : int
669
- The maximum number of examples to return per count.
670
662
 
671
663
  Returns
672
664
  -------
673
- NDArray[np.float64]
665
+ NDArray[np.uint8]
674
666
  Confusion matrix.
675
- NDArray[np.float64]
676
- Unmatched Predictions.
677
- NDArray[np.int32]
678
- Unmatched Ground Truths.
679
667
  """
680
-
681
- n_labels = label_metadata.shape[0]
668
+ n_pairs = detailed_pairs.shape[0]
682
669
  n_ious = iou_thresholds.shape[0]
683
670
  n_scores = score_thresholds.shape[0]
684
671
 
685
- confusion_matrix = -1 * np.ones(
686
- # (datum idx, gt idx, pd idx, pd score) * n_examples + count
687
- (n_ious, n_scores, n_labels, n_labels, 4 * n_examples + 1),
688
- dtype=np.float32,
689
- )
690
- unmatched_predictions = -1 * np.ones(
691
- # (datum idx, pd idx, pd score) * n_examples + count
692
- (n_ious, n_scores, n_labels, 3 * n_examples + 1),
693
- dtype=np.float32,
694
- )
695
- unmatched_ground_truths = -1 * np.ones(
696
- # (datum idx, gt idx) * n_examples + count
697
- (n_ious, n_scores, n_labels, 2 * n_examples + 1),
698
- dtype=np.int32,
672
+ pair_classifications = np.zeros(
673
+ (n_ious, n_scores, n_pairs),
674
+ dtype=np.uint8,
699
675
  )
700
676
 
701
- mask_gt_exists = data[:, 1] > -0.5
702
- mask_pd_exists = data[:, 2] > -0.5
703
- mask_label_match = np.isclose(data[:, 4], data[:, 5])
704
- mask_score_nonzero = data[:, 6] > 1e-9
705
- 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
706
692
 
707
693
  mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
708
694
  mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
709
- mask_gt_pd_mismatch = mask_gt_pd_exists & ~mask_label_match
710
695
 
711
- groundtruths = data[:, [0, 1]].astype(np.int32)
712
- predictions = data[:, [0, 2]].astype(np.int32)
696
+ mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
697
+
713
698
  for iou_idx in range(n_ious):
714
- mask_iou_threshold = data[:, 3] >= iou_thresholds[iou_idx]
699
+ mask_iou_threshold = ious >= iou_thresholds[iou_idx]
715
700
  mask_iou = mask_iou_nonzero & mask_iou_threshold
716
-
717
- groundtruths_passing_ious = np.unique(groundtruths[mask_iou], axis=0)
718
- mask_groundtruths_with_passing_ious = _isin(
719
- data=groundtruths,
720
- subset=groundtruths_passing_ious,
721
- )
722
- mask_groundtruths_without_passing_ious = (
723
- ~mask_groundtruths_with_passing_ious & mask_gt_exists
724
- )
725
-
726
- predictions_with_passing_ious = np.unique(
727
- predictions[mask_iou], axis=0
728
- )
729
- mask_predictions_with_passing_ious = _isin(
730
- data=predictions,
731
- subset=predictions_with_passing_ious,
732
- )
733
- mask_predictions_without_passing_ious = (
734
- ~mask_predictions_with_passing_ious & mask_pd_exists
735
- )
736
-
737
701
  for score_idx in range(n_scores):
738
- mask_score_threshold = data[:, 6] >= score_thresholds[score_idx]
702
+ mask_score_threshold = scores >= score_thresholds[score_idx]
739
703
  mask_score = mask_score_nonzero & mask_score_threshold
740
704
 
741
- groundtruths_with_passing_score = np.unique(
742
- groundtruths[mask_iou & mask_score], axis=0
743
- )
744
- mask_groundtruths_with_passing_score = _isin(
745
- data=groundtruths,
746
- subset=groundtruths_with_passing_score,
747
- )
748
- mask_groundtruths_without_passing_score = (
749
- ~mask_groundtruths_with_passing_score & mask_gt_exists
705
+ mask_thresholded_matched_pairs = (
706
+ mask_matched_pairs & mask_iou & mask_score
750
707
  )
751
708
 
752
- # create category masks
753
- mask_tp = mask_score & mask_iou & mask_gt_pd_match
754
- mask_misclf = mask_iou & (
755
- (
756
- ~mask_score
757
- & mask_gt_pd_match
758
- & mask_groundtruths_with_passing_score
759
- )
760
- | (mask_score & mask_gt_pd_mismatch)
761
- )
762
- mask_halluc = mask_score & mask_predictions_without_passing_ious
763
- mask_misprd = (
764
- mask_groundtruths_without_passing_ious
765
- | mask_groundtruths_without_passing_score
709
+ mask_true_positives = (
710
+ mask_thresholded_matched_pairs & mask_gt_pd_match
766
711
  )
712
+ mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
767
713
 
768
- # filter out true-positives from misclf and misprd
769
- mask_gts_with_tp_override = _isin(
770
- data=groundtruths[mask_misclf],
771
- subset=groundtruths[mask_tp],
772
- )
773
- mask_pds_with_tp_override = _isin(
774
- data=predictions[mask_misclf],
775
- subset=predictions[mask_tp],
776
- )
777
- mask_misprd[mask_misclf] |= (
778
- ~mask_gts_with_tp_override & mask_pds_with_tp_override
779
- )
780
- mask_misclf[mask_misclf] &= (
781
- ~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
+ ),
782
719
  )
783
-
784
- # count true positives
785
- tp_examples, tp_labels, tp_counts = _count_with_examples(
786
- data[mask_tp],
787
- unique_idx=[0, 2, 5],
788
- 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
+ ),
789
725
  )
790
726
 
791
- # count misclassifications
792
- (
793
- misclf_examples,
794
- misclf_labels,
795
- misclf_counts,
796
- ) = _count_with_examples(
797
- 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
798
731
  )
799
-
800
- # count unmatched predictions
801
- (
802
- halluc_examples,
803
- halluc_labels,
804
- halluc_counts,
805
- ) = _count_with_examples(
806
- 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
807
735
  )
808
736
 
809
- # count unmatched ground truths
810
- (
811
- misprd_examples,
812
- misprd_labels,
813
- misprd_counts,
814
- ) = _count_with_examples(
815
- 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
816
743
  )
817
-
818
- # store the counts
819
- confusion_matrix[
820
- iou_idx, score_idx, tp_labels, tp_labels, 0
821
- ] = tp_counts
822
- confusion_matrix[
823
- iou_idx,
824
- score_idx,
825
- misclf_labels[:, 0],
826
- misclf_labels[:, 1],
827
- 0,
828
- ] = misclf_counts
829
- unmatched_predictions[
830
- iou_idx,
831
- score_idx,
832
- halluc_labels,
833
- 0,
834
- ] = halluc_counts
835
- unmatched_ground_truths[
836
- iou_idx,
837
- score_idx,
838
- misprd_labels,
839
- 0,
840
- ] = misprd_counts
841
-
842
- # store examples
843
- if n_examples > 0:
844
- for label_idx in range(n_labels):
845
-
846
- # true-positive examples
847
- mask_tp_label = tp_examples[:, 5] == label_idx
848
- if mask_tp_label.sum() > 0:
849
- tp_label_examples = tp_examples[mask_tp_label][
850
- :n_examples
851
- ]
852
- confusion_matrix[
853
- iou_idx,
854
- score_idx,
855
- label_idx,
856
- label_idx,
857
- 1 : 4 * tp_label_examples.shape[0] + 1,
858
- ] = tp_label_examples[:, [0, 1, 2, 6]].flatten()
859
-
860
- # misclassification examples
861
- mask_misclf_gt_label = misclf_examples[:, 4] == label_idx
862
- if mask_misclf_gt_label.sum() > 0:
863
- for pd_label_idx in range(n_labels):
864
- mask_misclf_pd_label = (
865
- misclf_examples[:, 5] == pd_label_idx
866
- )
867
- mask_misclf_label_combo = (
868
- mask_misclf_gt_label & mask_misclf_pd_label
869
- )
870
- if mask_misclf_label_combo.sum() > 0:
871
- misclf_label_examples = misclf_examples[
872
- mask_misclf_label_combo
873
- ][:n_examples]
874
- confusion_matrix[
875
- iou_idx,
876
- score_idx,
877
- label_idx,
878
- pd_label_idx,
879
- 1 : 4 * misclf_label_examples.shape[0] + 1,
880
- ] = misclf_label_examples[
881
- :, [0, 1, 2, 6]
882
- ].flatten()
883
-
884
- # unmatched prediction examples
885
- mask_halluc_label = halluc_examples[:, 5] == label_idx
886
- if mask_halluc_label.sum() > 0:
887
- halluc_label_examples = halluc_examples[
888
- mask_halluc_label
889
- ][:n_examples]
890
- unmatched_predictions[
891
- iou_idx,
892
- score_idx,
893
- label_idx,
894
- 1 : 3 * halluc_label_examples.shape[0] + 1,
895
- ] = halluc_label_examples[:, [0, 2, 6]].flatten()
896
-
897
- # unmatched ground truth examples
898
- mask_misprd_label = misprd_examples[:, 4] == label_idx
899
- if misprd_examples.size > 0:
900
- misprd_label_examples = misprd_examples[
901
- mask_misprd_label
902
- ][:n_examples]
903
- unmatched_ground_truths[
904
- iou_idx,
905
- score_idx,
906
- label_idx,
907
- 1 : 2 * misprd_label_examples.shape[0] + 1,
908
- ] = misprd_label_examples[:, [0, 1]].flatten()
909
-
910
- return (
911
- confusion_matrix,
912
- unmatched_predictions,
913
- unmatched_ground_truths,
914
- ) # 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