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.
Files changed (96) hide show
  1. superiorvision/__init__.py +19 -0
  2. superiorvision-0.30.0.dev0.dist-info/METADATA +398 -0
  3. superiorvision-0.30.0.dev0.dist-info/RECORD +96 -0
  4. superiorvision-0.30.0.dev0.dist-info/WHEEL +5 -0
  5. superiorvision-0.30.0.dev0.dist-info/licenses/LICENSE.md +9 -0
  6. superiorvision-0.30.0.dev0.dist-info/top_level.txt +2 -0
  7. supervision/__init__.py +318 -0
  8. supervision/annotators/__init__.py +0 -0
  9. supervision/annotators/base.py +21 -0
  10. supervision/annotators/core.py +3551 -0
  11. supervision/annotators/utils.py +541 -0
  12. supervision/assets/__init__.py +4 -0
  13. supervision/assets/downloader.py +166 -0
  14. supervision/assets/list.py +84 -0
  15. supervision/classification/__init__.py +0 -0
  16. supervision/classification/core.py +216 -0
  17. supervision/config.py +13 -0
  18. supervision/dataset/__init__.py +0 -0
  19. supervision/dataset/core.py +1313 -0
  20. supervision/dataset/formats/__init__.py +0 -0
  21. supervision/dataset/formats/coco.py +708 -0
  22. supervision/dataset/formats/createml.py +331 -0
  23. supervision/dataset/formats/labelme.py +405 -0
  24. supervision/dataset/formats/pascal_voc.py +449 -0
  25. supervision/dataset/formats/yolo.py +502 -0
  26. supervision/dataset/utils.py +265 -0
  27. supervision/detection/__init__.py +0 -0
  28. supervision/detection/compact_mask.py +1927 -0
  29. supervision/detection/core.py +3864 -0
  30. supervision/detection/line_zone.py +924 -0
  31. supervision/detection/overlap_filter.py +426 -0
  32. supervision/detection/tensor_utils.py +1596 -0
  33. supervision/detection/tensor_views.py +48 -0
  34. supervision/detection/tools/__init__.py +0 -0
  35. supervision/detection/tools/csv_sink.py +242 -0
  36. supervision/detection/tools/inference_slicer.py +776 -0
  37. supervision/detection/tools/json_sink.py +215 -0
  38. supervision/detection/tools/polygon_zone.py +266 -0
  39. supervision/detection/tools/smoother.py +218 -0
  40. supervision/detection/tools/transformers.py +265 -0
  41. supervision/detection/utils/__init__.py +12 -0
  42. supervision/detection/utils/_typing.py +11 -0
  43. supervision/detection/utils/boxes.py +510 -0
  44. supervision/detection/utils/converters.py +830 -0
  45. supervision/detection/utils/internal.py +806 -0
  46. supervision/detection/utils/iou_and_nms.py +1606 -0
  47. supervision/detection/utils/masks.py +625 -0
  48. supervision/detection/utils/polygons.py +128 -0
  49. supervision/detection/utils/vlms.py +97 -0
  50. supervision/detection/vlm.py +945 -0
  51. supervision/draw/__init__.py +0 -0
  52. supervision/draw/base.py +14 -0
  53. supervision/draw/color.py +598 -0
  54. supervision/draw/utils.py +527 -0
  55. supervision/geometry/__init__.py +0 -0
  56. supervision/geometry/core.py +208 -0
  57. supervision/geometry/utils.py +54 -0
  58. supervision/key_points/__init__.py +0 -0
  59. supervision/key_points/annotators.py +997 -0
  60. supervision/key_points/core.py +1506 -0
  61. supervision/key_points/skeletons.py +2646 -0
  62. supervision/keypoint/__init__.py +13 -0
  63. supervision/keypoint/annotators.py +13 -0
  64. supervision/keypoint/core.py +8 -0
  65. supervision/metrics/__init__.py +40 -0
  66. supervision/metrics/core.py +74 -0
  67. supervision/metrics/detection.py +1632 -0
  68. supervision/metrics/f1_score.py +815 -0
  69. supervision/metrics/mean_average_precision.py +1723 -0
  70. supervision/metrics/mean_average_recall.py +734 -0
  71. supervision/metrics/precision.py +815 -0
  72. supervision/metrics/recall.py +774 -0
  73. supervision/metrics/utils/__init__.py +0 -0
  74. supervision/metrics/utils/matching.py +56 -0
  75. supervision/metrics/utils/object_size.py +256 -0
  76. supervision/metrics/utils/utils.py +9 -0
  77. supervision/py.typed +0 -0
  78. supervision/tracker/__init__.py +5 -0
  79. supervision/tracker/byte_tracker/__init__.py +0 -0
  80. supervision/tracker/byte_tracker/core.py +448 -0
  81. supervision/tracker/byte_tracker/kalman_filter.py +186 -0
  82. supervision/tracker/byte_tracker/matching.py +211 -0
  83. supervision/tracker/byte_tracker/single_object_track.py +194 -0
  84. supervision/tracker/byte_tracker/utils.py +34 -0
  85. supervision/utils/__init__.py +0 -0
  86. supervision/utils/conversion.py +223 -0
  87. supervision/utils/deprecate.py +54 -0
  88. supervision/utils/file.py +209 -0
  89. supervision/utils/image.py +988 -0
  90. supervision/utils/internal.py +209 -0
  91. supervision/utils/iterables.py +103 -0
  92. supervision/utils/logger.py +61 -0
  93. supervision/utils/notebook.py +124 -0
  94. supervision/utils/tensor.py +362 -0
  95. supervision/utils/video.py +533 -0
  96. 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
+ ![example_plot](
228
+ https://media.roboflow.com/supervision-docs/metrics/mAP_plot_example.png
229
+ ){ 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
+ ![example_plot](
1386
+ https://media.roboflow.com/supervision-docs/metrics/mAP_plot_example.png
1387
+ ){ 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