superiorvision 0.30.0.dev0__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.
- superiorvision/__init__.py +19 -0
- superiorvision-0.30.0.dev0.dist-info/METADATA +398 -0
- superiorvision-0.30.0.dev0.dist-info/RECORD +96 -0
- superiorvision-0.30.0.dev0.dist-info/WHEEL +5 -0
- superiorvision-0.30.0.dev0.dist-info/licenses/LICENSE.md +9 -0
- superiorvision-0.30.0.dev0.dist-info/top_level.txt +2 -0
- supervision/__init__.py +318 -0
- supervision/annotators/__init__.py +0 -0
- supervision/annotators/base.py +21 -0
- supervision/annotators/core.py +3551 -0
- supervision/annotators/utils.py +541 -0
- supervision/assets/__init__.py +4 -0
- supervision/assets/downloader.py +166 -0
- supervision/assets/list.py +84 -0
- supervision/classification/__init__.py +0 -0
- supervision/classification/core.py +216 -0
- supervision/config.py +13 -0
- supervision/dataset/__init__.py +0 -0
- supervision/dataset/core.py +1313 -0
- supervision/dataset/formats/__init__.py +0 -0
- supervision/dataset/formats/coco.py +708 -0
- supervision/dataset/formats/createml.py +331 -0
- supervision/dataset/formats/labelme.py +405 -0
- supervision/dataset/formats/pascal_voc.py +449 -0
- supervision/dataset/formats/yolo.py +502 -0
- supervision/dataset/utils.py +265 -0
- supervision/detection/__init__.py +0 -0
- supervision/detection/compact_mask.py +1927 -0
- supervision/detection/core.py +3864 -0
- supervision/detection/line_zone.py +924 -0
- supervision/detection/overlap_filter.py +426 -0
- supervision/detection/tensor_utils.py +1596 -0
- supervision/detection/tensor_views.py +48 -0
- supervision/detection/tools/__init__.py +0 -0
- supervision/detection/tools/csv_sink.py +242 -0
- supervision/detection/tools/inference_slicer.py +776 -0
- supervision/detection/tools/json_sink.py +215 -0
- supervision/detection/tools/polygon_zone.py +266 -0
- supervision/detection/tools/smoother.py +218 -0
- supervision/detection/tools/transformers.py +265 -0
- supervision/detection/utils/__init__.py +12 -0
- supervision/detection/utils/_typing.py +11 -0
- supervision/detection/utils/boxes.py +510 -0
- supervision/detection/utils/converters.py +830 -0
- supervision/detection/utils/internal.py +806 -0
- supervision/detection/utils/iou_and_nms.py +1606 -0
- supervision/detection/utils/masks.py +625 -0
- supervision/detection/utils/polygons.py +128 -0
- supervision/detection/utils/vlms.py +97 -0
- supervision/detection/vlm.py +945 -0
- supervision/draw/__init__.py +0 -0
- supervision/draw/base.py +14 -0
- supervision/draw/color.py +598 -0
- supervision/draw/utils.py +527 -0
- supervision/geometry/__init__.py +0 -0
- supervision/geometry/core.py +208 -0
- supervision/geometry/utils.py +54 -0
- supervision/key_points/__init__.py +0 -0
- supervision/key_points/annotators.py +997 -0
- supervision/key_points/core.py +1506 -0
- supervision/key_points/skeletons.py +2646 -0
- supervision/keypoint/__init__.py +13 -0
- supervision/keypoint/annotators.py +13 -0
- supervision/keypoint/core.py +8 -0
- supervision/metrics/__init__.py +40 -0
- supervision/metrics/core.py +74 -0
- supervision/metrics/detection.py +1632 -0
- supervision/metrics/f1_score.py +815 -0
- supervision/metrics/mean_average_precision.py +1723 -0
- supervision/metrics/mean_average_recall.py +734 -0
- supervision/metrics/precision.py +815 -0
- supervision/metrics/recall.py +774 -0
- supervision/metrics/utils/__init__.py +0 -0
- supervision/metrics/utils/matching.py +56 -0
- supervision/metrics/utils/object_size.py +256 -0
- supervision/metrics/utils/utils.py +9 -0
- supervision/py.typed +0 -0
- supervision/tracker/__init__.py +5 -0
- supervision/tracker/byte_tracker/__init__.py +0 -0
- supervision/tracker/byte_tracker/core.py +448 -0
- supervision/tracker/byte_tracker/kalman_filter.py +186 -0
- supervision/tracker/byte_tracker/matching.py +211 -0
- supervision/tracker/byte_tracker/single_object_track.py +194 -0
- supervision/tracker/byte_tracker/utils.py +34 -0
- supervision/utils/__init__.py +0 -0
- supervision/utils/conversion.py +223 -0
- supervision/utils/deprecate.py +54 -0
- supervision/utils/file.py +209 -0
- supervision/utils/image.py +988 -0
- supervision/utils/internal.py +209 -0
- supervision/utils/iterables.py +103 -0
- supervision/utils/logger.py +61 -0
- supervision/utils/notebook.py +124 -0
- supervision/utils/tensor.py +362 -0
- supervision/utils/video.py +533 -0
- supervision/validators/__init__.py +379 -0
|
@@ -0,0 +1,1723 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import datetime
|
|
5
|
+
import itertools
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
from supervision.config import ORIENTED_BOX_COORDINATES
|
|
16
|
+
from supervision.detection.core import Detections
|
|
17
|
+
from supervision.detection.utils.iou_and_nms import (
|
|
18
|
+
box_iou_batch_with_jaccard,
|
|
19
|
+
mask_iou_batch,
|
|
20
|
+
oriented_box_iou_batch,
|
|
21
|
+
)
|
|
22
|
+
from supervision.draw.color import LEGACY_COLOR_PALETTE
|
|
23
|
+
from supervision.metrics.core import Metric, MetricTarget
|
|
24
|
+
from supervision.metrics.utils.utils import ensure_pandas_installed
|
|
25
|
+
from supervision.utils.logger import _get_logger
|
|
26
|
+
|
|
27
|
+
logger = _get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _TypeCocoDict(TypedDict, total=False):
|
|
34
|
+
id: int
|
|
35
|
+
image_id: int
|
|
36
|
+
category_id: int
|
|
37
|
+
bbox: list[float]
|
|
38
|
+
area: float
|
|
39
|
+
iscrowd: int
|
|
40
|
+
ignore: int
|
|
41
|
+
_ignore: int
|
|
42
|
+
score: float
|
|
43
|
+
segmentation: list[list[float]]
|
|
44
|
+
name: str
|
|
45
|
+
supercategory: str
|
|
46
|
+
caption: str
|
|
47
|
+
keypoints: list[float]
|
|
48
|
+
# Metric-target-specific content: a boolean mask of shape (H, W) for
|
|
49
|
+
# `MetricTarget.MASKS` or an oriented box of shape (4, 2) for
|
|
50
|
+
# `MetricTarget.ORIENTED_BOUNDING_BOXES`. Absent for `MetricTarget.BOXES`.
|
|
51
|
+
# Invariant: shape/dtype match `metric_target` of the owning COCOEvaluator.
|
|
52
|
+
content: npt.NDArray[Any]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_TypeCocoDataset: TypeAlias = dict[str, list[_TypeCocoDict]]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _TypeEvaluationImageResult(TypedDict):
|
|
59
|
+
image_id: int
|
|
60
|
+
category_id: int
|
|
61
|
+
area_range: list[float] | tuple[float, float]
|
|
62
|
+
max_det: int
|
|
63
|
+
dt_ids: list[int]
|
|
64
|
+
gt_ids: list[int]
|
|
65
|
+
dtMatches: npt.NDArray[np.int64]
|
|
66
|
+
gtMatches: npt.NDArray[np.int64]
|
|
67
|
+
dtScores: list[float]
|
|
68
|
+
gtIgnore: npt.NDArray[np.int64]
|
|
69
|
+
dtIgnore: npt.NDArray[np.bool_]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class MeanAveragePrecisionResult:
|
|
74
|
+
"""
|
|
75
|
+
The result of the Mean Average Precision calculation.
|
|
76
|
+
|
|
77
|
+
Returns `-1` sentinel scores when no detections or targets are present.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
metric_target: the type of data used for the metric -
|
|
81
|
+
boxes, masks or oriented bounding boxes.
|
|
82
|
+
is_class_agnostic: When computing class-agnostic results, class ID
|
|
83
|
+
is set to `-1`.
|
|
84
|
+
mAP_scores: the mAP scores at each IoU threshold.
|
|
85
|
+
Shape: `(num_iou_thresholds,)`
|
|
86
|
+
ap_per_class: the average precision scores per
|
|
87
|
+
class and IoU threshold. Shape: `(num_target_classes, num_iou_thresholds)`
|
|
88
|
+
iou_thresholds: the IoU thresholds used in the calculations.
|
|
89
|
+
matched_classes: the class IDs of all matched classes.
|
|
90
|
+
Corresponds to the rows of `ap_per_class`.
|
|
91
|
+
small_objects: the mAP results
|
|
92
|
+
for small objects (area < 32²).
|
|
93
|
+
medium_objects: the mAP results
|
|
94
|
+
for medium objects (32² ≤ area < 96²).
|
|
95
|
+
large_objects: the mAP results
|
|
96
|
+
for large objects (area ≥ 96²).
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
metric_target: MetricTarget
|
|
100
|
+
is_class_agnostic: bool
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def map50_95(self) -> float:
|
|
104
|
+
"""the mAP score at IoU thresholds from `0.5` to `0.95`."""
|
|
105
|
+
valid_scores = self.mAP_scores[self.mAP_scores > -1]
|
|
106
|
+
if len(valid_scores) > 0:
|
|
107
|
+
return float(valid_scores.mean())
|
|
108
|
+
else:
|
|
109
|
+
return -1
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def map50(self) -> float:
|
|
113
|
+
"""the mAP score at IoU threshold of `0.5`."""
|
|
114
|
+
return float(self.mAP_scores[0])
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def map75(self) -> float:
|
|
118
|
+
"""the mAP score at IoU threshold of `0.75`."""
|
|
119
|
+
return float(self.mAP_scores[5])
|
|
120
|
+
|
|
121
|
+
mAP_scores: npt.NDArray[np.float64]
|
|
122
|
+
ap_per_class: npt.NDArray[np.float64]
|
|
123
|
+
iou_thresholds: npt.NDArray[np.float64]
|
|
124
|
+
matched_classes: npt.NDArray[np.int32]
|
|
125
|
+
small_objects: MeanAveragePrecisionResult | None = None
|
|
126
|
+
medium_objects: MeanAveragePrecisionResult | None = None
|
|
127
|
+
large_objects: MeanAveragePrecisionResult | None = None
|
|
128
|
+
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
"""
|
|
131
|
+
Formats the evaluation output metrics to match the structure used by pycocotools
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
```pycon
|
|
135
|
+
>>> import numpy as np
|
|
136
|
+
>>> import supervision as sv
|
|
137
|
+
>>> from supervision.metrics import MeanAveragePrecision
|
|
138
|
+
>>> predictions = sv.Detections(
|
|
139
|
+
... xyxy=np.array([[0, 0, 10, 10]]),
|
|
140
|
+
... class_id=np.array([0]),
|
|
141
|
+
... confidence=np.array([0.9])
|
|
142
|
+
... )
|
|
143
|
+
>>> targets = sv.Detections(
|
|
144
|
+
... xyxy=np.array([[0, 0, 10, 10]]),
|
|
145
|
+
... class_id=np.array([0])
|
|
146
|
+
... )
|
|
147
|
+
>>> map_metric = MeanAveragePrecision()
|
|
148
|
+
>>> map_result = map_metric.update(predictions, targets).compute()
|
|
149
|
+
>>> print(map_result) # doctest: +ELLIPSIS
|
|
150
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = ...
|
|
151
|
+
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = ...
|
|
152
|
+
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = ...
|
|
153
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = ...
|
|
154
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = ...
|
|
155
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = ...
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
"""
|
|
159
|
+
if (
|
|
160
|
+
self.small_objects is None
|
|
161
|
+
or self.medium_objects is None
|
|
162
|
+
or self.large_objects is None
|
|
163
|
+
):
|
|
164
|
+
return (
|
|
165
|
+
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= all | "
|
|
166
|
+
f"maxDets=100 ] = {self.map50_95:.3f}\n"
|
|
167
|
+
f"Average Precision (AP) @[ IoU=0.50 | area= all | "
|
|
168
|
+
f"maxDets=100 ] = {self.map50:.3f}\n"
|
|
169
|
+
f"Average Precision (AP) @[ IoU=0.75 | area= all | "
|
|
170
|
+
f"maxDets=100 ] = {self.map75:.3f}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= all | "
|
|
175
|
+
f"maxDets=100 ] = {self.map50_95:.3f}\n"
|
|
176
|
+
f"Average Precision (AP) @[ IoU=0.50 | area= all | "
|
|
177
|
+
f"maxDets=100 ] = {self.map50:.3f}\n"
|
|
178
|
+
f"Average Precision (AP) @[ IoU=0.75 | area= all | "
|
|
179
|
+
f"maxDets=100 ] = {self.map75:.3f}\n"
|
|
180
|
+
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= small | "
|
|
181
|
+
f"maxDets=100 ] = {self.small_objects.map50_95:.3f}\n"
|
|
182
|
+
f"Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | "
|
|
183
|
+
f"maxDets=100 ] = {self.medium_objects.map50_95:.3f}\n"
|
|
184
|
+
f"Average Precision (AP) @[ IoU=0.50:0.95 | area= large | "
|
|
185
|
+
f"maxDets=100 ] = {self.large_objects.map50_95:.3f}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def to_pandas(self) -> pd.DataFrame:
|
|
189
|
+
"""
|
|
190
|
+
Convert the result to a pandas DataFrame.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The result as a DataFrame.
|
|
194
|
+
"""
|
|
195
|
+
ensure_pandas_installed()
|
|
196
|
+
import pandas as pd
|
|
197
|
+
|
|
198
|
+
pandas_data: dict[str, object] = {
|
|
199
|
+
"mAP@50:95": self.map50_95,
|
|
200
|
+
"mAP@50": self.map50,
|
|
201
|
+
"mAP@75": self.map75,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if self.small_objects is not None:
|
|
205
|
+
small_objects_df = self.small_objects.to_pandas()
|
|
206
|
+
for key, value in small_objects_df.items():
|
|
207
|
+
pandas_data[f"small_objects_{key}"] = value
|
|
208
|
+
if self.medium_objects is not None:
|
|
209
|
+
medium_objects_df = self.medium_objects.to_pandas()
|
|
210
|
+
for key, value in medium_objects_df.items():
|
|
211
|
+
pandas_data[f"medium_objects_{key}"] = value
|
|
212
|
+
if self.large_objects is not None:
|
|
213
|
+
large_objects_df = self.large_objects.to_pandas()
|
|
214
|
+
for key, value in large_objects_df.items():
|
|
215
|
+
pandas_data[f"large_objects_{key}"] = value
|
|
216
|
+
|
|
217
|
+
# Average precisions are currently not included in the DataFrame.
|
|
218
|
+
return pd.DataFrame(
|
|
219
|
+
pandas_data,
|
|
220
|
+
index=[0],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def plot(self) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Plot the mAP results.
|
|
226
|
+
|
|
227
|
+
{ align=center width="800" }
|
|
230
|
+
"""
|
|
231
|
+
from matplotlib import pyplot as plt
|
|
232
|
+
|
|
233
|
+
labels = ["mAP@50:95", "mAP@50", "mAP@75"]
|
|
234
|
+
values = [self.map50_95, self.map50, self.map75]
|
|
235
|
+
colors = [LEGACY_COLOR_PALETTE[0]] * 3
|
|
236
|
+
|
|
237
|
+
if self.small_objects is not None:
|
|
238
|
+
labels += ["Small: mAP@50:95", "Small: mAP@50", "Small: mAP@75"]
|
|
239
|
+
values += [
|
|
240
|
+
self.small_objects.map50_95,
|
|
241
|
+
self.small_objects.map50,
|
|
242
|
+
self.small_objects.map75,
|
|
243
|
+
]
|
|
244
|
+
colors += [LEGACY_COLOR_PALETTE[3]] * 3
|
|
245
|
+
|
|
246
|
+
if self.medium_objects is not None:
|
|
247
|
+
labels += ["Medium: mAP@50:95", "Medium: mAP@50", "Medium: mAP@75"]
|
|
248
|
+
values += [
|
|
249
|
+
self.medium_objects.map50_95,
|
|
250
|
+
self.medium_objects.map50,
|
|
251
|
+
self.medium_objects.map75,
|
|
252
|
+
]
|
|
253
|
+
colors += [LEGACY_COLOR_PALETTE[2]] * 3
|
|
254
|
+
|
|
255
|
+
if self.large_objects is not None:
|
|
256
|
+
labels += ["Large: mAP@50:95", "Large: mAP@50", "Large: mAP@75"]
|
|
257
|
+
values += [
|
|
258
|
+
self.large_objects.map50_95,
|
|
259
|
+
self.large_objects.map50,
|
|
260
|
+
self.large_objects.map75,
|
|
261
|
+
]
|
|
262
|
+
colors += [LEGACY_COLOR_PALETTE[4]] * 3
|
|
263
|
+
|
|
264
|
+
plt.rcParams["font.family"] = "monospace"
|
|
265
|
+
|
|
266
|
+
_, ax = plt.subplots(figsize=(10, 6))
|
|
267
|
+
ax.set_ylim(0, 1)
|
|
268
|
+
ax.set_ylabel("Value", fontweight="bold")
|
|
269
|
+
ax.set_title("Mean Average Precision", fontweight="bold")
|
|
270
|
+
|
|
271
|
+
x_positions = range(len(labels))
|
|
272
|
+
bars = ax.bar(x_positions, values, color=colors, align="center")
|
|
273
|
+
|
|
274
|
+
ax.set_xticks(x_positions)
|
|
275
|
+
ax.set_xticklabels(labels, rotation=45, ha="right")
|
|
276
|
+
|
|
277
|
+
for bar in bars:
|
|
278
|
+
y_value = bar.get_height()
|
|
279
|
+
ax.text(
|
|
280
|
+
bar.get_x() + bar.get_width() / 2,
|
|
281
|
+
y_value + 0.02,
|
|
282
|
+
f"{y_value:.2f}",
|
|
283
|
+
ha="center",
|
|
284
|
+
va="bottom",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
plt.rcParams["font.family"] = "sans-serif"
|
|
288
|
+
|
|
289
|
+
plt.tight_layout()
|
|
290
|
+
plt.show()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class EvaluationDataset:
|
|
294
|
+
"""
|
|
295
|
+
Class used representing a dataset in the right format needed by the
|
|
296
|
+
`COCOEvaluator` class.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
def __init__(self, targets: _TypeCocoDataset | None = None) -> None:
|
|
300
|
+
"""
|
|
301
|
+
Constructor of EvaluationDataset object used to evaluate models with
|
|
302
|
+
Mean Average Precision.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
targets: The targets (ground truth) of the dataset in a the
|
|
306
|
+
COCO format.
|
|
307
|
+
"""
|
|
308
|
+
# Initialize members
|
|
309
|
+
# Initialize members
|
|
310
|
+
self.dataset: _TypeCocoDataset = {}
|
|
311
|
+
self.anns: dict[int, _TypeCocoDict] = {}
|
|
312
|
+
self.cats: dict[int, _TypeCocoDict] = {}
|
|
313
|
+
self.imgs: dict[int, _TypeCocoDict] = {}
|
|
314
|
+
self.img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list)
|
|
315
|
+
self.cat_to_imgs: dict[int, list[int]] = defaultdict(list)
|
|
316
|
+
|
|
317
|
+
if targets is None:
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
# Load dataset
|
|
321
|
+
self.dataset = targets
|
|
322
|
+
self.create_class_members()
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def empty(cls) -> EvaluationDataset:
|
|
326
|
+
return cls(targets=None)
|
|
327
|
+
|
|
328
|
+
def create_class_members(self) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Create index elements for the dataset.
|
|
331
|
+
"""
|
|
332
|
+
anns: dict[int, _TypeCocoDict] = {}
|
|
333
|
+
cats: dict[int, _TypeCocoDict] = {}
|
|
334
|
+
imgs: dict[int, _TypeCocoDict] = {}
|
|
335
|
+
img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list)
|
|
336
|
+
cat_to_imgs: dict[int, list[int]] = defaultdict(list)
|
|
337
|
+
if "annotations" in self.dataset:
|
|
338
|
+
for ann in self.dataset["annotations"]:
|
|
339
|
+
img_to_anns[ann["image_id"]].append(ann)
|
|
340
|
+
anns[ann["id"]] = ann
|
|
341
|
+
|
|
342
|
+
if "images" in self.dataset:
|
|
343
|
+
for img in self.dataset["images"]:
|
|
344
|
+
imgs[img["id"]] = img
|
|
345
|
+
|
|
346
|
+
if "categories" in self.dataset:
|
|
347
|
+
for cat in self.dataset["categories"]:
|
|
348
|
+
cats[cat["id"]] = cat
|
|
349
|
+
|
|
350
|
+
if "annotations" in self.dataset and "categories" in self.dataset:
|
|
351
|
+
for ann in self.dataset["annotations"]:
|
|
352
|
+
cat_to_imgs[ann["category_id"]].append(ann["image_id"])
|
|
353
|
+
|
|
354
|
+
# Populate class members
|
|
355
|
+
self.anns = anns
|
|
356
|
+
self.img_to_anns = img_to_anns
|
|
357
|
+
self.cat_to_imgs = cat_to_imgs
|
|
358
|
+
self.imgs = imgs
|
|
359
|
+
self.cats = cats
|
|
360
|
+
|
|
361
|
+
def get_annotation_ids(
|
|
362
|
+
self,
|
|
363
|
+
img_ids: list[int] | None = None,
|
|
364
|
+
cat_ids: list[int] | None = None,
|
|
365
|
+
area_range: tuple[float, float] | None = None,
|
|
366
|
+
iscrowd: bool = False,
|
|
367
|
+
) -> list[int]:
|
|
368
|
+
"""
|
|
369
|
+
Get annotation ids that satisfy given filter conditions.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
img_ids: ids of the images that we want to retrieve.
|
|
373
|
+
cat_ids: ids of the categories that we want to retrieve.
|
|
374
|
+
area_range: area range of the annotations that we want to retrieve
|
|
375
|
+
in the format [min_area, max_area].
|
|
376
|
+
iscrowd: if annotations to retrieve are `iscrowded=1`.
|
|
377
|
+
"""
|
|
378
|
+
# If there are no filters, we use all annotations
|
|
379
|
+
if not img_ids and not cat_ids and not area_range:
|
|
380
|
+
anns = self.dataset["annotations"]
|
|
381
|
+
else:
|
|
382
|
+
if img_ids:
|
|
383
|
+
lists = [
|
|
384
|
+
self.img_to_anns[img_id]
|
|
385
|
+
for img_id in img_ids
|
|
386
|
+
if img_id in self.img_to_anns
|
|
387
|
+
]
|
|
388
|
+
anns = list(itertools.chain.from_iterable(lists))
|
|
389
|
+
else:
|
|
390
|
+
anns = self.dataset["annotations"]
|
|
391
|
+
|
|
392
|
+
# Filter by category
|
|
393
|
+
anns = (
|
|
394
|
+
anns
|
|
395
|
+
if not cat_ids
|
|
396
|
+
else [ann for ann in anns if ann["category_id"] in cat_ids]
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Filter by area
|
|
400
|
+
anns = (
|
|
401
|
+
anns
|
|
402
|
+
if not area_range
|
|
403
|
+
else [
|
|
404
|
+
ann for ann in anns if area_range[0] < ann["area"] < area_range[1]
|
|
405
|
+
]
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Filter by iscrowd
|
|
409
|
+
if iscrowd is True:
|
|
410
|
+
ids = [ann["id"] for ann in anns if ann["iscrowd"] == 1]
|
|
411
|
+
else:
|
|
412
|
+
ids = [ann["id"] for ann in anns]
|
|
413
|
+
return ids
|
|
414
|
+
|
|
415
|
+
def get_category_ids(
|
|
416
|
+
self,
|
|
417
|
+
cat_names: list[str] | None = None,
|
|
418
|
+
supercategory_names: list[str] | None = None,
|
|
419
|
+
cat_ids: list[int] | None = None,
|
|
420
|
+
) -> list[int]:
|
|
421
|
+
"""
|
|
422
|
+
Get category ids that satisfy given filter conditions.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
cat_names: names of the categories to retrieve.
|
|
426
|
+
supercategory_names: names of the supercategories to retrieve.
|
|
427
|
+
cat_ids: ids of the categories to retrieve.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
ids: integer array of category ids.
|
|
431
|
+
"""
|
|
432
|
+
# If there are no filters, we use all categories
|
|
433
|
+
if not cat_names and not supercategory_names and not cat_ids:
|
|
434
|
+
cats = self.dataset["categories"]
|
|
435
|
+
else:
|
|
436
|
+
cats = self.dataset["categories"]
|
|
437
|
+
|
|
438
|
+
# Filter by name
|
|
439
|
+
cats = (
|
|
440
|
+
cats
|
|
441
|
+
if not cat_names
|
|
442
|
+
else [cat for cat in cats if cat["name"] in cat_names]
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Filter by supercategory
|
|
446
|
+
cats = (
|
|
447
|
+
cats
|
|
448
|
+
if not supercategory_names
|
|
449
|
+
else [
|
|
450
|
+
cat for cat in cats if cat["supercategory"] in supercategory_names
|
|
451
|
+
]
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Filter by id
|
|
455
|
+
cats = (
|
|
456
|
+
cats if not cat_ids else [cat for cat in cats if cat["id"] in cat_ids]
|
|
457
|
+
)
|
|
458
|
+
ids = [cat["id"] for cat in cats]
|
|
459
|
+
return ids
|
|
460
|
+
|
|
461
|
+
def get_image_ids(
|
|
462
|
+
self,
|
|
463
|
+
img_ids: list[int] | None = None,
|
|
464
|
+
cat_ids: list[int] | None = None,
|
|
465
|
+
) -> list[int]:
|
|
466
|
+
"""
|
|
467
|
+
Get image ids that satisfy given filter conditions.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
img_ids: ids of the images to retrieve.
|
|
471
|
+
cat_ids: ids of the categories to retrieve.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
ids: integer array of image ids.
|
|
475
|
+
"""
|
|
476
|
+
# If there are no filters, we use all images
|
|
477
|
+
if not img_ids and not cat_ids:
|
|
478
|
+
ids = self.imgs.keys()
|
|
479
|
+
return list(ids)
|
|
480
|
+
|
|
481
|
+
ids_set = set(img_ids) if img_ids else set()
|
|
482
|
+
|
|
483
|
+
if cat_ids:
|
|
484
|
+
for i, cat_id in enumerate(cat_ids):
|
|
485
|
+
if i == 0 and not ids_set:
|
|
486
|
+
ids_set = set(self.cat_to_imgs[cat_id])
|
|
487
|
+
else:
|
|
488
|
+
ids_set &= set(self.cat_to_imgs[cat_id])
|
|
489
|
+
|
|
490
|
+
return list(ids_set)
|
|
491
|
+
|
|
492
|
+
def get_annotations(self, ids: list[int] | None = None) -> list[_TypeCocoDict]:
|
|
493
|
+
"""
|
|
494
|
+
Get annotations with the specified ids.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
ids: integer ids specifying annotations.
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
anns: loaded annotations.
|
|
501
|
+
"""
|
|
502
|
+
if ids is None:
|
|
503
|
+
return []
|
|
504
|
+
return [self.anns[idx] for idx in ids]
|
|
505
|
+
|
|
506
|
+
def load_predictions(self, predictions: list[_TypeCocoDict]) -> EvaluationDataset:
|
|
507
|
+
"""
|
|
508
|
+
Load prediction result into an EvaluationDataset object.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
predictions: prediction result.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
EvaluationDataset object representing the predictions.
|
|
515
|
+
"""
|
|
516
|
+
# Create an empty EvaluationDataset object for the predictions
|
|
517
|
+
predictions_dataset = EvaluationDataset.empty()
|
|
518
|
+
predictions_dataset.dataset["images"] = list(self.dataset["images"])
|
|
519
|
+
|
|
520
|
+
if not isinstance(predictions, list):
|
|
521
|
+
raise ValueError("results must be a list")
|
|
522
|
+
|
|
523
|
+
# Handle empty predictions
|
|
524
|
+
if len(predictions) == 0:
|
|
525
|
+
predictions_dataset.dataset["annotations"] = []
|
|
526
|
+
return predictions_dataset
|
|
527
|
+
|
|
528
|
+
ids = [pred["image_id"] for pred in predictions]
|
|
529
|
+
|
|
530
|
+
# Make sure the image ids from predictions exist in the current dataset.
|
|
531
|
+
# A plain ``assert`` would be stripped under ``python -O``, so validate
|
|
532
|
+
# this public-input contract with an explicit exception instead.
|
|
533
|
+
if not set(ids) <= set(self.get_image_ids()):
|
|
534
|
+
raise ValueError("Results do not correspond to current coco set")
|
|
535
|
+
|
|
536
|
+
# Check if the predictions contain any unsupported keys
|
|
537
|
+
if "caption" in predictions[0]:
|
|
538
|
+
raise NotImplementedError(
|
|
539
|
+
"Evaluating predictions with caption is not supported."
|
|
540
|
+
)
|
|
541
|
+
elif "segmentation" in predictions[0]:
|
|
542
|
+
raise NotImplementedError(
|
|
543
|
+
"Evaluating predictions with segmentation is not supported."
|
|
544
|
+
)
|
|
545
|
+
elif "keypoints" in predictions[0]:
|
|
546
|
+
raise NotImplementedError(
|
|
547
|
+
"Evaluating predictions with keypoints is not supported."
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
elif "bbox" in predictions[0] and not predictions[0]["bbox"] == []:
|
|
551
|
+
predictions_dataset.dataset["categories"] = copy.deepcopy(
|
|
552
|
+
self.dataset["categories"]
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Prepare fields for every prediction of the given image
|
|
556
|
+
for idx, pred in enumerate(predictions):
|
|
557
|
+
x, y, w, h = pred["bbox"]
|
|
558
|
+
x1, x2, y1, y2 = [x, x + w, y, y + h]
|
|
559
|
+
|
|
560
|
+
# Make segmentation from bounding box coordinates
|
|
561
|
+
if "segmentation" not in pred:
|
|
562
|
+
pred["segmentation"] = [[x1, y1, x1, y2, x2, y2, x2, y1]]
|
|
563
|
+
# Use provided area if available
|
|
564
|
+
if "area" not in pred:
|
|
565
|
+
pred["area"] = w * h
|
|
566
|
+
pred["id"] = idx + 1
|
|
567
|
+
# For predictions we set iscrowd to 0
|
|
568
|
+
pred["iscrowd"] = 0
|
|
569
|
+
predictions_dataset.dataset["annotations"] = predictions
|
|
570
|
+
predictions_dataset.create_class_members()
|
|
571
|
+
return predictions_dataset
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
# Area ranges for object size in pixels
|
|
575
|
+
SMALL_OBJECT_AREA = 32**2
|
|
576
|
+
MEDIUM_OBJECT_AREA = 96**2
|
|
577
|
+
MAX_ALL_OBJECT_AREA = 1e5**2
|
|
578
|
+
|
|
579
|
+
# Smallest number to avoid division by zero
|
|
580
|
+
EPS = np.finfo(np.float32).eps
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _mask_iou_with_jaccard(
|
|
584
|
+
masks_true: list[npt.NDArray[np.bool_]],
|
|
585
|
+
masks_detection: list[npt.NDArray[np.bool_]],
|
|
586
|
+
is_crowd: list[bool],
|
|
587
|
+
) -> npt.NDArray[np.float64]:
|
|
588
|
+
"""
|
|
589
|
+
Calculate the IoU between detection masks (dt) and ground-truth masks (gt),
|
|
590
|
+
following the COCO convention: a detection may match any subregion of a
|
|
591
|
+
crowd ground truth, so for crowd rows the union collapses to the detection
|
|
592
|
+
area (mask counterpart of `box_iou_batch_with_jaccard`).
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
masks_true: List of ground-truth masks of shape `(H, W)`.
|
|
596
|
+
masks_detection: List of detection masks of shape `(H, W)`.
|
|
597
|
+
is_crowd: List indicating if each ground-truth mask is a crowd region.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Array of IoU values of shape `(len(masks_detection), len(masks_true))`.
|
|
601
|
+
"""
|
|
602
|
+
if len(masks_detection) == 0 or len(masks_true) == 0:
|
|
603
|
+
return np.empty((len(masks_detection), len(masks_true)), dtype=np.float64)
|
|
604
|
+
|
|
605
|
+
gt_masks = np.stack(masks_true).astype(bool)
|
|
606
|
+
dt_masks = np.stack(masks_detection).astype(bool)
|
|
607
|
+
crowd = np.asarray(is_crowd, dtype=bool)
|
|
608
|
+
|
|
609
|
+
# Compute base IoU via the optimised path (float32 + memory-chunked).
|
|
610
|
+
# mask_iou_batch returns (gt, dt); the evaluator expects (dt, gt).
|
|
611
|
+
iou: npt.NDArray[np.float64] = mask_iou_batch(gt_masks, dt_masks).T.astype(
|
|
612
|
+
np.float64
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
if not np.any(crowd):
|
|
616
|
+
return iou
|
|
617
|
+
|
|
618
|
+
# Override crowd columns: COCO convention collapses the union to the
|
|
619
|
+
# detection area, so a small detection inside a large crowd region scores
|
|
620
|
+
# IoU ≈ 1. Recompute only the crowd columns to avoid a full float64 matmul.
|
|
621
|
+
eps = np.spacing(1)
|
|
622
|
+
crowd_idx = np.where(crowd)[0]
|
|
623
|
+
gt_flat = gt_masks[crowd_idx].reshape(len(crowd_idx), -1).astype(np.float32)
|
|
624
|
+
dt_flat = dt_masks.reshape(dt_masks.shape[0], -1).astype(np.float32)
|
|
625
|
+
area_inter = (dt_flat @ gt_flat.T).astype(np.float64) # (dt, N_crowd)
|
|
626
|
+
area_dt = dt_flat.sum(axis=1).astype(np.float64) # (dt,)
|
|
627
|
+
iou[:, crowd_idx] = area_inter / (area_dt[:, None] + eps)
|
|
628
|
+
return iou
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class ObjectSize(Enum):
|
|
632
|
+
"""
|
|
633
|
+
Enum for object size.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
ALL = "all"
|
|
637
|
+
SMALL = "small"
|
|
638
|
+
MEDIUM = "medium"
|
|
639
|
+
LARGE = "large"
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class COCOEvaluatorParameters:
|
|
643
|
+
"""
|
|
644
|
+
Parameters for COCOEvaluator
|
|
645
|
+
"""
|
|
646
|
+
|
|
647
|
+
def __init__(self) -> None:
|
|
648
|
+
"""Initialize all parameters for evaluation"""
|
|
649
|
+
|
|
650
|
+
self.img_ids: list[int] = []
|
|
651
|
+
self.cat_ids: list[int] = []
|
|
652
|
+
|
|
653
|
+
# IoU thresholds [0.5, 0.55, 0.6, 0.65, ..., 0.95]
|
|
654
|
+
self.iou_thrs = np.linspace(
|
|
655
|
+
0.5,
|
|
656
|
+
0.95,
|
|
657
|
+
int(np.round((0.95 - 0.5) / 0.05)) + 1,
|
|
658
|
+
endpoint=True,
|
|
659
|
+
dtype=np.float32,
|
|
660
|
+
)
|
|
661
|
+
# 101 recall thresholds [0.0, 0.01, 0.02, ..., 1.00]
|
|
662
|
+
self.rec_thrs = np.linspace(
|
|
663
|
+
0.0,
|
|
664
|
+
1.00,
|
|
665
|
+
int(np.round((1.00 - 0.0) / 0.01)) + 1,
|
|
666
|
+
endpoint=True,
|
|
667
|
+
dtype=np.float32,
|
|
668
|
+
)
|
|
669
|
+
# 3 maximum detection thresholds [1, 10, 100]
|
|
670
|
+
self.max_dets = [1, 10, 100]
|
|
671
|
+
# Area ranges [0, 1e5], [0, 32], [32, 96], [96, 1e5]
|
|
672
|
+
self.area_range: list[list[float]] = [
|
|
673
|
+
[0, MAX_ALL_OBJECT_AREA],
|
|
674
|
+
[0, SMALL_OBJECT_AREA],
|
|
675
|
+
[SMALL_OBJECT_AREA, MEDIUM_OBJECT_AREA],
|
|
676
|
+
[MEDIUM_OBJECT_AREA, MAX_ALL_OBJECT_AREA],
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
class COCOEvaluator:
|
|
681
|
+
"""
|
|
682
|
+
Evaluator class to compute COCO metrics.
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
def __init__(
|
|
686
|
+
self,
|
|
687
|
+
coco_targets: EvaluationDataset,
|
|
688
|
+
coco_predictions: EvaluationDataset,
|
|
689
|
+
metric_target: MetricTarget = MetricTarget.BOXES,
|
|
690
|
+
) -> None:
|
|
691
|
+
"""
|
|
692
|
+
Constructor of COCOEvaluator object.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
coco_targets: The dataset with the ground truths.
|
|
696
|
+
coco_predictions: The dataset with the predictions.
|
|
697
|
+
metric_target: The type of detection data used to compute the IoU -
|
|
698
|
+
boxes, masks or oriented bounding boxes.
|
|
699
|
+
"""
|
|
700
|
+
if coco_targets is None:
|
|
701
|
+
raise ValueError("coco_targets must be provided")
|
|
702
|
+
if coco_predictions is None:
|
|
703
|
+
raise ValueError("coco_predictions must be provided")
|
|
704
|
+
|
|
705
|
+
self.coco_targets = coco_targets
|
|
706
|
+
self.coco_predictions = coco_predictions
|
|
707
|
+
self.metric_target = metric_target
|
|
708
|
+
# List of dictionaries containing the evaluation results
|
|
709
|
+
# len(eval_imgs) = (categories) * (area_ranges) * (images)
|
|
710
|
+
# For COCO 2017: len(eval_images) = 80 * 4 * 5000 = 1600000
|
|
711
|
+
self.eval_imgs: list[_TypeEvaluationImageResult | None] = []
|
|
712
|
+
# Dictionary of accumulated results
|
|
713
|
+
self.results: dict[str, object] = {}
|
|
714
|
+
# Dictionary of targets for evaluation
|
|
715
|
+
self._targets: defaultdict[tuple[int, int], list[_TypeCocoDict]] = defaultdict(
|
|
716
|
+
list
|
|
717
|
+
)
|
|
718
|
+
self._predictions: defaultdict[tuple[int, int], list[_TypeCocoDict]] = (
|
|
719
|
+
defaultdict(list)
|
|
720
|
+
)
|
|
721
|
+
# Parameters for evaluation
|
|
722
|
+
self.params = COCOEvaluatorParameters()
|
|
723
|
+
# List of results summarization
|
|
724
|
+
self.stats: list[object] = []
|
|
725
|
+
# Dictionary of IOUs between all targets and predictions
|
|
726
|
+
self.ious: dict[tuple[int, int], npt.NDArray[np.float32]] = {}
|
|
727
|
+
# Set image and category ids
|
|
728
|
+
self.params.img_ids = sorted(self.coco_targets.get_image_ids())
|
|
729
|
+
self.params.cat_ids = sorted(self.coco_targets.get_category_ids())
|
|
730
|
+
|
|
731
|
+
def _prepare_targets_and_predictions(self) -> None:
|
|
732
|
+
"""
|
|
733
|
+
Prepare targets and predictions for evaluation.
|
|
734
|
+
"""
|
|
735
|
+
# Get the target samples for the evaluation
|
|
736
|
+
annotation_ids = self.coco_targets.get_annotation_ids(
|
|
737
|
+
img_ids=self.params.img_ids, cat_ids=self.params.cat_ids
|
|
738
|
+
)
|
|
739
|
+
targets = self.coco_targets.get_annotations(annotation_ids)
|
|
740
|
+
# Get the prediction samples for the evaluation
|
|
741
|
+
prediction_ids = self.coco_predictions.get_annotation_ids(
|
|
742
|
+
img_ids=self.params.img_ids, cat_ids=self.params.cat_ids
|
|
743
|
+
)
|
|
744
|
+
predictions = self.coco_predictions.get_annotations(prediction_ids)
|
|
745
|
+
|
|
746
|
+
# Set ignore flag
|
|
747
|
+
for gt in targets:
|
|
748
|
+
ignore = int(gt.get("ignore", 0))
|
|
749
|
+
iscrowd = int(gt.get("iscrowd", 0))
|
|
750
|
+
gt["ignore"] = int(bool(ignore or iscrowd))
|
|
751
|
+
|
|
752
|
+
# Select targets
|
|
753
|
+
self._targets = defaultdict(list)
|
|
754
|
+
for gt in targets:
|
|
755
|
+
self._targets[gt["image_id"], gt["category_id"]].append(gt)
|
|
756
|
+
|
|
757
|
+
# Select predictions
|
|
758
|
+
self._predictions = defaultdict(list)
|
|
759
|
+
for dt in predictions:
|
|
760
|
+
self._predictions[dt["image_id"], dt["category_id"]].append(dt)
|
|
761
|
+
|
|
762
|
+
# Initialize evaluation results
|
|
763
|
+
self.eval_imgs = []
|
|
764
|
+
self.results = {}
|
|
765
|
+
|
|
766
|
+
def _compute_iou(self, img_id: int, cat_id: int) -> npt.NDArray[np.float32]:
|
|
767
|
+
"""
|
|
768
|
+
Compute the IoU between the targets and predictions for a given image and
|
|
769
|
+
category, using boxes, masks or oriented bounding boxes depending on the
|
|
770
|
+
configured metric target.
|
|
771
|
+
|
|
772
|
+
Args:
|
|
773
|
+
img_id: The image id.
|
|
774
|
+
cat_id: The category id.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
The IoU between the targets and predictions.
|
|
778
|
+
"""
|
|
779
|
+
|
|
780
|
+
gt = self._targets[img_id, cat_id]
|
|
781
|
+
dt = self._predictions[img_id, cat_id]
|
|
782
|
+
|
|
783
|
+
# If there is nothing to evaluate
|
|
784
|
+
if len(gt) == 0 and len(dt) == 0:
|
|
785
|
+
empty_result: npt.NDArray[np.float32] = np.array([], dtype=np.float32)
|
|
786
|
+
return empty_result
|
|
787
|
+
|
|
788
|
+
# Sort predictions by highest score first
|
|
789
|
+
inds = np.argsort([-d["score"] for d in dt], kind="stable")
|
|
790
|
+
dt = [dt[i] for i in inds]
|
|
791
|
+
|
|
792
|
+
# Truncate the predictions if there are more predictions than the max detections
|
|
793
|
+
# to evaluate
|
|
794
|
+
if len(dt) > self.params.max_dets[-1]:
|
|
795
|
+
dt = dt[0 : self.params.max_dets[-1]]
|
|
796
|
+
|
|
797
|
+
# Get the iscrowd flag for each gt
|
|
798
|
+
is_crowd = [bool(o["iscrowd"]) for o in gt]
|
|
799
|
+
|
|
800
|
+
# Compute iou between each prediction and gt region
|
|
801
|
+
if self.metric_target == MetricTarget.MASKS:
|
|
802
|
+
iou = _mask_iou_with_jaccard(
|
|
803
|
+
[g["content"] for g in gt], [d["content"] for d in dt], is_crowd
|
|
804
|
+
)
|
|
805
|
+
elif self.metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
|
806
|
+
# Crowd regions are not supported for oriented boxes; the standard
|
|
807
|
+
# IoU is used for every ground truth.
|
|
808
|
+
if len(gt) == 0 or len(dt) == 0:
|
|
809
|
+
iou = np.empty((len(dt), len(gt)), dtype=np.float64)
|
|
810
|
+
else:
|
|
811
|
+
gt_obb = np.stack([g["content"] for g in gt])
|
|
812
|
+
dt_obb = np.stack([d["content"] for d in dt])
|
|
813
|
+
# oriented_box_iou_batch returns (gt, dt);
|
|
814
|
+
# the evaluator expects (dt, gt).
|
|
815
|
+
iou = oriented_box_iou_batch(gt_obb, dt_obb).T.astype(np.float64)
|
|
816
|
+
else:
|
|
817
|
+
gt_boxes = [g["bbox"] for g in gt]
|
|
818
|
+
dt_boxes = [d["bbox"] for d in dt]
|
|
819
|
+
iou = box_iou_batch_with_jaccard(gt_boxes, dt_boxes, is_crowd)
|
|
820
|
+
return iou.astype(np.float32)
|
|
821
|
+
|
|
822
|
+
def _evaluate_image(
|
|
823
|
+
self,
|
|
824
|
+
img_id: int,
|
|
825
|
+
cat_id: int,
|
|
826
|
+
area_range: list[float] | tuple[float, float],
|
|
827
|
+
max_det: int,
|
|
828
|
+
) -> _TypeEvaluationImageResult | None:
|
|
829
|
+
"""
|
|
830
|
+
Perform evaluation for single category and image.
|
|
831
|
+
Args:
|
|
832
|
+
img_id: The image id.
|
|
833
|
+
cat_id: The category id.
|
|
834
|
+
area_range: The area range.
|
|
835
|
+
max_det: The maximum number of detections.
|
|
836
|
+
|
|
837
|
+
Returns:
|
|
838
|
+
The evaluation results.
|
|
839
|
+
"""
|
|
840
|
+
# Get targets (gt) and predictions (dt) for the given image and category
|
|
841
|
+
gt: list[_TypeCocoDict] = self._targets[img_id, cat_id]
|
|
842
|
+
dt: list[_TypeCocoDict] = self._predictions[img_id, cat_id]
|
|
843
|
+
|
|
844
|
+
# If there is nothing to evaluate
|
|
845
|
+
if len(gt) == 0 and len(dt) == 0:
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
min_area, max_area = area_range
|
|
849
|
+
|
|
850
|
+
# Create an `_ignore` flag for targets if they are set as ignore or their area
|
|
851
|
+
# is not in the range [min_area, max_area]
|
|
852
|
+
for g in gt:
|
|
853
|
+
if g["ignore"] or not (min_area <= g["area"] <= max_area):
|
|
854
|
+
g["_ignore"] = 1
|
|
855
|
+
else:
|
|
856
|
+
g["_ignore"] = 0
|
|
857
|
+
|
|
858
|
+
# Sort ground-truths by ignore flag (0: non ignored, 1: ignored)
|
|
859
|
+
gt_sorted = np.argsort([g["_ignore"] for g in gt], kind="stable")
|
|
860
|
+
gt = [gt[i] for i in gt_sorted]
|
|
861
|
+
|
|
862
|
+
# Sort predictions by scores in descending order
|
|
863
|
+
dt_sorted = np.argsort([-d["score"] for d in dt], kind="stable")
|
|
864
|
+
dt = [dt[i] for i in dt_sorted[0:max_det]]
|
|
865
|
+
|
|
866
|
+
# Load computed ious for the given image and category
|
|
867
|
+
ious = (
|
|
868
|
+
self.ious[img_id, cat_id][:, gt_sorted]
|
|
869
|
+
if len(self.ious[img_id, cat_id]) > 0
|
|
870
|
+
else self.ious[img_id, cat_id]
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# Get the number of thresholds, ground truths and detections
|
|
874
|
+
num_thresholds = len(self.params.iou_thrs)
|
|
875
|
+
num_ground_truths = len(gt)
|
|
876
|
+
num_detections = len(dt)
|
|
877
|
+
|
|
878
|
+
# Initialize matches: 0 means no match
|
|
879
|
+
gt_matches = np.zeros((num_thresholds, num_ground_truths), dtype=np.int64)
|
|
880
|
+
dt_matches = np.zeros((num_thresholds, num_detections), dtype=np.int64)
|
|
881
|
+
# Initialize ignore flags: 0 means no ignore
|
|
882
|
+
gt_ignore = np.array([g["_ignore"] for g in gt], dtype=np.int64)
|
|
883
|
+
dt_ignore = np.zeros((num_thresholds, num_detections), dtype=np.bool_)
|
|
884
|
+
if len(ious) != 0:
|
|
885
|
+
# Go through the iou thresholds
|
|
886
|
+
for tresh_idx, thresh in enumerate(self.params.iou_thrs):
|
|
887
|
+
# Go through the detections
|
|
888
|
+
for det_idx, det in enumerate(dt):
|
|
889
|
+
# Start the iou of the best match
|
|
890
|
+
iou_best_match = min([thresh, 1 - 1e-10])
|
|
891
|
+
# Set the best match index to -1 (unmatched)
|
|
892
|
+
best_match_idx = -1
|
|
893
|
+
# Go through the ground truths
|
|
894
|
+
for g_idx, g in enumerate(gt):
|
|
895
|
+
# If current gt is already matched, and not a crowd, continue
|
|
896
|
+
# if gt_matches[tresh_idx, g_idx] > 0 and not iscrowd[g_idx]:
|
|
897
|
+
iscrowd = int(g.get("iscrowd", 0))
|
|
898
|
+
if gt_matches[tresh_idx, g_idx] > 0 and not iscrowd:
|
|
899
|
+
continue
|
|
900
|
+
# Stop searching the ground truths
|
|
901
|
+
if (
|
|
902
|
+
best_match_idx > -1 # detection is matched to a gt
|
|
903
|
+
and gt_ignore[best_match_idx]
|
|
904
|
+
== 0 # matched gt is not ignored
|
|
905
|
+
and gt_ignore[g_idx] == 1 # current gt is ignored
|
|
906
|
+
):
|
|
907
|
+
break
|
|
908
|
+
|
|
909
|
+
# A new best match was found
|
|
910
|
+
if ious[det_idx, g_idx] >= iou_best_match:
|
|
911
|
+
iou_best_match = ious[det_idx, g_idx]
|
|
912
|
+
best_match_idx = g_idx
|
|
913
|
+
|
|
914
|
+
# A best match was found
|
|
915
|
+
if best_match_idx != -1:
|
|
916
|
+
dt_ignore[tresh_idx, det_idx] = gt_ignore[best_match_idx]
|
|
917
|
+
dt_matches[tresh_idx, det_idx] = gt[best_match_idx]["id"]
|
|
918
|
+
gt_matches[tresh_idx, best_match_idx] = det["id"]
|
|
919
|
+
|
|
920
|
+
# Set unmatched detections outside of area range to ignore
|
|
921
|
+
area_range_mask = np.array(
|
|
922
|
+
[d["area"] < min_area or d["area"] > max_area for d in dt]
|
|
923
|
+
).reshape((1, len(dt)))
|
|
924
|
+
|
|
925
|
+
# Update the ignore flags for detections
|
|
926
|
+
dt_ignore = np.logical_or(
|
|
927
|
+
dt_ignore,
|
|
928
|
+
np.logical_and(
|
|
929
|
+
dt_matches == 0, np.repeat(area_range_mask, num_thresholds, 0)
|
|
930
|
+
),
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
"image_id": img_id,
|
|
935
|
+
"category_id": cat_id,
|
|
936
|
+
"area_range": area_range,
|
|
937
|
+
"max_det": max_det,
|
|
938
|
+
"dt_ids": [d["id"] for d in dt],
|
|
939
|
+
"gt_ids": [g["id"] for g in gt],
|
|
940
|
+
"dtMatches": dt_matches,
|
|
941
|
+
"gtMatches": gt_matches,
|
|
942
|
+
"dtScores": [d["score"] for d in dt],
|
|
943
|
+
"gtIgnore": gt_ignore,
|
|
944
|
+
"dtIgnore": dt_ignore,
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
def _accumulate(self) -> None:
|
|
948
|
+
"""
|
|
949
|
+
Accumulate per image evaluation results and store the result in self.results
|
|
950
|
+
"""
|
|
951
|
+
# Get the number of thresholds, categories, area ranges, and max detections
|
|
952
|
+
num_iou_thresholds = len(self.params.iou_thrs)
|
|
953
|
+
num_recall_thresholds = len(self.params.rec_thrs)
|
|
954
|
+
num_categories = len(self.params.cat_ids)
|
|
955
|
+
num_area_ranges = len(self.params.area_range)
|
|
956
|
+
num_max_detections = len(self.params.max_dets)
|
|
957
|
+
num_imgs = len(self.params.img_ids)
|
|
958
|
+
|
|
959
|
+
# Initialize precision, recall, and scores arrays
|
|
960
|
+
# -1 means absent categories
|
|
961
|
+
precision = -np.ones(
|
|
962
|
+
(
|
|
963
|
+
num_iou_thresholds,
|
|
964
|
+
num_recall_thresholds,
|
|
965
|
+
num_categories,
|
|
966
|
+
num_area_ranges,
|
|
967
|
+
num_max_detections,
|
|
968
|
+
),
|
|
969
|
+
dtype=np.float32,
|
|
970
|
+
)
|
|
971
|
+
recall = -np.ones(
|
|
972
|
+
(num_iou_thresholds, num_categories, num_area_ranges, num_max_detections),
|
|
973
|
+
dtype=np.float32,
|
|
974
|
+
)
|
|
975
|
+
scores = -np.ones(
|
|
976
|
+
(
|
|
977
|
+
num_iou_thresholds,
|
|
978
|
+
num_recall_thresholds,
|
|
979
|
+
num_categories,
|
|
980
|
+
num_area_ranges,
|
|
981
|
+
num_max_detections,
|
|
982
|
+
),
|
|
983
|
+
dtype=np.float32,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
# Create sets for indexing
|
|
987
|
+
set_categories = set(self.params.cat_ids)
|
|
988
|
+
set_area_ranges: set[tuple[float, ...]] = {
|
|
989
|
+
tuple(a) for a in self.params.area_range
|
|
990
|
+
}
|
|
991
|
+
set_max_detections = set(self.params.max_dets)
|
|
992
|
+
set_image_ids = set(self.params.img_ids)
|
|
993
|
+
|
|
994
|
+
# Select category indexes to evaluate
|
|
995
|
+
selected_category_ids = [
|
|
996
|
+
n for n, k in enumerate(self.params.cat_ids) if k in set_categories
|
|
997
|
+
]
|
|
998
|
+
# Select max detections to evaluate
|
|
999
|
+
selected_max_detections = [
|
|
1000
|
+
m for m in self.params.max_dets if m in set_max_detections
|
|
1001
|
+
]
|
|
1002
|
+
# Select area ranges to evaluate
|
|
1003
|
+
selected_area_ranges_ids = [
|
|
1004
|
+
idx
|
|
1005
|
+
for idx, area in enumerate(self.params.area_range)
|
|
1006
|
+
if tuple(area) in set_area_ranges
|
|
1007
|
+
]
|
|
1008
|
+
# Select image indexes to evaluate
|
|
1009
|
+
image_inds = [
|
|
1010
|
+
n for n, i in enumerate(self.params.img_ids) if i in set_image_ids
|
|
1011
|
+
]
|
|
1012
|
+
|
|
1013
|
+
# Evaluating at all categories, area ranges, max number of detections, and
|
|
1014
|
+
# IoU thresholds
|
|
1015
|
+
|
|
1016
|
+
# Loop through categories
|
|
1017
|
+
for cat_idx, cat_eval_idx in enumerate(selected_category_ids):
|
|
1018
|
+
cat_offset = cat_eval_idx * num_area_ranges * num_imgs
|
|
1019
|
+
|
|
1020
|
+
# Loop through area ranges
|
|
1021
|
+
for area_idx, area_eval_idx in enumerate(selected_area_ranges_ids):
|
|
1022
|
+
area_offset = area_eval_idx * num_imgs
|
|
1023
|
+
|
|
1024
|
+
# Loop through max detections
|
|
1025
|
+
for max_det_idx, max_det in enumerate(selected_max_detections):
|
|
1026
|
+
eval_img_data_raw = [
|
|
1027
|
+
self.eval_imgs[cat_offset + area_offset + i] for i in image_inds
|
|
1028
|
+
]
|
|
1029
|
+
eval_img_data: list[_TypeEvaluationImageResult] = [
|
|
1030
|
+
e for e in eval_img_data_raw if e is not None
|
|
1031
|
+
]
|
|
1032
|
+
|
|
1033
|
+
# No image to evaluate
|
|
1034
|
+
if len(eval_img_data) == 0:
|
|
1035
|
+
continue
|
|
1036
|
+
|
|
1037
|
+
# Sort detected scores in descending order
|
|
1038
|
+
dt_scores = np.concatenate(
|
|
1039
|
+
[e["dtScores"][0:max_det] for e in eval_img_data]
|
|
1040
|
+
)
|
|
1041
|
+
inds = np.argsort(-dt_scores, kind="stable")
|
|
1042
|
+
dt_scores_sorted = dt_scores[inds]
|
|
1043
|
+
|
|
1044
|
+
# Get matches and ignored matches
|
|
1045
|
+
dt_matches = np.concatenate(
|
|
1046
|
+
[e["dtMatches"][:, 0:max_det] for e in eval_img_data], axis=1
|
|
1047
|
+
)[:, inds]
|
|
1048
|
+
dt_ignored = np.concatenate(
|
|
1049
|
+
[e["dtIgnore"][:, 0:max_det] for e in eval_img_data], axis=1
|
|
1050
|
+
)[:, inds]
|
|
1051
|
+
|
|
1052
|
+
# Get ignored ground truth objects
|
|
1053
|
+
gt_ignored = np.concatenate([e["gtIgnore"] for e in eval_img_data])
|
|
1054
|
+
num_non_ignored_gt = np.count_nonzero(gt_ignored == 0)
|
|
1055
|
+
|
|
1056
|
+
# No ground truth objects to evaluate
|
|
1057
|
+
if num_non_ignored_gt == 0:
|
|
1058
|
+
continue
|
|
1059
|
+
|
|
1060
|
+
# Compute true positives and false positives
|
|
1061
|
+
true_positives = np.logical_and(
|
|
1062
|
+
dt_matches, np.logical_not(dt_ignored)
|
|
1063
|
+
)
|
|
1064
|
+
false_positives = np.logical_and(
|
|
1065
|
+
np.logical_not(dt_matches), np.logical_not(dt_ignored)
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
tp_sum = np.cumsum(true_positives, axis=1).astype(dtype=np.float32)
|
|
1069
|
+
fp_sum = np.cumsum(false_positives, axis=1).astype(dtype=np.float32)
|
|
1070
|
+
|
|
1071
|
+
# Loop through thresholds
|
|
1072
|
+
for iou_thresh_idx, (tp, fp) in enumerate(zip(tp_sum, fp_sum)):
|
|
1073
|
+
tp = np.array(tp)
|
|
1074
|
+
fp = np.array(fp)
|
|
1075
|
+
num_tps = len(tp)
|
|
1076
|
+
# Recall: TP / Total number of ground truth objects
|
|
1077
|
+
rc = tp / np.float32(num_non_ignored_gt)
|
|
1078
|
+
# Precision: TP / (FP + TP)
|
|
1079
|
+
pr = (tp / (fp + tp + EPS)).tolist()
|
|
1080
|
+
# List to compute the precision at each recall threshold
|
|
1081
|
+
precision_at_recall = [0.0] * num_recall_thresholds
|
|
1082
|
+
# List to compute the score at each recall threshold
|
|
1083
|
+
score_at_recall = [0.0] * num_recall_thresholds
|
|
1084
|
+
|
|
1085
|
+
# Set recall to either the final recall value or 0 (when there
|
|
1086
|
+
# is no TP)
|
|
1087
|
+
recall[iou_thresh_idx, cat_idx, area_idx, max_det_idx] = (
|
|
1088
|
+
rc[-1] if num_tps else 0
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
# Loop through precision values
|
|
1092
|
+
for i in range(num_tps - 1, 0, -1):
|
|
1093
|
+
if pr[i] > pr[i - 1]:
|
|
1094
|
+
pr[i - 1] = pr[i]
|
|
1095
|
+
|
|
1096
|
+
recall_inds: npt.NDArray[np.int_] = np.searchsorted(
|
|
1097
|
+
rc, self.params.rec_thrs, side="left"
|
|
1098
|
+
)
|
|
1099
|
+
recall_inds_list: list[int] = recall_inds.tolist()
|
|
1100
|
+
for ri, pos_idx_value in enumerate(recall_inds_list):
|
|
1101
|
+
# Ensure pi is within the range of both arrays
|
|
1102
|
+
pos_idx_int: int = int(pos_idx_value)
|
|
1103
|
+
if 0 <= pos_idx_int < len(pr) and 0 <= pos_idx_int < len(
|
|
1104
|
+
dt_scores_sorted
|
|
1105
|
+
):
|
|
1106
|
+
precision_at_recall[ri] = pr[pos_idx_int]
|
|
1107
|
+
score_at_recall[ri] = dt_scores_sorted[pos_idx_int]
|
|
1108
|
+
|
|
1109
|
+
# Convert precision to numpy array
|
|
1110
|
+
precision[iou_thresh_idx, :, cat_idx, area_idx, max_det_idx] = (
|
|
1111
|
+
np.array(precision_at_recall, dtype=np.float32)
|
|
1112
|
+
)
|
|
1113
|
+
# Convert scores to numpy array
|
|
1114
|
+
scores[iou_thresh_idx, :, cat_idx, area_idx, max_det_idx] = (
|
|
1115
|
+
np.array(score_at_recall, dtype=np.float32)
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
self.results = {
|
|
1119
|
+
"params": self.params,
|
|
1120
|
+
"counts": [
|
|
1121
|
+
num_iou_thresholds,
|
|
1122
|
+
num_recall_thresholds,
|
|
1123
|
+
num_categories,
|
|
1124
|
+
num_area_ranges,
|
|
1125
|
+
num_max_detections,
|
|
1126
|
+
],
|
|
1127
|
+
"date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
1128
|
+
"precision": precision,
|
|
1129
|
+
"recall": recall,
|
|
1130
|
+
"scores": scores,
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
# Helper function to compute average precision while handling -1 sentinel values
|
|
1134
|
+
def compute_average_precision(
|
|
1135
|
+
precision_slice: npt.NDArray[np.float32],
|
|
1136
|
+
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
|
|
1137
|
+
"""Compute average precision while handling -1 sentinel values."""
|
|
1138
|
+
valid_mask = precision_slice != -1
|
|
1139
|
+
valid_precision = np.where(valid_mask, precision_slice, np.float32(0.0))
|
|
1140
|
+
|
|
1141
|
+
def mean_with_mask(
|
|
1142
|
+
axis: int | tuple[int, ...],
|
|
1143
|
+
) -> npt.NDArray[np.float64]:
|
|
1144
|
+
sums = valid_precision.sum(axis=axis, dtype=np.float64)
|
|
1145
|
+
counts = valid_mask.sum(axis=axis)
|
|
1146
|
+
means: npt.NDArray[np.float64] = np.divide(
|
|
1147
|
+
sums,
|
|
1148
|
+
counts,
|
|
1149
|
+
out=np.full(sums.shape, -1.0, dtype=np.float64),
|
|
1150
|
+
where=counts > 0,
|
|
1151
|
+
)
|
|
1152
|
+
return means
|
|
1153
|
+
|
|
1154
|
+
mAP_scores = mean_with_mask((1, 2))
|
|
1155
|
+
ap_per_class = mean_with_mask(1).transpose(1, 0)
|
|
1156
|
+
return mAP_scores, ap_per_class
|
|
1157
|
+
|
|
1158
|
+
# Average precision over all sizes, 100 max detections
|
|
1159
|
+
area_range_idx = list(ObjectSize).index(ObjectSize.ALL)
|
|
1160
|
+
max_100_dets_idx = self.params.max_dets.index(100)
|
|
1161
|
+
# Average precision [threshold, recall, classes]
|
|
1162
|
+
average_precision_all_sizes = precision[
|
|
1163
|
+
:, :, :, area_range_idx, max_100_dets_idx
|
|
1164
|
+
]
|
|
1165
|
+
# mAP over thresholds (dimension=num_thresholds)
|
|
1166
|
+
# Exclude -1 sentinel values when computing mean
|
|
1167
|
+
mAP_scores_all_sizes, ap_per_class_all_sizes = compute_average_precision(
|
|
1168
|
+
average_precision_all_sizes
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
# Average precision for SMALL objects and 100 max detections
|
|
1172
|
+
small_area_range_idx = list(ObjectSize).index(ObjectSize.SMALL)
|
|
1173
|
+
average_precision_small = precision[
|
|
1174
|
+
:, :, :, small_area_range_idx, max_100_dets_idx
|
|
1175
|
+
]
|
|
1176
|
+
mAP_scores_small, ap_per_class_small = compute_average_precision(
|
|
1177
|
+
average_precision_small
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# Average precision for MEDIUM objects and 100 max detections
|
|
1181
|
+
medium_area_range_idx = list(ObjectSize).index(ObjectSize.MEDIUM)
|
|
1182
|
+
average_precision_medium = precision[
|
|
1183
|
+
:, :, :, medium_area_range_idx, max_100_dets_idx
|
|
1184
|
+
]
|
|
1185
|
+
mAP_scores_medium, ap_per_class_medium = compute_average_precision(
|
|
1186
|
+
average_precision_medium
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
# Average precision for LARGE objects and 100 max detections
|
|
1190
|
+
large_area_range_idx = list(ObjectSize).index(ObjectSize.LARGE)
|
|
1191
|
+
average_precision_large = precision[
|
|
1192
|
+
:, :, :, large_area_range_idx, max_100_dets_idx
|
|
1193
|
+
]
|
|
1194
|
+
mAP_scores_large, ap_per_class_large = compute_average_precision(
|
|
1195
|
+
average_precision_large
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
self.results = {
|
|
1199
|
+
"params": self.params,
|
|
1200
|
+
"counts": [
|
|
1201
|
+
num_iou_thresholds,
|
|
1202
|
+
num_recall_thresholds,
|
|
1203
|
+
num_categories,
|
|
1204
|
+
num_area_ranges,
|
|
1205
|
+
num_max_detections,
|
|
1206
|
+
],
|
|
1207
|
+
"date": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
1208
|
+
"precision": precision,
|
|
1209
|
+
"recall": recall,
|
|
1210
|
+
"scores": scores,
|
|
1211
|
+
"mAP_scores_all_sizes": mAP_scores_all_sizes,
|
|
1212
|
+
"ap_per_class_all_sizes": ap_per_class_all_sizes,
|
|
1213
|
+
"mAP_scores_small": mAP_scores_small,
|
|
1214
|
+
"ap_per_class_small": ap_per_class_small,
|
|
1215
|
+
"mAP_scores_medium": mAP_scores_medium,
|
|
1216
|
+
"ap_per_class_medium": ap_per_class_medium,
|
|
1217
|
+
"mAP_scores_large": mAP_scores_large,
|
|
1218
|
+
"ap_per_class_large": ap_per_class_large,
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
def _pycocotools_summarize(self) -> None:
|
|
1222
|
+
"""
|
|
1223
|
+
Compute and display summary metrics for evaluation results.
|
|
1224
|
+
"""
|
|
1225
|
+
|
|
1226
|
+
def _summarize(
|
|
1227
|
+
use_ap: bool = True,
|
|
1228
|
+
iou_thr: float | None = None,
|
|
1229
|
+
area_range: ObjectSize = ObjectSize.ALL,
|
|
1230
|
+
max_dets: int = 100,
|
|
1231
|
+
) -> float:
|
|
1232
|
+
iStr = " {:<18} {} @[ IoU={:<9} | area={:>6s} | maxDets={:>3d} ] = {:0.10f}"
|
|
1233
|
+
titleStr = "Average Precision" if use_ap else "Average Recall"
|
|
1234
|
+
typeStr = "(AP)" if use_ap else "(AR)"
|
|
1235
|
+
iou_str = (
|
|
1236
|
+
f"{self.params.iou_thrs[0]:0.2f}:{self.params.iou_thrs[-1]:0.2f}"
|
|
1237
|
+
if iou_thr is None
|
|
1238
|
+
else f"{iou_thr:0.2f}"
|
|
1239
|
+
)
|
|
1240
|
+
all_object_sizes = list(ObjectSize)
|
|
1241
|
+
area_range_idx = all_object_sizes.index(area_range)
|
|
1242
|
+
max_detections_idx = self.params.max_dets.index(max_dets)
|
|
1243
|
+
if use_ap:
|
|
1244
|
+
# Dimension of precision:
|
|
1245
|
+
# threshold x recall x classes x areas x max detections
|
|
1246
|
+
s: npt.NDArray[np.float32] = np.asarray(
|
|
1247
|
+
self.results["precision"], dtype=np.float32
|
|
1248
|
+
)
|
|
1249
|
+
# IOU
|
|
1250
|
+
if iou_thr is not None:
|
|
1251
|
+
t = np.where(iou_thr == self.params.iou_thrs)[0]
|
|
1252
|
+
s = s[t]
|
|
1253
|
+
s = s[:, :, :, area_range_idx, max_detections_idx]
|
|
1254
|
+
else:
|
|
1255
|
+
# Dimension of recall:
|
|
1256
|
+
# threshold x classes x areas x max detections
|
|
1257
|
+
s = np.asarray(self.results["recall"], dtype=np.float32)
|
|
1258
|
+
if iou_thr is not None:
|
|
1259
|
+
t = np.where(iou_thr == self.params.iou_thrs)[0]
|
|
1260
|
+
s = s[t]
|
|
1261
|
+
s = s[:, :, area_range_idx, max_detections_idx]
|
|
1262
|
+
if len(s[s > -1]) == 0:
|
|
1263
|
+
mean_s = -1.0
|
|
1264
|
+
else:
|
|
1265
|
+
mean_s = float(np.mean(s[s > -1]))
|
|
1266
|
+
logger.info(
|
|
1267
|
+
iStr.format(titleStr, typeStr, iou_str, area_range, max_dets, mean_s)
|
|
1268
|
+
)
|
|
1269
|
+
return mean_s
|
|
1270
|
+
|
|
1271
|
+
def _summarize_predictions() -> npt.NDArray[np.float32]:
|
|
1272
|
+
stats: npt.NDArray[np.float32] = np.zeros((12,), dtype=np.float32)
|
|
1273
|
+
stats[0] = _summarize(use_ap=True)
|
|
1274
|
+
stats[1] = _summarize(
|
|
1275
|
+
use_ap=True, iou_thr=0.5, max_dets=self.params.max_dets[2]
|
|
1276
|
+
)
|
|
1277
|
+
stats[2] = _summarize(
|
|
1278
|
+
use_ap=True, iou_thr=0.75, max_dets=self.params.max_dets[2]
|
|
1279
|
+
)
|
|
1280
|
+
stats[3] = _summarize(
|
|
1281
|
+
use_ap=True,
|
|
1282
|
+
area_range=ObjectSize.SMALL,
|
|
1283
|
+
max_dets=self.params.max_dets[2],
|
|
1284
|
+
)
|
|
1285
|
+
stats[4] = _summarize(
|
|
1286
|
+
use_ap=True,
|
|
1287
|
+
area_range=ObjectSize.MEDIUM,
|
|
1288
|
+
max_dets=self.params.max_dets[2],
|
|
1289
|
+
)
|
|
1290
|
+
stats[5] = _summarize(
|
|
1291
|
+
use_ap=True,
|
|
1292
|
+
area_range=ObjectSize.LARGE,
|
|
1293
|
+
max_dets=self.params.max_dets[2],
|
|
1294
|
+
)
|
|
1295
|
+
stats[6] = _summarize(use_ap=False, max_dets=self.params.max_dets[0])
|
|
1296
|
+
stats[7] = _summarize(use_ap=False, max_dets=self.params.max_dets[1])
|
|
1297
|
+
stats[8] = _summarize(use_ap=False, max_dets=self.params.max_dets[2])
|
|
1298
|
+
stats[9] = _summarize(
|
|
1299
|
+
use_ap=False,
|
|
1300
|
+
area_range=ObjectSize.SMALL,
|
|
1301
|
+
max_dets=self.params.max_dets[2],
|
|
1302
|
+
)
|
|
1303
|
+
stats[10] = _summarize(
|
|
1304
|
+
use_ap=False,
|
|
1305
|
+
area_range=ObjectSize.MEDIUM,
|
|
1306
|
+
max_dets=self.params.max_dets[2],
|
|
1307
|
+
)
|
|
1308
|
+
stats[11] = _summarize(
|
|
1309
|
+
use_ap=False,
|
|
1310
|
+
area_range=ObjectSize.LARGE,
|
|
1311
|
+
max_dets=self.params.max_dets[2],
|
|
1312
|
+
)
|
|
1313
|
+
return stats
|
|
1314
|
+
|
|
1315
|
+
if len(self.results) != 0:
|
|
1316
|
+
self.stats = _summarize_predictions().tolist()
|
|
1317
|
+
|
|
1318
|
+
def evaluate(self) -> None:
|
|
1319
|
+
"""
|
|
1320
|
+
Start the per image evaluation on all images and keeep results in
|
|
1321
|
+
self.eval_imgs (a list of dictionaries).
|
|
1322
|
+
"""
|
|
1323
|
+
# Select all parameters to evaluate
|
|
1324
|
+
self.params.img_ids = list(np.unique(self.params.img_ids))
|
|
1325
|
+
self.params.cat_ids = list(np.unique(self.params.cat_ids))
|
|
1326
|
+
self.params.max_dets = sorted(self.params.max_dets)
|
|
1327
|
+
|
|
1328
|
+
self._prepare_targets_and_predictions()
|
|
1329
|
+
|
|
1330
|
+
# Compute IOUs between all targets and predictions for all images and categories
|
|
1331
|
+
self.ious = {
|
|
1332
|
+
(img_id, cat_id): self._compute_iou(img_id, cat_id)
|
|
1333
|
+
for img_id in self.params.img_ids
|
|
1334
|
+
for cat_id in self.params.cat_ids
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
# Select the largest max area (the last element containing 100 dets
|
|
1338
|
+
max_det = self.params.max_dets[-1]
|
|
1339
|
+
|
|
1340
|
+
# Evaluate each image with all categories, area range and max detections
|
|
1341
|
+
self.eval_imgs = [
|
|
1342
|
+
self._evaluate_image(img_id, cat_id, area_range, max_det)
|
|
1343
|
+
for cat_id in self.params.cat_ids
|
|
1344
|
+
for area_range in self.params.area_range
|
|
1345
|
+
for img_id in self.params.img_ids
|
|
1346
|
+
]
|
|
1347
|
+
|
|
1348
|
+
# Accumulate results
|
|
1349
|
+
self._accumulate()
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
class MeanAveragePrecision(Metric[MeanAveragePrecisionResult]):
|
|
1353
|
+
"""
|
|
1354
|
+
Mean Average Precision (mAP) is a metric used to evaluate object detection models.
|
|
1355
|
+
It is the average of the precision-recall curves at different IoU thresholds.
|
|
1356
|
+
|
|
1357
|
+
Examples:
|
|
1358
|
+
```pycon
|
|
1359
|
+
>>> import numpy as np
|
|
1360
|
+
>>> import supervision as sv
|
|
1361
|
+
>>> from supervision.metrics import MeanAveragePrecision
|
|
1362
|
+
>>> predictions = sv.Detections(
|
|
1363
|
+
... xyxy=np.array([[0, 0, 10, 10]]),
|
|
1364
|
+
... class_id=np.array([0]),
|
|
1365
|
+
... confidence=np.array([0.9])
|
|
1366
|
+
... )
|
|
1367
|
+
>>> targets = sv.Detections(
|
|
1368
|
+
... xyxy=np.array([[0, 0, 10, 10]]),
|
|
1369
|
+
... class_id=np.array([0])
|
|
1370
|
+
... )
|
|
1371
|
+
>>> map_metric = MeanAveragePrecision()
|
|
1372
|
+
>>> map_result = map_metric.update(predictions, targets).compute()
|
|
1373
|
+
>>> round(float(map_result.map50), 2)
|
|
1374
|
+
1.0
|
|
1375
|
+
>>> print(map_result)
|
|
1376
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= all | maxDets=100 ] = 1.000
|
|
1377
|
+
Average Precision (AP) @[ IoU=0.50 | area= all | maxDets=100 ] = 1.000
|
|
1378
|
+
Average Precision (AP) @[ IoU=0.75 | area= all | maxDets=100 ] = 1.000
|
|
1379
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 1.000
|
|
1380
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = -1.000
|
|
1381
|
+
Average Precision (AP) @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = -1.000
|
|
1382
|
+
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
{ align=center width="800" }
|
|
1388
|
+
"""
|
|
1389
|
+
|
|
1390
|
+
def __init__(
|
|
1391
|
+
self,
|
|
1392
|
+
metric_target: MetricTarget = MetricTarget.BOXES,
|
|
1393
|
+
class_agnostic: bool = False,
|
|
1394
|
+
class_mapping: dict[int, int] | None = None,
|
|
1395
|
+
image_indices: list[int] | None = None,
|
|
1396
|
+
) -> None:
|
|
1397
|
+
"""
|
|
1398
|
+
Initialize the Mean Average Precision metric.
|
|
1399
|
+
|
|
1400
|
+
Args:
|
|
1401
|
+
metric_target: The type of detection data to use.
|
|
1402
|
+
class_agnostic: Whether to treat all data as a single class.
|
|
1403
|
+
class_mapping: A dictionary to map class IDs to new IDs.
|
|
1404
|
+
image_indices: The indices of the images to use.
|
|
1405
|
+
"""
|
|
1406
|
+
self._metric_target = metric_target
|
|
1407
|
+
self._class_agnostic = class_agnostic
|
|
1408
|
+
|
|
1409
|
+
self._predictions_list: list[Detections] = []
|
|
1410
|
+
self._targets_list: list[Detections] = []
|
|
1411
|
+
self._class_mapping = class_mapping
|
|
1412
|
+
self._image_indices = image_indices
|
|
1413
|
+
|
|
1414
|
+
def reset(self) -> None:
|
|
1415
|
+
"""
|
|
1416
|
+
Reset the metric to its initial state, clearing all stored data.
|
|
1417
|
+
"""
|
|
1418
|
+
self._predictions_list = []
|
|
1419
|
+
self._targets_list = []
|
|
1420
|
+
|
|
1421
|
+
def update(
|
|
1422
|
+
self,
|
|
1423
|
+
predictions: Detections | list[Detections],
|
|
1424
|
+
targets: Detections | list[Detections],
|
|
1425
|
+
) -> MeanAveragePrecision:
|
|
1426
|
+
"""
|
|
1427
|
+
Add new predictions and targets to the metric, but do not compute the result.
|
|
1428
|
+
|
|
1429
|
+
Args:
|
|
1430
|
+
predictions: The predicted detections.
|
|
1431
|
+
targets: The ground-truth detections.
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
The updated metric instance.
|
|
1435
|
+
"""
|
|
1436
|
+
if not isinstance(predictions, list):
|
|
1437
|
+
predictions = [predictions]
|
|
1438
|
+
if not isinstance(targets, list):
|
|
1439
|
+
targets = [targets]
|
|
1440
|
+
|
|
1441
|
+
if len(predictions) != len(targets):
|
|
1442
|
+
raise ValueError(
|
|
1443
|
+
f"The number of predictions ({len(predictions)}) and"
|
|
1444
|
+
f" targets ({len(targets)}) during the update must be the same."
|
|
1445
|
+
)
|
|
1446
|
+
|
|
1447
|
+
if self._class_agnostic:
|
|
1448
|
+
predictions = deepcopy(predictions)
|
|
1449
|
+
targets = deepcopy(targets)
|
|
1450
|
+
|
|
1451
|
+
for prediction in predictions:
|
|
1452
|
+
if prediction.class_id is not None:
|
|
1453
|
+
prediction.class_id[:] = -1
|
|
1454
|
+
for target in targets:
|
|
1455
|
+
if target.class_id is not None:
|
|
1456
|
+
target.class_id[:] = -1
|
|
1457
|
+
|
|
1458
|
+
self._predictions_list.extend(predictions)
|
|
1459
|
+
self._targets_list.extend(targets)
|
|
1460
|
+
|
|
1461
|
+
return self
|
|
1462
|
+
|
|
1463
|
+
def _detections_content(self, detections: Detections) -> npt.NDArray[Any] | None:
|
|
1464
|
+
"""Return per-detection masks or oriented boxes for the metric target,
|
|
1465
|
+
or `None` for the box target and for empty detections."""
|
|
1466
|
+
if self._metric_target == MetricTarget.BOXES or len(detections) == 0:
|
|
1467
|
+
return None
|
|
1468
|
+
if self._metric_target == MetricTarget.MASKS:
|
|
1469
|
+
if detections.mask is None:
|
|
1470
|
+
raise ValueError(
|
|
1471
|
+
"MeanAveragePrecision with `MetricTarget.MASKS` requires"
|
|
1472
|
+
" masks on both predictions and targets."
|
|
1473
|
+
)
|
|
1474
|
+
return np.asarray(detections.mask).astype(bool)
|
|
1475
|
+
if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES:
|
|
1476
|
+
obb = detections.data.get(ORIENTED_BOX_COORDINATES)
|
|
1477
|
+
if obb is None:
|
|
1478
|
+
raise ValueError(
|
|
1479
|
+
"MeanAveragePrecision with"
|
|
1480
|
+
" `MetricTarget.ORIENTED_BOUNDING_BOXES` requires"
|
|
1481
|
+
f" `{ORIENTED_BOX_COORDINATES}` in `data` on both"
|
|
1482
|
+
" predictions and targets."
|
|
1483
|
+
)
|
|
1484
|
+
return np.asarray(obb, dtype=np.float32).reshape(-1, 4, 2)
|
|
1485
|
+
raise ValueError(f"Invalid metric target: {self._metric_target}")
|
|
1486
|
+
|
|
1487
|
+
def _content_area(
|
|
1488
|
+
self, xywh: list[float], content: npt.NDArray[Any] | None, idx: int
|
|
1489
|
+
) -> float:
|
|
1490
|
+
"""Compute the default annotation area for the metric target: bbox area
|
|
1491
|
+
for boxes, pixel count for masks, polygon area for oriented boxes."""
|
|
1492
|
+
if content is None:
|
|
1493
|
+
return float(xywh[2] * xywh[3])
|
|
1494
|
+
if self._metric_target == MetricTarget.MASKS:
|
|
1495
|
+
return float(np.count_nonzero(content[idx]))
|
|
1496
|
+
x, y = content[idx, :, 0], content[idx, :, 1]
|
|
1497
|
+
# Shoelace formula
|
|
1498
|
+
return float(0.5 * abs(np.sum(x * np.roll(y, -1) - np.roll(x, -1) * y)))
|
|
1499
|
+
|
|
1500
|
+
def _prepare_targets(
|
|
1501
|
+
self, targets: list[Detections]
|
|
1502
|
+
) -> dict[str, list[_TypeCocoDict]]:
|
|
1503
|
+
"""Transform targets into a dictionary that can be used by the COCO evaluator"""
|
|
1504
|
+
images: list[_TypeCocoDict] = [{"id": img_id} for img_id in range(len(targets))]
|
|
1505
|
+
if self._image_indices is not None:
|
|
1506
|
+
images = [{"id": self._image_indices[img["id"]]} for img in images]
|
|
1507
|
+
# Annotations list
|
|
1508
|
+
annotations: list[_TypeCocoDict] = []
|
|
1509
|
+
for image_id, image_targets in enumerate(targets):
|
|
1510
|
+
if self._image_indices is not None:
|
|
1511
|
+
image_id = self._image_indices[image_id]
|
|
1512
|
+
|
|
1513
|
+
# Ensure xyxy is not None
|
|
1514
|
+
if image_targets.xyxy is None:
|
|
1515
|
+
continue
|
|
1516
|
+
|
|
1517
|
+
content = self._detections_content(image_targets)
|
|
1518
|
+
for target_idx, xyxy in enumerate(image_targets.xyxy):
|
|
1519
|
+
xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]
|
|
1520
|
+
|
|
1521
|
+
# Default values
|
|
1522
|
+
category_id = 0
|
|
1523
|
+
|
|
1524
|
+
if image_targets.class_id is not None:
|
|
1525
|
+
cls_id = image_targets.class_id[target_idx]
|
|
1526
|
+
if self._class_mapping is not None:
|
|
1527
|
+
category_id = self._class_mapping[int(cls_id)]
|
|
1528
|
+
else:
|
|
1529
|
+
category_id = int(cls_id)
|
|
1530
|
+
|
|
1531
|
+
# Use area from data if available, otherwise calculate from the
|
|
1532
|
+
# metric target content (bbox, mask or oriented box)
|
|
1533
|
+
area = None
|
|
1534
|
+
if image_targets.data is not None and "area" in image_targets.data:
|
|
1535
|
+
area_data: npt.NDArray[np.float32] = np.asarray(
|
|
1536
|
+
image_targets.data["area"], dtype=np.float32
|
|
1537
|
+
)
|
|
1538
|
+
area = float(area_data[target_idx])
|
|
1539
|
+
|
|
1540
|
+
if area is None:
|
|
1541
|
+
area = self._content_area(xywh, content, target_idx)
|
|
1542
|
+
|
|
1543
|
+
iscrowd = 0
|
|
1544
|
+
if image_targets.data is not None and "iscrowd" in image_targets.data:
|
|
1545
|
+
iscrowd_data: npt.NDArray[np.int64] = np.asarray(
|
|
1546
|
+
image_targets.data["iscrowd"], dtype=np.int64
|
|
1547
|
+
)
|
|
1548
|
+
iscrowd = int(iscrowd_data[target_idx])
|
|
1549
|
+
|
|
1550
|
+
ignore = 0
|
|
1551
|
+
if image_targets.data is not None and "ignore" in image_targets.data:
|
|
1552
|
+
ignore_data: npt.NDArray[np.int64] = np.asarray(
|
|
1553
|
+
image_targets.data["ignore"], dtype=np.int64
|
|
1554
|
+
)
|
|
1555
|
+
ignore = int(ignore_data[target_idx])
|
|
1556
|
+
|
|
1557
|
+
dict_annotation: _TypeCocoDict = {
|
|
1558
|
+
"area": area,
|
|
1559
|
+
"iscrowd": iscrowd,
|
|
1560
|
+
"image_id": image_id,
|
|
1561
|
+
"bbox": xywh,
|
|
1562
|
+
"category_id": category_id,
|
|
1563
|
+
"id": len(annotations) + 1, # Start IDs from 1 (0 means no match)
|
|
1564
|
+
"ignore": ignore,
|
|
1565
|
+
}
|
|
1566
|
+
if content is not None:
|
|
1567
|
+
dict_annotation["content"] = content[target_idx]
|
|
1568
|
+
annotations.append(dict_annotation)
|
|
1569
|
+
# Category list
|
|
1570
|
+
all_cat_ids = {annotation["category_id"] for annotation in annotations}
|
|
1571
|
+
categories: list[_TypeCocoDict] = [{"id": cat_id} for cat_id in all_cat_ids]
|
|
1572
|
+
# Create coco dictionary
|
|
1573
|
+
return {
|
|
1574
|
+
"images": images,
|
|
1575
|
+
"annotations": annotations,
|
|
1576
|
+
"categories": categories,
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
def _prepare_predictions(
|
|
1580
|
+
self, predictions: list[Detections]
|
|
1581
|
+
) -> list[_TypeCocoDict]:
|
|
1582
|
+
"""Transform predictions into a list of predictions that can be used by the COCO
|
|
1583
|
+
evaluator."""
|
|
1584
|
+
coco_predictions: list[_TypeCocoDict] = []
|
|
1585
|
+
for image_id, image_predictions in enumerate(predictions):
|
|
1586
|
+
if self._image_indices is not None:
|
|
1587
|
+
image_id = self._image_indices[image_id]
|
|
1588
|
+
|
|
1589
|
+
if image_predictions.xyxy is None:
|
|
1590
|
+
continue
|
|
1591
|
+
|
|
1592
|
+
content = self._detections_content(image_predictions)
|
|
1593
|
+
for pred_idx, xyxy in enumerate(image_predictions.xyxy):
|
|
1594
|
+
xywh = [xyxy[0], xyxy[1], xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]]
|
|
1595
|
+
|
|
1596
|
+
category_id = 0
|
|
1597
|
+
score = 0.0
|
|
1598
|
+
|
|
1599
|
+
if image_predictions.class_id is not None:
|
|
1600
|
+
cls_id = image_predictions.class_id[pred_idx]
|
|
1601
|
+
if self._class_mapping is not None:
|
|
1602
|
+
category_id = self._class_mapping[int(cls_id)]
|
|
1603
|
+
else:
|
|
1604
|
+
category_id = int(cls_id)
|
|
1605
|
+
|
|
1606
|
+
if image_predictions.confidence is not None:
|
|
1607
|
+
score = float(image_predictions.confidence[pred_idx])
|
|
1608
|
+
|
|
1609
|
+
# Use area from data if available, otherwise calculate from the
|
|
1610
|
+
# metric target content (bbox, mask or oriented box)
|
|
1611
|
+
area = None
|
|
1612
|
+
if (
|
|
1613
|
+
image_predictions.data is not None
|
|
1614
|
+
and "area" in image_predictions.data
|
|
1615
|
+
):
|
|
1616
|
+
area_data: npt.NDArray[np.float32] = np.asarray(
|
|
1617
|
+
image_predictions.data["area"], dtype=np.float32
|
|
1618
|
+
)
|
|
1619
|
+
area = float(area_data[pred_idx])
|
|
1620
|
+
|
|
1621
|
+
if area is None:
|
|
1622
|
+
area = self._content_area(xywh, content, pred_idx)
|
|
1623
|
+
|
|
1624
|
+
dict_prediction: _TypeCocoDict = {
|
|
1625
|
+
"image_id": image_id,
|
|
1626
|
+
"bbox": xywh,
|
|
1627
|
+
"score": score,
|
|
1628
|
+
"category_id": category_id,
|
|
1629
|
+
"area": area,
|
|
1630
|
+
"id": len(coco_predictions) + 1,
|
|
1631
|
+
}
|
|
1632
|
+
if content is not None:
|
|
1633
|
+
dict_prediction["content"] = content[pred_idx]
|
|
1634
|
+
coco_predictions.append(dict_prediction)
|
|
1635
|
+
return coco_predictions
|
|
1636
|
+
|
|
1637
|
+
def compute(self) -> MeanAveragePrecisionResult:
|
|
1638
|
+
"""
|
|
1639
|
+
Calculate Mean Average Precision based on predicted and ground-truth
|
|
1640
|
+
detections at different thresholds using the COCO evaluation metrics.
|
|
1641
|
+
Source: https://github.com/rafaelpadilla/review_object_detection_metrics
|
|
1642
|
+
|
|
1643
|
+
Returns:
|
|
1644
|
+
The Mean Average Precision result.
|
|
1645
|
+
"""
|
|
1646
|
+
total_images_predictions = len(self._predictions_list)
|
|
1647
|
+
total_images_targets = len(self._targets_list)
|
|
1648
|
+
|
|
1649
|
+
if total_images_predictions != total_images_targets:
|
|
1650
|
+
raise ValueError(
|
|
1651
|
+
f"The number of predictions ({total_images_predictions}) and"
|
|
1652
|
+
f" targets ({total_images_targets}) during the evaluation must be"
|
|
1653
|
+
" the same."
|
|
1654
|
+
)
|
|
1655
|
+
dict_targets = self._prepare_targets(self._targets_list)
|
|
1656
|
+
lst_predictions = self._prepare_predictions(self._predictions_list)
|
|
1657
|
+
# Create a coco object with the targets
|
|
1658
|
+
coco_gt = EvaluationDataset(targets=dict_targets)
|
|
1659
|
+
# Include the predictions to coco object
|
|
1660
|
+
coco_det = coco_gt.load_predictions(lst_predictions)
|
|
1661
|
+
# Create a coco evaluator with the predictions
|
|
1662
|
+
cocoEval = COCOEvaluator(coco_gt, coco_det, metric_target=self._metric_target)
|
|
1663
|
+
|
|
1664
|
+
# Evaluate on all images
|
|
1665
|
+
cocoEval.evaluate()
|
|
1666
|
+
|
|
1667
|
+
# Create MeanAveragePrecisionResult object for small objects
|
|
1668
|
+
mAP_small = MeanAveragePrecisionResult(
|
|
1669
|
+
metric_target=self._metric_target,
|
|
1670
|
+
is_class_agnostic=self._class_agnostic,
|
|
1671
|
+
mAP_scores=np.asarray(
|
|
1672
|
+
cocoEval.results["mAP_scores_small"], dtype=np.float64
|
|
1673
|
+
),
|
|
1674
|
+
ap_per_class=np.asarray(
|
|
1675
|
+
cocoEval.results["ap_per_class_small"], dtype=np.float64
|
|
1676
|
+
),
|
|
1677
|
+
iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
|
|
1678
|
+
matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
|
|
1679
|
+
)
|
|
1680
|
+
# Create MeanAveragePrecisionResult object for medium objects
|
|
1681
|
+
mAP_medium = MeanAveragePrecisionResult(
|
|
1682
|
+
metric_target=self._metric_target,
|
|
1683
|
+
is_class_agnostic=self._class_agnostic,
|
|
1684
|
+
mAP_scores=np.asarray(
|
|
1685
|
+
cocoEval.results["mAP_scores_medium"], dtype=np.float64
|
|
1686
|
+
),
|
|
1687
|
+
ap_per_class=np.asarray(
|
|
1688
|
+
cocoEval.results["ap_per_class_medium"], dtype=np.float64
|
|
1689
|
+
),
|
|
1690
|
+
iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
|
|
1691
|
+
matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
|
|
1692
|
+
)
|
|
1693
|
+
# Create MeanAveragePrecisionResult object for large objects
|
|
1694
|
+
mAP_large = MeanAveragePrecisionResult(
|
|
1695
|
+
metric_target=self._metric_target,
|
|
1696
|
+
is_class_agnostic=self._class_agnostic,
|
|
1697
|
+
mAP_scores=np.asarray(
|
|
1698
|
+
cocoEval.results["mAP_scores_large"], dtype=np.float64
|
|
1699
|
+
),
|
|
1700
|
+
ap_per_class=np.asarray(
|
|
1701
|
+
cocoEval.results["ap_per_class_large"], dtype=np.float64
|
|
1702
|
+
),
|
|
1703
|
+
iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
|
|
1704
|
+
matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
# Create the final MeanAveragePrecisionResult object
|
|
1708
|
+
mAP_result = MeanAveragePrecisionResult(
|
|
1709
|
+
metric_target=self._metric_target,
|
|
1710
|
+
is_class_agnostic=self._class_agnostic,
|
|
1711
|
+
mAP_scores=np.asarray(
|
|
1712
|
+
cocoEval.results["mAP_scores_all_sizes"], dtype=np.float64
|
|
1713
|
+
),
|
|
1714
|
+
ap_per_class=np.asarray(
|
|
1715
|
+
cocoEval.results["ap_per_class_all_sizes"], dtype=np.float64
|
|
1716
|
+
),
|
|
1717
|
+
iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64),
|
|
1718
|
+
matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32),
|
|
1719
|
+
small_objects=mAP_small,
|
|
1720
|
+
medium_objects=mAP_medium,
|
|
1721
|
+
large_objects=mAP_large,
|
|
1722
|
+
)
|
|
1723
|
+
return mAP_result
|