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