valor-lite 0.37.1__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.

Files changed (49) hide show
  1. valor_lite/LICENSE +21 -0
  2. valor_lite/__init__.py +0 -0
  3. valor_lite/cache/__init__.py +11 -0
  4. valor_lite/cache/compute.py +154 -0
  5. valor_lite/cache/ephemeral.py +302 -0
  6. valor_lite/cache/persistent.py +529 -0
  7. valor_lite/classification/__init__.py +14 -0
  8. valor_lite/classification/annotation.py +45 -0
  9. valor_lite/classification/computation.py +378 -0
  10. valor_lite/classification/evaluator.py +879 -0
  11. valor_lite/classification/loader.py +97 -0
  12. valor_lite/classification/metric.py +535 -0
  13. valor_lite/classification/numpy_compatibility.py +13 -0
  14. valor_lite/classification/shared.py +184 -0
  15. valor_lite/classification/utilities.py +314 -0
  16. valor_lite/exceptions.py +20 -0
  17. valor_lite/object_detection/__init__.py +17 -0
  18. valor_lite/object_detection/annotation.py +238 -0
  19. valor_lite/object_detection/computation.py +841 -0
  20. valor_lite/object_detection/evaluator.py +805 -0
  21. valor_lite/object_detection/loader.py +292 -0
  22. valor_lite/object_detection/metric.py +850 -0
  23. valor_lite/object_detection/shared.py +185 -0
  24. valor_lite/object_detection/utilities.py +396 -0
  25. valor_lite/schemas.py +11 -0
  26. valor_lite/semantic_segmentation/__init__.py +15 -0
  27. valor_lite/semantic_segmentation/annotation.py +123 -0
  28. valor_lite/semantic_segmentation/computation.py +165 -0
  29. valor_lite/semantic_segmentation/evaluator.py +414 -0
  30. valor_lite/semantic_segmentation/loader.py +205 -0
  31. valor_lite/semantic_segmentation/metric.py +275 -0
  32. valor_lite/semantic_segmentation/shared.py +149 -0
  33. valor_lite/semantic_segmentation/utilities.py +88 -0
  34. valor_lite/text_generation/__init__.py +15 -0
  35. valor_lite/text_generation/annotation.py +56 -0
  36. valor_lite/text_generation/computation.py +611 -0
  37. valor_lite/text_generation/llm/__init__.py +0 -0
  38. valor_lite/text_generation/llm/exceptions.py +14 -0
  39. valor_lite/text_generation/llm/generation.py +903 -0
  40. valor_lite/text_generation/llm/instructions.py +814 -0
  41. valor_lite/text_generation/llm/integrations.py +226 -0
  42. valor_lite/text_generation/llm/utilities.py +43 -0
  43. valor_lite/text_generation/llm/validators.py +68 -0
  44. valor_lite/text_generation/manager.py +697 -0
  45. valor_lite/text_generation/metric.py +381 -0
  46. valor_lite-0.37.1.dist-info/METADATA +174 -0
  47. valor_lite-0.37.1.dist-info/RECORD +49 -0
  48. valor_lite-0.37.1.dist-info/WHEEL +5 -0
  49. valor_lite-0.37.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,841 @@
