valor-lite 0.36.5__py3-none-any.whl → 0.37.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- valor_lite/cache/__init__.py +11 -0
- valor_lite/cache/compute.py +211 -0
- valor_lite/cache/ephemeral.py +302 -0
- valor_lite/cache/persistent.py +536 -0
- valor_lite/classification/__init__.py +5 -10
- valor_lite/classification/annotation.py +4 -0
- valor_lite/classification/computation.py +233 -251
- valor_lite/classification/evaluator.py +882 -0
- valor_lite/classification/loader.py +97 -0
- valor_lite/classification/metric.py +141 -4
- valor_lite/classification/shared.py +184 -0
- valor_lite/classification/utilities.py +221 -118
- valor_lite/exceptions.py +5 -0
- valor_lite/object_detection/__init__.py +5 -4
- valor_lite/object_detection/annotation.py +13 -1
- valor_lite/object_detection/computation.py +367 -304
- valor_lite/object_detection/evaluator.py +804 -0
- valor_lite/object_detection/loader.py +292 -0
- valor_lite/object_detection/metric.py +152 -3
- valor_lite/object_detection/shared.py +206 -0
- valor_lite/object_detection/utilities.py +182 -109
- valor_lite/semantic_segmentation/__init__.py +5 -4
- valor_lite/semantic_segmentation/annotation.py +7 -0
- valor_lite/semantic_segmentation/computation.py +20 -110
- valor_lite/semantic_segmentation/evaluator.py +414 -0
- valor_lite/semantic_segmentation/loader.py +205 -0
- valor_lite/semantic_segmentation/shared.py +149 -0
- valor_lite/semantic_segmentation/utilities.py +6 -23
- {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/METADATA +3 -1
- valor_lite-0.37.5.dist-info/RECORD +49 -0
- {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/WHEEL +1 -1
- valor_lite/classification/manager.py +0 -545
- valor_lite/object_detection/manager.py +0 -865
- valor_lite/profiling.py +0 -374
- valor_lite/semantic_segmentation/benchmark.py +0 -237
- valor_lite/semantic_segmentation/manager.py +0 -446
- valor_lite-0.36.5.dist-info/RECORD +0 -41
- {valor_lite-0.36.5.dist-info → valor_lite-0.37.5.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from enum import IntFlag, auto
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
|
+
import pyarrow as pa
|
|
4
5
|
import shapely
|
|
5
6
|
from numpy.typing import NDArray
|
|
6
7
|
|
|
8
|
+
EPSILON = 1e-9
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
def compute_bbox_iou(data: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
9
12
|
"""
|
|
@@ -70,7 +73,7 @@ def compute_bbox_iou(data: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
|
70
73
|
np.divide(
|
|
71
74
|
intersection_area,
|
|
72
75
|
union_area,
|
|
73
|
-
where=union_area >=
|
|
76
|
+
where=union_area >= EPSILON,
|
|
74
77
|
out=ious,
|
|
75
78
|
)
|
|
76
79
|
return ious
|
|
@@ -117,7 +120,7 @@ def compute_bitmask_iou(data: NDArray[np.bool_]) -> NDArray[np.float64]:
|
|
|
117
120
|
np.divide(
|
|
118
121
|
intersection_,
|
|
119
122
|
union_,
|
|
120
|
-
where=union_ >=
|
|
123
|
+
where=union_ >= EPSILON,
|
|
121
124
|
out=ious,
|
|
122
125
|
)
|
|
123
126
|
return ious
|
|
@@ -167,291 +170,236 @@ def compute_polygon_iou(
|
|
|
167
170
|
np.divide(
|
|
168
171
|
intersection_areas,
|
|
169
172
|
union_areas,
|
|
170
|
-
where=union_areas >=
|
|
173
|
+
where=union_areas >= EPSILON,
|
|
171
174
|
out=ious,
|
|
172
175
|
)
|
|
173
176
|
return ious
|
|
174
177
|
|
|
175
178
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) -> NDArray[np.int32]:
|
|
179
|
+
def rank_pairs(
|
|
180
|
+
sorted_pairs: NDArray[np.float64],
|
|
181
|
+
) -> tuple[NDArray[np.float64], NDArray[np.intp]]:
|
|
180
182
|
"""
|
|
181
|
-
|
|
183
|
+
Prunes and ranks prediction pairs.
|
|
184
|
+
|
|
185
|
+
Should result in a single pair per prediction annotation.
|
|
182
186
|
|
|
183
187
|
Parameters
|
|
184
188
|
----------
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
sorted_pairs : NDArray[np.float64]
|
|
190
|
+
Ranked annotation pairs.
|
|
191
|
+
Index 0 - Datum Index
|
|
192
|
+
Index 1 - GroundTruth Index
|
|
193
|
+
Index 2 - Prediction Index
|
|
194
|
+
Index 3 - GroundTruth Label Index
|
|
195
|
+
Index 4 - Prediction Label Index
|
|
196
|
+
Index 5 - IOU
|
|
197
|
+
Index 6 - Score
|
|
194
198
|
|
|
195
199
|
Returns
|
|
196
200
|
-------
|
|
197
|
-
NDArray[
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
NDArray[float64]
|
|
202
|
+
Ranked prediction pairs.
|
|
203
|
+
NDArray[intp]
|
|
204
|
+
Indices of ranked prediction pairs.
|
|
201
205
|
"""
|
|
202
|
-
label_metadata = np.zeros((n_labels, 2), dtype=np.int32)
|
|
203
|
-
|
|
204
|
-
ground_truth_pairs = ids[:, (0, 1, 3)]
|
|
205
|
-
ground_truth_pairs = ground_truth_pairs[ground_truth_pairs[:, 1] >= 0]
|
|
206
|
-
unique_pairs = np.unique(ground_truth_pairs, axis=0)
|
|
207
|
-
label_indices, unique_counts = np.unique(
|
|
208
|
-
unique_pairs[:, 2], return_counts=True
|
|
209
|
-
)
|
|
210
|
-
label_metadata[label_indices.astype(np.int32), 0] = unique_counts
|
|
211
|
-
|
|
212
|
-
prediction_pairs = ids[:, (0, 2, 4)]
|
|
213
|
-
prediction_pairs = prediction_pairs[prediction_pairs[:, 1] >= 0]
|
|
214
|
-
unique_pairs = np.unique(prediction_pairs, axis=0)
|
|
215
|
-
label_indices, unique_counts = np.unique(
|
|
216
|
-
unique_pairs[:, 2], return_counts=True
|
|
217
|
-
)
|
|
218
|
-
label_metadata[label_indices.astype(np.int32), 1] = unique_counts
|
|
219
206
|
|
|
220
|
-
|
|
207
|
+
# remove unmatched ground truths
|
|
208
|
+
mask_predictions = sorted_pairs[:, 2] >= 0.0
|
|
209
|
+
pairs = sorted_pairs[mask_predictions]
|
|
210
|
+
indices = np.where(mask_predictions)[0]
|
|
221
211
|
|
|
212
|
+
# find best fits for prediction
|
|
213
|
+
mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
|
|
214
|
+
matched_predictions = np.unique(pairs[mask_label_match, 2])
|
|
222
215
|
|
|
223
|
-
|
|
224
|
-
detailed_pairs: NDArray[np.float64],
|
|
225
|
-
mask_datums: NDArray[np.bool_],
|
|
226
|
-
mask_predictions: NDArray[np.bool_],
|
|
227
|
-
mask_ground_truths: NDArray[np.bool_],
|
|
228
|
-
n_labels: int,
|
|
229
|
-
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.int32],]:
|
|
230
|
-
"""
|
|
231
|
-
Performs filtering on a detailed cache.
|
|
216
|
+
mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
|
|
232
217
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
detailed_pairs : NDArray[float64]
|
|
236
|
-
A list of sorted detailed pairs with size (N, 7).
|
|
237
|
-
mask_datums : NDArray[bool]
|
|
238
|
-
A boolean mask with size (N,).
|
|
239
|
-
mask_ground_truths : NDArray[bool]
|
|
240
|
-
A boolean mask with size (N,).
|
|
241
|
-
mask_predictions : NDArray[bool]
|
|
242
|
-
A boolean mask with size (N,).
|
|
243
|
-
n_labels : int
|
|
244
|
-
The total number of unique labels.
|
|
218
|
+
pairs = pairs[mask_label_match | mask_unmatched_predictions]
|
|
219
|
+
indices = indices[mask_label_match | mask_unmatched_predictions]
|
|
245
220
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Filtered detailed pairs.
|
|
250
|
-
NDArray[float64]
|
|
251
|
-
Filtered ranked pairs.
|
|
252
|
-
NDArray[int32]
|
|
253
|
-
Label metadata.
|
|
254
|
-
"""
|
|
255
|
-
# filter datums
|
|
256
|
-
detailed_pairs = detailed_pairs[mask_datums].copy()
|
|
257
|
-
|
|
258
|
-
# filter ground truths
|
|
259
|
-
if mask_ground_truths.any():
|
|
260
|
-
invalid_groundtruth_indices = np.where(mask_ground_truths)[0]
|
|
261
|
-
detailed_pairs[
|
|
262
|
-
invalid_groundtruth_indices[:, None], (1, 3, 5)
|
|
263
|
-
] = np.array([[-1, -1, 0]])
|
|
264
|
-
|
|
265
|
-
# filter predictions
|
|
266
|
-
if mask_predictions.any():
|
|
267
|
-
invalid_prediction_indices = np.where(mask_predictions)[0]
|
|
268
|
-
detailed_pairs[
|
|
269
|
-
invalid_prediction_indices[:, None], (2, 4, 5, 6)
|
|
270
|
-
] = np.array([[-1, -1, 0, -1]])
|
|
271
|
-
|
|
272
|
-
# filter null pairs
|
|
273
|
-
mask_null_pairs = np.all(
|
|
274
|
-
np.isclose(
|
|
275
|
-
detailed_pairs[:, 1:5],
|
|
276
|
-
np.array([-1.0, -1.0, -1.0, -1.0]),
|
|
277
|
-
),
|
|
278
|
-
axis=1,
|
|
221
|
+
# only keep the highest ranked prediction (datum_id, prediction_id, predicted_label_id)
|
|
222
|
+
_, unique_indices = np.unique(
|
|
223
|
+
pairs[:, [0, 2, 4]], axis=0, return_index=True
|
|
279
224
|
)
|
|
280
|
-
|
|
225
|
+
pairs = pairs[unique_indices]
|
|
226
|
+
indices = indices[unique_indices]
|
|
281
227
|
|
|
282
|
-
#
|
|
283
|
-
|
|
228
|
+
# np.unique orders its results by value, we need to sort the indices to maintain the results of the lexsort
|
|
229
|
+
sorted_indices = np.lexsort(
|
|
284
230
|
(
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
-detailed_pairs[:, 6], # score
|
|
231
|
+
-pairs[:, 5], # iou
|
|
232
|
+
-pairs[:, 6], # score
|
|
288
233
|
)
|
|
289
234
|
)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
ids=detailed_pairs[:, :5].astype(np.int32),
|
|
293
|
-
n_labels=n_labels,
|
|
294
|
-
)
|
|
295
|
-
ranked_pairs = rank_pairs(
|
|
296
|
-
detailed_pairs=detailed_pairs,
|
|
297
|
-
label_metadata=label_metadata,
|
|
298
|
-
)
|
|
299
|
-
return (
|
|
300
|
-
detailed_pairs,
|
|
301
|
-
ranked_pairs,
|
|
302
|
-
label_metadata,
|
|
303
|
-
)
|
|
235
|
+
pairs = pairs[sorted_indices]
|
|
236
|
+
indices = indices[sorted_indices]
|
|
304
237
|
|
|
238
|
+
return pairs, indices
|
|
305
239
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
240
|
+
|
|
241
|
+
def calculate_ranking_boundaries(
|
|
242
|
+
ranked_pairs: NDArray[np.float64],
|
|
309
243
|
) -> NDArray[np.float64]:
|
|
310
244
|
"""
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Only ground truths and predictions that provide unique information are kept. The unkept
|
|
314
|
-
pairs are represented via the label metadata array.
|
|
245
|
+
Determine IOU boundaries for computing AP across chunks.
|
|
315
246
|
|
|
316
247
|
Parameters
|
|
317
248
|
----------
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
label_metadata : NDArray[np.int32]
|
|
328
|
-
Array containing label counts with shape (n_labels, 2)
|
|
329
|
-
Index 0 - Ground truth label count
|
|
330
|
-
Index 1 - Prediction label count
|
|
249
|
+
ranked_pairs : NDArray[np.float64]
|
|
250
|
+
Ranked annotation pairs.
|
|
251
|
+
Index 0 - Datum Index
|
|
252
|
+
Index 1 - GroundTruth Index
|
|
253
|
+
Index 2 - Prediction Index
|
|
254
|
+
Index 3 - GroundTruth Label Index
|
|
255
|
+
Index 4 - Prediction Label Index
|
|
256
|
+
Index 5 - IOU
|
|
257
|
+
Index 6 - Score
|
|
331
258
|
|
|
332
259
|
Returns
|
|
333
260
|
-------
|
|
334
261
|
NDArray[np.float64]
|
|
335
|
-
|
|
262
|
+
A 1-D array containing the lower IOU boundary for classifying pairs as true-positive across chunks.
|
|
336
263
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
# find best fits for prediction
|
|
343
|
-
mask_label_match = np.isclose(pairs[:, 3], pairs[:, 4])
|
|
344
|
-
matched_predictions = np.unique(pairs[mask_label_match, 2])
|
|
345
|
-
mask_unmatched_predictions = ~np.isin(pairs[:, 2], matched_predictions)
|
|
346
|
-
pairs = pairs[mask_label_match | mask_unmatched_predictions]
|
|
264
|
+
ids = ranked_pairs[:, (0, 1, 2, 3, 4)].astype(np.int64)
|
|
265
|
+
gts = ids[:, (0, 1, 3)]
|
|
266
|
+
gt_labels = ids[:, 3]
|
|
267
|
+
pd_labels = ids[:, 4]
|
|
268
|
+
ious = ranked_pairs[:, 5]
|
|
347
269
|
|
|
348
|
-
#
|
|
349
|
-
|
|
350
|
-
|
|
270
|
+
# set default boundary to 2.0 as it will be used to check lower boundary in range [0-1].
|
|
271
|
+
iou_boundary = np.ones_like(ious) * 2
|
|
272
|
+
|
|
273
|
+
mask_matching_labels = gt_labels == pd_labels
|
|
274
|
+
mask_valid_gts = gts[:, 1] >= 0
|
|
275
|
+
unique_gts = np.unique(gts[mask_valid_gts], axis=0)
|
|
276
|
+
for gt in unique_gts:
|
|
277
|
+
mask_gt = (gts == gt).all(axis=1)
|
|
278
|
+
mask_gt &= mask_matching_labels
|
|
279
|
+
if mask_gt.sum() <= 1:
|
|
280
|
+
iou_boundary[mask_gt] = 0.0
|
|
351
281
|
continue
|
|
352
|
-
pairs = pairs[pairs[:, 4] != label_idx]
|
|
353
282
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
283
|
+
running_max = np.maximum.accumulate(ious[mask_gt])
|
|
284
|
+
mask_rmax = np.isclose(running_max, ious[mask_gt])
|
|
285
|
+
mask_rmax[1:] &= running_max[1:] > running_max[:-1]
|
|
286
|
+
mask_gt[mask_gt] &= mask_rmax
|
|
287
|
+
|
|
288
|
+
indices = np.where(mask_gt)[0]
|
|
289
|
+
|
|
290
|
+
iou_boundary[indices[0]] = 0.0
|
|
291
|
+
iou_boundary[indices[1:]] = ious[indices[:-1]]
|
|
292
|
+
|
|
293
|
+
return iou_boundary
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def rank_table(tbl: pa.Table) -> pa.Table:
|
|
297
|
+
"""Rank table for AP computation."""
|
|
298
|
+
numeric_columns = [
|
|
299
|
+
"datum_id",
|
|
300
|
+
"gt_id",
|
|
301
|
+
"pd_id",
|
|
302
|
+
"gt_label_id",
|
|
303
|
+
"pd_label_id",
|
|
304
|
+
"iou",
|
|
305
|
+
"pd_score",
|
|
306
|
+
]
|
|
307
|
+
sorting_args = [
|
|
308
|
+
("pd_score", "descending"),
|
|
309
|
+
("iou", "descending"),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
# initial sort
|
|
313
|
+
sorted_tbl = tbl.sort_by(sorting_args)
|
|
314
|
+
pairs = np.column_stack(
|
|
315
|
+
[sorted_tbl[col].to_numpy() for col in numeric_columns]
|
|
316
|
+
)
|
|
357
317
|
|
|
358
|
-
#
|
|
359
|
-
indices =
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
318
|
+
# rank pairs
|
|
319
|
+
ranked_pairs, indices = rank_pairs(pairs)
|
|
320
|
+
ranked_tbl = sorted_tbl.take(indices)
|
|
321
|
+
|
|
322
|
+
# find boundaries
|
|
323
|
+
lower_iou_bound = calculate_ranking_boundaries(ranked_pairs)
|
|
324
|
+
ranked_tbl = ranked_tbl.append_column(
|
|
325
|
+
pa.field("iou_prev", pa.float64()),
|
|
326
|
+
pa.array(lower_iou_bound, type=pa.float64()),
|
|
364
327
|
)
|
|
365
|
-
pairs = pairs[indices]
|
|
366
328
|
|
|
367
|
-
return
|
|
329
|
+
return ranked_tbl
|
|
368
330
|
|
|
369
331
|
|
|
370
|
-
def
|
|
332
|
+
def compute_counts(
|
|
371
333
|
ranked_pairs: NDArray[np.float64],
|
|
372
|
-
label_metadata: NDArray[np.int32],
|
|
373
334
|
iou_thresholds: NDArray[np.float64],
|
|
374
335
|
score_thresholds: NDArray[np.float64],
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
tuple[
|
|
381
|
-
NDArray[np.float64],
|
|
382
|
-
NDArray[np.float64],
|
|
383
|
-
],
|
|
384
|
-
NDArray[np.float64],
|
|
385
|
-
NDArray[np.float64],
|
|
386
|
-
]:
|
|
336
|
+
number_of_groundtruths_per_label: NDArray[np.uint64],
|
|
337
|
+
number_of_labels: int,
|
|
338
|
+
running_counts: NDArray[np.uint64],
|
|
339
|
+
pr_curve: NDArray[np.float64],
|
|
340
|
+
) -> NDArray[np.uint64]:
|
|
387
341
|
"""
|
|
388
342
|
Computes Object Detection metrics.
|
|
389
343
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
Index 0 - Datum Index
|
|
393
|
-
Index 1 - GroundTruth Index
|
|
394
|
-
Index 2 - Prediction Index
|
|
395
|
-
Index 3 - IOU
|
|
396
|
-
Index 4 - GroundTruth Label Index
|
|
397
|
-
Index 5 - Prediction Label Index
|
|
398
|
-
Index 6 - Score
|
|
344
|
+
Precision-recall curve and running counts are updated in-place.
|
|
399
345
|
|
|
400
346
|
Parameters
|
|
401
347
|
----------
|
|
402
348
|
ranked_pairs : NDArray[np.float64]
|
|
403
349
|
A ranked array summarizing the IOU calculations of one or more pairs.
|
|
404
|
-
|
|
405
|
-
|
|
350
|
+
Index 0 - Datum Index
|
|
351
|
+
Index 1 - GroundTruth Index
|
|
352
|
+
Index 2 - Prediction Index
|
|
353
|
+
Index 3 - GroundTruth Label Index
|
|
354
|
+
Index 4 - Prediction Label Index
|
|
355
|
+
Index 5 - IOU
|
|
356
|
+
Index 6 - Score
|
|
357
|
+
Index 7 - IOU Lower Boundary
|
|
406
358
|
iou_thresholds : NDArray[np.float64]
|
|
407
359
|
A 1-D array containing IOU thresholds.
|
|
408
360
|
score_thresholds : NDArray[np.float64]
|
|
409
361
|
A 1-D array containing score thresholds.
|
|
362
|
+
number_of_groundtruths_per_label : NDArray[np.uint64]
|
|
363
|
+
A 1-D array containing total number of ground truths per label.
|
|
364
|
+
number_of_labels : int
|
|
365
|
+
Total number of unique labels.
|
|
366
|
+
running_counts : NDArray[np.uint64]
|
|
367
|
+
A 2-D array containing running counts of total predictions and true-positive. This array is mutated.
|
|
368
|
+
pr_curve : NDArray[np.float64]
|
|
369
|
+
A 2-D array containing 101-point binning of precision and score over a fixed recall interval. This array is mutated.
|
|
410
370
|
|
|
411
371
|
Returns
|
|
412
372
|
-------
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
tuple[NDArray[np.float64], NDArray[np.float64]]
|
|
416
|
-
Average Recall results (AR, mAR).
|
|
417
|
-
NDArray[np.float64]
|
|
418
|
-
Precision, Recall, TP, FP, FN, F1 Score.
|
|
419
|
-
NDArray[np.float64]
|
|
420
|
-
Interpolated Precision-Recall Curves.
|
|
373
|
+
NDArray[uint64]
|
|
374
|
+
Batched counts of TP, FP, FN.
|
|
421
375
|
"""
|
|
422
376
|
n_rows = ranked_pairs.shape[0]
|
|
423
|
-
n_labels =
|
|
377
|
+
n_labels = number_of_labels
|
|
424
378
|
n_ious = iou_thresholds.shape[0]
|
|
425
379
|
n_scores = score_thresholds.shape[0]
|
|
426
380
|
|
|
427
381
|
# initialize result arrays
|
|
428
|
-
|
|
429
|
-
mAP = np.zeros(n_ious, dtype=np.float64)
|
|
430
|
-
average_recall = np.zeros((n_scores, n_labels), dtype=np.float64)
|
|
431
|
-
mAR = np.zeros(n_scores, dtype=np.float64)
|
|
432
|
-
counts = np.zeros((n_ious, n_scores, n_labels, 6), dtype=np.float64)
|
|
433
|
-
pr_curve = np.zeros((n_ious, n_labels, 101, 2))
|
|
382
|
+
counts = np.zeros((n_ious, n_scores, 3, n_labels), dtype=np.uint64)
|
|
434
383
|
|
|
435
384
|
# start computation
|
|
436
|
-
ids = ranked_pairs[:, :5].astype(np.
|
|
385
|
+
ids = ranked_pairs[:, :5].astype(np.int64)
|
|
437
386
|
gt_ids = ids[:, 1]
|
|
438
387
|
gt_labels = ids[:, 3]
|
|
439
388
|
pd_labels = ids[:, 4]
|
|
440
389
|
ious = ranked_pairs[:, 5]
|
|
441
390
|
scores = ranked_pairs[:, 6]
|
|
391
|
+
prev_ious = ranked_pairs[:, 7]
|
|
392
|
+
|
|
393
|
+
unique_pd_labels, _ = np.unique(pd_labels, return_index=True)
|
|
442
394
|
|
|
443
|
-
unique_pd_labels, unique_pd_indices = np.unique(
|
|
444
|
-
pd_labels, return_index=True
|
|
445
|
-
)
|
|
446
|
-
gt_count = label_metadata[:, 0]
|
|
447
395
|
running_total_count = np.zeros(
|
|
448
396
|
(n_ious, n_rows),
|
|
449
|
-
dtype=np.
|
|
397
|
+
dtype=np.uint64,
|
|
450
398
|
)
|
|
451
399
|
running_tp_count = np.zeros_like(running_total_count)
|
|
452
|
-
running_gt_count =
|
|
400
|
+
running_gt_count = number_of_groundtruths_per_label[pd_labels]
|
|
453
401
|
|
|
454
|
-
mask_score_nonzero = scores >
|
|
402
|
+
mask_score_nonzero = scores > EPSILON
|
|
455
403
|
mask_gt_exists = gt_ids >= 0.0
|
|
456
404
|
mask_labels_match = np.isclose(gt_labels, pd_labels)
|
|
457
405
|
|
|
@@ -459,23 +407,22 @@ def compute_precion_recall(
|
|
|
459
407
|
|
|
460
408
|
mask_tp = mask_score_nonzero & mask_gt_exists_labels_match
|
|
461
409
|
mask_fp = mask_score_nonzero
|
|
462
|
-
mask_fn = mask_gt_exists_labels_match
|
|
463
410
|
|
|
464
411
|
for iou_idx in range(n_ious):
|
|
465
|
-
|
|
412
|
+
mask_iou_curr = ious >= iou_thresholds[iou_idx]
|
|
413
|
+
mask_iou_prev = prev_ious < iou_thresholds[iou_idx]
|
|
414
|
+
mask_iou = mask_iou_curr & mask_iou_prev
|
|
466
415
|
|
|
467
416
|
mask_tp_outer = mask_tp & mask_iou
|
|
468
417
|
mask_fp_outer = mask_fp & (
|
|
469
418
|
(~mask_gt_exists_labels_match & mask_iou) | ~mask_iou
|
|
470
419
|
)
|
|
471
|
-
mask_fn_outer = mask_fn & mask_iou
|
|
472
420
|
|
|
473
421
|
for score_idx in range(n_scores):
|
|
474
422
|
mask_score_thresh = scores >= score_thresholds[score_idx]
|
|
475
423
|
|
|
476
424
|
mask_tp_inner = mask_tp_outer & mask_score_thresh
|
|
477
425
|
mask_fp_inner = mask_fp_outer & mask_score_thresh
|
|
478
|
-
mask_fn_inner = mask_fn_outer & ~mask_score_thresh
|
|
479
426
|
|
|
480
427
|
# create true-positive mask score threshold
|
|
481
428
|
tp_candidates = ids[mask_tp_inner]
|
|
@@ -491,108 +438,150 @@ def compute_precion_recall(
|
|
|
491
438
|
mask_fp_inner |= mask_tp_inner & ~true_positives_mask
|
|
492
439
|
|
|
493
440
|
# calculate intermediates
|
|
494
|
-
|
|
441
|
+
counts[iou_idx, score_idx, 0, :] = np.bincount(
|
|
495
442
|
pd_labels,
|
|
496
443
|
weights=true_positives_mask,
|
|
497
444
|
minlength=n_labels,
|
|
498
|
-
)
|
|
499
|
-
|
|
445
|
+
)
|
|
446
|
+
# fp count
|
|
447
|
+
counts[iou_idx, score_idx, 1, :] = np.bincount(
|
|
500
448
|
pd_labels[mask_fp_inner],
|
|
501
449
|
minlength=n_labels,
|
|
502
|
-
).astype(np.float64)
|
|
503
|
-
fn_count = np.bincount(
|
|
504
|
-
pd_labels[mask_fn_inner],
|
|
505
|
-
minlength=n_labels,
|
|
506
450
|
)
|
|
507
451
|
|
|
508
|
-
fn_count = gt_count - tp_count
|
|
509
|
-
tp_fp_count = tp_count + fp_count
|
|
510
|
-
|
|
511
|
-
# calculate component metrics
|
|
512
|
-
recall = np.zeros_like(tp_count)
|
|
513
|
-
np.divide(tp_count, gt_count, where=gt_count > 1e-9, out=recall)
|
|
514
|
-
|
|
515
|
-
precision = np.zeros_like(tp_count)
|
|
516
|
-
np.divide(
|
|
517
|
-
tp_count, tp_fp_count, where=tp_fp_count > 1e-9, out=precision
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
f1_score = np.zeros_like(precision)
|
|
521
|
-
np.divide(
|
|
522
|
-
2 * np.multiply(precision, recall),
|
|
523
|
-
(precision + recall),
|
|
524
|
-
where=(precision + recall) > 1e-9,
|
|
525
|
-
out=f1_score,
|
|
526
|
-
dtype=np.float64,
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
counts[iou_idx][score_idx] = np.concatenate(
|
|
530
|
-
(
|
|
531
|
-
tp_count[:, np.newaxis],
|
|
532
|
-
fp_count[:, np.newaxis],
|
|
533
|
-
fn_count[:, np.newaxis],
|
|
534
|
-
precision[:, np.newaxis],
|
|
535
|
-
recall[:, np.newaxis],
|
|
536
|
-
f1_score[:, np.newaxis],
|
|
537
|
-
),
|
|
538
|
-
axis=1,
|
|
539
|
-
)
|
|
540
|
-
|
|
541
|
-
# calculate recall for AR
|
|
542
|
-
average_recall[score_idx] += recall
|
|
543
|
-
|
|
544
|
-
# create true-positive mask score threshold
|
|
545
|
-
tp_candidates = ids[mask_tp_outer]
|
|
546
|
-
_, indices_gt_unique = np.unique(
|
|
547
|
-
tp_candidates[:, [0, 1, 3]], axis=0, return_index=True
|
|
548
|
-
)
|
|
549
|
-
mask_gt_unique = np.zeros(tp_candidates.shape[0], dtype=np.bool_)
|
|
550
|
-
mask_gt_unique[indices_gt_unique] = True
|
|
551
|
-
true_positives_mask = np.zeros(n_rows, dtype=np.bool_)
|
|
552
|
-
true_positives_mask[mask_tp_outer] = mask_gt_unique
|
|
553
|
-
|
|
554
452
|
# count running tp and total for AP
|
|
555
453
|
for pd_label in unique_pd_labels:
|
|
556
454
|
mask_pd_label = pd_labels == pd_label
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
455
|
+
total_count = mask_pd_label.sum()
|
|
456
|
+
if total_count == 0:
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
# running total prediction count
|
|
460
|
+
running_total_count[iou_idx, mask_pd_label] = np.arange(
|
|
461
|
+
running_counts[iou_idx, pd_label, 0] + 1,
|
|
462
|
+
running_counts[iou_idx, pd_label, 0] + total_count + 1,
|
|
560
463
|
)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
464
|
+
running_counts[iou_idx, pd_label, 0] += total_count
|
|
465
|
+
|
|
466
|
+
# running true-positive count
|
|
467
|
+
mask_tp_for_counting = mask_pd_label & mask_tp_outer
|
|
468
|
+
tp_count = mask_tp_for_counting.sum()
|
|
469
|
+
running_tp_count[iou_idx, mask_tp_for_counting] = np.arange(
|
|
470
|
+
running_counts[iou_idx, pd_label, 1] + 1,
|
|
471
|
+
running_counts[iou_idx, pd_label, 1] + tp_count + 1,
|
|
564
472
|
)
|
|
473
|
+
running_counts[iou_idx, pd_label, 1] += tp_count
|
|
565
474
|
|
|
566
475
|
# calculate running precision-recall points for AP
|
|
567
|
-
precision = np.zeros_like(running_total_count)
|
|
476
|
+
precision = np.zeros_like(running_total_count, dtype=np.float64)
|
|
568
477
|
np.divide(
|
|
569
478
|
running_tp_count,
|
|
570
479
|
running_total_count,
|
|
571
|
-
where=running_total_count >
|
|
480
|
+
where=running_total_count > 0,
|
|
572
481
|
out=precision,
|
|
573
482
|
)
|
|
574
|
-
recall = np.zeros_like(running_total_count)
|
|
483
|
+
recall = np.zeros_like(running_total_count, dtype=np.float64)
|
|
575
484
|
np.divide(
|
|
576
485
|
running_tp_count,
|
|
577
486
|
running_gt_count,
|
|
578
|
-
where=running_gt_count >
|
|
487
|
+
where=running_gt_count > 0,
|
|
579
488
|
out=recall,
|
|
580
489
|
)
|
|
581
490
|
recall_index = np.floor(recall * 100.0).astype(np.int32)
|
|
582
491
|
|
|
583
|
-
#
|
|
492
|
+
# sort precision in descending order
|
|
493
|
+
precision_indices = np.argsort(-precision, axis=1)
|
|
494
|
+
|
|
495
|
+
# populate precision-recall curve
|
|
584
496
|
for iou_idx in range(n_ious):
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
497
|
+
labeled_recall = np.hstack(
|
|
498
|
+
[
|
|
499
|
+
pd_labels.reshape(-1, 1),
|
|
500
|
+
recall_index[iou_idx, :].reshape(-1, 1),
|
|
501
|
+
]
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# extract maximum score per (label, recall) bin
|
|
505
|
+
# arrays are already ordered by descending score
|
|
506
|
+
lr_pairs, recall_indices = np.unique(
|
|
507
|
+
labeled_recall, return_index=True, axis=0
|
|
590
508
|
)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
509
|
+
li = lr_pairs[:, 0]
|
|
510
|
+
ri = lr_pairs[:, 1]
|
|
511
|
+
pr_curve[iou_idx, li, ri, 1] = np.maximum(
|
|
512
|
+
pr_curve[iou_idx, li, ri, 1],
|
|
513
|
+
scores[recall_indices],
|
|
594
514
|
)
|
|
595
515
|
|
|
516
|
+
# extract maximum precision per (label, recall) bin
|
|
517
|
+
# reorder arrays into descending precision order
|
|
518
|
+
indices = precision_indices[iou_idx]
|
|
519
|
+
sorted_precision = precision[iou_idx, indices]
|
|
520
|
+
sorted_labeled_recall = labeled_recall[indices]
|
|
521
|
+
lr_pairs, recall_indices = np.unique(
|
|
522
|
+
sorted_labeled_recall, return_index=True, axis=0
|
|
523
|
+
)
|
|
524
|
+
li = lr_pairs[:, 0]
|
|
525
|
+
ri = lr_pairs[:, 1]
|
|
526
|
+
pr_curve[iou_idx, li, ri, 0] = np.maximum(
|
|
527
|
+
pr_curve[iou_idx, li, ri, 0],
|
|
528
|
+
sorted_precision[recall_indices],
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return counts
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def compute_precision_recall_f1(
|
|
535
|
+
counts: NDArray[np.uint64],
|
|
536
|
+
number_of_groundtruths_per_label: NDArray[np.uint64],
|
|
537
|
+
) -> NDArray[np.float64]:
|
|
538
|
+
|
|
539
|
+
prec_rec_f1 = np.zeros_like(counts, dtype=np.float64)
|
|
540
|
+
|
|
541
|
+
# alias
|
|
542
|
+
tp_count = counts[:, :, 0, :]
|
|
543
|
+
fp_count = counts[:, :, 1, :]
|
|
544
|
+
tp_fp_count = tp_count + fp_count
|
|
545
|
+
|
|
546
|
+
# calculate component metrics
|
|
547
|
+
np.divide(
|
|
548
|
+
tp_count,
|
|
549
|
+
tp_fp_count,
|
|
550
|
+
where=tp_fp_count > 0,
|
|
551
|
+
out=prec_rec_f1[:, :, 0, :],
|
|
552
|
+
)
|
|
553
|
+
np.divide(
|
|
554
|
+
tp_count,
|
|
555
|
+
number_of_groundtruths_per_label,
|
|
556
|
+
where=number_of_groundtruths_per_label > 0,
|
|
557
|
+
out=prec_rec_f1[:, :, 1, :],
|
|
558
|
+
)
|
|
559
|
+
p = prec_rec_f1[:, :, 0, :]
|
|
560
|
+
r = prec_rec_f1[:, :, 1, :]
|
|
561
|
+
np.divide(
|
|
562
|
+
2 * np.multiply(p, r),
|
|
563
|
+
(p + r),
|
|
564
|
+
where=(p + r) > EPSILON,
|
|
565
|
+
out=prec_rec_f1[:, :, 2, :],
|
|
566
|
+
)
|
|
567
|
+
return prec_rec_f1
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def compute_average_recall(prec_rec_f1: NDArray[np.float64]):
|
|
571
|
+
recall = prec_rec_f1[:, :, 1, :]
|
|
572
|
+
average_recall = recall.mean(axis=0)
|
|
573
|
+
mAR = average_recall.mean(axis=-1)
|
|
574
|
+
return average_recall, mAR
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def compute_average_precision(pr_curve: NDArray[np.float64]):
|
|
578
|
+
n_ious = pr_curve.shape[0]
|
|
579
|
+
n_labels = pr_curve.shape[1]
|
|
580
|
+
|
|
581
|
+
# initialize result arrays
|
|
582
|
+
average_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
|
|
583
|
+
mAP = np.zeros(n_ious, dtype=np.float64)
|
|
584
|
+
|
|
596
585
|
# calculate average precision
|
|
597
586
|
running_max_precision = np.zeros((n_ious, n_labels), dtype=np.float64)
|
|
598
587
|
running_max_score = np.zeros((n_labels), dtype=np.float64)
|
|
@@ -616,24 +605,11 @@ def compute_precion_recall(
|
|
|
616
605
|
|
|
617
606
|
average_precision = average_precision / 101.0
|
|
618
607
|
|
|
619
|
-
# calculate average recall
|
|
620
|
-
average_recall = average_recall / n_ious
|
|
621
|
-
|
|
622
608
|
# calculate mAP and mAR
|
|
623
|
-
if
|
|
624
|
-
mAP
|
|
625
|
-
axis=1
|
|
626
|
-
)
|
|
627
|
-
mAR: NDArray[np.float64] = average_recall[:, unique_pd_labels].mean(
|
|
628
|
-
axis=1
|
|
629
|
-
)
|
|
609
|
+
if average_precision.size > 0:
|
|
610
|
+
mAP = average_precision.mean(axis=1)
|
|
630
611
|
|
|
631
|
-
return
|
|
632
|
-
(average_precision.astype(np.float64), mAP),
|
|
633
|
-
(average_recall, mAR),
|
|
634
|
-
counts,
|
|
635
|
-
pr_curve,
|
|
636
|
-
)
|
|
612
|
+
return average_precision, mAP, pr_curve
|
|
637
613
|
|
|
638
614
|
|
|
639
615
|
def _isin(
|
|
@@ -666,6 +642,7 @@ def _isin(
|
|
|
666
642
|
|
|
667
643
|
|
|
668
644
|
class PairClassification(IntFlag):
|
|
645
|
+
NULL = auto()
|
|
669
646
|
TP = auto()
|
|
670
647
|
FP_FN_MISCLF = auto()
|
|
671
648
|
FP_UNMATCHED = auto()
|
|
@@ -704,11 +681,13 @@ def mask_pairs_greedily(
|
|
|
704
681
|
return mask_matches
|
|
705
682
|
|
|
706
683
|
|
|
707
|
-
def
|
|
684
|
+
def compute_pair_classifications(
|
|
708
685
|
detailed_pairs: NDArray[np.float64],
|
|
709
686
|
iou_thresholds: NDArray[np.float64],
|
|
710
687
|
score_thresholds: NDArray[np.float64],
|
|
711
|
-
) ->
|
|
688
|
+
) -> tuple[
|
|
689
|
+
NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_], NDArray[np.bool_]
|
|
690
|
+
]:
|
|
712
691
|
"""
|
|
713
692
|
Compute detailed counts.
|
|
714
693
|
|
|
@@ -760,8 +739,8 @@ def compute_confusion_matrix(
|
|
|
760
739
|
mask_gt_exists = gt_ids > -0.5
|
|
761
740
|
mask_pd_exists = pd_ids > -0.5
|
|
762
741
|
mask_label_match = np.isclose(gt_labels, pd_labels)
|
|
763
|
-
mask_score_nonzero = scores >
|
|
764
|
-
mask_iou_nonzero = ious >
|
|
742
|
+
mask_score_nonzero = scores > EPSILON
|
|
743
|
+
mask_iou_nonzero = ious > EPSILON
|
|
765
744
|
|
|
766
745
|
mask_gt_pd_exists = mask_gt_exists & mask_pd_exists
|
|
767
746
|
mask_gt_pd_match = mask_gt_pd_exists & mask_label_match
|
|
@@ -821,4 +800,88 @@ def compute_confusion_matrix(
|
|
|
821
800
|
iou_idx, score_idx, mask_unmatched_groundtruths
|
|
822
801
|
] |= np.uint8(PairClassification.FN_UNMATCHED)
|
|
823
802
|
|
|
824
|
-
|
|
803
|
+
mask_tp = np.bitwise_and(pair_classifications, PairClassification.TP) > 0
|
|
804
|
+
mask_fp_fn_misclf = (
|
|
805
|
+
np.bitwise_and(pair_classifications, PairClassification.FP_FN_MISCLF)
|
|
806
|
+
> 0
|
|
807
|
+
)
|
|
808
|
+
mask_fp_unmatched = (
|
|
809
|
+
np.bitwise_and(pair_classifications, PairClassification.FP_UNMATCHED)
|
|
810
|
+
> 0
|
|
811
|
+
)
|
|
812
|
+
mask_fn_unmatched = (
|
|
813
|
+
np.bitwise_and(pair_classifications, PairClassification.FN_UNMATCHED)
|
|
814
|
+
> 0
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
mask_tp,
|
|
819
|
+
mask_fp_fn_misclf,
|
|
820
|
+
mask_fp_unmatched,
|
|
821
|
+
mask_fn_unmatched,
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def compute_confusion_matrix(
|
|
826
|
+
detailed_pairs: NDArray[np.float64],
|
|
827
|
+
mask_tp: NDArray[np.bool_],
|
|
828
|
+
mask_fp_fn_misclf: NDArray[np.bool_],
|
|
829
|
+
mask_fp_unmatched: NDArray[np.bool_],
|
|
830
|
+
mask_fn_unmatched: NDArray[np.bool_],
|
|
831
|
+
number_of_labels: int,
|
|
832
|
+
iou_thresholds: NDArray[np.float64],
|
|
833
|
+
score_thresholds: NDArray[np.float64],
|
|
834
|
+
):
|
|
835
|
+
n_ious = iou_thresholds.size
|
|
836
|
+
n_scores = score_thresholds.size
|
|
837
|
+
ids = detailed_pairs[:, :5].astype(np.int64)
|
|
838
|
+
|
|
839
|
+
# initialize arrays
|
|
840
|
+
confusion_matrices = np.zeros(
|
|
841
|
+
(n_ious, n_scores, number_of_labels, number_of_labels), dtype=np.uint64
|
|
842
|
+
)
|
|
843
|
+
unmatched_groundtruths = np.zeros(
|
|
844
|
+
(n_ious, n_scores, number_of_labels), dtype=np.uint64
|
|
845
|
+
)
|
|
846
|
+
unmatched_predictions = np.zeros_like(unmatched_groundtruths)
|
|
847
|
+
|
|
848
|
+
mask_matched = mask_tp | mask_fp_fn_misclf
|
|
849
|
+
for iou_idx in range(n_ious):
|
|
850
|
+
for score_idx in range(n_scores):
|
|
851
|
+
# matched annotations
|
|
852
|
+
unique_pairs = np.unique(
|
|
853
|
+
ids[np.ix_(mask_matched[iou_idx, score_idx], (0, 1, 2, 3, 4))], # type: ignore - numpy ix_ typing
|
|
854
|
+
axis=0,
|
|
855
|
+
)
|
|
856
|
+
unique_labels, unique_label_counts = np.unique(
|
|
857
|
+
unique_pairs[:, (3, 4)], axis=0, return_counts=True
|
|
858
|
+
)
|
|
859
|
+
confusion_matrices[
|
|
860
|
+
iou_idx, score_idx, unique_labels[:, 0], unique_labels[:, 1]
|
|
861
|
+
] = unique_label_counts
|
|
862
|
+
|
|
863
|
+
# unmatched groundtruths
|
|
864
|
+
unique_pairs = np.unique(
|
|
865
|
+
ids[np.ix_(mask_fn_unmatched[iou_idx, score_idx], (0, 1, 3))], # type: ignore - numpy ix_ typing
|
|
866
|
+
axis=0,
|
|
867
|
+
)
|
|
868
|
+
unique_labels, unique_label_counts = np.unique(
|
|
869
|
+
unique_pairs[:, 2], return_counts=True
|
|
870
|
+
)
|
|
871
|
+
unmatched_groundtruths[
|
|
872
|
+
iou_idx, score_idx, unique_labels
|
|
873
|
+
] = unique_label_counts
|
|
874
|
+
|
|
875
|
+
# unmatched predictions
|
|
876
|
+
unique_pairs = np.unique(
|
|
877
|
+
ids[np.ix_(mask_fp_unmatched[iou_idx, score_idx], (0, 2, 4))], # type: ignore - numpy ix_ typing
|
|
878
|
+
axis=0,
|
|
879
|
+
)
|
|
880
|
+
unique_labels, unique_label_counts = np.unique(
|
|
881
|
+
unique_pairs[:, 2], return_counts=True
|
|
882
|
+
)
|
|
883
|
+
unmatched_predictions[
|
|
884
|
+
iou_idx, score_idx, unique_labels
|
|
885
|
+
] = unique_label_counts
|
|
886
|
+
|
|
887
|
+
return confusion_matrices, unmatched_groundtruths, unmatched_predictions
|