valor-lite 0.36.6__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 +368 -299
  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 -100
  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.6.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.6.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 -864
  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.6.dist-info/RECORD +0 -41
  38. {valor_lite-0.36.6.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,285 +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
206
 
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
-
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]
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]
341
269
 
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]
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
281
+ continue
282
+
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
+ )
347
317
 
348
- # only keep the highest ranked pair
349
- _, indices = np.unique(pairs[:, [0, 2, 4]], axis=0, return_index=True)
350
- pairs = pairs[indices]
318
+ # rank pairs
319
+ ranked_pairs, indices = rank_pairs(pairs)
320
+ ranked_tbl = sorted_tbl.take(indices)
351
321
 
352
- # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
353
- indices = np.lexsort(
354
- (
355
- -pairs[:, 5], # iou
356
- -pairs[:, 6], # score
357
- )
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()),
358
327
  )
359
- pairs = pairs[indices]
360
328
 
361
- return pairs
329
+ return ranked_tbl
362
330
 
363
331
 
364
- def compute_precion_recall(
332
+ def compute_counts(
365
333
  ranked_pairs: NDArray[np.float64],
366
- label_metadata: NDArray[np.int32],
367
334
  iou_thresholds: NDArray[np.float64],
368
335
  score_thresholds: NDArray[np.float64],
369
- ) -> tuple[
370
- tuple[
371
- NDArray[np.float64],
372
- NDArray[np.float64],
373
- ],
374
- tuple[
375
- NDArray[np.float64],
376
- NDArray[np.float64],
377
- ],
378
- NDArray[np.float64],
379
- NDArray[np.float64],
380
- ]:
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]:
381
341
  """
382
342
  Computes Object Detection metrics.
383
343
 
384
- Takes data with shape (N, 7):
385
-
386
- Index 0 - Datum Index
387
- Index 1 - GroundTruth Index
388
- Index 2 - Prediction Index
389
- Index 3 - IOU
390
- Index 4 - GroundTruth Label Index
391
- Index 5 - Prediction Label Index
392
- Index 6 - Score
344
+ Precision-recall curve and running counts are updated in-place.
393
345
 
394
346
  Parameters
395
347
  ----------
396
348
  ranked_pairs : NDArray[np.float64]
397
349
  A ranked array summarizing the IOU calculations of one or more pairs.
398
- label_metadata : NDArray[np.int32]
399
- 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
400
358
  iou_thresholds : NDArray[np.float64]
401
359
  A 1-D array containing IOU thresholds.
402
360
  score_thresholds : NDArray[np.float64]
403
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.
404
370
 
405
371
  Returns
406
372
  -------
407
- tuple[NDArray[np.float64], NDArray[np.float64]]
408
- Average Precision results (AP, mAP).
409
- tuple[NDArray[np.float64], NDArray[np.float64]]
410
- Average Recall results (AR, mAR).
411
- NDArray[np.float64]
412
- Precision, Recall, TP, FP, FN, F1 Score.
413
- NDArray[np.float64]
414
- Interpolated Precision-Recall Curves.
373
+ NDArray[uint64]
374
+ Batched counts of TP, FP, FN.
415
375
  """
416
376
  n_rows = ranked_pairs.shape[0]
417
- n_labels = label_metadata.shape[0]
377
+ n_labels = number_of_labels
418
378
  n_ious = iou_thresholds.shape[0]
419
379
  n_scores = score_thresholds.shape[0]
420
380
 
421
381
  # initialize result arrays
422
- average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
423
- mAP = np.zeros(n_ious, dtype=np.float64)
424
- average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
425
- mAR = np.zeros(n_scores, dtype=np.float64)
426
- counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
427
- pr_curve = np.zeros((n_ious, n_labels, 101, 2))
382
+ counts = np.zeros((n_ious, n_scores, 3, n_labels), dtype=np.uint64)
428
383
 
429
384
  # start computation
430
- ids = ranked_pairs[:, :5].astype(np.int32)
385
+ ids = ranked_pairs[:, :5].astype(np.int64)
431
386
  gt_ids = ids[:, 1]
432
387
  gt_labels = ids[:, 3]
433
388
  pd_labels = ids[:, 4]
434
389
  ious = ranked_pairs[:, 5]
435
390
  scores = ranked_pairs[:, 6]
391
+ prev_ious = ranked_pairs[:, 7]
392
+
393
+ unique_pd_labels, _ = np.unique(pd_labels, return_index=True)
436
394
 
437
- unique_pd_labels, unique_pd_indices = np.unique(
438
- pd_labels, return_index=True
439
- )
440
- gt_count = label_metadata[:, 0]
441
395
  running_total_count = np.zeros(
442
396
  (n_ious, n_rows),
443
- dtype=np.float64,
397
+ dtype=np.uint64,
444
398
  )
