valor-lite 0.36.5__py3-none-any.whl → 0.37.5__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.
Files changed (38) hide show
  1. valor_lite/cache/__init__.py +11 -0
  2. valor_lite/cache/compute.py +211 -0
  3. valor_lite/cache/ephemeral.py +302 -0
  4. valor_lite/cache/persistent.py +536 -0
  5. valor_lite/classification/__init__.py +5 -10
  6. valor_lite/classification/annotation.py +4 -0
  7. valor_lite/classification/computation.py +233 -251
  8. valor_lite/classification/evaluator.py +882 -0
  9. valor_lite/classification/loader.py +97 -0
  10. valor_lite/classification/metric.py +141 -4
  11. valor_lite/classification/shared.py +184 -0
  12. valor_lite/classification/utilities.py +221 -118
  13. valor_lite/exceptions.py +5 -0
  14. valor_lite/object_detection/__init__.py +5 -4
  15. valor_lite/object_detection/annotation.py +13 -1
  16. valor_lite/object_detection/computation.py +367 -304
  17. valor_lite/object_detection/evaluator.py +804 -0
  18. valor_lite/object_detection/loader.py +292 -0
  19. valor_lite/object_detection/metric.py +152 -3
  20. valor_lite/object_detection/shared.py +206 -0
  21. valor_lite/object_detection/utilities.py +182 -109
  22. valor_lite/semantic_segmentation/__init__.py +5 -4
  23. valor_lite/semantic_segmentation/annotation.py +7 -0
  24. valor_lite/semantic_segmentation/computation.py +20 -110
  25. valor_lite/semantic_segmentation/evaluator.py +414 -0
  26. valor_lite/semantic_segmentation/loader.py +205 -0
  27. valor_lite/semantic_segmentation/shared.py +149 -0
  28. valor_lite/semantic_segmentation/utilities.py +6 -23
  29. {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/METADATA +3 -1
  30. valor_lite-0.37.5.dist-info/RECORD +49 -0
  31. {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/WHEEL +1 -1
  32. valor_lite/classification/manager.py +0 -545
  33. valor_lite/object_detection/manager.py +0 -865
  34. valor_lite/profiling.py +0 -374
  35. valor_lite/semantic_segmentation/benchmark.py +0 -237
  36. valor_lite/semantic_segmentation/manager.py +0 -446
  37. valor_lite-0.36.5.dist-info/RECORD +0 -41
  38. {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,12 @@
1
1
  from enum import IntFlag, auto
2
2
 
3
3
  import numpy as np
4
+ import pyarrow as pa
4
5
  import shapely
5
6
  from numpy.typing import NDArray
6
7
 
8
+ EPSILON = 1e-9
9
+
7
10
 
8
11
  def compute_bbox_iou(data: NDArray[np.float64]) -> NDArray[np.float64]:
9
12
  """
@@ -70,7 +73,7 @@ def compute_bbox_iou(data: NDArray[np.float64]) -> NDArray[np.float64]:
70
73
  np.divide(
71
74
  intersection_area,
72
75
  union_area,
73
- where=union_area >= 1e-9,
76
+ where=union_area >= EPSILON,
74
77
  out=ious,
75
78
  )
76
79
  return ious
@@ -117,7 +120,7 @@ def compute_bitmask_iou(data: NDArray[np.bool_]) -> NDArray[np.float64]:
117
120
  np.divide(
118
121
  intersection_,
119
122
  union_,
120
- where=union_ >= 1e-9,
123
+ where=union_ >= EPSILON,
121
124
  out=ious,
122
125
  )
123
126
  return ious
@@ -167,291 +170,236 @@ def compute_polygon_iou(
167
170
  np.divide(
168
171
  intersection_areas,
169
172
  union_areas,
170
- where=union_areas >= 1e-9,
173
+ where=union_areas >= EPSILON,
171
174
  out=ious,
172
175
  )
173
176
  return ious
174
177
 
175
178
 
176
- def compute_label_metadata(
177
- ids: NDArray[np.int32],
178
- n_labels: int,
179
- ) -> NDArray[np.int32]:
179
+ def rank_pairs(
180
+ sorted_pairs: NDArray[np.float64],
181
+ ) -> tuple[NDArray[np.float64], NDArray[np.intp]]:
180
182
  """
181
- Computes label metadata returning a count of annotations per label.
183
+ Prunes and ranks prediction pairs.
184
+
185
+ Should result in a single pair per prediction annotation.
182
186
 
183
187
  Parameters
184
188
  ----------
185
- detailed_pairs : NDArray[np.int32]
186
- Detailed annotation pairings with shape (N, 7).
187
- Index 0 - Datum Index
188
- Index 1 - GroundTruth Index
189
- Index 2 - Prediction Index
190
- Index 3 - GroundTruth Label Index
191
- Index 4 - Prediction Label Index
192
- n_labels : int
193
- The total number of unique labels.
189
+ sorted_pairs : NDArray[np.float64]
190
+ Ranked annotation pairs.
191
+ Index 0 - Datum Index
192
+ Index 1 - GroundTruth Index
193
+ Index 2 - Prediction Index
194
+ Index 3 - GroundTruth Label Index
195
+ Index 4 - Prediction Label Index
196
+ Index 5 - IOU
197
+ Index 6 - Score
194
198
 
195
199
  Returns
196
200
  -------
197
- NDArray[np.int32]
198
- The label metadata array with shape (n_labels, 2).
199
- Index 0 - Ground truth label count
200
- Index 1 - Prediction label count
201
+ NDArray[float64]
202
+ Ranked prediction pairs.
203
+ NDArray[intp]
204
+ Indices of ranked prediction pairs.
201
205
  """
202
- label_metadata = np.zeros((n_labels, 2), dtype=np.int32)
203
-
204
- ground_truth_pairs = ids[:, (0, 1, 3)]
205
- ground_truth_pairs = ground_truth_pairs[ground_truth_pairs[:, 1] >= 0]
206
- unique_pairs = np.unique(ground_truth_pairs, axis=0)
207
- label_indices, unique_counts = np.unique(
208
- unique_pairs[:, 2], return_counts=True
209
- )
210
- label_metadata[label_indices.astype(np.int32), 0] = unique_counts
211
-
212
- prediction_pairs = ids[:, (0, 2, 4)]
213
- prediction_pairs = prediction_pairs[prediction_pairs[:, 1] >= 0]
214
- unique_pairs = np.unique(prediction_pairs, axis=0)
215
- label_indices, unique_counts = np.unique(
216
- unique_pairs[:, 2], return_counts=True
217
- )
218
- label_metadata[label_indices.astype(np.int32), 1] = unique_counts
219
206
 
220
- return label_metadata
207
+ # remove unmatched ground truths
208
+ mask_predictions = sorted_pairs[:, 2] >= 0.0
209
+ pairs = sorted_pairs[mask_predictions]
210
+ indices = np.where(mask_predictions)[0]
221
211
 
212
+ # find best fits for prediction
213
+ mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
214
+ matched_predictions = np.unique(pairs[mask_label_match, 2])
222
215
 
223
- def filter_cache(
224
- detailed_pairs: NDArray[np.float64],
225
- mask_datums: NDArray[np.bool_],
226
- mask_predictions: NDArray[np.bool_],
227
- mask_ground_truths: NDArray[np.bool_],
228
- n_labels: int,
229
- ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32],]:
230
- """
231
- Performs filtering on a detailed cache.
216
+ mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
232
217
 
233
- Parameters
234
- ----------
235
- detailed_pairs : NDArray[float64]
236
- A list of sorted detailed pairs with size (N, 7).
237
- mask_datums : NDArray[bool]
238
- A boolean mask with size (N,).
239
- mask_ground_truths : NDArray[bool]
240
- A boolean mask with size (N,).
241
- mask_predictions : NDArray[bool]
242
- A boolean mask with size (N,).
243
- n_labels : int
244
- The total number of unique labels.
218
+ pairs = pairs[mask_label_match | mask_unmatched_predictions]
219
+ indices = indices[mask_label_match | mask_unmatched_predictions]
245
220
 
246
- Returns
247
- -------
248
- NDArray[float64]
249
- Filtered detailed pairs.
250
- NDArray[float64]
251
- Filtered ranked pairs.
252
- NDArray[int32]
253
- Label metadata.
254
- """
255
- # filter datums
256
- detailed_pairs = detailed_pairs[mask_datums].copy()
257
-
258
- # filter ground truths
259
- if mask_ground_truths.any():
260
- invalid_groundtruth_indices = np.where(mask_ground_truths)[0]
261
- detailed_pairs[
262
- invalid_groundtruth_indices[:, None], (1, 3, 5)
263
- ] = np.array([[-1, -1, 0]])
264
-
265
- # filter predictions
266
- if mask_predictions.any():
267
- invalid_prediction_indices = np.where(mask_predictions)[0]
268
- detailed_pairs[
269
- invalid_prediction_indices[:, None], (2, 4, 5, 6)
270
- ] = np.array([[-1, -1, 0, -1]])
271
-
272
- # filter null pairs
273
- mask_null_pairs = np.all(
274
- np.isclose(
275
- detailed_pairs[:, 1:5],
276
- np.array([-1.0, -1.0, -1.0, -1.0]),
277
- ),
278
- axis=1,
221
+ # only keep the highest ranked prediction (datum_id, prediction_id, predicted_label_id)
222
+ _, unique_indices = np.unique(
223
+ pairs[:, [0, 2, 4]], axis=0, return_index=True
279
224
  )
280
- detailed_pairs = detailed_pairs[~mask_null_pairs]
225
+ pairs = pairs[unique_indices]
226
+ indices = indices[unique_indices]
281
227
 
282
- # sorts by score, iou with ground truth id as a tie-breaker
283
- indices = np.lexsort(
228
+ # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
229
+ sorted_indices = np.lexsort(
284
230
  (
285
- detailed_pairs[:, 1], # ground truth id
286
- -detailed_pairs[:, 5], # iou
287
- -detailed_pairs[:, 6], # score
231
+ -pairs[:, 5], # iou
232
+ -pairs[:, 6], # score
288
233
  )
289
234
  )
290
- detailed_pairs = detailed_pairs[indices]
291
- label_metadata = compute_label_metadata(
292
- ids=detailed_pairs[:, :5].astype(np.int32),
293
- n_labels=n_labels,
294
- )
295
- ranked_pairs = rank_pairs(
296
- detailed_pairs=detailed_pairs,
297
- label_metadata=label_metadata,
298
- )
299
- return (
300
- detailed_pairs,
301
- ranked_pairs,
302
- label_metadata,
303
- )
235
+ pairs = pairs[sorted_indices]
236
+ indices = indices[sorted_indices]
304
237
 
238
+ return pairs, indices
305
239
 
306
- def rank_pairs(
307
- detailed_pairs: NDArray[np.float64],
308
- label_metadata: NDArray[np.int32],
240
+
241
+ def calculate_ranking_boundaries(
242
+ ranked_pairs: NDArray[np.float64],
309
243
  ) -> NDArray[np.float64]:
310
244
  """
311
- Highly optimized pair ranking for computing precision and recall based metrics.
312
-
313
- Only ground truths and predictions that provide unique information are kept. The unkept
314
- pairs are represented via the label metadata array.
245
+ Determine IOU boundaries for computing AP across chunks.
315
246
 
316
247
  Parameters
317
248
  ----------
318
- detailed_pairs : NDArray[np.float64]
319
- Detailed annotation pairs with shape (n_pairs, 7).
320
- Index 0 - Datum Index
321
- Index 1 - GroundTruth Index
322
- Index 2 - Prediction Index
323
- Index 3 - GroundTruth Label Index
324
- Index 4 - Prediction Label Index
325
- Index 5 - IOU
326
- Index 6 - Score
327
- label_metadata : NDArray[np.int32]
328
- Array containing label counts with shape (n_labels, 2)
329
- Index 0 - Ground truth label count
330
- Index 1 - Prediction label count
249
+ ranked_pairs : NDArray[np.float64]
250
+ Ranked annotation pairs.
251
+ Index 0 - Datum Index
252
+ Index 1 - GroundTruth Index
253
+ Index 2 - Prediction Index
254
+ Index 3 - GroundTruth Label Index
255
+ Index 4 - Prediction Label Index
256
+ Index 5 - IOU
257
+ Index 6 - Score
331
258
 
332
259
  Returns
333
260
  -------
334
261
  NDArray[np.float64]
335
- Array of ranked pairs for precision-recall metric computation.
262
+ A 1-D array containing the lower IOU boundary for classifying pairs as true-positive across chunks.
336
263
  """
337
- pairs = detailed_pairs
338
-
339
- # remove null predictions
340
- pairs = pairs[pairs[:, 2] >= 0.0]
341
-
342
- # find best fits for prediction
343
- mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
344
- matched_predictions = np.unique(pairs[mask_label_match, 2])
345
- mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
346
- pairs = pairs[mask_label_match | mask_unmatched_predictions]
264
+ ids = ranked_pairs[:, (0, 1, 2, 3, 4)].astype(np.int64)
265
+ gts = ids[:, (0, 1, 3)]
266
+ gt_labels = ids[:, 3]
267
+ pd_labels = ids[:, 4]
268
+ ious = ranked_pairs[:, 5]
347
269
 
348
- # remove predictions for labels that have no ground truths
349
- for label_idx, count in enumerate(label_metadata[:, 0]):
350
- if count > 0:
270
+ # set default boundary to 2.0 as it will be used to check lower boundary in range [0-1].
271
+ iou_boundary = np.ones_like(ious) * 2
272
+
273
+ mask_matching_labels = gt_labels == pd_labels
274
+ mask_valid_gts = gts[:, 1] >= 0
275
+ unique_gts = np.unique(gts[mask_valid_gts], axis=0)
276
+ for gt in unique_gts:
277
+ mask_gt = (gts == gt).all(axis=1)
278
+ mask_gt &= mask_matching_labels
279
+ if mask_gt.sum() <= 1:
280
+ iou_boundary[mask_gt] = 0.0
351
281
  continue
352
- pairs = pairs[pairs[:, 4] != label_idx]
353
282
 
354
- # only keep the highest ranked pair
355
- _, indices = np.unique(pairs[:, [0, 2, 4]], axis=0, return_index=True)
356
- pairs = pairs[indices]
283
+ running_max = np.maximum.accumulate(ious[mask_gt])
284
+ mask_rmax = np.isclose(running_max, ious[mask_gt])
285
+ mask_rmax[1:] &= running_max[1:] > running_max[:-1]
286
+ mask_gt[mask_gt] &= mask_rmax
287
+
288
+ indices = np.where(mask_gt)[0]
289
+
290
+ iou_boundary[indices[0]] = 0.0
291
+ iou_boundary[indices[1:]] = ious[indices[:-1]]
292
+
293
+ return iou_boundary
294
+
295
+
296
+ def rank_table(tbl: pa.Table) -> pa.Table:
297
+ """Rank table for AP computation."""
298
+ numeric_columns = [
299
+ "datum_id",
300
+ "gt_id",
301
+ "pd_id",
302
+ "gt_label_id",
303
+ "pd_label_id",
304
+ "iou",
305
+ "pd_score",
306
+ ]
307
+ sorting_args = [
308
+ ("pd_score", "descending"),
309
+ ("iou", "descending"),
310
+ ]
311
+
312
+ # initial sort
313
+ sorted_tbl = tbl.sort_by(sorting_args)
314
+ pairs = np.column_stack(
315
+ [sorted_tbl[col].to_numpy() for col in numeric_columns]
316
+ )
357
317
 
358
- # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
359
- indices = np.lexsort(
360
- (
361
- -pairs[:, 5], # iou
362
- -pairs[:, 6], # score
363
- )
318
+ # rank pairs
319
+ ranked_pairs, indices = rank_pairs(pairs)
320
+ ranked_tbl = sorted_tbl.take(indices)
321
+
322
+ # find boundaries
323
+ lower_iou_bound = calculate_ranking_boundaries(ranked_pairs)
324
+ ranked_tbl = ranked_tbl.append_column(
325
+ pa.field("iou_prev", pa.float64()),
326
+ pa.array(lower_iou_bound, type=pa.float64()),
364
327
  )
365
- pairs = pairs[indices]
366
328
 
367
- return pairs
329
+ return ranked_tbl
368
330
 
369
331
 
370
- def compute_precion_recall(
332
+ def compute_counts(
371
333
  ranked_pairs: NDArray[np.float64],
372
- label_metadata: NDArray[np.int32],
373
334
  iou_thresholds: NDArray[np.float64],
374
335
  score_thresholds: NDArray[np.float64],
375
- ) -> tuple[
376
- tuple[
377
- NDArray[np.float64],
378
- NDArray[np.float64],
379
- ],
380
- tuple[
381
- NDArray[np.float64],
382
- NDArray[np.float64],
383
- ],
384
- NDArray[np.float64],
385
- NDArray[np.float64],
386
- ]:
336
+ number_of_groundtruths_per_label: NDArray[np.uint64],
337
+ number_of_labels: int,
338
+ running_counts: NDArray[np.uint64],
339
+ pr_curve: NDArray[np.float64],
340
+ ) -> NDArray[np.uint64]:
387
341
  """
388
342
  Computes Object Detection metrics.
389
343
 
390
- Takes data with shape (N, 7):
391
-
392
- Index 0 - Datum Index
393
- Index 1 - GroundTruth Index
394
- Index 2 - Prediction Index
395
- Index 3 - IOU
396
- Index 4 - GroundTruth Label Index
397
- Index 5 - Prediction Label Index
398
- Index 6 - Score
344
+ Precision-recall curve and running counts are updated in-place.
399
345
 
400
346
  Parameters
401
347
  ----------
402
348
  ranked_pairs : NDArray[np.float64]
403
349
  A ranked array summarizing the IOU calculations of one or more pairs.
404
- label_metadata : NDArray[np.int32]
405
- An array containing metadata related to labels.
350
+ Index 0 - Datum Index
351
+ Index 1 - GroundTruth Index
352
+ Index 2 - Prediction Index
353
+ Index 3 - GroundTruth Label Index
354
+ Index 4 - Prediction Label Index
355
+ Index 5 - IOU
356
+ Index 6 - Score
357
+ Index 7 - IOU Lower Boundary
406
358
  iou_thresholds : NDArray[np.float64]
407
359
  A 1-D array containing IOU thresholds.
408
360
  score_thresholds : NDArray[np.float64]
409
361
  A 1-D array containing score thresholds.
362
+ number_of_groundtruths_per_label : NDArray[np.uint64]
363
+ A 1-D array containing total number of ground truths per label.
364
+ number_of_labels : int
365
+ Total number of unique labels.
366
+ running_counts : NDArray[np.uint64]
367
+ A 2-D array containing running counts of total predictions and true-positive. This array is mutated.
368
+ pr_curve : NDArray[np.float64]
369
+ A 2-D array containing 101-point binning of precision and score over a fixed recall interval. This array is mutated.
410
370
 
411
371
  Returns
412
372
  -------
413
- tuple[NDArray[np.float64], NDArray[np.float64]]
414
- Average Precision results (AP, mAP).
415
- tuple[NDArray[np.float64], NDArray[np.float64]]
416
- Average Recall results (AR, mAR).
417
- NDArray[np.float64]
418
- Precision, Recall, TP, FP, FN, F1 Score.
419
- NDArray[np.float64]
420
- Interpolated Precision-Recall Curves.
373
+ NDArray[uint64]
374
+ Batched counts of TP, FP, FN.
421
375
  """
422
376
  n_rows = ranked_pairs.shape[0]
423
- n_labels = label_metadata.shape[0]
377
+ n_labels = number_of_labels
424
378
  n_ious = iou_thresholds.shape[0]
425
379
  n_scores = score_thresholds.shape[0]
426
380
 
427
381
  # initialize result arrays
428
- average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
429
- mAP = np.zeros(n_ious, dtype=np.float64)
430
- average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
431
- mAR = np.zeros(n_scores, dtype=np.float64)
432
- counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
433
- pr_curve = np.zeros((n_ious, n_labels, 101, 2))
382
+ counts = np.zeros((n_ious, n_scores, 3, n_labels), dtype=np.uint64)
434
383
 
435
384
  # start computation
436
- ids = ranked_pairs[:, :5].astype(np.int32)
385
+ ids = ranked_pairs[:, :5].astype(np.int64)
437
386
  gt_ids = ids[:, 1]
438
387
  gt_labels = ids[:, 3]
439
388
  pd_labels = ids[:, 4]
440
389
  ious = ranked_pairs[:, 5]
441
390
  scores = ranked_pairs[:, 6]
391
+ prev_ious = ranked_pairs[:, 7]
392
+
393
+ unique_pd_labels, _ = np.unique(pd_labels, return_index=True)
442
394
 
443
- unique_pd_labels, unique_pd_indices = np.unique(
444
- pd_labels, return_index=True
445
- )
446
- gt_count = label_metadata[:, 0]
447
395
  running_total_count = np.zeros(
448
396
  (n_ious, n_rows),
449
- dtype=np.float64,
397
+ dtype=np.uint64,
450
398
  )
451
399
  running_tp_count = np.zeros_like(running_total_count)
452
- running_gt_count = np.zeros_like(running_total_count)
400
+ running_gt_count = number_of_groundtruths_per_label[pd_labels]
453
401
 
454
- mask_score_nonzero = scores > 1e-9
402
+ mask_score_nonzero = scores > EPSILON
455
403
  mask_gt_exists = gt_ids >= 0.0
456
404
  mask_labels_match = np.isclose(gt_labels, pd_labels)
457
405
 
@@ -459,23 +407,22 @@ def compute_precion_recall(
459
407
 
460
408
  mask_tp = mask_score_nonzero & mask_gt_exists_labels_match
461
409
  mask_fp = mask_score_nonzero
462
- mask_fn = mask_gt_exists_labels_match
463
410
 
464
411
  for iou_idx in range(n_ious):
465
- mask_iou = ious >= iou_thresholds[iou_idx]
412
+ mask_iou_curr = ious >= iou_thresholds[iou_idx]
413
+ mask_iou_prev = prev_ious < iou_thresholds[iou_idx]
414
+ mask_iou = mask_iou_curr & mask_iou_prev
466
415
 
467
416
  mask_tp_outer = mask_tp & mask_iou
468
417
  mask_fp_outer = mask_fp & (
469
418
  (~mask_gt_exists_labels_match & mask_iou) | ~mask_iou
470
419
  )
471
- mask_fn_outer = mask_fn & mask_iou
472
420
 
473
421
  for score_idx in range(n_scores):
474
422
  mask_score_thresh = scores >= score_thresholds[score_idx]
475
423
 
476
424
  mask_tp_inner = mask_tp_outer & mask_score_thresh
477
425
  mask_fp_inner = mask_fp_outer & mask_score_thresh
478
- mask_fn_inner = mask_fn_outer & ~mask_score_thresh
479
426
 
480
427
  # create true-positive mask score threshold
481
428
  tp_candidates = ids[mask_tp_inner]
@@ -491,108 +438,150 @@ def compute_precion_recall(
491
438
  mask_fp_inner |= mask_tp_inner & ~true_positives_mask
492
439
 
493
440
  # calculate intermediates
494
- tp_count = np.bincount(
441
+ counts[iou_idx, score_idx, 0, :] = np.bincount(
495
442
  pd_labels,
496
443
  weights=true_positives_mask,
497
444
  minlength=n_labels,
498
- ).astype(np.float64)
499
- fp_count = np.bincount(
445
+ )
446
+ # fp count
447
+ counts[iou_idx, score_idx, 1, :] = np.bincount(
500
448
  pd_labels[mask_fp_inner],
501
449
  minlength=n_labels,
502
- ).astype(np.float64)
503
- fn_count = np.bincount(
504
- pd_labels[mask_fn_inner],
505
- minlength=n_labels,
506
450
  )
507
451
 
508
- fn_count = gt_count - tp_count
509
- tp_fp_count = tp_count + fp_count
510
-
511
- # calculate component metrics
512
- recall = np.zeros_like(tp_count)
513
- np.divide(tp_count, gt_count, where=gt_count > 1e-9, out=recall)
514
-
515
- precision = np.zeros_like(tp_count)
516
- np.divide(
517
- tp_count, tp_fp_count, where=tp_fp_count > 1e-9, out=precision
518
- )
519
-
520
- f1_score = np.zeros_like(precision)
521
- np.divide(
522
- 2 * np.multiply(precision, recall),
523
- (precision + recall),
524
- where=(precision + recall) > 1e-9,
525
- out=f1_score,
526
- dtype=np.float64,
527
- )
528
-
529
- counts[iou_idx][score_idx] = np.concatenate(
530
- (
531
- tp_count[:, np.newaxis],
532
- fp_count[:, np.newaxis],
533
- fn_count[:, np.newaxis],
534
- precision[:, np.newaxis],
535
- recall[:, np.newaxis],
536
- f1_score[:, np.newaxis],
537
- ),
538
- axis=1,
539
- )
540
-
541
- # calculate recall for AR
542
- average_recall[score_idx] += recall
543
-
544
- # create true-positive mask score threshold
545
- tp_candidates = ids[mask_tp_outer]
546
- _, indices_gt_unique = np.unique(
547
- tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
548
- )
549
- mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
550
- mask_gt_unique[indices_gt_unique] = True
551
- true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
552
- true_positives_mask[mask_tp_outer] = mask_gt_unique
553
-
554
452
  # count running tp and total for AP
555
453
  for pd_label in unique_pd_labels:
556
454
  mask_pd_label = pd_labels == pd_label
557
- running_gt_count[iou_idx][mask_pd_label] = gt_count[pd_label]
558
- running_total_count[iou_idx][mask_pd_label] = np.arange(
559
- 1, mask_pd_label.sum() + 1
455
+ total_count = mask_pd_label.sum()
456
+ if total_count == 0:
457
+ continue
458
+
459
+ # running total prediction count
460
+ running_total_count[iou_idx, mask_pd_label] = np.arange(
461
+ running_counts[iou_idx, pd_label, 0] + 1,
462
+ running_counts[iou_idx, pd_label, 0] + total_count + 1,
560
463
  )
561
- mask_tp_for_counting = mask_pd_label & true_positives_mask
562
- running_tp_count[iou_idx][mask_tp_for_counting] = np.arange(
563
- 1, mask_tp_for_counting.sum() + 1
464
+ running_counts[iou_idx, pd_label, 0] += total_count
465
+
466
+ # running true-positive count
467
+ mask_tp_for_counting = mask_pd_label & mask_tp_outer
468
+ tp_count = mask_tp_for_counting.sum()
469
+ running_tp_count[iou_idx, mask_tp_for_counting] = np.arange(
470
+ running_counts[iou_idx, pd_label, 1] + 1,
471
+ running_counts[iou_idx, pd_label, 1] + tp_count + 1,
564
472
  )
473
+ running_counts[iou_idx, pd_label, 1] += tp_count
565
474
 
566
475
  # calculate running precision-recall points for AP
567
- precision = np.zeros_like(running_total_count)
476
+ precision = np.zeros_like(running_total_count, dtype=np.float64)
568
477
  np.divide(
569
478
  running_tp_count,
570
479
  running_total_count,
571
- where=running_total_count > 1e-9,
480
+ where=running_total_count > 0,
572
481
  out=precision,
573
482
  )
574
- recall = np.zeros_like(running_total_count)
483
+ recall = np.zeros_like(running_total_count, dtype=np.float64)
575
484
  np.divide(
576
485
  running_tp_count,
577
486
  running_gt_count,
578
- where=running_gt_count > 1e-9,
487
+ where=running_gt_count > 0,
579
488
  out=recall,
580
489
  )
581
490
  recall_index = np.floor(recall * 100.0).astype(np.int32)
582
491
 
583
- # bin precision-recall curve
492
+ # sort precision in descending order
493
+ precision_indices = np.argsort(-precision, axis=1)
494
+
495
+ # populate precision-recall curve
584
496
  for iou_idx in range(n_ious):
585
- p = precision[iou_idx]
586
- r = recall_index[iou_idx]
587
- pr_curve[iou_idx, pd_labels, r, 0] = np.maximum(
588
- pr_curve[iou_idx, pd_labels, r, 0],
589
- p,
497
+ labeled_recall = np.hstack(
498
+ [
499
+ pd_labels.reshape(-1, 1),
500
+ recall_index[iou_idx, :].reshape(-1, 1),
501
+ ]
502
+ )
503
+
504
+ # extract maximum score per (label, recall) bin
505
+ # arrays are already ordered by descending score
506
+ lr_pairs, recall_indices = np.unique(
507
+ labeled_recall, return_index=True, axis=0
590
508
  )
591
- pr_curve[iou_idx, pd_labels, r, 1] = np.maximum(
592
- pr_curve[iou_idx, pd_labels, r, 1],
593
- scores,
509
+ li = lr_pairs[:, 0]
510
+ ri = lr_pairs[:, 1]
511
+ pr_curve[iou_idx, li, ri, 1] = np.maximum(
512
+ pr_curve[iou_idx, li, ri, 1],
513
+ scores[recall_indices],
594
514
  )
595
515
 
516
+ # extract maximum precision per (label, recall) bin
517
+ # reorder arrays into descending precision order
518
+ indices = precision_indices[iou_idx]
519
+ sorted_precision = precision[iou_idx, indices]
520
+ sorted_labeled_recall = labeled_recall[indices]
521
+ lr_pairs, recall_indices = np.unique(
522
+ sorted_labeled_recall, return_index=True, axis=0
523
+ )
524
+ li = lr_pairs[:, 0]
525
+ ri = lr_pairs[:, 1]
526
+ pr_curve[iou_idx, li, ri, 0] = np.maximum(
527
+ pr_curve[iou_idx, li, ri, 0],
528
+ sorted_precision[recall_indices],
529
+ )
530
+
531
+ return counts
532
+
533
+
534
+ def compute_precision_recall_f1(
535
+ counts: NDArray[np.uint64],
536
+ number_of_groundtruths_per_label: NDArray[np.uint64],
537
+ ) -> NDArray[np.float64]:
538
+
539
+ prec_rec_f1 = np.zeros_like(counts, dtype=np.float64)
540
+
541
+ # alias
542
+ tp_count = counts[:, :, 0, :]
543
+ fp_count = counts[:, :, 1, :]
544
+ tp_fp_count = tp_count + fp_count
545
+
546
+ # calculate component metrics
547
+ np.divide(
548
+ tp_count,
549
+ tp_fp_count,
550
+ where=tp_fp_count > 0,
551
+ out=prec_rec_f1[:, :, 0, :],
552
+ )
553
+ np.divide(
554
+ tp_count,
555
+ number_of_groundtruths_per_label,
556
+ where=number_of_groundtruths_per_label > 0,
557
+ out=prec_rec_f1[:, :, 1, :],
558
+ )
559
+ p = prec_rec_f1[:, :, 0, :]
560
+ r = prec_rec_f1[:, :, 1, :]
561
+ np.divide(
562
+ 2 * np.multiply(p, r),
563
+ (p + r),
564
+ where=(p + r) > EPSILON,
565
+ out=prec_rec_f1[:, :, 2, :],
566
+ )
567
+ return prec_rec_f1
568
+
569
+
570
+ def compute_average_recall(prec_rec_f1: NDArray[np.float64]):
571
+ recall = prec_rec_f1[:, :, 1, :]
572
+ average_recall = recall.mean(axis=0)
573
+ mAR = average_recall.mean(axis=-1)
574
+ return average_recall, mAR
575
+
576
+
577
+ def compute_average_precision(pr_curve: NDArray[np.float64]):
578
+ n_ious = pr_curve.shape[0]
579
+ n_labels = pr_curve.shape[1]
580
+
581
+ # initialize result arrays
582
+ average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
583
+ mAP = np.zeros(n_ious, dtype=np.float64)
584
+
596
585
  # calculate average precision
597
586
  running_max_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
598
587
  running_max_score = np.zeros((n_labels), dtype=np.float64)
@@ -616,24 +605,11 @@ def compute_precion_recall(
616
605
 
617
606
  average_precision = average_precision / 101.0
618
607
 
619
- # calculate average recall
620
- average_recall = average_recall / n_ious
621
-
622
608
  # calculate mAP and mAR
623
- if unique_pd_labels.size > 0:
624
- mAP: NDArray[np.float64] = average_precision[:, unique_pd_labels].mean(
625
- axis=1
626
- )
627
- mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
628
- axis=1
629
- )
609
+ if average_precision.size > 0:
610
+ mAP = average_precision.mean(axis=1)
630
611
 
631
- return (
632
- (average_precision.astype(np.float64), mAP),
633
- (average_recall, mAR),
634
- counts,
635
- pr_curve,
636
- )
612
+ return average_precision, mAP, pr_curve
637
613
 
638
614
 
639
615
  def _isin(
@@ -666,6 +642,7 @@ def _isin(
666
642
 
667
643
 
668
644
  class PairClassification(IntFlag):
645
+ NULL = auto()
669
646
  TP = auto()
670
647
  FP_FN_MISCLF = auto()
671
648
  FP_UNMATCHED = auto()
@@ -704,11 +681,13 @@ def mask_pairs_greedily(
704
681
  return mask_matches
705
682
 
706
683
 
707
- def compute_confusion_matrix(
684
+ def compute_pair_classifications(
708
685
  detailed_pairs: NDArray[np.float64],
709
686
  iou_thresholds: NDArray[np.float64],
710
687
  score_thresholds: NDArray[np.float64],
711
- ) -> NDArray[np.uint8]:
688
+ ) -> tuple[
689
+ NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_]
690
+ ]:
712
691
  """
713
692
  Compute detailed counts.
714
693
 
@@ -760,8 +739,8 @@ def compute_confusion_matrix(
760
739
  mask_gt_exists = gt_ids > -0.5
761
740
  mask_pd_exists = pd_ids > -0.5
762
741
  mask_label_match = np.isclose(gt_labels, pd_labels)
763
- mask_score_nonzero = scores > 1e-9
764
- mask_iou_nonzero = ious > 1e-9
742
+ mask_score_nonzero = scores > EPSILON
743
+ mask_iou_nonzero = ious > EPSILON
765
744
 
766
745
  mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
767
746
  mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
@@ -821,4 +800,88 @@ def compute_confusion_matrix(
821
800
  iou_idx, score_idx, mask_unmatched_groundtruths
822
801
  ] |= np.uint8(PairClassification.FN_UNMATCHED)
823
802
 
824
- return pair_classifications
803
+ mask_tp = np.bitwise_and(pair_classifications, PairClassification.TP) > 0
804
+ mask_fp_fn_misclf = (
805
+ np.bitwise_and(pair_classifications, PairClassification.FP_FN_MISCLF)
806
+ > 0
807
+ )
808
+ mask_fp_unmatched = (
809
+ np.bitwise_and(pair_classifications, PairClassification.FP_UNMATCHED)
810
+ > 0
811
+ )
812
+ mask_fn_unmatched = (
813
+ np.bitwise_and(pair_classifications, PairClassification.FN_UNMATCHED)
814
+ > 0
815
+ )
816
+
817
+ return (
818
+ mask_tp,
819
+ mask_fp_fn_misclf,
820
+ mask_fp_unmatched,
821
+ mask_fn_unmatched,
822
+ )
823
+
824
+
825
+ def compute_confusion_matrix(
826
+ detailed_pairs: NDArray[np.float64],
827
+ mask_tp: NDArray[np.bool_],
828
+ mask_fp_fn_misclf: NDArray[np.bool_],
829
+ mask_fp_unmatched: NDArray[np.bool_],
830
+ mask_fn_unmatched: NDArray[np.bool_],
831
+ number_of_labels: int,
832
+ iou_thresholds: NDArray[np.float64],
833
+ score_thresholds: NDArray[np.float64],
834
+ ):
835
+ n_ious = iou_thresholds.size
836
+ n_scores = score_thresholds.size
837
+ ids = detailed_pairs[:, :5].astype(np.int64)
838
+
839
+ # initialize arrays
840
+ confusion_matrices = np.zeros(
841
+ (n_ious, n_scores, number_of_labels, number_of_labels), dtype=np.uint64
842
+ )
843
+ unmatched_groundtruths = np.zeros(
844
+ (n_ious, n_scores, number_of_labels), dtype=np.uint64
845
+ )
846
+ unmatched_predictions = np.zeros_like(unmatched_groundtruths)
847
+
848
+ mask_matched = mask_tp | mask_fp_fn_misclf
849
+ for iou_idx in range(n_ious):
850
+ for score_idx in range(n_scores):
851
+ # matched annotations
852
+ unique_pairs = np.unique(
853
+ ids[np.ix_(mask_matched[iou_idx, score_idx], (0, 1, 2, 3, 4))], # type: ignore - numpy ix_ typing
854
+ axis=0,
855
+ )
856
+ unique_labels, unique_label_counts = np.unique(
857
+ unique_pairs[:, (3, 4)], axis=0, return_counts=True
858
+ )
859
+ confusion_matrices[
860
+ iou_idx, score_idx, unique_labels[:, 0], unique_labels[:, 1]
861
+ ] = unique_label_counts
862
+
863
+ # unmatched groundtruths
864
+ unique_pairs = np.unique(
865
+ ids[np.ix_(mask_fn_unmatched[iou_idx, score_idx], (0, 1, 3))], # type: ignore - numpy ix_ typing
866
+ axis=0,
867
+ )
868
+ unique_labels, unique_label_counts = np.unique(
869
+ unique_pairs[:, 2], return_counts=True
870
+ )
871
+ unmatched_groundtruths[
872
+ iou_idx, score_idx, unique_labels
873
+ ] = unique_label_counts
874
+
875
+ # unmatched predictions
876
+ unique_pairs = np.unique(
877
+ ids[np.ix_(mask_fp_unmatched[iou_idx, score_idx], (0, 2, 4))], # type: ignore - numpy ix_ typing
878
+ axis=0,
879
+ )
880
+ unique_labels, unique_label_counts = np.unique(
881
+ unique_pairs[:, 2], return_counts=True
882
+ )
883
+ unmatched_predictions[
884
+ iou_idx, score_idx, unique_labels
885
+ ] = unique_label_counts
886
+
887
+ return confusion_matrices, unmatched_groundtruths, unmatched_predictions