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