445
399
  running_tp_count = np.zeros_like(running_total_count)
446
- running_gt_count = np.zeros_like(running_total_count)
400
+ running_gt_count = number_of_groundtruths_per_label[pd_labels]
447
401
 
448
- mask_score_nonzero = scores > 1e-9
402
+ mask_score_nonzero = scores > EPSILON
449
403
  mask_gt_exists = gt_ids >= 0.0
450
404
  mask_labels_match = np.isclose(gt_labels, pd_labels)
451
405
 
@@ -453,23 +407,22 @@ def compute_precion_recall(
453
407
 
454
408
  mask_tp = mask_score_nonzero & mask_gt_exists_labels_match
455
409
  mask_fp = mask_score_nonzero
456
- mask_fn = mask_gt_exists_labels_match
457
410
 
458
411
  for iou_idx in range(n_ious):
459
- 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
460
415
 
461
416
  mask_tp_outer = mask_tp & mask_iou
462
417
  mask_fp_outer = mask_fp & (
463
418
  (~mask_gt_exists_labels_match & mask_iou) | ~mask_iou
464
419
  )
465
- mask_fn_outer = mask_fn & mask_iou
466
420
 
467
421
  for score_idx in range(n_scores):
468
422
  mask_score_thresh = scores >= score_thresholds[score_idx]
469
423
 
470
424
  mask_tp_inner = mask_tp_outer & mask_score_thresh
471
425
  mask_fp_inner = mask_fp_outer & mask_score_thresh
472
- mask_fn_inner = mask_fn_outer & ~mask_score_thresh
473
426
 
474
427
  # create true-positive mask score threshold
475
428
  tp_candidates = ids[mask_tp_inner]
@@ -485,107 +438,149 @@ def compute_precion_recall(
485
438
  mask_fp_inner |= mask_tp_inner & ~true_positives_mask
486
439
 
487
440
  # calculate intermediates
488
- tp_count = np.bincount(
441
+ counts[iou_idx, score_idx, 0, :] = np.bincount(
489
442
  pd_labels,
490
443
  weights=true_positives_mask,
491
444
  minlength=n_labels,
492
- ).astype(np.float64)
493
- fp_count = np.bincount(
445
+ )
446
+ # fp count
447
+ counts[iou_idx, score_idx, 1, :] = np.bincount(
494
448
  pd_labels[mask_fp_inner],
495
449
  minlength=n_labels,
496
- ).astype(np.float64)
497
- fn_count = np.bincount(
498
- pd_labels[mask_fn_inner],
499
- minlength=n_labels,
500
450
  )
501
451
 
502
- fn_count = gt_count - tp_count
503
- tp_fp_count = tp_count + fp_count
504
-
505
- # calculate component metrics
506
- recall = np.zeros_like(tp_count)
507
- np.divide(tp_count, gt_count, where=gt_count > 1e-9, out=recall)
508
-
509
- precision = np.zeros_like(tp_count)
510
- np.divide(
511
- tp_count, tp_fp_count, where=tp_fp_count > 1e-9, out=precision
512
- )
513
-
514
- f1_score = np.zeros_like(precision)
515
- np.divide(
516
- 2 * np.multiply(precision, recall),
517
- (precision + recall),
518
- where=(precision + recall) > 1e-9,
519
- out=f1_score,
520
- dtype=np.float64,
521
- )
522
-
523
- counts[iou_idx][score_idx] = np.concatenate(
524
- (
525
- tp_count[:, np.newaxis],
526
- fp_count[:, np.newaxis],
527
- fn_count[:, np.newaxis],
528
- precision[:, np.newaxis],
529
- recall[:, np.newaxis],
530
- f1_score[:, np.newaxis],
531
- ),
532
- axis=1,
533
- )
534
-
535
- # calculate recall for AR
536
- average_recall[score_idx] += recall
537
-
538
- # create true-positive mask score threshold
539
- tp_candidates = ids[mask_tp_outer]
540
- _, indices_gt_unique = np.unique(
541
- tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
542
- )
543
- mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
544
- mask_gt_unique[indices_gt_unique] = True
545
- true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
546
- true_positives_mask[mask_tp_outer] = mask_gt_unique
547
-
548
452
  # count running tp and total for AP
549
453
  for pd_label in unique_pd_labels:
550
454
  mask_pd_label = pd_labels == pd_label
551
- running_gt_count[iou_idx][mask_pd_label] = gt_count[pd_label]
552
- running_total_count[iou_idx][mask_pd_label] = np.arange(
553
- 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,
554
463
  )
555
- mask_tp_for_counting = mask_pd_label & true_positives_mask
556
- running_tp_count[iou_idx][mask_tp_for_counting] = np.arange(
557
- 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,
558
472
  )
473
+ running_counts[iou_idx, pd_label, 1] += tp_count
559
474
 
560
475
  # calculate running precision-recall points for AP
561
- precision = np.zeros_like(running_total_count)
476
+ precision = np.zeros_like(running_total_count, dtype=np.float64)
562
477
  np.divide(
563
478
  running_tp_count,
564
479
  running_total_count,
565
- where=running_total_count > 1e-9,
480
+ where=running_total_count > 0,
566
481
  out=precision,
567
482
  )
568
- recall = np.zeros_like(running_total_count)
483
+ recall = np.zeros_like(running_total_count, dtype=np.float64)
569
484
  np.divide(
570
485
  running_tp_count,
571
486
  running_gt_count,
572
- where=running_gt_count > 1e-9,
487
+ where=running_gt_count > 0,
573
488
  out=recall,
574
489
  )
575
490
  recall_index = np.floor(recall * 100.0).astype(np.int32)
576
491
 
577
- # bin precision-recall curve
492
+ # sort precision in descending order
493
+ precision_indices = np.argsort(-precision, axis=1)
494
+
495
+ # populate precision-recall curve
578
496
  for iou_idx in range(n_ious):
579
- p = precision[iou_idx]
580
- r = recall_index[iou_idx]
581
- pr_curve[iou_idx, pd_labels, r, 0] = np.maximum(
582
- pr_curve[iou_idx, pd_labels, r, 0],
583
- p,
497
+ labeled_recall = np.hstack(
498
+ [
499
+ pd_labels.reshape(-1, 1),
500
+ recall_index[iou_idx, :].reshape(-1, 1),
501
+ ]
584
502
  )
585
- pr_curve[iou_idx, pd_labels, r, 1] = np.maximum(
586
- pr_curve[iou_idx, pd_labels, r, 1],
587
- scores,
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
588
508
  )
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],
514
+ )
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)
589
584
 
590
585
  # calculate average precision
591
586
  running_max_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
@@ -610,24 +605,11 @@ def compute_precion_recall(
610
605
 
611
606
  average_precision = average_precision / 101.0
612
607
 
613
- # calculate average recall
614
- average_recall = average_recall / n_ious
615
-
616
608
  # calculate mAP and mAR
617
- if unique_pd_labels.size > 0:
618
- mAP: NDArray[np.float64] = average_precision[:, unique_pd_labels].mean(
619
- axis=1
620
- )
621
- mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
622
- axis=1
623
- )
609
+ if average_precision.size > 0:
610
+ mAP = average_precision.mean(axis=1)
624
611
 
625
- return (
626
- (average_precision.astype(np.float64), mAP),
627
- (average_recall, mAR),
628
- counts,
629
- pr_curve,
630
- )
612
+ return average_precision, mAP, pr_curve
631
613
 
632
614
 
633
615
  def _isin(
@@ -660,6 +642,7 @@ def _isin(
660
642
 
661
643
 
662
644
  class PairClassification(IntFlag):
645
+ NULL = auto()
663
646
  TP = auto()
664
647
  FP_FN_MISCLF = auto()
665
648
  FP_UNMATCHED = auto()
@@ -698,11 +681,13 @@ def mask_pairs_greedily(
698
681
  return mask_matches
699
682
 
700
683
 
701
- def compute_confusion_matrix(
684
+ def compute_pair_classifications(
702
685
  detailed_pairs: NDArray[np.float64],
703
686
  iou_thresholds: NDArray[np.float64],
704
687
  score_thresholds: NDArray[np.float64],
705
- ) -> NDArray[np.uint8]:
688
+ ) -> tuple[
689
+ NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_]
690
+ ]:
706
691
  """
707
692
  Compute detailed counts.
708
693
 
@@ -754,8 +739,8 @@ def compute_confusion_matrix(
754
739
  mask_gt_exists = gt_ids > -0.5
755
740
  mask_pd_exists = pd_ids > -0.5
756
741
  mask_label_match = np.isclose(gt_labels, pd_labels)
757
- mask_score_nonzero = scores > 1e-9
758
- mask_iou_nonzero = ious > 1e-9
742
+ mask_score_nonzero = scores > EPSILON
743
+ mask_iou_nonzero = ious > EPSILON
759
744
 
760
745
  mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
761
746
  mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
@@ -815,4 +800,88 @@ def compute_confusion_matrix(
815
800
  iou_idx, score_idx, mask_unmatched_groundtruths
816
801
  ] |= np.uint8(PairClassification.FN_UNMATCHED)
817
802
 
818
- 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