eye-cv 1.0.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.
- eye/__init__.py +115 -0
- eye/__init___supervision_original.py +120 -0
- eye/annotators/__init__.py +0 -0
- eye/annotators/base.py +22 -0
- eye/annotators/core.py +2699 -0
- eye/annotators/line.py +107 -0
- eye/annotators/modern.py +529 -0
- eye/annotators/trace.py +142 -0
- eye/annotators/utils.py +177 -0
- eye/assets/__init__.py +2 -0
- eye/assets/downloader.py +95 -0
- eye/assets/list.py +83 -0
- eye/classification/__init__.py +0 -0
- eye/classification/core.py +188 -0
- eye/config.py +2 -0
- eye/core/__init__.py +0 -0
- eye/core/trackers/__init__.py +1 -0
- eye/core/trackers/botsort_tracker.py +336 -0
- eye/core/trackers/bytetrack_tracker.py +284 -0
- eye/core/trackers/sort_tracker.py +200 -0
- eye/core/tracking.py +146 -0
- eye/dataset/__init__.py +0 -0
- eye/dataset/core.py +919 -0
- eye/dataset/formats/__init__.py +0 -0
- eye/dataset/formats/coco.py +258 -0
- eye/dataset/formats/pascal_voc.py +279 -0
- eye/dataset/formats/yolo.py +272 -0
- eye/dataset/utils.py +259 -0
- eye/detection/__init__.py +0 -0
- eye/detection/auto_convert.py +155 -0
- eye/detection/core.py +1529 -0
- eye/detection/detections_enhanced.py +392 -0
- eye/detection/line_zone.py +859 -0
- eye/detection/lmm.py +184 -0
- eye/detection/overlap_filter.py +270 -0
- eye/detection/tools/__init__.py +0 -0
- eye/detection/tools/csv_sink.py +181 -0
- eye/detection/tools/inference_slicer.py +288 -0
- eye/detection/tools/json_sink.py +142 -0
- eye/detection/tools/polygon_zone.py +202 -0
- eye/detection/tools/smoother.py +123 -0
- eye/detection/tools/smoothing.py +179 -0
- eye/detection/tools/smoothing_config.py +202 -0
- eye/detection/tools/transformers.py +247 -0
- eye/detection/utils.py +1175 -0
- eye/draw/__init__.py +0 -0
- eye/draw/color.py +154 -0
- eye/draw/utils.py +374 -0
- eye/filters.py +112 -0
- eye/geometry/__init__.py +0 -0
- eye/geometry/core.py +128 -0
- eye/geometry/utils.py +47 -0
- eye/keypoint/__init__.py +0 -0
- eye/keypoint/annotators.py +442 -0
- eye/keypoint/core.py +687 -0
- eye/keypoint/skeletons.py +2647 -0
- eye/metrics/__init__.py +21 -0
- eye/metrics/core.py +72 -0
- eye/metrics/detection.py +843 -0
- eye/metrics/f1_score.py +648 -0
- eye/metrics/mean_average_precision.py +628 -0
- eye/metrics/mean_average_recall.py +697 -0
- eye/metrics/precision.py +653 -0
- eye/metrics/recall.py +652 -0
- eye/metrics/utils/__init__.py +0 -0
- eye/metrics/utils/object_size.py +158 -0
- eye/metrics/utils/utils.py +9 -0
- eye/py.typed +0 -0
- eye/quick.py +104 -0
- eye/tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/__init__.py +0 -0
- eye/tracker/byte_tracker/core.py +386 -0
- eye/tracker/byte_tracker/kalman_filter.py +205 -0
- eye/tracker/byte_tracker/matching.py +69 -0
- eye/tracker/byte_tracker/single_object_track.py +178 -0
- eye/tracker/byte_tracker/utils.py +18 -0
- eye/utils/__init__.py +0 -0
- eye/utils/conversion.py +132 -0
- eye/utils/file.py +159 -0
- eye/utils/image.py +794 -0
- eye/utils/internal.py +200 -0
- eye/utils/iterables.py +84 -0
- eye/utils/notebook.py +114 -0
- eye/utils/video.py +307 -0
- eye/utils_eye/__init__.py +1 -0
- eye/utils_eye/geometry.py +71 -0
- eye/utils_eye/nms.py +55 -0
- eye/validators/__init__.py +140 -0
- eye/web.py +271 -0
- eye_cv-1.0.0.dist-info/METADATA +319 -0
- eye_cv-1.0.0.dist-info/RECORD +94 -0
- eye_cv-1.0.0.dist-info/WHEEL +5 -0
- eye_cv-1.0.0.dist-info/licenses/LICENSE +21 -0
- eye_cv-1.0.0.dist-info/top_level.txt +1 -0
eye/metrics/detection.py
ADDED
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import matplotlib
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from eye.dataset.core import DetectionDataset
|
|
11
|
+
from eye.detection.core import Detections
|
|
12
|
+
from eye.detection.utils import box_iou_batch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detections_to_tensor(
|
|
16
|
+
detections: Detections, with_confidence: bool = False
|
|
17
|
+
) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Convert eye Detections to numpy tensors for further computation
|
|
20
|
+
Args:
|
|
21
|
+
detections (sv.Detections): Detections/Targets in the format of sv.Detections
|
|
22
|
+
with_confidence (bool): Whether to include confidence in the tensor
|
|
23
|
+
Returns:
|
|
24
|
+
(np.ndarray): Detections as numpy tensors as in (xyxy, class_id,
|
|
25
|
+
confidence) order
|
|
26
|
+
"""
|
|
27
|
+
if detections.class_id is None:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"ConfusionMatrix can only be calculated for Detections with class_id"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
arrays_to_concat = [detections.xyxy, np.expand_dims(detections.class_id, 1)]
|
|
33
|
+
|
|
34
|
+
if with_confidence:
|
|
35
|
+
if detections.confidence is None:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"ConfusionMatrix can only be calculated for Detections with confidence"
|
|
38
|
+
)
|
|
39
|
+
arrays_to_concat.append(np.expand_dims(detections.confidence, 1))
|
|
40
|
+
|
|
41
|
+
return np.concatenate(arrays_to_concat, axis=1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def validate_input_tensors(predictions: List[np.ndarray], targets: List[np.ndarray]):
|
|
45
|
+
"""
|
|
46
|
+
Checks for shape consistency of input tensors.
|
|
47
|
+
"""
|
|
48
|
+
if len(predictions) != len(targets):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Number of predictions ({len(predictions)}) and"
|
|
51
|
+
f"targets ({len(targets)}) must be equal."
|
|
52
|
+
)
|
|
53
|
+
if len(predictions) > 0:
|
|
54
|
+
if not isinstance(predictions[0], np.ndarray) or not isinstance(
|
|
55
|
+
targets[0], np.ndarray
|
|
56
|
+
):
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Predictions and targets must be lists of numpy arrays."
|
|
59
|
+
f"Got {type(predictions[0])} and {type(targets[0])} instead."
|
|
60
|
+
)
|
|
61
|
+
if predictions[0].shape[1] != 6:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Predictions must have shape (N, 6)."
|
|
64
|
+
f"Got {predictions[0].shape} instead."
|
|
65
|
+
)
|
|
66
|
+
if targets[0].shape[1] != 5:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Targets must have shape (N, 5). Got {targets[0].shape} instead."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ConfusionMatrix:
|
|
74
|
+
"""
|
|
75
|
+
Confusion matrix for object detection tasks.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
matrix (np.ndarray): An 2D `np.ndarray` of shape
|
|
79
|
+
`(len(classes) + 1, len(classes) + 1)`
|
|
80
|
+
containing the number of `TP`, `FP`, `FN` and `TN` for each class.
|
|
81
|
+
classes (List[str]): Model class names.
|
|
82
|
+
conf_threshold (float): Detection confidence threshold between `0` and `1`.
|
|
83
|
+
Detections with lower confidence will be excluded from the matrix.
|
|
84
|
+
iou_threshold (float): Detection IoU threshold between `0` and `1`.
|
|
85
|
+
Detections with lower IoU will be classified as `FP`.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
matrix: np.ndarray
|
|
89
|
+
classes: List[str]
|
|
90
|
+
conf_threshold: float
|
|
91
|
+
iou_threshold: float
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_detections(
|
|
95
|
+
cls,
|
|
96
|
+
predictions: List[Detections],
|
|
97
|
+
targets: List[Detections],
|
|
98
|
+
classes: List[str],
|
|
99
|
+
conf_threshold: float = 0.3,
|
|
100
|
+
iou_threshold: float = 0.5,
|
|
101
|
+
) -> ConfusionMatrix:
|
|
102
|
+
"""
|
|
103
|
+
Calculate confusion matrix based on predicted and ground-truth detections.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
targets (List[Detections]): Detections objects from ground-truth.
|
|
107
|
+
predictions (List[Detections]): Detections objects predicted by the model.
|
|
108
|
+
classes (List[str]): Model class names.
|
|
109
|
+
conf_threshold (float): Detection confidence threshold between `0` and `1`.
|
|
110
|
+
Detections with lower confidence will be excluded.
|
|
111
|
+
iou_threshold (float): Detection IoU threshold between `0` and `1`.
|
|
112
|
+
Detections with lower IoU will be classified as `FP`.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
ConfusionMatrix: New instance of ConfusionMatrix.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
```python
|
|
119
|
+
import eye as sv
|
|
120
|
+
|
|
121
|
+
targets = [
|
|
122
|
+
sv.Detections(...),
|
|
123
|
+
sv.Detections(...)
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
predictions = [
|
|
127
|
+
sv.Detections(...),
|
|
128
|
+
sv.Detections(...)
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
confusion_matrix = sv.ConfusionMatrix.from_detections(
|
|
132
|
+
predictions=predictions,
|
|
133
|
+
targets=target,
|
|
134
|
+
classes=['person', ...]
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
print(confusion_matrix.matrix)
|
|
138
|
+
# np.array([
|
|
139
|
+
# [0., 0., 0., 0.],
|
|
140
|
+
# [0., 1., 0., 1.],
|
|
141
|
+
# [0., 1., 1., 0.],
|
|
142
|
+
# [1., 1., 0., 0.]
|
|
143
|
+
# ])
|
|
144
|
+
```
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
prediction_tensors = []
|
|
148
|
+
target_tensors = []
|
|
149
|
+
for prediction, target in zip(predictions, targets):
|
|
150
|
+
prediction_tensors.append(
|
|
151
|
+
detections_to_tensor(prediction, with_confidence=True)
|
|
152
|
+
)
|
|
153
|
+
target_tensors.append(detections_to_tensor(target, with_confidence=False))
|
|
154
|
+
return cls.from_tensors(
|
|
155
|
+
predictions=prediction_tensors,
|
|
156
|
+
targets=target_tensors,
|
|
157
|
+
classes=classes,
|
|
158
|
+
conf_threshold=conf_threshold,
|
|
159
|
+
iou_threshold=iou_threshold,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_tensors(
|
|
164
|
+
cls,
|
|
165
|
+
predictions: List[np.ndarray],
|
|
166
|
+
targets: List[np.ndarray],
|
|
167
|
+
classes: List[str],
|
|
168
|
+
conf_threshold: float = 0.3,
|
|
169
|
+
iou_threshold: float = 0.5,
|
|
170
|
+
) -> ConfusionMatrix:
|
|
171
|
+
"""
|
|
172
|
+
Calculate confusion matrix based on predicted and ground-truth detections.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
predictions (List[np.ndarray]): Each element of the list describes a single
|
|
176
|
+
image and has `shape = (M, 6)` where `M` is the number of detected
|
|
177
|
+
objects. Each row is expected to be in
|
|
178
|
+
`(x_min, y_min, x_max, y_max, class, conf)` format.
|
|
179
|
+
targets (List[np.ndarray]): Each element of the list describes a single
|
|
180
|
+
image and has `shape = (N, 5)` where `N` is the number of
|
|
181
|
+
ground-truth objects. Each row is expected to be in
|
|
182
|
+
`(x_min, y_min, x_max, y_max, class)` format.
|
|
183
|
+
classes (List[str]): Model class names.
|
|
184
|
+
conf_threshold (float): Detection confidence threshold between `0` and `1`.
|
|
185
|
+
Detections with lower confidence will be excluded.
|
|
186
|
+
iou_threshold (float): Detection iou threshold between `0` and `1`.
|
|
187
|
+
Detections with lower iou will be classified as `FP`.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
ConfusionMatrix: New instance of ConfusionMatrix.
|
|
191
|
+
|
|
192
|
+
Example:
|
|
193
|
+
```python
|
|
194
|
+
import eye as sv
|
|
195
|
+
import numpy as np
|
|
196
|
+
|
|
197
|
+
targets = (
|
|
198
|
+
[
|
|
199
|
+
np.array(
|
|
200
|
+
[
|
|
201
|
+
[0.0, 0.0, 3.0, 3.0, 1],
|
|
202
|
+
[2.0, 2.0, 5.0, 5.0, 1],
|
|
203
|
+
[6.0, 1.0, 8.0, 3.0, 2],
|
|
204
|
+
]
|
|
205
|
+
),
|
|
206
|
+
np.array([1.0, 1.0, 2.0, 2.0, 2]),
|
|
207
|
+
]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
predictions = [
|
|
211
|
+
np.array(
|
|
212
|
+
[
|
|
213
|
+
[0.0, 0.0, 3.0, 3.0, 1, 0.9],
|
|
214
|
+
[0.1, 0.1, 3.0, 3.0, 0, 0.9],
|
|
215
|
+
[6.0, 1.0, 8.0, 3.0, 1, 0.8],
|
|
216
|
+
[1.0, 6.0, 2.0, 7.0, 1, 0.8],
|
|
217
|
+
]
|
|
218
|
+
),
|
|
219
|
+
np.array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
confusion_matrix = sv.ConfusionMatrix.from_tensors(
|
|
223
|
+
predictions=predictions,
|
|
224
|
+
targets=targets,
|
|
225
|
+
classes=['person', ...]
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
print(confusion_matrix.matrix)
|
|
229
|
+
# np.array([
|
|
230
|
+
# [0., 0., 0., 0.],
|
|
231
|
+
# [0., 1., 0., 1.],
|
|
232
|
+
# [0., 1., 1., 0.],
|
|
233
|
+
# [1., 1., 0., 0.]
|
|
234
|
+
# ])
|
|
235
|
+
```
|
|
236
|
+
"""
|
|
237
|
+
validate_input_tensors(predictions, targets)
|
|
238
|
+
|
|
239
|
+
num_classes = len(classes)
|
|
240
|
+
matrix = np.zeros((num_classes + 1, num_classes + 1))
|
|
241
|
+
for true_batch, detection_batch in zip(targets, predictions):
|
|
242
|
+
matrix += cls.evaluate_detection_batch(
|
|
243
|
+
predictions=detection_batch,
|
|
244
|
+
targets=true_batch,
|
|
245
|
+
num_classes=num_classes,
|
|
246
|
+
conf_threshold=conf_threshold,
|
|
247
|
+
iou_threshold=iou_threshold,
|
|
248
|
+
)
|
|
249
|
+
return cls(
|
|
250
|
+
matrix=matrix,
|
|
251
|
+
classes=classes,
|
|
252
|
+
conf_threshold=conf_threshold,
|
|
253
|
+
iou_threshold=iou_threshold,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def evaluate_detection_batch(
|
|
258
|
+
predictions: np.ndarray,
|
|
259
|
+
targets: np.ndarray,
|
|
260
|
+
num_classes: int,
|
|
261
|
+
conf_threshold: float,
|
|
262
|
+
iou_threshold: float,
|
|
263
|
+
) -> np.ndarray:
|
|
264
|
+
"""
|
|
265
|
+
Calculate confusion matrix for a batch of detections for a single image.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
predictions (np.ndarray): Batch prediction. Describes a single image and
|
|
269
|
+
has `shape = (M, 6)` where `M` is the number of detected objects.
|
|
270
|
+
Each row is expected to be in
|
|
271
|
+
`(x_min, y_min, x_max, y_max, class, conf)` format.
|
|
272
|
+
targets (np.ndarray): Batch target labels. Describes a single image and
|
|
273
|
+
has `shape = (N, 5)` where `N` is the number of ground-truth objects.
|
|
274
|
+
Each row is expected to be in
|
|
275
|
+
`(x_min, y_min, x_max, y_max, class)` format.
|
|
276
|
+
num_classes (int): Number of classes.
|
|
277
|
+
conf_threshold (float): Detection confidence threshold between `0` and `1`.
|
|
278
|
+
Detections with lower confidence will be excluded.
|
|
279
|
+
iou_threshold (float): Detection iou threshold between `0` and `1`.
|
|
280
|
+
Detections with lower iou will be classified as `FP`.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
np.ndarray: Confusion matrix based on a single image.
|
|
284
|
+
"""
|
|
285
|
+
result_matrix = np.zeros((num_classes + 1, num_classes + 1))
|
|
286
|
+
|
|
287
|
+
conf_idx = 5
|
|
288
|
+
confidence = predictions[:, conf_idx]
|
|
289
|
+
detection_batch_filtered = predictions[confidence > conf_threshold]
|
|
290
|
+
|
|
291
|
+
class_id_idx = 4
|
|
292
|
+
true_classes = np.array(targets[:, class_id_idx], dtype=np.int16)
|
|
293
|
+
detection_classes = np.array(
|
|
294
|
+
detection_batch_filtered[:, class_id_idx], dtype=np.int16
|
|
295
|
+
)
|
|
296
|
+
true_boxes = targets[:, :class_id_idx]
|
|
297
|
+
detection_boxes = detection_batch_filtered[:, :class_id_idx]
|
|
298
|
+
|
|
299
|
+
iou_batch = box_iou_batch(
|
|
300
|
+
boxes_true=true_boxes, boxes_detection=detection_boxes
|
|
301
|
+
)
|
|
302
|
+
matched_idx = np.asarray(iou_batch > iou_threshold).nonzero()
|
|
303
|
+
|
|
304
|
+
if matched_idx[0].shape[0]:
|
|
305
|
+
matches = np.stack(
|
|
306
|
+
(matched_idx[0], matched_idx[1], iou_batch[matched_idx]), axis=1
|
|
307
|
+
)
|
|
308
|
+
matches = ConfusionMatrix._drop_extra_matches(matches=matches)
|
|
309
|
+
else:
|
|
310
|
+
matches = np.zeros((0, 3))
|
|
311
|
+
|
|
312
|
+
matched_true_idx, matched_detection_idx, _ = matches.transpose().astype(
|
|
313
|
+
np.int16
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
for i, true_class_value in enumerate(true_classes):
|
|
317
|
+
j = matched_true_idx == i
|
|
318
|
+
if matches.shape[0] > 0 and sum(j) == 1:
|
|
319
|
+
result_matrix[
|
|
320
|
+
true_class_value, detection_classes[matched_detection_idx[j]]
|
|
321
|
+
] += 1 # TP
|
|
322
|
+
else:
|
|
323
|
+
result_matrix[true_class_value, num_classes] += 1 # FN
|
|
324
|
+
|
|
325
|
+
for i, detection_class_value in enumerate(detection_classes):
|
|
326
|
+
if not any(matched_detection_idx == i):
|
|
327
|
+
result_matrix[num_classes, detection_class_value] += 1 # FP
|
|
328
|
+
|
|
329
|
+
return result_matrix
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _drop_extra_matches(matches: np.ndarray) -> np.ndarray:
|
|
333
|
+
"""
|
|
334
|
+
Deduplicate matches. If there are multiple matches for the same true or
|
|
335
|
+
predicted box, only the one with the highest IoU is kept.
|
|
336
|
+
"""
|
|
337
|
+
if matches.shape[0] > 0:
|
|
338
|
+
matches = matches[matches[:, 2].argsort()[::-1]]
|
|
339
|
+
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
|
340
|
+
matches = matches[matches[:, 2].argsort()[::-1]]
|
|
341
|
+
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
|
342
|
+
return matches
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def benchmark(
|
|
346
|
+
cls,
|
|
347
|
+
dataset: DetectionDataset,
|
|
348
|
+
callback: Callable[[np.ndarray], Detections],
|
|
349
|
+
conf_threshold: float = 0.3,
|
|
350
|
+
iou_threshold: float = 0.5,
|
|
351
|
+
) -> ConfusionMatrix:
|
|
352
|
+
"""
|
|
353
|
+
Calculate confusion matrix from dataset and callback function.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
dataset (DetectionDataset): Object detection dataset used for evaluation.
|
|
357
|
+
callback (Callable[[np.ndarray], Detections]): Function that takes an image
|
|
358
|
+
as input and returns Detections object.
|
|
359
|
+
conf_threshold (float): Detection confidence threshold between `0` and `1`.
|
|
360
|
+
Detections with lower confidence will be excluded.
|
|
361
|
+
iou_threshold (float): Detection IoU threshold between `0` and `1`.
|
|
362
|
+
Detections with lower IoU will be classified as `FP`.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
ConfusionMatrix: New instance of ConfusionMatrix.
|
|
366
|
+
|
|
367
|
+
Example:
|
|
368
|
+
```python
|
|
369
|
+
import eye as sv
|
|
370
|
+
from ultralytics import YOLO
|
|
371
|
+
|
|
372
|
+
dataset = sv.DetectionDataset.from_yolo(...)
|
|
373
|
+
|
|
374
|
+
model = YOLO(...)
|
|
375
|
+
def callback(image: np.ndarray) -> sv.Detections:
|
|
376
|
+
result = model(image)[0]
|
|
377
|
+
return sv.Detections.from_ultralytics(result)
|
|
378
|
+
|
|
379
|
+
confusion_matrix = sv.ConfusionMatrix.benchmark(
|
|
380
|
+
dataset = dataset,
|
|
381
|
+
callback = callback
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
print(confusion_matrix.matrix)
|
|
385
|
+
# np.array([
|
|
386
|
+
# [0., 0., 0., 0.],
|
|
387
|
+
# [0., 1., 0., 1.],
|
|
388
|
+
# [0., 1., 1., 0.],
|
|
389
|
+
# [1., 1., 0., 0.]
|
|
390
|
+
# ])
|
|
391
|
+
```
|
|
392
|
+
"""
|
|
393
|
+
predictions, targets = [], []
|
|
394
|
+
for _, image, annotation in dataset:
|
|
395
|
+
predictions_batch = callback(image)
|
|
396
|
+
predictions.append(predictions_batch)
|
|
397
|
+
targets.append(annotation)
|
|
398
|
+
return cls.from_detections(
|
|
399
|
+
predictions=predictions,
|
|
400
|
+
targets=targets,
|
|
401
|
+
classes=dataset.classes,
|
|
402
|
+
conf_threshold=conf_threshold,
|
|
403
|
+
iou_threshold=iou_threshold,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def plot(
|
|
407
|
+
self,
|
|
408
|
+
save_path: Optional[str] = None,
|
|
409
|
+
title: Optional[str] = None,
|
|
410
|
+
classes: Optional[List[str]] = None,
|
|
411
|
+
normalize: bool = False,
|
|
412
|
+
fig_size: Tuple[int, int] = (12, 10),
|
|
413
|
+
) -> matplotlib.figure.Figure:
|
|
414
|
+
"""
|
|
415
|
+
Create confusion matrix plot and save it at selected location.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
save_path (Optional[str]): Path to save the plot. If not provided,
|
|
419
|
+
plot will be displayed.
|
|
420
|
+
title (Optional[str]): Title of the plot.
|
|
421
|
+
classes (Optional[List[str]]): List of classes to be displayed on the plot.
|
|
422
|
+
If not provided, all classes will be displayed.
|
|
423
|
+
normalize (bool): If True, normalize the confusion matrix.
|
|
424
|
+
fig_size (Tuple[int, int]): Size of the plot.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
matplotlib.figure.Figure: Confusion matrix plot.
|
|
428
|
+
"""
|
|
429
|
+
|
|
430
|
+
array = self.matrix.copy()
|
|
431
|
+
|
|
432
|
+
if normalize:
|
|
433
|
+
eps = 1e-8
|
|
434
|
+
array = array / (array.sum(0).reshape(1, -1) + eps)
|
|
435
|
+
|
|
436
|
+
array[array < 0.005] = np.nan
|
|
437
|
+
|
|
438
|
+
fig, ax = plt.subplots(figsize=fig_size, tight_layout=True, facecolor="white")
|
|
439
|
+
|
|
440
|
+
class_names = classes if classes is not None else self.classes
|
|
441
|
+
use_labels_for_ticks = class_names is not None and (0 < len(class_names) < 99)
|
|
442
|
+
if use_labels_for_ticks:
|
|
443
|
+
x_tick_labels = [*class_names, "FN"]
|
|
444
|
+
y_tick_labels = [*class_names, "FP"]
|
|
445
|
+
num_ticks = len(x_tick_labels)
|
|
446
|
+
else:
|
|
447
|
+
x_tick_labels = None
|
|
448
|
+
y_tick_labels = None
|
|
449
|
+
num_ticks = len(array)
|
|
450
|
+
im = ax.imshow(array, cmap="Blues")
|
|
451
|
+
|
|
452
|
+
cbar = ax.figure.colorbar(im, ax=ax)
|
|
453
|
+
cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array))
|
|
454
|
+
|
|
455
|
+
if x_tick_labels is None:
|
|
456
|
+
tick_interval = 2
|
|
457
|
+
else:
|
|
458
|
+
tick_interval = 1
|
|
459
|
+
ax.set_xticks(np.arange(0, num_ticks, tick_interval), labels=x_tick_labels)
|
|
460
|
+
ax.set_yticks(np.arange(0, num_ticks, tick_interval), labels=y_tick_labels)
|
|
461
|
+
|
|
462
|
+
plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="default")
|
|
463
|
+
|
|
464
|
+
labelsize = 10 if num_ticks < 50 else 8
|
|
465
|
+
ax.tick_params(axis="both", which="both", labelsize=labelsize)
|
|
466
|
+
|
|
467
|
+
if num_ticks < 30:
|
|
468
|
+
for i in range(array.shape[0]):
|
|
469
|
+
for j in range(array.shape[1]):
|
|
470
|
+
n_preds = array[i, j]
|
|
471
|
+
if not np.isnan(n_preds):
|
|
472
|
+
ax.text(
|
|
473
|
+
j,
|
|
474
|
+
i,
|
|
475
|
+
f"{n_preds:.2f}" if normalize else f"{n_preds:.0f}",
|
|
476
|
+
ha="center",
|
|
477
|
+
va="center",
|
|
478
|
+
color="black"
|
|
479
|
+
if n_preds < 0.5 * np.nanmax(array)
|
|
480
|
+
else "white",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
if title:
|
|
484
|
+
ax.set_title(title, fontsize=20)
|
|
485
|
+
|
|
486
|
+
ax.set_xlabel("Predicted")
|
|
487
|
+
ax.set_ylabel("True")
|
|
488
|
+
ax.set_facecolor("white")
|
|
489
|
+
if save_path:
|
|
490
|
+
fig.savefig(
|
|
491
|
+
save_path, dpi=250, facecolor=fig.get_facecolor(), transparent=True
|
|
492
|
+
)
|
|
493
|
+
return fig
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@dataclass(frozen=True)
|
|
497
|
+
class MeanAveragePrecision:
|
|
498
|
+
"""
|
|
499
|
+
Mean Average Precision for object detection tasks.
|
|
500
|
+
|
|
501
|
+
Attributes:
|
|
502
|
+
map50_95 (float): Mean Average Precision (mAP) calculated over IoU thresholds
|
|
503
|
+
ranging from `0.50` to `0.95` with a step size of `0.05`.
|
|
504
|
+
map50 (float): Mean Average Precision (mAP) calculated specifically at
|
|
505
|
+
an IoU threshold of `0.50`.
|
|
506
|
+
map75 (float): Mean Average Precision (mAP) calculated specifically at
|
|
507
|
+
an IoU threshold of `0.75`.
|
|
508
|
+
per_class_ap50_95 (np.ndarray): Average Precision (AP) values calculated over
|
|
509
|
+
IoU thresholds ranging from `0.50` to `0.95` with a step size of `0.05`,
|
|
510
|
+
provided for each individual class.
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
map50_95: float
|
|
514
|
+
map50: float
|
|
515
|
+
map75: float
|
|
516
|
+
per_class_ap50_95: np.ndarray
|
|
517
|
+
|
|
518
|
+
@classmethod
|
|
519
|
+
def from_detections(
|
|
520
|
+
cls,
|
|
521
|
+
predictions: List[Detections],
|
|
522
|
+
targets: List[Detections],
|
|
523
|
+
) -> MeanAveragePrecision:
|
|
524
|
+
"""
|
|
525
|
+
Calculate mean average precision based on predicted and ground-truth detections.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
targets (List[Detections]): Detections objects from ground-truth.
|
|
529
|
+
predictions (List[Detections]): Detections objects predicted by the model.
|
|
530
|
+
Returns:
|
|
531
|
+
MeanAveragePrecision: New instance of ConfusionMatrix.
|
|
532
|
+
|
|
533
|
+
Example:
|
|
534
|
+
```python
|
|
535
|
+
import eye as sv
|
|
536
|
+
|
|
537
|
+
targets = [
|
|
538
|
+
sv.Detections(...),
|
|
539
|
+
sv.Detections(...)
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
predictions = [
|
|
543
|
+
sv.Detections(...),
|
|
544
|
+
sv.Detections(...)
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
mean_average_precision = sv.MeanAveragePrecision.from_detections(
|
|
548
|
+
predictions=predictions,
|
|
549
|
+
targets=target,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
print(mean_average_precison.map50_95)
|
|
553
|
+
# 0.2899
|
|
554
|
+
```
|
|
555
|
+
"""
|
|
556
|
+
prediction_tensors = []
|
|
557
|
+
target_tensors = []
|
|
558
|
+
for prediction, target in zip(predictions, targets):
|
|
559
|
+
prediction_tensors.append(
|
|
560
|
+
detections_to_tensor(prediction, with_confidence=True)
|
|
561
|
+
)
|
|
562
|
+
target_tensors.append(detections_to_tensor(target, with_confidence=False))
|
|
563
|
+
return cls.from_tensors(
|
|
564
|
+
predictions=prediction_tensors,
|
|
565
|
+
targets=target_tensors,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
@classmethod
|
|
569
|
+
def benchmark(
|
|
570
|
+
cls,
|
|
571
|
+
dataset: DetectionDataset,
|
|
572
|
+
callback: Callable[[np.ndarray], Detections],
|
|
573
|
+
) -> MeanAveragePrecision:
|
|
574
|
+
"""
|
|
575
|
+
Calculate mean average precision from dataset and callback function.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
dataset (DetectionDataset): Object detection dataset used for evaluation.
|
|
579
|
+
callback (Callable[[np.ndarray], Detections]): Function that takes
|
|
580
|
+
an image as input and returns Detections object.
|
|
581
|
+
Returns:
|
|
582
|
+
MeanAveragePrecision: New instance of MeanAveragePrecision.
|
|
583
|
+
|
|
584
|
+
Example:
|
|
585
|
+
```python
|
|
586
|
+
import eye as sv
|
|
587
|
+
from ultralytics import YOLO
|
|
588
|
+
|
|
589
|
+
dataset = sv.DetectionDataset.from_yolo(...)
|
|
590
|
+
|
|
591
|
+
model = YOLO(...)
|
|
592
|
+
def callback(image: np.ndarray) -> sv.Detections:
|
|
593
|
+
result = model(image)[0]
|
|
594
|
+
return sv.Detections.from_ultralytics(result)
|
|
595
|
+
|
|
596
|
+
mean_average_precision = sv.MeanAveragePrecision.benchmark(
|
|
597
|
+
dataset = dataset,
|
|
598
|
+
callback = callback
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
print(mean_average_precision.map50_95)
|
|
602
|
+
# 0.433
|
|
603
|
+
```
|
|
604
|
+
"""
|
|
605
|
+
predictions, targets = [], []
|
|
606
|
+
for _, image, annotation in dataset:
|
|
607
|
+
predictions_batch = callback(image)
|
|
608
|
+
predictions.append(predictions_batch)
|
|
609
|
+
targets.append(annotation)
|
|
610
|
+
return cls.from_detections(
|
|
611
|
+
predictions=predictions,
|
|
612
|
+
targets=targets,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
@classmethod
|
|
616
|
+
def from_tensors(
|
|
617
|
+
cls,
|
|
618
|
+
predictions: List[np.ndarray],
|
|
619
|
+
targets: List[np.ndarray],
|
|
620
|
+
) -> MeanAveragePrecision:
|
|
621
|
+
"""
|
|
622
|
+
Calculate Mean Average Precision based on predicted and ground-truth
|
|
623
|
+
detections at different threshold.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
predictions (List[np.ndarray]): Each element of the list describes
|
|
627
|
+
a single image and has `shape = (M, 6)` where `M` is
|
|
628
|
+
the number of detected objects. Each row is expected to be
|
|
629
|
+
in `(x_min, y_min, x_max, y_max, class, conf)` format.
|
|
630
|
+
targets (List[np.ndarray]): Each element of the list describes a single
|
|
631
|
+
image and has `shape = (N, 5)` where `N` is the
|
|
632
|
+
number of ground-truth objects. Each row is expected to be in
|
|
633
|
+
`(x_min, y_min, x_max, y_max, class)` format.
|
|
634
|
+
Returns:
|
|
635
|
+
MeanAveragePrecision: New instance of MeanAveragePrecision.
|
|
636
|
+
|
|
637
|
+
Example:
|
|
638
|
+
```python
|
|
639
|
+
import eye as sv
|
|
640
|
+
import numpy as np
|
|
641
|
+
|
|
642
|
+
targets = (
|
|
643
|
+
[
|
|
644
|
+
np.array(
|
|
645
|
+
[
|
|
646
|
+
[0.0, 0.0, 3.0, 3.0, 1],
|
|
647
|
+
[2.0, 2.0, 5.0, 5.0, 1],
|
|
648
|
+
[6.0, 1.0, 8.0, 3.0, 2],
|
|
649
|
+
]
|
|
650
|
+
),
|
|
651
|
+
np.array([[1.0, 1.0, 2.0, 2.0, 2]]),
|
|
652
|
+
]
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
predictions = [
|
|
656
|
+
np.array(
|
|
657
|
+
[
|
|
658
|
+
[0.0, 0.0, 3.0, 3.0, 1, 0.9],
|
|
659
|
+
[0.1, 0.1, 3.0, 3.0, 0, 0.9],
|
|
660
|
+
[6.0, 1.0, 8.0, 3.0, 1, 0.8],
|
|
661
|
+
[1.0, 6.0, 2.0, 7.0, 1, 0.8],
|
|
662
|
+
]
|
|
663
|
+
),
|
|
664
|
+
np.array([[1.0, 1.0, 2.0, 2.0, 2, 0.8]])
|
|
665
|
+
]
|
|
666
|
+
|
|
667
|
+
mean_average_precision = sv.MeanAveragePrecision.from_tensors(
|
|
668
|
+
predictions=predictions,
|
|
669
|
+
targets=targets,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
print(mean_average_precision.map50_95)
|
|
673
|
+
# 0.6649
|
|
674
|
+
```
|
|
675
|
+
"""
|
|
676
|
+
validate_input_tensors(predictions, targets)
|
|
677
|
+
iou_thresholds = np.linspace(0.5, 0.95, 10)
|
|
678
|
+
stats = []
|
|
679
|
+
|
|
680
|
+
# Gather matching stats for predictions and targets
|
|
681
|
+
for true_objs, predicted_objs in zip(targets, predictions):
|
|
682
|
+
if predicted_objs.shape[0] == 0:
|
|
683
|
+
if true_objs.shape[0]:
|
|
684
|
+
stats.append(
|
|
685
|
+
(
|
|
686
|
+
np.zeros((0, iou_thresholds.size), dtype=bool),
|
|
687
|
+
*np.zeros((2, 0)),
|
|
688
|
+
true_objs[:, 4],
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
continue
|
|
692
|
+
|
|
693
|
+
if true_objs.shape[0]:
|
|
694
|
+
matches = cls._match_detection_batch(
|
|
695
|
+
predicted_objs, true_objs, iou_thresholds
|
|
696
|
+
)
|
|
697
|
+
stats.append(
|
|
698
|
+
(
|
|
699
|
+
matches,
|
|
700
|
+
predicted_objs[:, 5],
|
|
701
|
+
predicted_objs[:, 4],
|
|
702
|
+
true_objs[:, 4],
|
|
703
|
+
)
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Compute average precisions if any matches exist
|
|
707
|
+
if stats:
|
|
708
|
+
concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)]
|
|
709
|
+
average_precisions = cls._average_precisions_per_class(*concatenated_stats)
|
|
710
|
+
map50 = average_precisions[:, 0].mean()
|
|
711
|
+
map75 = average_precisions[:, 5].mean()
|
|
712
|
+
map50_95 = average_precisions.mean()
|
|
713
|
+
else:
|
|
714
|
+
map50, map75, map50_95 = 0, 0, 0
|
|
715
|
+
average_precisions = []
|
|
716
|
+
|
|
717
|
+
return cls(
|
|
718
|
+
map50_95=map50_95,
|
|
719
|
+
map50=map50,
|
|
720
|
+
map75=map75,
|
|
721
|
+
per_class_ap50_95=average_precisions,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
@staticmethod
|
|
725
|
+
def compute_average_precision(recall: np.ndarray, precision: np.ndarray) -> float:
|
|
726
|
+
"""
|
|
727
|
+
Compute the average precision using 101-point interpolation (COCO), given
|
|
728
|
+
the recall and precision curves.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
recall (np.ndarray): The recall curve.
|
|
732
|
+
precision (np.ndarray): The precision curve.
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
float: Average precision.
|
|
736
|
+
"""
|
|
737
|
+
extended_recall = np.concatenate(([0.0], recall, [1.0]))
|
|
738
|
+
extended_precision = np.concatenate(([1.0], precision, [0.0]))
|
|
739
|
+
max_accumulated_precision = np.flip(
|
|
740
|
+
np.maximum.accumulate(np.flip(extended_precision))
|
|
741
|
+
)
|
|
742
|
+
interpolated_recall_levels = np.linspace(0, 1, 101)
|
|
743
|
+
interpolated_precision = np.interp(
|
|
744
|
+
interpolated_recall_levels, extended_recall, max_accumulated_precision
|
|
745
|
+
)
|
|
746
|
+
average_precision = np.trapz(interpolated_precision, interpolated_recall_levels)
|
|
747
|
+
return average_precision
|
|
748
|
+
|
|
749
|
+
@staticmethod
|
|
750
|
+
def _match_detection_batch(
|
|
751
|
+
predictions: np.ndarray, targets: np.ndarray, iou_thresholds: np.ndarray
|
|
752
|
+
) -> np.ndarray:
|
|
753
|
+
"""
|
|
754
|
+
Match predictions with target labels based on IoU levels.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
predictions (np.ndarray): Batch prediction. Describes a single image and
|
|
758
|
+
has `shape = (M, 6)` where `M` is the number of detected objects.
|
|
759
|
+
Each row is expected to be in
|
|
760
|
+
`(x_min, y_min, x_max, y_max, class, conf)` format.
|
|
761
|
+
targets (np.ndarray): Batch target labels. Describes a single image and
|
|
762
|
+
has `shape = (N, 5)` where `N` is the number of ground-truth objects.
|
|
763
|
+
Each row is expected to be in
|
|
764
|
+
`(x_min, y_min, x_max, y_max, class)` format.
|
|
765
|
+
iou_thresholds (np.ndarray): Array contains different IoU thresholds.
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
np.ndarray: Matched prediction with target labels result.
|
|
769
|
+
"""
|
|
770
|
+
num_predictions, num_iou_levels = predictions.shape[0], iou_thresholds.shape[0]
|
|
771
|
+
correct = np.zeros((num_predictions, num_iou_levels), dtype=bool)
|
|
772
|
+
iou = box_iou_batch(targets[:, :4], predictions[:, :4])
|
|
773
|
+
correct_class = targets[:, 4:5] == predictions[:, 4]
|
|
774
|
+
|
|
775
|
+
for i, iou_level in enumerate(iou_thresholds):
|
|
776
|
+
matched_indices = np.where((iou >= iou_level) & correct_class)
|
|
777
|
+
|
|
778
|
+
if matched_indices[0].shape[0]:
|
|
779
|
+
combined_indices = np.stack(matched_indices, axis=1)
|
|
780
|
+
iou_values = iou[matched_indices][:, None]
|
|
781
|
+
matches = np.hstack([combined_indices, iou_values])
|
|
782
|
+
|
|
783
|
+
if matched_indices[0].shape[0] > 1:
|
|
784
|
+
matches = matches[matches[:, 2].argsort()[::-1]]
|
|
785
|
+
matches = matches[np.unique(matches[:, 1], return_index=True)[1]]
|
|
786
|
+
matches = matches[np.unique(matches[:, 0], return_index=True)[1]]
|
|
787
|
+
|
|
788
|
+
correct[matches[:, 1].astype(int), i] = True
|
|
789
|
+
|
|
790
|
+
return correct
|
|
791
|
+
|
|
792
|
+
@staticmethod
|
|
793
|
+
def _average_precisions_per_class(
|
|
794
|
+
matches: np.ndarray,
|
|
795
|
+
prediction_confidence: np.ndarray,
|
|
796
|
+
prediction_class_ids: np.ndarray,
|
|
797
|
+
true_class_ids: np.ndarray,
|
|
798
|
+
eps: float = 1e-16,
|
|
799
|
+
) -> np.ndarray:
|
|
800
|
+
"""
|
|
801
|
+
Compute the average precision, given the recall and precision curves.
|
|
802
|
+
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
matches (np.ndarray): True positives.
|
|
806
|
+
prediction_confidence (np.ndarray): Objectness value from 0-1.
|
|
807
|
+
prediction_class_ids (np.ndarray): Predicted object classes.
|
|
808
|
+
true_class_ids (np.ndarray): True object classes.
|
|
809
|
+
eps (float): Small value to prevent division by zero.
|
|
810
|
+
|
|
811
|
+
Returns:
|
|
812
|
+
np.ndarray: Average precision for different IoU levels.
|
|
813
|
+
"""
|
|
814
|
+
sorted_indices = np.argsort(-prediction_confidence)
|
|
815
|
+
matches = matches[sorted_indices]
|
|
816
|
+
prediction_class_ids = prediction_class_ids[sorted_indices]
|
|
817
|
+
|
|
818
|
+
unique_classes, class_counts = np.unique(true_class_ids, return_counts=True)
|
|
819
|
+
num_classes = unique_classes.shape[0]
|
|
820
|
+
|
|
821
|
+
average_precisions = np.zeros((num_classes, matches.shape[1]))
|
|
822
|
+
|
|
823
|
+
for class_idx, class_id in enumerate(unique_classes):
|
|
824
|
+
is_class = prediction_class_ids == class_id
|
|
825
|
+
total_true = class_counts[class_idx]
|
|
826
|
+
total_prediction = is_class.sum()
|
|
827
|
+
|
|
828
|
+
if total_prediction == 0 or total_true == 0:
|
|
829
|
+
continue
|
|
830
|
+
|
|
831
|
+
false_positives = (1 - matches[is_class]).cumsum(0)
|
|
832
|
+
true_positives = matches[is_class].cumsum(0)
|
|
833
|
+
recall = true_positives / (total_true + eps)
|
|
834
|
+
precision = true_positives / (true_positives + false_positives)
|
|
835
|
+
|
|
836
|
+
for iou_level_idx in range(matches.shape[1]):
|
|
837
|
+
average_precisions[class_idx, iou_level_idx] = (
|
|
838
|
+
MeanAveragePrecision.compute_average_precision(
|
|
839
|
+
recall[:, iou_level_idx], precision[:, iou_level_idx]
|
|
840
|
+
)
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return average_precisions
|