valor-lite 0.34.3__py3-none-any.whl → 0.36.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of valor-lite might be problematic. Click here for more details.
- valor_lite/classification/computation.py +147 -38
- valor_lite/classification/manager.py +229 -236
- valor_lite/classification/metric.py +5 -8
- valor_lite/classification/utilities.py +18 -14
- valor_lite/object_detection/__init__.py +2 -15
- valor_lite/object_detection/annotation.py +24 -48
- valor_lite/object_detection/computation.py +324 -384
- valor_lite/object_detection/manager.py +549 -456
- valor_lite/object_detection/metric.py +16 -34
- valor_lite/object_detection/utilities.py +134 -305
- valor_lite/semantic_segmentation/__init__.py +3 -3
- valor_lite/semantic_segmentation/annotation.py +32 -103
- valor_lite/semantic_segmentation/benchmark.py +87 -1
- valor_lite/semantic_segmentation/computation.py +96 -14
- valor_lite/semantic_segmentation/manager.py +199 -222
- valor_lite/semantic_segmentation/utilities.py +3 -3
- {valor_lite-0.34.3.dist-info → valor_lite-0.36.0.dist-info}/METADATA +2 -2
- {valor_lite-0.34.3.dist-info → valor_lite-0.36.0.dist-info}/RECORD +20 -20
- {valor_lite-0.34.3.dist-info → valor_lite-0.36.0.dist-info}/WHEEL +1 -1
- {valor_lite-0.34.3.dist-info → valor_lite-0.36.0.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from enum import IntFlag, auto
|
|
3
|
+
|
|
1
4
|
import numpy as np
|
|
2
5
|
import shapely
|
|
3
6
|
from numpy.typing import NDArray
|
|
@@ -171,99 +174,210 @@ def compute_polygon_iou(
|
|
|
171
174
|
return ious
|
|
172
175
|
|
|
173
176
|
|
|
174
|
-
def
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
) -> NDArray[np.
|
|
178
|
-
"""
|
|
179
|
-
Computes ranked pairs for a datum.
|
|
177
|
+
def compute_label_metadata(
|
|
178
|
+
ids: NDArray[np.int32],
|
|
179
|
+
n_labels: int,
|
|
180
|
+
) -> NDArray[np.int32]:
|
|
180
181
|
"""
|
|
182
|
+
Computes label metadata returning a count of annotations per label.
|
|
181
183
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
detailed_pairs : NDArray[np.int32]
|
|
187
|
+
Detailed annotation pairings with shape (N, 7).
|
|
188
|
+
Index 0 - Datum Index
|
|
189
|
+
Index 1 - GroundTruth Index
|
|
190
|
+
Index 2 - Prediction Index
|
|
191
|
+
Index 3 - GroundTruth Label Index
|
|
192
|
+
Index 4 - Prediction Label Index
|
|
193
|
+
n_labels : int
|
|
194
|
+
The total number of unique labels.
|
|
184
195
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
Returns
|
|
197
|
+
-------
|
|
198
|
+
NDArray[np.int32]
|
|
199
|
+
The label metadata array with shape (n_labels, 2).
|
|
200
|
+
Index 0 - Ground truth label count
|
|
201
|
+
Index 1 - Prediction label count
|
|
202
|
+
"""
|
|
203
|
+
label_metadata = np.zeros((n_labels, 2), dtype=np.int32)
|
|
190
204
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
-data[:, 6],
|
|
197
|
-
)
|
|
205
|
+
ground_truth_pairs = ids[:, (0, 1, 3)]
|
|
206
|
+
ground_truth_pairs = ground_truth_pairs[ground_truth_pairs[:, 1] >= 0]
|
|
207
|
+
unique_pairs = np.unique(ground_truth_pairs, axis=0)
|
|
208
|
+
label_indices, unique_counts = np.unique(
|
|
209
|
+
unique_pairs[:, 2], return_counts=True
|
|
198
210
|
)
|
|
199
|
-
|
|
211
|
+
label_metadata[label_indices.astype(np.int32), 0] = unique_counts
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
prediction_pairs = ids[:, (0, 2, 4)]
|
|
214
|
+
prediction_pairs = prediction_pairs[prediction_pairs[:, 1] >= 0]
|
|
215
|
+
unique_pairs = np.unique(prediction_pairs, axis=0)
|
|
216
|
+
label_indices, unique_counts = np.unique(
|
|
217
|
+
unique_pairs[:, 2], return_counts=True
|
|
218
|
+
)
|
|
219
|
+
label_metadata[label_indices.astype(np.int32), 1] = unique_counts
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
_, indices = np.unique(data[:, [0, 2, 5]], axis=0, return_index=True)
|
|
221
|
+
return label_metadata
|
|
209
222
|
|
|
210
|
-
# np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
|
|
211
|
-
data = data[indices, :]
|
|
212
223
|
|
|
213
|
-
|
|
224
|
+
def filter_cache(
|
|
225
|
+
detailed_pairs: NDArray[np.float64],
|
|
226
|
+
mask_datums: NDArray[np.bool_],
|
|
227
|
+
mask_predictions: NDArray[np.bool_],
|
|
228
|
+
mask_ground_truths: NDArray[np.bool_],
|
|
229
|
+
n_labels: int,
|
|
230
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32],]:
|
|
231
|
+
"""
|
|
232
|
+
Performs filtering on a detailed cache.
|
|
214
233
|
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
detailed_pairs : NDArray[float64]
|
|
237
|
+
A list of sorted detailed pairs with size (N, 7).
|
|
238
|
+
mask_datums : NDArray[bool]
|
|
239
|
+
A boolean mask with size (N,).
|
|
240
|
+
mask_ground_truths : NDArray[bool]
|
|
241
|
+
A boolean mask with size (N,).
|
|
242
|
+
mask_predictions : NDArray[bool]
|
|
243
|
+
A boolean mask with size (N,).
|
|
244
|
+
n_labels : int
|
|
245
|
+
The total number of unique labels.
|
|
215
246
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
NDArray[float64]
|
|
250
|
+
Filtered detailed pairs.
|
|
251
|
+
NDArray[float64]
|
|
252
|
+
Filtered ranked pairs.
|
|
253
|
+
NDArray[int32]
|
|
254
|
+
Label metadata.
|
|
220
255
|
"""
|
|
221
|
-
|
|
256
|
+
# filter datums
|
|
257
|
+
detailed_pairs = detailed_pairs[mask_datums].copy()
|
|
258
|
+
|
|
259
|
+
# filter ground truths
|
|
260
|
+
if mask_ground_truths.any():
|
|
261
|
+
invalid_groundtruth_indices = np.where(mask_ground_truths)[0]
|
|
262
|
+
detailed_pairs[
|
|
263
|
+
invalid_groundtruth_indices[:, None], (1, 3, 5)
|
|
264
|
+
] = np.array([[-1, -1, 0]])
|
|
265
|
+
|
|
266
|
+
# filter predictions
|
|
267
|
+
if mask_predictions.any():
|
|
268
|
+
invalid_prediction_indices = np.where(mask_predictions)[0]
|
|
269
|
+
detailed_pairs[
|
|
270
|
+
invalid_prediction_indices[:, None], (2, 4, 5, 6)
|
|
271
|
+
] = np.array([[-1, -1, 0, -1]])
|
|
272
|
+
|
|
273
|
+
# filter null pairs
|
|
274
|
+
mask_null_pairs = np.all(
|
|
275
|
+
np.isclose(
|
|
276
|
+
detailed_pairs[:, 1:5],
|
|
277
|
+
np.array([-1.0, -1.0, -1.0, -1.0]),
|
|
278
|
+
),
|
|
279
|
+
axis=1,
|
|
280
|
+
)
|
|
281
|
+
detailed_pairs = detailed_pairs[~mask_null_pairs]
|
|
282
|
+
|
|
283
|
+
if detailed_pairs.size == 0:
|
|
284
|
+
warnings.warn("no valid filtered pairs")
|
|
285
|
+
return (
|
|
286
|
+
np.array([], dtype=np.float64),
|
|
287
|
+
np.array([], dtype=np.float64),
|
|
288
|
+
np.zeros((n_labels, 2), dtype=np.int32),
|
|
289
|
+
)
|
|
222
290
|
|
|
223
|
-
|
|
291
|
+
# sorts by score, iou with ground truth id as a tie-breaker
|
|
292
|
+
indices = np.lexsort(
|
|
293
|
+
(
|
|
294
|
+
detailed_pairs[:, 1], # ground truth id
|
|
295
|
+
-detailed_pairs[:, 5], # iou
|
|
296
|
+
-detailed_pairs[:, 6], # score
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
detailed_pairs = detailed_pairs[indices]
|
|
300
|
+
label_metadata = compute_label_metadata(
|
|
301
|
+
ids=detailed_pairs[:, :5].astype(np.int32),
|
|
302
|
+
n_labels=n_labels,
|
|
303
|
+
)
|
|
304
|
+
ranked_pairs = rank_pairs(
|
|
305
|
+
detailed_pairs=detailed_pairs,
|
|
306
|
+
label_metadata=label_metadata,
|
|
307
|
+
)
|
|
308
|
+
return (
|
|
309
|
+
detailed_pairs,
|
|
310
|
+
ranked_pairs,
|
|
311
|
+
label_metadata,
|
|
312
|
+
)
|
|
224
313
|
|
|
225
|
-
Index 0 - Datum Index
|
|
226
|
-
Index 1 - GroundTruth Index
|
|
227
|
-
Index 2 - Prediction Index
|
|
228
|
-
Index 3 - IOU
|
|
229
|
-
Index 4 - GroundTruth Label Index
|
|
230
|
-
Index 5 - Prediction Label Index
|
|
231
|
-
Index 6 - Score
|
|
232
314
|
|
|
233
|
-
|
|
315
|
+
def rank_pairs(
|
|
316
|
+
detailed_pairs: NDArray[np.float64],
|
|
317
|
+
label_metadata: NDArray[np.int32],
|
|
318
|
+
) -> NDArray[np.float64]:
|
|
319
|
+
"""
|
|
320
|
+
Highly optimized pair ranking for computing precision and recall based metrics.
|
|
321
|
+
|
|
322
|
+
Only ground truths and predictions that provide unique information are kept. The unkept
|
|
323
|
+
pairs are represented via the label metadata array.
|
|
234
324
|
|
|
235
325
|
Parameters
|
|
236
326
|
----------
|
|
237
|
-
|
|
238
|
-
|
|
327
|
+
detailed_pairs : NDArray[np.float64]
|
|
328
|
+
Detailed annotation pairs with shape (n_pairs, 7).
|
|
329
|
+
Index 0 - Datum Index
|
|
330
|
+
Index 1 - GroundTruth Index
|
|
331
|
+
Index 2 - Prediction Index
|
|
332
|
+
Index 3 - GroundTruth Label Index
|
|
333
|
+
Index 4 - Prediction Label Index
|
|
334
|
+
Index 5 - IOU
|
|
335
|
+
Index 6 - Score
|
|
239
336
|
label_metadata : NDArray[np.int32]
|
|
240
|
-
|
|
337
|
+
Array containing label counts with shape (n_labels, 2)
|
|
338
|
+
Index 0 - Ground truth label count
|
|
339
|
+
Index 1 - Prediction label count
|
|
241
340
|
|
|
242
341
|
Returns
|
|
243
342
|
-------
|
|
244
343
|
NDArray[np.float64]
|
|
245
|
-
|
|
344
|
+
Array of ranked pairs for precision-recall metric computation.
|
|
246
345
|
"""
|
|
346
|
+
pairs = detailed_pairs
|
|
247
347
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
]
|
|
255
|
-
|
|
348
|
+
# remove null predictions
|
|
349
|
+
pairs = pairs[pairs[:, 2] >= 0.0]
|
|
350
|
+
|
|
351
|
+
# find best fits for prediction
|
|
352
|
+
mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
|
|
353
|
+
matched_predictions = np.unique(pairs[mask_label_match, 2])
|
|
354
|
+
mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
|
|
355
|
+
pairs = pairs[mask_label_match | mask_unmatched_predictions]
|
|
356
|
+
|
|
357
|
+
# remove predictions for labels that have no ground truths
|
|
358
|
+
for label_idx, count in enumerate(label_metadata[:, 0]):
|
|
359
|
+
if count > 0:
|
|
360
|
+
continue
|
|
361
|
+
pairs = pairs[pairs[:, 4] != label_idx]
|
|
362
|
+
|
|
363
|
+
# only keep the highest ranked pair
|
|
364
|
+
_, indices = np.unique(pairs[:, [0, 2, 4]], axis=0, return_index=True)
|
|
365
|
+
pairs = pairs[indices]
|
|
366
|
+
|
|
367
|
+
# np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
|
|
256
368
|
indices = np.lexsort(
|
|
257
369
|
(
|
|
258
|
-
-
|
|
259
|
-
-
|
|
370
|
+
-pairs[:, 5], # iou
|
|
371
|
+
-pairs[:, 6], # score
|
|
260
372
|
)
|
|
261
373
|
)
|
|
262
|
-
|
|
374
|
+
pairs = pairs[indices]
|
|
375
|
+
|
|
376
|
+
return pairs
|
|
263
377
|
|
|
264
378
|
|
|
265
379
|
def compute_precion_recall(
|
|
266
|
-
|
|
380
|
+
ranked_pairs: NDArray[np.float64],
|
|
267
381
|
label_metadata: NDArray[np.int32],
|
|
268
382
|
iou_thresholds: NDArray[np.float64],
|
|
269
383
|
score_thresholds: NDArray[np.float64],
|
|
@@ -271,14 +385,10 @@ def compute_precion_recall(
|
|
|
271
385
|
tuple[
|
|
272
386
|
NDArray[np.float64],
|
|
273
387
|
NDArray[np.float64],
|
|
274
|
-
NDArray[np.float64],
|
|
275
|
-
float,
|
|
276
388
|
],
|
|
277
389
|
tuple[
|
|
278
390
|
NDArray[np.float64],
|
|
279
391
|
NDArray[np.float64],
|
|
280
|
-
NDArray[np.float64],
|
|
281
|
-
float,
|
|
282
392
|
],
|
|
283
393
|
NDArray[np.float64],
|
|
284
394
|
NDArray[np.float64],
|
|
@@ -298,8 +408,8 @@ def compute_precion_recall(
|
|
|
298
408
|
|
|
299
409
|
Parameters
|
|
300
410
|
----------
|
|
301
|
-
|
|
302
|
-
A
|
|
411
|
+
ranked_pairs : NDArray[np.float64]
|
|
412
|
+
A ranked array summarizing the IOU calculations of one or more pairs.
|
|
303
413
|
label_metadata : NDArray[np.int32]
|
|
304
414
|
An array containing metadata related to labels.
|
|
305
415
|
iou_thresholds : NDArray[np.float64]
|
|
@@ -309,32 +419,45 @@ def compute_precion_recall(
|
|
|
309
419
|
|
|
310
420
|
Returns
|
|
311
421
|
-------
|
|
312
|
-
tuple[NDArray[np.float64], NDArray[np.float64]
|
|
313
|
-
Average Precision results.
|
|
314
|
-
tuple[NDArray[np.float64], NDArray[np.float64]
|
|
315
|
-
Average Recall results.
|
|
422
|
+
tuple[NDArray[np.float64], NDArray[np.float64]]
|
|
423
|
+
Average Precision results (AP, mAP).
|
|
424
|
+
tuple[NDArray[np.float64], NDArray[np.float64]]
|
|
425
|
+
Average Recall results (AR, mAR).
|
|
316
426
|
NDArray[np.float64]
|
|
317
427
|
Precision, Recall, TP, FP, FN, F1 Score.
|
|
318
428
|
NDArray[np.float64]
|
|
319
429
|
Interpolated Precision-Recall Curves.
|
|
320
430
|
"""
|
|
321
|
-
|
|
322
|
-
n_rows = data.shape[0]
|
|
431
|
+
n_rows = ranked_pairs.shape[0]
|
|
323
432
|
n_labels = label_metadata.shape[0]
|
|
324
433
|
n_ious = iou_thresholds.shape[0]
|
|
325
434
|
n_scores = score_thresholds.shape[0]
|
|
326
435
|
|
|
327
|
-
|
|
328
|
-
raise ValueError("At least one IOU threshold must be passed.")
|
|
329
|
-
elif n_scores == 0:
|
|
330
|
-
raise ValueError("At least one score threshold must be passed.")
|
|
331
|
-
|
|
436
|
+
# initialize result arrays
|
|
332
437
|
average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
|
|
438
|
+
mAP = np.zeros(n_ious, dtype=np.float64)
|
|
333
439
|
average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
|
|
440
|
+
mAR = np.zeros(n_scores, dtype=np.float64)
|
|
334
441
|
counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
|
|
442
|
+
pr_curve = np.zeros((n_ious, n_labels, 101, 2))
|
|
443
|
+
|
|
444
|
+
if ranked_pairs.size == 0:
|
|
445
|
+
warnings.warn("no valid ranked pairs")
|
|
446
|
+
return (
|
|
447
|
+
(average_precision, mAP),
|
|
448
|
+
(average_recall, mAR),
|
|
449
|
+
counts,
|
|
450
|
+
pr_curve,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# start computation
|
|
454
|
+
ids = ranked_pairs[:, :5].astype(np.int32)
|
|
455
|
+
gt_ids = ids[:, 1]
|
|
456
|
+
gt_labels = ids[:, 3]
|
|
457
|
+
pd_labels = ids[:, 4]
|
|
458
|
+
ious = ranked_pairs[:, 5]
|
|
459
|
+
scores = ranked_pairs[:, 6]
|
|
335
460
|
|
|
336
|
-
pd_labels = data[:, 5].astype(np.int32)
|
|
337
|
-
scores = data[:, 6]
|
|
338
461
|
unique_pd_labels, unique_pd_indices = np.unique(
|
|
339
462
|
pd_labels, return_index=True
|
|
340
463
|
)
|
|
@@ -346,9 +469,9 @@ def compute_precion_recall(
|
|
|
346
469
|
running_tp_count = np.zeros_like(running_total_count)
|
|
347
470
|
running_gt_count = np.zeros_like(running_total_count)
|
|
348
471
|
|
|
349
|
-
mask_score_nonzero =
|
|
350
|
-
mask_gt_exists =
|
|
351
|
-
mask_labels_match = np.isclose(
|
|
472
|
+
mask_score_nonzero = scores > 1e-9
|
|
473
|
+
mask_gt_exists = gt_ids >= 0.0
|
|
474
|
+
mask_labels_match = np.isclose(gt_labels, pd_labels)
|
|
352
475
|
|
|
353
476
|
mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
|
|
354
477
|
|
|
@@ -357,7 +480,7 @@ def compute_precion_recall(
|
|
|
357
480
|
mask_fn = mask_gt_exists_labels_match
|
|
358
481
|
|
|
359
482
|
for iou_idx in range(n_ious):
|
|
360
|
-
mask_iou =
|
|
483
|
+
mask_iou = ious >= iou_thresholds[iou_idx]
|
|
361
484
|
|
|
362
485
|
mask_tp_outer = mask_tp & mask_iou
|
|
363
486
|
mask_fp_outer = mask_fp & (
|
|
@@ -366,16 +489,16 @@ def compute_precion_recall(
|
|
|
366
489
|
mask_fn_outer = mask_fn & mask_iou
|
|
367
490
|
|
|
368
491
|
for score_idx in range(n_scores):
|
|
369
|
-
mask_score_thresh =
|
|
492
|
+
mask_score_thresh = scores >= score_thresholds[score_idx]
|
|
370
493
|
|
|
371
494
|
mask_tp_inner = mask_tp_outer & mask_score_thresh
|
|
372
495
|
mask_fp_inner = mask_fp_outer & mask_score_thresh
|
|
373
496
|
mask_fn_inner = mask_fn_outer & ~mask_score_thresh
|
|
374
497
|
|
|
375
498
|
# create true-positive mask score threshold
|
|
376
|
-
tp_candidates =
|
|
499
|
+
tp_candidates = ids[mask_tp_inner]
|
|
377
500
|
_, indices_gt_unique = np.unique(
|
|
378
|
-
tp_candidates[:, [0, 1,
|
|
501
|
+
tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
|
|
379
502
|
)
|
|
380
503
|
mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
|
|
381
504
|
mask_gt_unique[indices_gt_unique] = True
|
|
@@ -437,9 +560,9 @@ def compute_precion_recall(
|
|
|
437
560
|
average_recall[score_idx] += recall
|
|
438
561
|
|
|
439
562
|
# create true-positive mask score threshold
|
|
440
|
-
tp_candidates =
|
|
563
|
+
tp_candidates = ids[mask_tp_outer]
|
|
441
564
|
_, indices_gt_unique = np.unique(
|
|
442
|
-
tp_candidates[:, [0, 1,
|
|
565
|
+
tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
|
|
443
566
|
)
|
|
444
567
|
mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
|
|
445
568
|
mask_gt_unique[indices_gt_unique] = True
|
|
@@ -476,7 +599,6 @@ def compute_precion_recall(
|
|
|
476
599
|
recall_index = np.floor(recall * 100.0).astype(np.int32)
|
|
477
600
|
|
|
478
601
|
# bin precision-recall curve
|
|
479
|
-
pr_curve = np.zeros((n_ious, n_labels, 101, 2))
|
|
480
602
|
for iou_idx in range(n_ious):
|
|
481
603
|
p = precision[iou_idx]
|
|
482
604
|
r = recall_index[iou_idx]
|
|
@@ -523,80 +645,18 @@ def compute_precion_recall(
|
|
|
523
645
|
mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
|
|
524
646
|
axis=1
|
|
525
647
|
)
|
|
526
|
-
else:
|
|
527
|
-
mAP = np.zeros(n_ious, dtype=np.float64)
|
|
528
|
-
mAR = np.zeros(n_scores, dtype=np.float64)
|
|
529
|
-
|
|
530
|
-
# calculate AR and AR averaged over thresholds
|
|
531
|
-
APAveragedOverIOUs = average_precision.mean(axis=0)
|
|
532
|
-
ARAveragedOverScores = average_recall.mean(axis=0)
|
|
533
|
-
|
|
534
|
-
# calculate mAP and mAR averaged over thresholds
|
|
535
|
-
mAPAveragedOverIOUs = mAP.mean(axis=0)
|
|
536
|
-
mARAveragedOverScores = mAR.mean(axis=0)
|
|
537
|
-
|
|
538
|
-
ap_results = (
|
|
539
|
-
average_precision,
|
|
540
|
-
mAP,
|
|
541
|
-
APAveragedOverIOUs,
|
|
542
|
-
mAPAveragedOverIOUs,
|
|
543
|
-
)
|
|
544
|
-
ar_results = (
|
|
545
|
-
average_recall,
|
|
546
|
-
mAR,
|
|
547
|
-
ARAveragedOverScores,
|
|
548
|
-
mARAveragedOverScores,
|
|
549
|
-
)
|
|
550
648
|
|
|
551
649
|
return (
|
|
552
|
-
|
|
553
|
-
|
|
650
|
+
(average_precision.astype(np.float64), mAP),
|
|
651
|
+
(average_recall, mAR),
|
|
554
652
|
counts,
|
|
555
653
|
pr_curve,
|
|
556
654
|
)
|
|
557
655
|
|
|
558
656
|
|
|
559
|
-
def _count_with_examples(
|
|
560
|
-
data: NDArray[np.float64],
|
|
561
|
-
unique_idx: int | list[int],
|
|
562
|
-
label_idx: int | list[int],
|
|
563
|
-
) -> tuple[NDArray[np.float64], NDArray[np.int32], NDArray[np.intp]]:
|
|
564
|
-
"""
|
|
565
|
-
Helper function for counting occurences of unique detailed pairs.
|
|
566
|
-
|
|
567
|
-
Parameters
|
|
568
|
-
----------
|
|
569
|
-
data : NDArray[np.float64]
|
|
570
|
-
A masked portion of a detailed pairs array.
|
|
571
|
-
unique_idx : int | list[int]
|
|
572
|
-
The index or indices upon which uniqueness is constrained.
|
|
573
|
-
label_idx : int | list[int]
|
|
574
|
-
The index or indices within the unique index or indices that encode labels.
|
|
575
|
-
|
|
576
|
-
Returns
|
|
577
|
-
-------
|
|
578
|
-
NDArray[np.float64]
|
|
579
|
-
Examples drawn from the data input.
|
|
580
|
-
NDArray[np.int32]
|
|
581
|
-
Unique label indices.
|
|
582
|
-
NDArray[np.intp]
|
|
583
|
-
Counts for each unique label index.
|
|
584
|
-
"""
|
|
585
|
-
unique_rows, indices = np.unique(
|
|
586
|
-
data.astype(np.int32)[:, unique_idx],
|
|
587
|
-
return_index=True,
|
|
588
|
-
axis=0,
|
|
589
|
-
)
|
|
590
|
-
examples = data[indices]
|
|
591
|
-
labels, counts = np.unique(
|
|
592
|
-
unique_rows[:, label_idx], return_counts=True, axis=0
|
|
593
|
-
)
|
|
594
|
-
return examples, labels, counts
|
|
595
|
-
|
|
596
|
-
|
|
597
657
|
def _isin(
|
|
598
|
-
data: NDArray
|
|
599
|
-
subset: NDArray
|
|
658
|
+
data: NDArray,
|
|
659
|
+
subset: NDArray,
|
|
600
660
|
) -> NDArray[np.bool_]:
|
|
601
661
|
"""
|
|
602
662
|
Creates a mask of rows that exist within the subset.
|
|
@@ -614,22 +674,59 @@ def _isin(
|
|
|
614
674
|
Returns a bool mask with shape (N,).
|
|
615
675
|
"""
|
|
616
676
|
combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
|
|
617
|
-
np.
|
|
677
|
+
np.int32
|
|
618
678
|
)
|
|
619
679
|
combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
|
|
620
680
|
:, 1
|
|
621
|
-
].astype(np.
|
|
681
|
+
].astype(np.int32)
|
|
622
682
|
mask = np.isin(combined_data, combined_subset, assume_unique=False)
|
|
623
683
|
return mask
|
|
624
684
|
|
|
625
685
|
|
|
686
|
+
class PairClassification(IntFlag):
|
|
687
|
+
TP = auto()
|
|
688
|
+
FP_FN_MISCLF = auto()
|
|
689
|
+
FP_UNMATCHED = auto()
|
|
690
|
+
FN_UNMATCHED = auto()
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def mask_pairs_greedily(
|
|
694
|
+
pairs: NDArray[np.float64],
|
|
695
|
+
):
|
|
696
|
+
groundtruths = pairs[:, 1].astype(np.int32)
|
|
697
|
+
predictions = pairs[:, 2].astype(np.int32)
|
|
698
|
+
|
|
699
|
+
# Pre‑allocate "seen" flags for every possible x and y
|
|
700
|
+
max_gt = groundtruths.max()
|
|
701
|
+
max_pd = predictions.max()
|
|
702
|
+
used_gt = np.zeros(max_gt + 1, dtype=np.bool_)
|
|
703
|
+
used_pd = np.zeros(max_pd + 1, dtype=np.bool_)
|
|
704
|
+
|
|
705
|
+
# This mask will mark which pairs to keep
|
|
706
|
+
keep = np.zeros(pairs.shape[0], dtype=bool)
|
|
707
|
+
|
|
708
|
+
for idx in range(groundtruths.shape[0]):
|
|
709
|
+
gidx = groundtruths[idx]
|
|
710
|
+
pidx = predictions[idx]
|
|
711
|
+
|
|
712
|
+
if not (gidx < 0 or pidx < 0 or used_gt[gidx] or used_pd[pidx]):
|
|
713
|
+
keep[idx] = True
|
|
714
|
+
used_gt[gidx] = True
|
|
715
|
+
used_pd[pidx] = True
|
|
716
|
+
|
|
717
|
+
mask_matches = _isin(
|
|
718
|
+
data=pairs[:, (1, 2)],
|
|
719
|
+
subset=np.unique(pairs[np.ix_(keep, (1, 2))], axis=0), # type: ignore - np.ix_ typing
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
return mask_matches
|
|
723
|
+
|
|
724
|
+
|
|
626
725
|
def compute_confusion_matrix(
|
|
627
|
-
|
|
628
|
-
label_metadata: NDArray[np.int32],
|
|
726
|
+
detailed_pairs: NDArray[np.float64],
|
|
629
727
|
iou_thresholds: NDArray[np.float64],
|
|
630
728
|
score_thresholds: NDArray[np.float64],
|
|
631
|
-
|
|
632
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32]]:
|
|
729
|
+
) -> NDArray[np.uint8]:
|
|
633
730
|
"""
|
|
634
731
|
Compute detailed counts.
|
|
635
732
|
|
|
@@ -638,265 +735,108 @@ def compute_confusion_matrix(
|
|
|
638
735
|
Index 0 - Datum Index
|
|
639
736
|
Index 1 - GroundTruth Index
|
|
640
737
|
Index 2 - Prediction Index
|
|
641
|
-
Index 3 -
|
|
642
|
-
Index 4 -
|
|
643
|
-
Index 5 -
|
|
738
|
+
Index 3 - GroundTruth Label Index
|
|
739
|
+
Index 4 - Prediction Label Index
|
|
740
|
+
Index 5 - IOU
|
|
644
741
|
Index 6 - Score
|
|
645
742
|
|
|
646
743
|
Parameters
|
|
647
744
|
----------
|
|
648
|
-
|
|
649
|
-
|
|
745
|
+
detailed_pairs : NDArray[np.float64]
|
|
746
|
+
An unsorted array summarizing the IOU calculations of one or more pairs.
|
|
650
747
|
label_metadata : NDArray[np.int32]
|
|
651
748
|
An array containing metadata related to labels.
|
|
652
749
|
iou_thresholds : NDArray[np.float64]
|
|
653
750
|
A 1-D array containing IOU thresholds.
|
|
654
751
|
score_thresholds : NDArray[np.float64]
|
|
655
752
|
A 1-D array containing score thresholds.
|
|
656
|
-
n_examples : int
|
|
657
|
-
The maximum number of examples to return per count.
|
|
658
753
|
|
|
659
754
|
Returns
|
|
660
755
|
-------
|
|
661
|
-
NDArray[np.
|
|
756
|
+
NDArray[np.uint8]
|
|
662
757
|
Confusion matrix.
|
|
663
|
-
NDArray[np.float64]
|
|
664
|
-
Unmatched Predictions.
|
|
665
|
-
NDArray[np.int32]
|
|
666
|
-
Unmatched Ground Truths.
|
|
667
758
|
"""
|
|
668
|
-
|
|
669
|
-
n_labels = label_metadata.shape[0]
|
|
759
|
+
n_pairs = detailed_pairs.shape[0]
|
|
670
760
|
n_ious = iou_thresholds.shape[0]
|
|
671
761
|
n_scores = score_thresholds.shape[0]
|
|
672
762
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
dtype=np.float32,
|
|
677
|
-
)
|
|
678
|
-
unmatched_predictions = -1 * np.ones(
|
|
679
|
-
# (datum idx, pd idx, pd score) * n_examples + count
|
|
680
|
-
(n_ious, n_scores, n_labels, 3 * n_examples + 1),
|
|
681
|
-
dtype=np.float32,
|
|
682
|
-
)
|
|
683
|
-
unmatched_ground_truths = -1 * np.ones(
|
|
684
|
-
# (datum idx, gt idx) * n_examples + count
|
|
685
|
-
(n_ious, n_scores, n_labels, 2 * n_examples + 1),
|
|
686
|
-
dtype=np.int32,
|
|
763
|
+
pair_classifications = np.zeros(
|
|
764
|
+
(n_ious, n_scores, n_pairs),
|
|
765
|
+
dtype=np.uint8,
|
|
687
766
|
)
|
|
688
767
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
768
|
+
ids = detailed_pairs[:, :5].astype(np.int32)
|
|
769
|
+
groundtruths = ids[:, (0, 1)]
|
|
770
|
+
predictions = ids[:, (0, 2)]
|
|
771
|
+
gt_ids = ids[:, 1]
|
|
772
|
+
pd_ids = ids[:, 2]
|
|
773
|
+
gt_labels = ids[:, 3]
|
|
774
|
+
pd_labels = ids[:, 4]
|
|
775
|
+
ious = detailed_pairs[:, 5]
|
|
776
|
+
scores = detailed_pairs[:, 6]
|
|
777
|
+
|
|
778
|
+
mask_gt_exists = gt_ids > -0.5
|
|
779
|
+
mask_pd_exists = pd_ids > -0.5
|
|
780
|
+
mask_label_match = np.isclose(gt_labels, pd_labels)
|
|
781
|
+
mask_score_nonzero = scores > 1e-9
|
|
782
|
+
mask_iou_nonzero = ious > 1e-9
|
|
694
783
|
|
|
695
784
|
mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
|
|
696
785
|
mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
|
|
697
|
-
mask_gt_pd_mismatch = mask_gt_pd_exists & ~mask_label_match
|
|
698
786
|
|
|
699
|
-
|
|
700
|
-
|
|
787
|
+
mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
|
|
788
|
+
|
|
701
789
|
for iou_idx in range(n_ious):
|
|
702
|
-
mask_iou_threshold =
|
|
790
|
+
mask_iou_threshold = ious >= iou_thresholds[iou_idx]
|
|
703
791
|
mask_iou = mask_iou_nonzero & mask_iou_threshold
|
|
704
|
-
|
|
705
|
-
groundtruths_passing_ious = np.unique(groundtruths[mask_iou], axis=0)
|
|
706
|
-
mask_groundtruths_with_passing_ious = _isin(
|
|
707
|
-
data=groundtruths,
|
|
708
|
-
subset=groundtruths_passing_ious,
|
|
709
|
-
)
|
|
710
|
-
mask_groundtruths_without_passing_ious = (
|
|
711
|
-
~mask_groundtruths_with_passing_ious & mask_gt_exists
|
|
712
|
-
)
|
|
713
|
-
|
|
714
|
-
predictions_with_passing_ious = np.unique(
|
|
715
|
-
predictions[mask_iou], axis=0
|
|
716
|
-
)
|
|
717
|
-
mask_predictions_with_passing_ious = _isin(
|
|
718
|
-
data=predictions,
|
|
719
|
-
subset=predictions_with_passing_ious,
|
|
720
|
-
)
|
|
721
|
-
mask_predictions_without_passing_ious = (
|
|
722
|
-
~mask_predictions_with_passing_ious & mask_pd_exists
|
|
723
|
-
)
|
|
724
|
-
|
|
725
792
|
for score_idx in range(n_scores):
|
|
726
|
-
mask_score_threshold =
|
|
793
|
+
mask_score_threshold = scores >= score_thresholds[score_idx]
|
|
727
794
|
mask_score = mask_score_nonzero & mask_score_threshold
|
|
728
795
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
)
|
|
732
|
-
mask_groundtruths_with_passing_score = _isin(
|
|
733
|
-
data=groundtruths,
|
|
734
|
-
subset=groundtruths_with_passing_score,
|
|
735
|
-
)
|
|
736
|
-
mask_groundtruths_without_passing_score = (
|
|
737
|
-
~mask_groundtruths_with_passing_score & mask_gt_exists
|
|
796
|
+
mask_thresholded_matched_pairs = (
|
|
797
|
+
mask_matched_pairs & mask_iou & mask_score
|
|
738
798
|
)
|
|
739
799
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
mask_misclf = mask_iou & (
|
|
743
|
-
(
|
|
744
|
-
~mask_score
|
|
745
|
-
& mask_gt_pd_match
|
|
746
|
-
& mask_groundtruths_with_passing_score
|
|
747
|
-
)
|
|
748
|
-
| (mask_score & mask_gt_pd_mismatch)
|
|
749
|
-
)
|
|
750
|
-
mask_halluc = mask_score & mask_predictions_without_passing_ious
|
|
751
|
-
mask_misprd = (
|
|
752
|
-
mask_groundtruths_without_passing_ious
|
|
753
|
-
| mask_groundtruths_without_passing_score
|
|
800
|
+
mask_true_positives = (
|
|
801
|
+
mask_thresholded_matched_pairs & mask_gt_pd_match
|
|
754
802
|
)
|
|
803
|
+
mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
|
|
755
804
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
mask_pds_with_tp_override = _isin(
|
|
762
|
-
data=predictions[mask_misclf],
|
|
763
|
-
subset=predictions[mask_tp],
|
|
764
|
-
)
|
|
765
|
-
mask_misprd[mask_misclf] |= (
|
|
766
|
-
~mask_gts_with_tp_override & mask_pds_with_tp_override
|
|
805
|
+
mask_groundtruths_in_thresholded_matched_pairs = _isin(
|
|
806
|
+
data=groundtruths,
|
|
807
|
+
subset=np.unique(
|
|
808
|
+
groundtruths[mask_thresholded_matched_pairs], axis=0
|
|
809
|
+
),
|
|
767
810
|
)
|
|
768
|
-
|
|
769
|
-
|
|
811
|
+
mask_predictions_in_thresholded_matched_pairs = _isin(
|
|
812
|
+
data=predictions,
|
|
813
|
+
subset=np.unique(
|
|
814
|
+
predictions[mask_thresholded_matched_pairs], axis=0
|
|
815
|
+
),
|
|
770
816
|
)
|
|
771
817
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
label_idx=2,
|
|
818
|
+
mask_unmatched_predictions = (
|
|
819
|
+
~mask_predictions_in_thresholded_matched_pairs
|
|
820
|
+
& mask_pd_exists
|
|
821
|
+
& mask_score
|
|
777
822
|
)
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
misclf_examples,
|
|
782
|
-
misclf_labels,
|
|
783
|
-
misclf_counts,
|
|
784
|
-
) = _count_with_examples(
|
|
785
|
-
data[mask_misclf], unique_idx=[0, 1, 2, 4, 5], label_idx=[3, 4]
|
|
823
|
+
mask_unmatched_groundtruths = (
|
|
824
|
+
~mask_groundtruths_in_thresholded_matched_pairs
|
|
825
|
+
& mask_gt_exists
|
|
786
826
|
)
|
|
787
827
|
|
|
788
|
-
#
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
data[mask_halluc], unique_idx=[0, 2, 5], label_idx=2
|
|
828
|
+
# classify pairings
|
|
829
|
+
pair_classifications[
|
|
830
|
+
iou_idx, score_idx, mask_true_positives
|
|
831
|
+
] |= np.uint8(PairClassification.TP)
|
|
832
|
+
pair_classifications[iou_idx, score_idx, mask_misclf] |= np.uint8(
|
|
833
|
+
PairClassification.FP_FN_MISCLF
|
|
795
834
|
)
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
(
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
# store the counts
|
|
807
|
-
confusion_matrix[
|
|
808
|
-
iou_idx, score_idx, tp_labels, tp_labels, 0
|
|
809
|
-
] = tp_counts
|
|
810
|
-
confusion_matrix[
|
|
811
|
-
iou_idx,
|
|
812
|
-
score_idx,
|
|
813
|
-
misclf_labels[:, 0],
|
|
814
|
-
misclf_labels[:, 1],
|
|
815
|
-
0,
|
|
816
|
-
] = misclf_counts
|
|
817
|
-
unmatched_predictions[
|
|
818
|
-
iou_idx,
|
|
819
|
-
score_idx,
|
|
820
|
-
halluc_labels,
|
|
821
|
-
0,
|
|
822
|
-
] = halluc_counts
|
|
823
|
-
unmatched_ground_truths[
|
|
824
|
-
iou_idx,
|
|
825
|
-
score_idx,
|
|
826
|
-
misprd_labels,
|
|
827
|
-
0,
|
|
828
|
-
] = misprd_counts
|
|
829
|
-
|
|
830
|
-
# store examples
|
|
831
|
-
if n_examples > 0:
|
|
832
|
-
for label_idx in range(n_labels):
|
|
833
|
-
|
|
834
|
-
# true-positive examples
|
|
835
|
-
mask_tp_label = tp_examples[:, 5] == label_idx
|
|
836
|
-
if mask_tp_label.sum() > 0:
|
|
837
|
-
tp_label_examples = tp_examples[mask_tp_label][
|
|
838
|
-
:n_examples
|
|
839
|
-
]
|
|
840
|
-
confusion_matrix[
|
|
841
|
-
iou_idx,
|
|
842
|
-
score_idx,
|
|
843
|
-
label_idx,
|
|
844
|
-
label_idx,
|
|
845
|
-
1 : 4 * tp_label_examples.shape[0] + 1,
|
|
846
|
-
] = tp_label_examples[:, [0, 1, 2, 6]].flatten()
|
|
847
|
-
|
|
848
|
-
# misclassification examples
|
|
849
|
-
mask_misclf_gt_label = misclf_examples[:, 4] == label_idx
|
|
850
|
-
if mask_misclf_gt_label.sum() > 0:
|
|
851
|
-
for pd_label_idx in range(n_labels):
|
|
852
|
-
mask_misclf_pd_label = (
|
|
853
|
-
misclf_examples[:, 5] == pd_label_idx
|
|
854
|
-
)
|
|
855
|
-
mask_misclf_label_combo = (
|
|
856
|
-
mask_misclf_gt_label & mask_misclf_pd_label
|
|
857
|
-
)
|
|
858
|
-
if mask_misclf_label_combo.sum() > 0:
|
|
859
|
-
misclf_label_examples = misclf_examples[
|
|
860
|
-
mask_misclf_label_combo
|
|
861
|
-
][:n_examples]
|
|
862
|
-
confusion_matrix[
|
|
863
|
-
iou_idx,
|
|
864
|
-
score_idx,
|
|
865
|
-
label_idx,
|
|
866
|
-
pd_label_idx,
|
|
867
|
-
1 : 4 * misclf_label_examples.shape[0] + 1,
|
|
868
|
-
] = misclf_label_examples[
|
|
869
|
-
:, [0, 1, 2, 6]
|
|
870
|
-
].flatten()
|
|
871
|
-
|
|
872
|
-
# unmatched prediction examples
|
|
873
|
-
mask_halluc_label = halluc_examples[:, 5] == label_idx
|
|
874
|
-
if mask_halluc_label.sum() > 0:
|
|
875
|
-
halluc_label_examples = halluc_examples[
|
|
876
|
-
mask_halluc_label
|
|
877
|
-
][:n_examples]
|
|
878
|
-
unmatched_predictions[
|
|
879
|
-
iou_idx,
|
|
880
|
-
score_idx,
|
|
881
|
-
label_idx,
|
|
882
|
-
1 : 3 * halluc_label_examples.shape[0] + 1,
|
|
883
|
-
] = halluc_label_examples[:, [0, 2, 6]].flatten()
|
|
884
|
-
|
|
885
|
-
# unmatched ground truth examples
|
|
886
|
-
mask_misprd_label = misprd_examples[:, 4] == label_idx
|
|
887
|
-
if misprd_examples.size > 0:
|
|
888
|
-
misprd_label_examples = misprd_examples[
|
|
889
|
-
mask_misprd_label
|
|
890
|
-
][:n_examples]
|
|
891
|
-
unmatched_ground_truths[
|
|
892
|
-
iou_idx,
|
|
893
|
-
score_idx,
|
|
894
|
-
label_idx,
|
|
895
|
-
1 : 2 * misprd_label_examples.shape[0] + 1,
|
|
896
|
-
] = misprd_label_examples[:, [0, 1]].flatten()
|
|
897
|
-
|
|
898
|
-
return (
|
|
899
|
-
confusion_matrix,
|
|
900
|
-
unmatched_predictions,
|
|
901
|
-
unmatched_ground_truths,
|
|
902
|
-
) # type: ignore[reportReturnType]
|
|
835
|
+
pair_classifications[
|
|
836
|
+
iou_idx, score_idx, mask_unmatched_predictions
|
|
837
|
+
] |= np.uint8(PairClassification.FP_UNMATCHED)
|
|
838
|
+
pair_classifications[
|
|
839
|
+
iou_idx, score_idx, mask_unmatched_groundtruths
|
|
840
|
+
] |= np.uint8(PairClassification.FN_UNMATCHED)
|
|
841
|
+
|
|
842
|
+
return pair_classifications
|