valor-lite 0.34.3__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 +235 -386
- valor_lite/object_detection/manager.py +458 -374
- valor_lite/object_detection/metric.py +16 -34
- valor_lite/object_detection/utilities.py +134 -305
- {valor_lite-0.34.3.dist-info → valor_lite-0.35.0.dist-info}/METADATA +1 -1
- {valor_lite-0.34.3.dist-info → valor_lite-0.35.0.dist-info}/RECORD +10 -10
- {valor_lite-0.34.3.dist-info → valor_lite-0.35.0.dist-info}/WHEEL +1 -1
- {valor_lite-0.34.3.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
|
-
|
|
211
|
+
label_metadata[label_indices.astype(np.int32), 0] = unique_counts
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
_, indices = np.unique(data[:, [0, 2, 5]], axis=0, return_index=True)
|
|
209
|
-
|
|
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, :]
|
|
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,14 +294,10 @@ 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],
|
|
@@ -298,8 +317,8 @@ def compute_precion_recall(
|
|
|
298
317
|
|
|
299
318
|
Parameters
|
|
300
319
|
----------
|
|
301
|
-
|
|
302
|
-
A
|
|
320
|
+
ranked_pairs : NDArray[np.float64]
|
|
321
|
+
A ranked array summarizing the IOU calculations of one or more pairs.
|
|
303
322
|
label_metadata : NDArray[np.int32]
|
|
304
323
|
An array containing metadata related to labels.
|
|
305
324
|
iou_thresholds : NDArray[np.float64]
|
|
@@ -309,32 +328,45 @@ def compute_precion_recall(
|
|
|
309
328
|
|
|
310
329
|
Returns
|
|
311
330
|
-------
|
|
312
|
-
tuple[NDArray[np.float64], NDArray[np.float64]
|
|
313
|
-
Average Precision results.
|
|
314
|
-
tuple[NDArray[np.float64], NDArray[np.float64]
|
|
315
|
-
Average Recall results.
|
|
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).
|
|
316
335
|
NDArray[np.float64]
|
|
317
336
|
Precision, Recall, TP, FP, FN, F1 Score.
|
|
318
337
|
NDArray[np.float64]
|
|
319
338
|
Interpolated Precision-Recall Curves.
|
|
320
339
|
"""
|
|
321
|
-
|
|
322
|
-
n_rows = data.shape[0]
|
|
340
|
+
n_rows = ranked_pairs.shape[0]
|
|
323
341
|
n_labels = label_metadata.shape[0]
|
|
324
342
|
n_ious = iou_thresholds.shape[0]
|
|
325
343
|
n_scores = score_thresholds.shape[0]
|
|
326
344
|
|
|
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
|
-
|
|
345
|
+
# initialize result arrays
|
|
332
346
|
average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
|
|
347
|
+
mAP = np.zeros(n_ious, dtype=np.float64)
|
|
333
348
|
average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
|
|
349
|
+
mAR = np.zeros(n_scores, dtype=np.float64)
|
|
334
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]
|
|
335
369
|
|
|
336
|
-
pd_labels = data[:, 5].astype(np.int32)
|
|
337
|
-
scores = data[:, 6]
|
|
338
370
|
unique_pd_labels, unique_pd_indices = np.unique(
|
|
339
371
|
pd_labels, return_index=True
|
|
340
372
|
)
|
|
@@ -346,9 +378,9 @@ def compute_precion_recall(
|
|
|
346
378
|
running_tp_count = np.zeros_like(running_total_count)
|
|
347
379
|
running_gt_count = np.zeros_like(running_total_count)
|
|
348
380
|
|
|
349
|
-
mask_score_nonzero =
|
|
350
|
-
mask_gt_exists =
|
|
351
|
-
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)
|
|
352
384
|
|
|
353
385
|
mask_gt_exists_labels_match = mask_gt_exists & mask_labels_match
|
|
354
386
|
|
|
@@ -357,7 +389,7 @@ def compute_precion_recall(
|
|
|
357
389
|
mask_fn = mask_gt_exists_labels_match
|
|
358
390
|
|
|
359
391
|
for iou_idx in range(n_ious):
|
|
360
|
-
mask_iou =
|
|
392
|
+
mask_iou = ious >= iou_thresholds[iou_idx]
|
|
361
393
|
|
|
362
394
|
mask_tp_outer = mask_tp & mask_iou
|
|
363
395
|
mask_fp_outer = mask_fp & (
|
|
@@ -366,16 +398,16 @@ def compute_precion_recall(
|
|
|
366
398
|
mask_fn_outer = mask_fn & mask_iou
|
|
367
399
|
|
|
368
400
|
for score_idx in range(n_scores):
|
|
369
|
-
mask_score_thresh =
|
|
401
|
+
mask_score_thresh = scores >= score_thresholds[score_idx]
|
|
370
402
|
|
|
371
403
|
mask_tp_inner = mask_tp_outer & mask_score_thresh
|
|
372
404
|
mask_fp_inner = mask_fp_outer & mask_score_thresh
|
|
373
405
|
mask_fn_inner = mask_fn_outer & ~mask_score_thresh
|
|
374
406
|
|
|
375
407
|
# create true-positive mask score threshold
|
|
376
|
-
tp_candidates =
|
|
408
|
+
tp_candidates = ids[mask_tp_inner]
|
|
377
409
|
_, indices_gt_unique = np.unique(
|
|
378
|
-
tp_candidates[:, [0, 1,
|
|
410
|
+
tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
|
|
379
411
|
)
|
|
380
412
|
mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
|
|
381
413
|
mask_gt_unique[indices_gt_unique] = True
|
|
@@ -437,9 +469,9 @@ def compute_precion_recall(
|
|
|
437
469
|
average_recall[score_idx] += recall
|
|
438
470
|
|
|
439
471
|
# create true-positive mask score threshold
|
|
440
|
-
tp_candidates =
|
|
472
|
+
tp_candidates = ids[mask_tp_outer]
|
|
441
473
|
_, indices_gt_unique = np.unique(
|
|
442
|
-
tp_candidates[:, [0, 1,
|
|
474
|
+
tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
|
|
443
475
|
)
|
|
444
476
|
mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
|
|
445
477
|
mask_gt_unique[indices_gt_unique] = True
|
|
@@ -476,7 +508,6 @@ def compute_precion_recall(
|
|
|
476
508
|
recall_index = np.floor(recall * 100.0).astype(np.int32)
|
|
477
509
|
|
|
478
510
|
# bin precision-recall curve
|
|
479
|
-
pr_curve = np.zeros((n_ious, n_labels, 101, 2))
|
|
480
511
|
for iou_idx in range(n_ious):
|
|
481
512
|
p = precision[iou_idx]
|
|
482
513
|
r = recall_index[iou_idx]
|
|
@@ -523,80 +554,18 @@ def compute_precion_recall(
|
|
|
523
554
|
mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
|
|
524
555
|
axis=1
|
|
525
556
|
)
|
|
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
557
|
|
|
551
558
|
return (
|
|
552
|
-
|
|
553
|
-
|
|
559
|
+
(average_precision.astype(np.float64), mAP),
|
|
560
|
+
(average_recall, mAR),
|
|
554
561
|
counts,
|
|
555
562
|
pr_curve,
|
|
556
563
|
)
|
|
557
564
|
|
|
558
565
|
|
|
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
566
|
def _isin(
|
|
598
|
-
data: NDArray
|
|
599
|
-
subset: NDArray
|
|
567
|
+
data: NDArray,
|
|
568
|
+
subset: NDArray,
|
|
600
569
|
) -> NDArray[np.bool_]:
|
|
601
570
|
"""
|
|
602
571
|
Creates a mask of rows that exist within the subset.
|
|
@@ -614,22 +583,59 @@ def _isin(
|
|
|
614
583
|
Returns a bool mask with shape (N,).
|
|
615
584
|
"""
|
|
616
585
|
combined_data = (data[:, 0].astype(np.int64) << 32) | data[:, 1].astype(
|
|
617
|
-
np.
|
|
586
|
+
np.int32
|
|
618
587
|
)
|
|
619
588
|
combined_subset = (subset[:, 0].astype(np.int64) << 32) | subset[
|
|
620
589
|
:, 1
|
|
621
|
-
].astype(np.
|
|
590
|
+
].astype(np.int32)
|
|
622
591
|
mask = np.isin(combined_data, combined_subset, assume_unique=False)
|
|
623
592
|
return mask
|
|
624
593
|
|
|
625
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
|
+
|
|
626
634
|
def compute_confusion_matrix(
|
|
627
|
-
|
|
628
|
-
label_metadata: NDArray[np.int32],
|
|
635
|
+
detailed_pairs: NDArray[np.float64],
|
|
629
636
|
iou_thresholds: NDArray[np.float64],
|
|
630
637
|
score_thresholds: NDArray[np.float64],
|
|
631
|
-
|
|
632
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32]]:
|
|
638
|
+
) -> NDArray[np.uint8]:
|
|
633
639
|
"""
|
|
634
640
|
Compute detailed counts.
|
|
635
641
|
|
|
@@ -638,265 +644,108 @@ def compute_confusion_matrix(
|
|
|
638
644
|
Index 0 - Datum Index
|
|
639
645
|
Index 1 - GroundTruth Index
|
|
640
646
|
Index 2 - Prediction Index
|
|
641
|
-
Index 3 -
|
|
642
|
-
Index 4 -
|
|
643
|
-
Index 5 -
|
|
647
|
+
Index 3 - GroundTruth Label Index
|
|
648
|
+
Index 4 - Prediction Label Index
|
|
649
|
+
Index 5 - IOU
|
|
644
650
|
Index 6 - Score
|
|
645
651
|
|
|
646
652
|
Parameters
|
|
647
653
|
----------
|
|
648
|
-
|
|
649
|
-
|
|
654
|
+
detailed_pairs : NDArray[np.float64]
|
|
655
|
+
An unsorted array summarizing the IOU calculations of one or more pairs.
|
|
650
656
|
label_metadata : NDArray[np.int32]
|
|
651
657
|
An array containing metadata related to labels.
|
|
652
658
|
iou_thresholds : NDArray[np.float64]
|
|
653
659
|
A 1-D array containing IOU thresholds.
|
|
654
660
|
score_thresholds : NDArray[np.float64]
|
|
655
661
|
A 1-D array containing score thresholds.
|
|
656
|
-
n_examples : int
|
|
657
|
-
The maximum number of examples to return per count.
|
|
658
662
|
|
|
659
663
|
Returns
|
|
660
664
|
-------
|
|
661
|
-
NDArray[np.
|
|
665
|
+
NDArray[np.uint8]
|
|
662
666
|
Confusion matrix.
|
|
663
|
-
NDArray[np.float64]
|
|
664
|
-
Unmatched Predictions.
|
|
665
|
-
NDArray[np.int32]
|
|
666
|
-
Unmatched Ground Truths.
|
|
667
667
|
"""
|
|
668
|
-
|
|
669
|
-
n_labels = label_metadata.shape[0]
|
|
668
|
+
n_pairs = detailed_pairs.shape[0]
|
|
670
669
|
n_ious = iou_thresholds.shape[0]
|
|
671
670
|
n_scores = score_thresholds.shape[0]
|
|
672
671
|
|
|
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,
|
|
672
|
+
pair_classifications = np.zeros(
|
|
673
|
+
(n_ious, n_scores, n_pairs),
|
|
674
|
+
dtype=np.uint8,
|
|
687
675
|
)
|
|
688
676
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
|
694
692
|
|
|
695
693
|
mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
|
|
696
694
|
mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
|
|
697
|
-
mask_gt_pd_mismatch = mask_gt_pd_exists & ~mask_label_match
|
|
698
695
|
|
|
699
|
-
|
|
700
|
-
|
|
696
|
+
mask_matched_pairs = mask_pairs_greedily(pairs=detailed_pairs)
|
|
697
|
+
|
|
701
698
|
for iou_idx in range(n_ious):
|
|
702
|
-
mask_iou_threshold =
|
|
699
|
+
mask_iou_threshold = ious >= iou_thresholds[iou_idx]
|
|
703
700
|
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
701
|
for score_idx in range(n_scores):
|
|
726
|
-
mask_score_threshold =
|
|
702
|
+
mask_score_threshold = scores >= score_thresholds[score_idx]
|
|
727
703
|
mask_score = mask_score_nonzero & mask_score_threshold
|
|
728
704
|
|
|
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
|
|
705
|
+
mask_thresholded_matched_pairs = (
|
|
706
|
+
mask_matched_pairs & mask_iou & mask_score
|
|
738
707
|
)
|
|
739
708
|
|
|
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
|
|
709
|
+
mask_true_positives = (
|
|
710
|
+
mask_thresholded_matched_pairs & mask_gt_pd_match
|
|
754
711
|
)
|
|
712
|
+
mask_misclf = mask_thresholded_matched_pairs & ~mask_gt_pd_match
|
|
755
713
|
|
|
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
|
|
767
|
-
)
|
|
768
|
-
mask_misclf[mask_misclf] &= (
|
|
769
|
-
~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
|
+
),
|
|
770
719
|
)
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
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
|
+
),
|
|
777
725
|
)
|
|
778
726
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
misclf_counts,
|
|
784
|
-
) = _count_with_examples(
|
|
785
|
-
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
|
|
786
731
|
)
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
halluc_examples,
|
|
791
|
-
halluc_labels,
|
|
792
|
-
halluc_counts,
|
|
793
|
-
) = _count_with_examples(
|
|
794
|
-
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
|
|
795
735
|
)
|
|
796
736
|
|
|
797
|
-
#
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
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
|
|
804
743
|
)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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]
|
|
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
|