1
+ from enum import IntFlag, auto
2
+
3
+ import numpy as np
4
+ import pyarrow as pa
5
+ import shapely
6
+ from numpy.typing import NDArray
7
+
8
+ EPSILON = 1e-9
9
+
10
+
11
+ def compute_bbox_iou(data: NDArray[np.float64]) -> NDArray[np.float64]:
12
+ """
13
+ Computes intersection-over-union (IOU) for axis-aligned bounding boxes.
14
+
15
+ Takes data with shape (N, 8):
16
+
17
+ Index 0 - xmin for Box 1
18
+ Index 1 - xmax for Box 1
19
+ Index 2 - ymin for Box 1
20
+ Index 3 - ymax for Box 1
21
+ Index 4 - xmin for Box 2
22
+ Index 5 - xmax for Box 2
23
+ Index 6 - ymin for Box 2
24
+ Index 7 - ymax for Box 2
25
+
26
+ Returns data with shape (N, 1):
27
+
28
+ Index 0 - IOU
29
+
30
+ Parameters
31
+ ----------
32
+ data : NDArray[np.float64]
33
+ A sorted array of bounding box pairs.
34
+
35
+ Returns
36
+ -------
37
+ NDArray[np.float64]
38
+ Computed IOU's.
39
+ """
40
+ if data.size == 0:
41
+ return np.array([], dtype=np.float64)
42
+
43
+ n_pairs = data.shape[0]
44
+
45
+ xmin1, xmax1, ymin1, ymax1 = (
46
+ data[:, 0, 0],
47
+ data[:, 0, 1],
48
+ data[:, 0, 2],
49
+ data[:, 0, 3],
50
+ )
51
+ xmin2, xmax2, ymin2, ymax2 = (
52
+ data[:, 1, 0],
53
+ data[:, 1, 1],
54
+ data[:, 1, 2],
55
+ data[:, 1, 3],
56
+ )
57
+
58
+ xmin = np.maximum(xmin1, xmin2)
59
+ ymin = np.maximum(ymin1, ymin2)
60
+ xmax = np.minimum(xmax1, xmax2)
61
+ ymax = np.minimum(ymax1, ymax2)
62
+
63
+ intersection_width = np.maximum(0, xmax - xmin)
64
+ intersection_height = np.maximum(0, ymax - ymin)
65
+ intersection_area = intersection_width * intersection_height
66
+
67
+ area1 = (xmax1 - xmin1) * (ymax1 - ymin1)
68
+ area2 = (xmax2 - xmin2) * (ymax2 - ymin2)
69
+
70
+ union_area = area1 + area2 - intersection_area
71
+
72
+ ious = np.zeros(n_pairs, dtype=np.float64)
73
+ np.divide(
74
+ intersection_area,
75
+ union_area,
76
+ where=union_area >= EPSILON,
77
+ out=ious,
78
+ )
79
+ return ious
80
+
81
+
82
+ def compute_bitmask_iou(data: NDArray[np.bool_]) -> NDArray[np.float64]:
83
+ """
84
+ Computes intersection-over-union (IOU) for bitmasks.
85
+
86
+ Takes data with shape (N, 2):
87
+
88
+ Index 0 - first bitmask
89
+ Index 1 - second bitmask
90
+
91
+ Returns data with shape (N, 1):
92
+
93
+ Index 0 - IOU
94
+
95
+ Parameters
96
+ ----------
97
+ data : NDArray[np.float64]
98
+ A sorted array of bitmask pairs.
99
+
100
+ Returns
101
+ -------
102
+ NDArray[np.float64]
103
+ Computed IOU's.
104
+ """
105
+
106
+ if data.size == 0:
107
+ return np.array([], dtype=np.float64)
108
+
109
+ n_pairs = data.shape[0]
110
+ lhs = data[:, 0, :, :].reshape(n_pairs, -1)
111
+ rhs = data[:, 1, :, :].reshape(n_pairs, -1)
112
+
113
+ lhs_sum = lhs.sum(axis=1)
114
+ rhs_sum = rhs.sum(axis=1)
115
+
116
+ intersection_ = np.logical_and(lhs, rhs).sum(axis=1)
117
+ union_ = lhs_sum + rhs_sum - intersection_
118
+
119
+ ious = np.zeros(n_pairs, dtype=np.float64)
120
+ np.divide(
121
+ intersection_,
122
+ union_,
123
+ where=union_ >= EPSILON,
124
+ out=ious,
125
+ )
126
+ return ious
127
+
128
+
129
+ def compute_polygon_iou(
130
+ data: NDArray[np.float64],
131
+ ) -> NDArray[np.float64]:
132
+ """
133
+ Computes intersection-over-union (IOU) for shapely polygons.
134
+
135
+ Takes data with shape (N, 2):
136
+
137
+ Index 0 - first polygon
138
+ Index 1 - second polygon
139
+
140
+ Returns data with shape (N, 1):
141
+
142
+ Index 0 - IOU
143
+
144
+ Parameters
145
+ ----------
146
+ data : NDArray[np.float64]
147
+ A sorted array of polygon pairs.
148
+
149
+ Returns
150
+ -------
151
+ NDArray[np.float64]
152
+ Computed IOU's.
153
+ """
154
+
155
+ if data.size == 0:
156
+ return np.array([], dtype=np.float64)
157
+
158
+ n_pairs = data.shape[0]
159
+
160
+ lhs = data[:, 0]
161
+ rhs = data[:, 1]
162
+
163
+ intersections = shapely.intersection(lhs, rhs)
164
+ intersection_areas = shapely.area(intersections)
165
+
166
+ unions = shapely.union(lhs, rhs)
167
+ union_areas = shapely.area(unions)
168
+
169
+ ious = np.zeros(n_pairs, dtype=np.float64)
170
+ np.divide(
171
+ intersection_areas,
172
+ union_areas,
173
+ where=union_areas >= EPSILON,
174
+ out=ious,
175
+ )
176
+ return ious
177
+
178
+
179
+ def rank_pairs(sorted_pairs: NDArray[np.float64]):
180
+ """
181
+ Prunes and ranks prediction pairs.
182
+
183
+ Should result in a single pair per prediction annotation.
184
+ """
185
+
186
+ # remove unmatched ground truths
187
+ mask_predictions = sorted_pairs[:, 2] >= 0.0
188
+ pairs = sorted_pairs[mask_predictions]
189
+ indices = np.where(mask_predictions)[0]
190
+
191
+ # find best fits for prediction
192
+ mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
193
+ matched_predictions = np.unique(pairs[mask_label_match, 2])
194
+
195
+ mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
196
+
197
+ pairs = pairs[mask_label_match | mask_unmatched_predictions]
198
+ indices = indices[mask_label_match | mask_unmatched_predictions]
199
+
200
+ # only keep the highest ranked pair
201
+ _, unique_indices = np.unique(pairs[:, [0, 2]], axis=0, return_index=True)
202
+ pairs = pairs[unique_indices]
203
+ indices = indices[unique_indices]
204
+
205
+ # np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
206
+ sorted_indices = np.lexsort(
207
+ (
208
+ -pairs[:, 5], # iou
209
+ -pairs[:, 6], # score
210
+ )
211
+ )
212
+ pairs = pairs[sorted_indices]
213
+ indices = indices[sorted_indices]
214
+
215
+ return pairs, indices
216
+
217
+
218
+ def calculate_ranking_boundaries(
219
+ ranked_pairs: NDArray[np.float64], number_of_labels: int
220
+ ):
221
+ dt_gt_ids = ranked_pairs[:, (0, 1)].astype(np.int64)
222
+ gt_ids = dt_gt_ids[:, 1]
223
+ ious = ranked_pairs[:, 5]
224
+
225
+ unique_gts, gt_counts = np.unique(
226
+ dt_gt_ids,
227
+ return_counts=True,
228
+ axis=0,
229
+ )
230
+ unique_gts = unique_gts[gt_counts > 1] # select gts with many pairs
231
+ unique_gts = unique_gts[unique_gts[:, 1] >= 0] # remove null
232
+
233
+ winning_predictions = np.ones_like(ious, dtype=np.bool_)
234
+ winning_predictions[gt_ids < 0] = False # null gts cannot be won
235
+ iou_boundary = np.zeros_like(ious)
236
+
237
+ for gt in unique_gts:
238
+ mask_gts = (
239
+ ranked_pairs[:, (0, 1)].astype(np.int64) == (gt[0], gt[1])
240
+ ).all(axis=1)
241
+ for label in range(number_of_labels):
242
+ mask_plabel = (ranked_pairs[:, 4] == label) & mask_gts
243
+ if mask_plabel.sum() <= 1:
244
+ continue
245
+
246
+ # mark sequence of increasing IOUs starting from index 0
247
+ labeled_ious = ranked_pairs[mask_plabel, 5]
248
+ mask_increasing_iou = np.ones_like(labeled_ious, dtype=np.bool_)
249
+ mask_increasing_iou[1:] = labeled_ious[1:] > labeled_ious[:-1]
250
+ idx_dec = np.where(~mask_increasing_iou)[0]
251
+ if idx_dec.size == 1:
252
+ mask_increasing_iou[idx_dec[0] :] = False
253
+
254
+ # define IOU lower bound
255
+ iou_boundary[mask_plabel][1:] = labeled_ious[:-1]
256
+ iou_boundary[mask_plabel][
257
+ ~mask_increasing_iou
258
+ ] = 2.0 # arbitrary >1.0 value
259
+
260
+ # mark first element (highest score)
261
+ indices = np.where(mask_gts)[0][1:]
262
+ winning_predictions[indices] = False
263
+
264
+ return iou_boundary, winning_predictions
265
+
266
+
267
+ def rank_table(tbl: pa.Table, number_of_labels: int) -> pa.Table:
268
+ numeric_columns = [
269
+ "datum_id",
270
+ "gt_id",
271
+ "pd_id",
272
+ "gt_label_id",
273
+ "pd_label_id",
274
+ "iou",
275
+ "pd_score",
276
+ ]
277
+ sorting_args = [
278
+ ("pd_score", "descending"),
279
+ ("iou", "descending"),
280
+ ]
281
+ sorted_tbl = tbl.sort_by(sorting_args)
282
+ pairs = np.column_stack(
283
+ [sorted_tbl[col].to_numpy() for col in numeric_columns]
284
+ )
285
+ pairs, indices = rank_pairs(pairs)
286
+ ranked_tbl = sorted_tbl.take(indices)
287
+ lower_iou_bound, winning_predictions = calculate_ranking_boundaries(
288
+ pairs, number_of_labels=number_of_labels
289
+ )
290
+ ranked_tbl = ranked_tbl.append_column(
291
+ pa.field("high_score", pa.bool_()),
292
+ pa.array(winning_predictions, type=pa.bool_()),
293
+ )
294
+ ranked_tbl = ranked_tbl.append_column(
295
+ pa.field("iou_prev", pa.float64()),
296
+ pa.array(lower_iou_bound, type=pa.float64()),
297
+ )
298
+ ranked_tbl = ranked_tbl.sort_by(sorting_args)
299
+ return ranked_tbl
300
+
301
+
302
+ def compute_counts(
303
+ ranked_pairs: NDArray[np.float64],
304
+ iou_thresholds: NDArray[np.float64],
305
+ score_thresholds: NDArray[np.float64],
306
+ number_of_groundtruths_per_label: NDArray[np.uint64],
307
+ number_of_labels: int,
308
+ running_counts: NDArray[np.uint64],
309
+ ) -> tuple:
310
+ """
311
+ Computes Object Detection metrics.
312
+
313
+ Takes data with shape (N, 7):
314
+
315
+ Index 0 - Datum Index
316
+ Index 1 - GroundTruth Index
317
+ Index 2 - Prediction Index
318
+ Index 3 - GroundTruth Label Index
319
+ Index 4 - Prediction Label Index
320
+ Index 5 - IOU
321
+ Index 6 - Score
322
+ Index 7 - IOU Lower Boundary
323
+ Index 8 - Winning Prediction
324
+
325
+ Parameters
326
+ ----------
327
+ ranked_pairs : NDArray[np.float64]
328
+ A ranked array summarizing the IOU calculations of one or more pairs.
329
+ iou_thresholds : NDArray[np.float64]
330
+ A 1-D array containing IOU thresholds.
331
+ score_thresholds : NDArray[np.float64]
332
+ A 1-D array containing score thresholds.
333
+
334
+ Returns
335
+ -------
336
+ tuple[NDArray[np.float64], NDArray[np.float64]]
337
+ Average Precision results (AP, mAP).
338
+ tuple[NDArray[np.float64], NDArray[np.float64]]
339
+ Average Recall results (AR, mAR).
340
+ NDArray[np.float64]
341
+ Precision, Recall, TP, FP, FN, F1 Score.
342
+ NDArray[np.float64]
343
+ Interpolated Precision-Recall Curves.
344
+ """
345
+ n_rows = ranked_pairs.shape[0]
346
+ n_labels = number_of_labels
347
+ n_ious = iou_thresholds.shape[0]
348
+ n_scores = score_thresholds.shape[0]
349
+
350
+ # initialize result arrays
351
+ counts = np.zeros((n_ious, n_scores, 3, n_labels), dtype=np.uint64)
352
+ pr_curve = np.zeros((n_ious, n_labels, 101, 2))
353
+
354
+ # start computation
355
+ ids = ranked_pairs[:, :5].astype(np.int64)
356
+ gt_ids = ids[:, 1]
357
+ gt_labels = ids[:, 3]
358
+ pd_labels = ids[:, 4]
359
+ ious = ranked_pairs[:, 5]
360
+ scores = ranked_pairs[:, 6]
361
+ prev_ious = ranked_pairs[:, 7]
362
+ winners = ranked_pairs[:, 8].astype(np.bool_)
363
+
364
+ unique_pd_labels, _ = np.unique(pd_labels, return_index=True)
365
+
366
+ running_total_count = np.zeros(
367
+ (n_ious, n_rows),
368
+ dtype=np.uint64,
369
+ )
370
+ running_tp_count = np.zeros_like(running_total_count)
371
+ running_gt_count = number_of_groundtruths_per_label[pd_labels]
372
+
373
+ mask_score_nonzero = scores > EPSILON
374
+ mask_gt_exists = gt_ids >= 0.0
375
+ mask_labels_match = np.isclose(gt_labels, pd_labels)
376
+
377
+ mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
378
+
379
+ mask_tp = mask_score_nonzero & mask_gt_exists_labels_match
380
+ mask_fp = mask_score_nonzero
381
+
382
+ for iou_idx in range(n_ious):
383
+ mask_iou_curr = ious >= iou_thresholds[iou_idx]
384
+ mask_iou_prev = prev_ious < iou_thresholds[iou_idx]
385
+ mask_iou = mask_iou_curr & mask_iou_prev
386
+
387
+ mask_tp_outer = mask_tp & mask_iou & winners
388
+ mask_fp_outer = mask_fp & (
389
+ (~mask_gt_exists_labels_match & mask_iou) | ~mask_iou | ~winners
390
+ )
391
+
392
+ for score_idx in range(n_scores):
393
+ mask_score_thresh = scores >= score_thresholds[score_idx]
394
+
395
+ mask_tp_inner = mask_tp_outer & mask_score_thresh
396
+ mask_fp_inner = mask_fp_outer & mask_score_thresh
397
+
398
+ # create true-positive mask score threshold
399
+ tp_candidates = ids[mask_tp_inner]
400
+ _, indices_gt_unique = np.unique(
401
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
402
+ )
403
+ mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
404
+ mask_gt_unique[indices_gt_unique] = True
405
+
406
+ true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
407
+ true_positives_mask[mask_tp_inner] = mask_gt_unique
408
+
409
+ mask_fp_inner |= mask_tp_inner & ~true_positives_mask
410
+
411
+ # calculate intermediates
412
+ counts[iou_idx, score_idx, 0, :] = np.bincount(
413
+ pd_labels,
414
+ weights=true_positives_mask,
415
+ minlength=n_labels,
416
+ )
417
+ # fp count
418
+ counts[iou_idx, score_idx, 1, :] = np.bincount(
419
+ pd_labels[mask_fp_inner],
420
+ minlength=n_labels,
421
+ )
422
+
423
+ # create true-positive mask score threshold
424
+ tp_candidates = ids[mask_tp_outer]
425
+ _, indices_gt_unique = np.unique(
426
+ tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
427
+ )
428
+ mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
429
+ mask_gt_unique[indices_gt_unique] = True
430
+ true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
431
+ true_positives_mask[mask_tp_outer] = mask_gt_unique
432
+
433
+ # count running tp and total for AP
434
+ for pd_label in unique_pd_labels:
435
+ mask_pd_label = pd_labels == pd_label
436
+
437
+ # running total prediction count
438
+ total_count = mask_pd_label.sum()
439
+ running_total_count[iou_idx][mask_pd_label] = np.arange(
440
+ running_counts[iou_idx, pd_label, 0],
441
+ running_counts[iou_idx, pd_label, 0] + total_count,
442
+ )
443
+ running_counts[iou_idx, pd_label, 0] += total_count
444
+
445
+ # running true-positive count
446
+ mask_tp_for_counting = mask_pd_label & true_positives_mask
447
+ tp_count = mask_tp_for_counting.sum()
448
+ running_tp_count[iou_idx][mask_tp_for_counting] = np.arange(
449
+ running_counts[iou_idx, pd_label, 1],
450
+ running_counts[iou_idx, pd_label, 1] + tp_count,
451
+ )
452
+ running_counts[iou_idx, pd_label, 1] += tp_count
453
+
454
+ # calculate running precision-recall points for AP
455
+ precision = np.zeros_like(running_total_count, dtype=np.float64)
456
+ np.divide(
457
+ running_tp_count,
458
+ running_total_count,
459
+ where=running_total_count > 0,
460
+ out=precision,
461
+ )
462
+ recall = np.zeros_like(running_total_count, dtype=np.float64)
463
+ np.divide(
464
+ running_tp_count,
465
+ running_gt_count,
466
+ where=running_gt_count > 0,
467
+ out=recall,
468
+ )
469
+ recall_index = np.floor(recall * 100.0).astype(np.int32)
470
+
471
+ # bin precision-recall curve
472
+ for iou_idx in range(n_ious):
473
+ pr_curve[iou_idx, pd_labels, recall_index[iou_idx], 0] = np.maximum(
474
+ pr_curve[iou_idx, pd_labels, recall_index[iou_idx], 0],
475
+ precision[iou_idx],
476
+ )
477
+ pr_curve[iou_idx, pd_labels, recall_index[iou_idx], 1] = np.maximum(
478
+ pr_curve[iou_idx, pd_labels, recall_index[iou_idx], 1],
479
+ scores,
480
+ )
481
+
482
+ return (
483
+ counts,
484
+ pr_curve,
485
+ )
486
+
487
+
488
+ def compute_precision_recall_f1(
489
+ counts: NDArray[np.uint64],
490
+ number_of_groundtruths_per_label: NDArray[np.uint64],
491
+ ) -> NDArray[np.float64]:
492
+
493
+ prec_rec_f1 = np.zeros_like(counts, dtype=np.float64)
494
+
495
+ # alias
496
+ tp_count = counts[:, :, 0, :]
497
+ fp_count = counts[:, :, 1, :]
498
+ tp_fp_count = tp_count + fp_count
499
+
500
+ # calculate component metrics
501
+ np.divide(
502
+ tp_count,
503
+ tp_fp_count,
504
+ where=tp_fp_count > 0,
505
+ out=prec_rec_f1[:, :, 0, :],
506
+ )
507
+ np.divide(
508
+ tp_count,
509
+ number_of_groundtruths_per_label,
510
+ where=number_of_groundtruths_per_label > 0,
511
+ out=prec_rec_f1[:, :, 1, :],
512
+ )
513
+ p = prec_rec_f1[:, :, 0, :]
514
+ r = prec_rec_f1[:, :, 1, :]
515
+ np.divide(
516
+ 2 * np.multiply(p, r),
517
+ (p + r),
518
+ where=(p + r) > EPSILON,
519
+ out=prec_rec_f1[:, :, 2, :],
520
+ )
521
+ return prec_rec_f1
522
+
523
+
524
+ def compute_average_recall(prec_rec_f1: NDArray[np.float64]):
525
+ recall = prec_rec_f1[:, :, 1, :]
526
+ average_recall = recall.mean(axis=0)
527
+ mAR = average_recall.mean(axis=-1)
528
+ return average_recall, mAR
529
+
530
+
531
+ def compute_average_precision(pr_curve: NDArray[np.float64]):
532
+ n_ious = pr_curve.shape[0]
533
+ n_labels = pr_curve.shape[1]
534
+
535
+ # initialize result arrays
536
+ average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
537
+ mAP = np.zeros(n_ious, dtype=np.float64)
538
+
539
+ # calculate average precision
540
+ running_max_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
541
+ running_max_score = np.zeros((n_labels), dtype=np.float64)
542
+ for recall in range(100, -1, -1):
543
+
544
+ # running max precision
545
+ running_max_precision = np.maximum(
546
+ pr_curve[:, :, recall, 0],
547
+ running_max_precision,
548
+ )
549
+ pr_curve[:, :, recall, 0] = running_max_precision
550
+
551
+ # running max score
552
+ running_max_score = np.maximum(
553
+ pr_curve[:, :, recall, 1],
554
+ running_max_score,
555
+ )
556
+ pr_curve[:, :, recall, 1] = running_max_score
557
+
558
+ average_precision += running_max_precision
559
+
560
+ average_precision = average_precision / 101.0
561
+
562
+ # calculate mAP and mAR
563
+ if average_precision.size > 0:
564
+ mAP = average_precision.mean(axis=1)
565
+
566
+ return average_precision, mAP, pr_curve
567
+
568
+
569
+ def _isin(
570
+ data: NDArray,
571
+ subset: NDArray,
572
+ ) -> NDArray[np.bool_]:
573
+ """
574
+ Creates a mask of rows that exist within the subset.
575
+
576
+ Parameters
577
+ ----------
578
+ data : NDArray[np.int32]
579
+ An array with shape (N, 2).
580
+ subset : NDArray[np.int32]
581
+ An array with shape (M, 2) where N >= M.
582
+
583
+ Returns
584
+ -------
585
+ NDArray[np.bool_]
586
+ Returns a bool mask with shape (N,).
587
+ """
588
+ combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
589
+ np.int32
590
+ )
591
+ combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
592
+ :, 1
593
+ ].astype(np.int32)
594
+ mask = np.isin(combined_data, combined_subset, assume_unique=False)
595
+ return mask
596
+
597
+
598
+ class PairClassification(IntFlag):
599
+ NULL = auto()
600
+ TP = auto()
601
+ FP_FN_MISCLF = auto()
602
+ FP_UNMATCHED = auto()
603
+ FN_UNMATCHED = auto()
604
+
605
+
606
+ def mask_pairs_greedily(
607
+ pairs: NDArray[np.float64],
608
+ ):
609
+ groundtruths = pairs[:, 1].astype(np.int32)
610
+ predictions = pairs[:, 2].astype(np.int32)
611
+
612
+ # Pre‑allocate "seen" flags for every possible x and y
613
+ max_gt = groundtruths.max()
614
+ max_pd = predictions.max()
615
+ used_gt = np.zeros(max_gt + 1, dtype=np.bool_)
616
+ used_pd = np.zeros(max_pd + 1, dtype=np.bool_)
617
+
618
+ # This mask will mark which pairs to keep
619
+ keep = np.zeros(pairs.shape[0], dtype=bool)
620
+
621
+ for idx in range(groundtruths.shape[0]):
622
+ gidx = groundtruths[idx]
623
+ pidx = predictions[idx]
624
+
625
+ if not (gidx < 0 or pidx < 0 or used_gt[gidx] or used_pd[pidx]):
626
+ keep[idx] = True
627
+ used_gt[gidx] = True
628
+ used_pd[pidx] = True
629
+
630
+ mask_matches = _isin(
631
+ data=pairs[:, (1, 2)],
632
+ subset=np.unique(pairs[np.ix_(keep, (1, 2))], axis=0), # type: ignore - np.ix_ typing
633
+ )
634
+
635
+ return mask_matches
636
+
637
+
638
+ def compute_pair_classifications(
639
+ detailed_pairs: NDArray[np.float64],
640
+ iou_thresholds: NDArray[np.float64],
641
+ score_thresholds: NDArray[np.float64],
642
+ ) -> tuple[
643
+ NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_]
644
+ ]:
645
+ """
646
+ Compute detailed counts.
647
+
648
+ Takes data with shape (N, 7):
649
+
650
+ Index 0 - Datum Index
651
+ Index 1 - GroundTruth Index
652
+ Index 2 - Prediction Index
653
+ Index 3 - GroundTruth Label Index
654
+ Index 4 - Prediction Label Index
655
+ Index 5 - IOU
656
+ Index 6 - Score
657
+
658
+ Parameters
659
+ ----------
660
+ detailed_pairs : NDArray[np.float64]
661
+ An unsorted array summarizing the IOU calculations of one or more pairs.
662
+ label_metadata : NDArray[np.int32]
663
+ An array containing metadata related to labels.
664
+ iou_thresholds : NDArray[np.float64]
665
+ A 1-D array containing IOU thresholds.
666
+ score_thresholds : NDArray[np.float64]
667
+ A 1-D array containing score thresholds.
668
+
669
+ Returns
670
+ -------
671
+ NDArray[np.uint8]
672
+ Confusion matrix.
673
+ """
674
+ n_pairs = detailed_pairs.shape[0]
675
+ n_ious = iou_thresholds.shape[0]
676
+ n_scores = score_thresholds.shape[0]
677
+
678
+ pair_classifications = np.zeros(
679
+ (n_ious, n_scores, n_pairs),
680
+ dtype=np.uint8,
681
+ )
682
+
683
+ ids = detailed_pairs[:, :5].astype(np.int32)
684
+ groundtruths = ids[:, (0, 1)]
685
+ predictions = ids[:, (0, 2)]
686
+ gt_ids = ids[:, 1]
687
+ pd_ids = ids[:, 2]
688
+ gt_labels = ids[:, 3]
689
+ pd_labels = ids[:, 4]
690
+ ious = detailed_pairs[:, 5]
691
+ scores = detailed_pairs[:, 6]
692
+
693
+ mask_gt_exists = gt_ids > -0.5
694
+ mask_pd_exists = pd_ids > -0.5
695
+ mask_label_match = np.isclose(gt_labels, pd_labels)
696
+ mask_score_nonzero = scores > EPSILON
697
+ mask_iou_nonzero = ious > EPSILON
698
+
699
+ mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
700
+ mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
701
+
702
+ mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
703
+
704
+ for iou_idx in range(n_ious):
705
+ mask_iou_threshold = ious >= iou_thresholds[iou_idx]
706
+ mask_iou = mask_iou_nonzero & mask_iou_threshold
707
+ for score_idx in range(n_scores):
708
+ mask_score_threshold = scores >= score_thresholds[score_idx]
709
+ mask_score = mask_score_nonzero & mask_score_threshold
710
+
711
+ mask_thresholded_matched_pairs = (
712
+ mask_matched_pairs & mask_iou & mask_score
713
+ )
714
+
715
+ mask_true_positives = (
716
+ mask_thresholded_matched_pairs & mask_gt_pd_match
717
+ )
718
+ mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
719
+
720
+ mask_groundtruths_in_thresholded_matched_pairs = _isin(
721
+ data=groundtruths,
722
+ subset=np.unique(
723
+ groundtruths[mask_thresholded_matched_pairs], axis=0
724
+ ),
725
+ )
726
+ mask_predictions_in_thresholded_matched_pairs = _isin(
727
+ data=predictions,
728
+ subset=np.unique(
729
+ predictions[mask_thresholded_matched_pairs], axis=0
730
+ ),
731
+ )
732
+
733
+ mask_unmatched_predictions = (
734
+ ~mask_predictions_in_thresholded_matched_pairs
735
+ & mask_pd_exists
736
+ & mask_score
737
+ )
738
+ mask_unmatched_groundtruths = (
739
+ ~mask_groundtruths_in_thresholded_matched_pairs
740
+ & mask_gt_exists
741
+ )
742
+
743
+ # classify pairings
744
+ pair_classifications[
745
+ iou_idx, score_idx, mask_true_positives
746
+ ] |= np.uint8(PairClassification.TP)
747
+ pair_classifications[iou_idx, score_idx, mask_misclf] |= np.uint8(
748
+ PairClassification.FP_FN_MISCLF
749
+ )
750
+ pair_classifications[
751
+ iou_idx, score_idx, mask_unmatched_predictions
752
+ ] |= np.uint8(PairClassification.FP_UNMATCHED)
753
+ pair_classifications[
754
+ iou_idx, score_idx, mask_unmatched_groundtruths
755
+ ] |= np.uint8(PairClassification.FN_UNMATCHED)
756
+
757
+ mask_tp = np.bitwise_and(pair_classifications, PairClassification.TP) > 0
758
+ mask_fp_fn_misclf = (
759
+ np.bitwise_and(pair_classifications, PairClassification.FP_FN_MISCLF)
760
+ > 0
761
+ )
762
+ mask_fp_unmatched = (
763
+ np.bitwise_and(pair_classifications, PairClassification.FP_UNMATCHED)
764
+ > 0
765
+ )
766
+ mask_fn_unmatched = (
767
+ np.bitwise_and(pair_classifications, PairClassification.FN_UNMATCHED)
768
+ > 0
769
+ )
770
+
771
+ return (
772
+ mask_tp,
773
+ mask_fp_fn_misclf,
774
+ mask_fp_unmatched,
775
+ mask_fn_unmatched,
776
+ )
777
+
778
+
779
+ def compute_confusion_matrix(
780
+ detailed_pairs: NDArray[np.float64],
781
+ mask_tp: NDArray[np.bool_],
782
+ mask_fp_fn_misclf: NDArray[np.bool_],
783
+ mask_fp_unmatched: NDArray[np.bool_],
784
+ mask_fn_unmatched: NDArray[np.bool_],
785
+ number_of_labels: int,
786
+ iou_thresholds: NDArray[np.float64],
787
+ score_thresholds: NDArray[np.float64],
788
+ ):
789
+ n_ious = iou_thresholds.size
790
+ n_scores = score_thresholds.size
791
+ ids = detailed_pairs[:, :5].astype(np.int64)
792
+
793
+ # initialize arrays
794
+ confusion_matrices = np.zeros(
795
+ (n_ious, n_scores, number_of_labels, number_of_labels), dtype=np.uint64
796
+ )
797
+ unmatched_groundtruths = np.zeros(
798
+ (n_ious, n_scores, number_of_labels), dtype=np.uint64
799
+ )
800
+ unmatched_predictions = np.zeros_like(unmatched_groundtruths)
801
+
802
+ mask_matched = mask_tp | mask_fp_fn_misclf
803
+ for iou_idx in range(n_ious):
804
+ for score_idx in range(n_scores):
805
+ # matched annotations
806
+ unique_pairs = np.unique(
807
+ ids[np.ix_(mask_matched[iou_idx, score_idx], (0, 1, 2, 3, 4))], # type: ignore - numpy ix_ typing
808
+ axis=0,
809
+ )
810
+ unique_labels, unique_label_counts = np.unique(
811
+ unique_pairs[:, (3, 4)], axis=0, return_counts=True
812
+ )
813
+ confusion_matrices[
814
+ iou_idx, score_idx, unique_labels[:, 0], unique_labels[:, 1]
815
+ ] = unique_label_counts
816
+
817
+ # unmatched groundtruths
818
+ unique_pairs = np.unique(
819
+ ids[np.ix_(mask_fn_unmatched[iou_idx, score_idx], (0, 1, 3))], # type: ignore - numpy ix_ typing
820
+ axis=0,
821
+ )
822
+ unique_labels, unique_label_counts = np.unique(
823
+ unique_pairs[:, 2], return_counts=True
824
+ )
825
+ unmatched_groundtruths[
826
+ iou_idx, score_idx, unique_labels
827
+ ] = unique_label_counts
828
+
829
+ # unmatched predictions
830
+ unique_pairs = np.unique(
831
+ ids[np.ix_(mask_fp_unmatched[iou_idx, score_idx], (0, 2, 4))], # type: ignore - numpy ix_ typing
832
+ axis=0,
833
+ )
834
+ unique_labels, unique_label_counts = np.unique(
835
+ unique_pairs[:, 2], return_counts=True
836
+ )
837
+ unmatched_predictions[
838
+ iou_idx, score_idx, unique_labels
839
+ ] = unique_label_counts
840
+
841
+ return confusion_matrices, unmatched_groundtruths, unmatched_predictions