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,708 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
from tqdm.auto import tqdm
|
|
11
|
+
|
|
12
|
+
from supervision.config import COCO_RAW_SEGMENTATION
|
|
13
|
+
from supervision.dataset.utils import (
|
|
14
|
+
approximate_mask_with_polygons,
|
|
15
|
+
check_no_basename_collisions,
|
|
16
|
+
map_detections_class_id,
|
|
17
|
+
)
|
|
18
|
+
from supervision.detection.core import Detections
|
|
19
|
+
from supervision.detection.utils.converters import (
|
|
20
|
+
mask_to_rle,
|
|
21
|
+
polygon_to_mask,
|
|
22
|
+
rle_to_mask,
|
|
23
|
+
)
|
|
24
|
+
from supervision.detection.utils.masks import contains_holes, contains_multiple_segments
|
|
25
|
+
from supervision.utils.file import read_json_file, save_json_file
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from supervision.dataset.core import DetectionDataset
|
|
29
|
+
|
|
30
|
+
CocoDict = dict[str, Any]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def coco_categories_to_classes(coco_categories: list[CocoDict]) -> list[str]:
|
|
34
|
+
return [
|
|
35
|
+
category["name"]
|
|
36
|
+
for category in sorted(coco_categories, key=lambda category: category["id"])
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_coco_class_index_mapping(
|
|
41
|
+
coco_categories: list[CocoDict], target_classes: list[str]
|
|
42
|
+
) -> dict[int, int]:
|
|
43
|
+
source_class_to_index = {
|
|
44
|
+
category["name"]: category["id"] for category in coco_categories
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
source_class_to_index[target_class_name]: target_class_index
|
|
48
|
+
for target_class_index, target_class_name in enumerate(target_classes)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def classes_to_coco_categories(classes: list[str]) -> list[CocoDict]:
|
|
53
|
+
"""Convert a list of class names to COCO ``categories`` entries.
|
|
54
|
+
|
|
55
|
+
Category ids are emitted 1-indexed to comply with the COCO specification
|
|
56
|
+
and tools such as CVAT, which require ``category_id`` values to start at
|
|
57
|
+
``1``. The id assigned to the class at position ``class_index`` is
|
|
58
|
+
``class_index + 1``, keeping it consistent with the ``category_id`` written
|
|
59
|
+
by [`detections_to_coco_annotations`](#detections_to_coco_annotations).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
classes: Class names ordered by their internal (0-indexed) class id.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A list of COCO category dictionaries with 1-indexed ``id`` values.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
```python
|
|
69
|
+
from supervision.dataset.formats.coco import classes_to_coco_categories
|
|
70
|
+
|
|
71
|
+
classes_to_coco_categories(classes=["cat", "dog"])
|
|
72
|
+
# [
|
|
73
|
+
# {"id": 1, "name": "cat", "supercategory": "common-objects"},
|
|
74
|
+
# {"id": 2, "name": "dog", "supercategory": "common-objects"},
|
|
75
|
+
# ]
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
return [
|
|
79
|
+
{
|
|
80
|
+
"id": class_index + 1,
|
|
81
|
+
"name": class_name,
|
|
82
|
+
"supercategory": "common-objects",
|
|
83
|
+
}
|
|
84
|
+
for class_index, class_name in enumerate(classes)
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def group_coco_annotations_by_image_id(
|
|
89
|
+
coco_annotations: list[CocoDict],
|
|
90
|
+
) -> dict[int, list[CocoDict]]:
|
|
91
|
+
annotations: dict[int, list[CocoDict]] = {}
|
|
92
|
+
for annotation in coco_annotations:
|
|
93
|
+
image_id = annotation["image_id"]
|
|
94
|
+
if image_id not in annotations:
|
|
95
|
+
annotations[image_id] = []
|
|
96
|
+
annotations[image_id].append(annotation)
|
|
97
|
+
return annotations
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def coco_annotations_to_masks(
|
|
101
|
+
image_annotations: list[CocoDict], resolution_wh: tuple[int, int]
|
|
102
|
+
) -> npt.NDArray[np.bool_]:
|
|
103
|
+
height, width = resolution_wh[1], resolution_wh[0]
|
|
104
|
+
empty_mask: npt.NDArray[np.bool_] = np.zeros((height, width), dtype=bool)
|
|
105
|
+
masks = []
|
|
106
|
+
|
|
107
|
+
for image_annotation in image_annotations:
|
|
108
|
+
segmentation = image_annotation.get("segmentation")
|
|
109
|
+
if not segmentation:
|
|
110
|
+
# `force_masks=True` may request masks even for bbox-only annotations.
|
|
111
|
+
# Keep detection count aligned by emitting an empty mask for that object.
|
|
112
|
+
masks.append(empty_mask.copy())
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
if isinstance(segmentation, dict):
|
|
116
|
+
if "counts" not in segmentation:
|
|
117
|
+
warnings.warn(
|
|
118
|
+
"Skipping annotation "
|
|
119
|
+
f"{image_annotation.get('id', '?')}: segmentation is a dict but "
|
|
120
|
+
"missing 'counts' key (expected RLE format)",
|
|
121
|
+
stacklevel=2,
|
|
122
|
+
)
|
|
123
|
+
masks.append(empty_mask.copy())
|
|
124
|
+
continue
|
|
125
|
+
masks.append(
|
|
126
|
+
rle_to_mask(rle=segmentation["counts"], resolution_wh=resolution_wh)
|
|
127
|
+
)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if not isinstance(segmentation, list):
|
|
131
|
+
masks.append(empty_mask.copy())
|
|
132
|
+
continue
|
|
133
|
+
polygons = segmentation if isinstance(segmentation[0], list) else [segmentation]
|
|
134
|
+
|
|
135
|
+
object_mask = empty_mask.copy()
|
|
136
|
+
for polygon in polygons:
|
|
137
|
+
polygon_array: npt.NDArray[np.int32] = np.reshape(
|
|
138
|
+
np.asarray(polygon, dtype=np.int32), (-1, 2)
|
|
139
|
+
)
|
|
140
|
+
if polygon_array.size == 0:
|
|
141
|
+
warnings.warn(
|
|
142
|
+
"Skipping empty polygon while loading COCO segmentation for "
|
|
143
|
+
f"annotation id={image_annotation.get('id')}.",
|
|
144
|
+
stacklevel=2,
|
|
145
|
+
)
|
|
146
|
+
continue
|
|
147
|
+
# COCO polygon segmentation can contain multiple disjoint parts.
|
|
148
|
+
# Merge all parts into a single per-object mask.
|
|
149
|
+
object_mask |= polygon_to_mask(
|
|
150
|
+
polygon=polygon_array, resolution_wh=resolution_wh
|
|
151
|
+
).astype(bool)
|
|
152
|
+
|
|
153
|
+
masks.append(object_mask)
|
|
154
|
+
|
|
155
|
+
return np.asarray(masks, dtype=bool)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def coco_annotations_to_detections(
|
|
159
|
+
image_annotations: list[CocoDict],
|
|
160
|
+
resolution_wh: tuple[int, int],
|
|
161
|
+
with_masks: bool,
|
|
162
|
+
use_iscrowd: bool = True,
|
|
163
|
+
) -> Detections:
|
|
164
|
+
"""Convert COCO annotation dicts for a single image into a `Detections` object.
|
|
165
|
+
|
|
166
|
+
.. warning::
|
|
167
|
+
The returned ``Detections.class_id`` contains **raw COCO** ``category_id``
|
|
168
|
+
values, not the final 0-indexed internal class ids. Callers **must** pass
|
|
169
|
+
the result through :func:`map_detections_class_id` with the appropriate
|
|
170
|
+
``source_to_target_mapping`` (built by
|
|
171
|
+
:func:`build_coco_class_index_mapping`) before the ``class_id`` values are
|
|
172
|
+
meaningful. Skipping the remap step yields 1-based ids in a field that the
|
|
173
|
+
rest of supervision treats as 0-based.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
image_annotations: List of COCO annotation dicts for one image.
|
|
177
|
+
resolution_wh: ``(width, height)`` of the image, used for mask decoding.
|
|
178
|
+
with_masks: Whether to decode segmentation fields into binary masks.
|
|
179
|
+
use_iscrowd: When ``True``, store ``iscrowd`` and ``area`` in
|
|
180
|
+
``Detections.data``.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Detections with ``class_id`` set to raw COCO ``category_id`` values.
|
|
184
|
+
Call :func:`map_detections_class_id` on the result before use.
|
|
185
|
+
When ``with_masks=False``, ``detections.data[COCO_RAW_SEGMENTATION]`` is
|
|
186
|
+
populated as an object array (shape ``(N,)``) holding the raw polygon list or
|
|
187
|
+
RLE dict per annotation; consumed by :func:`detections_to_coco_annotations`
|
|
188
|
+
for a coordinate-preserving round-trip.
|
|
189
|
+
"""
|
|
190
|
+
if not image_annotations:
|
|
191
|
+
return Detections.empty()
|
|
192
|
+
|
|
193
|
+
class_ids = [
|
|
194
|
+
image_annotation["category_id"] for image_annotation in image_annotations
|
|
195
|
+
]
|
|
196
|
+
xyxy_list = [image_annotation["bbox"] for image_annotation in image_annotations]
|
|
197
|
+
xyxy: npt.NDArray[np.float32] = np.asarray(xyxy_list, dtype=np.float32)
|
|
198
|
+
xyxy[:, 2:4] += xyxy[:, 0:2]
|
|
199
|
+
|
|
200
|
+
data: dict[str, npt.NDArray[np.generic] | list[Any]] = {}
|
|
201
|
+
if use_iscrowd:
|
|
202
|
+
iscrowd = [
|
|
203
|
+
image_annotation.get("iscrowd", 0) for image_annotation in image_annotations
|
|
204
|
+
]
|
|
205
|
+
area = []
|
|
206
|
+
for image_annotation in image_annotations:
|
|
207
|
+
if "area" in image_annotation:
|
|
208
|
+
area.append(image_annotation["area"])
|
|
209
|
+
elif with_masks and _with_seg_mask(image_annotation):
|
|
210
|
+
area.append(np.nan)
|
|
211
|
+
else:
|
|
212
|
+
area.append(image_annotation["bbox"][2] * image_annotation["bbox"][3])
|
|
213
|
+
data = dict(
|
|
214
|
+
iscrowd=np.asarray(iscrowd, dtype=int), area=np.asarray(area, dtype=float)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if with_masks:
|
|
218
|
+
mask = coco_annotations_to_masks(
|
|
219
|
+
image_annotations=image_annotations, resolution_wh=resolution_wh
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
mask = None
|
|
223
|
+
# Preserve raw polygon/RLE data so as_coco() can round-trip without
|
|
224
|
+
# binary-mask encoding. Stored as an object array (one entry per detection).
|
|
225
|
+
raw_segs: npt.NDArray[np.object_] = np.empty(
|
|
226
|
+
len(image_annotations), dtype=object
|
|
227
|
+
)
|
|
228
|
+
for k, _ann in enumerate(image_annotations):
|
|
229
|
+
raw_segs[k] = _ann.get("segmentation", [])
|
|
230
|
+
data[COCO_RAW_SEGMENTATION] = raw_segs
|
|
231
|
+
|
|
232
|
+
return Detections(
|
|
233
|
+
class_id=np.asarray(class_ids, dtype=int), xyxy=xyxy, mask=mask, data=data
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def detections_to_coco_annotations(
|
|
238
|
+
detections: Detections,
|
|
239
|
+
image_id: int,
|
|
240
|
+
annotation_id: int,
|
|
241
|
+
min_image_area_percentage: float = 0.0,
|
|
242
|
+
max_image_area_percentage: float = 1.0,
|
|
243
|
+
approximation_percentage: float = 0.75,
|
|
244
|
+
) -> tuple[list[CocoDict], int]:
|
|
245
|
+
"""Convert `Detections` to COCO ``annotations`` entries.
|
|
246
|
+
|
|
247
|
+
The internal 0-indexed ``Detections.class_id`` is serialized as a 1-indexed
|
|
248
|
+
COCO ``category_id`` (``category_id = class_id + 1``). This complies with the
|
|
249
|
+
COCO specification and tools such as CVAT, and stays consistent with the ids
|
|
250
|
+
emitted by [`classes_to_coco_categories`](#classes_to_coco_categories), so a
|
|
251
|
+
detection with internal ``class_id=k`` maps to ``category_id=k + 1``.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
detections: The detections to convert. ``class_id`` must not be ``None``.
|
|
255
|
+
image_id: COCO ``image_id`` shared by every produced annotation.
|
|
256
|
+
annotation_id: First annotation id to assign; incremented per detection.
|
|
257
|
+
min_image_area_percentage: Lower bound on detection area / image area,
|
|
258
|
+
used only when approximating masks with polygons.
|
|
259
|
+
max_image_area_percentage: Upper bound on detection area / image area,
|
|
260
|
+
used only when approximating masks with polygons.
|
|
261
|
+
approximation_percentage: Polygon-simplification ratio in ``[0, 1)``.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
A ``(coco_annotations, next_annotation_id)`` tuple, where
|
|
265
|
+
``next_annotation_id`` is one greater than the last id assigned.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ValueError: If any detection has ``class_id`` equal to ``None``.
|
|
269
|
+
|
|
270
|
+
Note:
|
|
271
|
+
For ``iscrowd=0`` annotations, ``segmentation`` is a
|
|
272
|
+
``list[list[float]]`` where each inner list encodes one polygon
|
|
273
|
+
part as flat ``[x1, y1, x2, y2, ...]`` coordinates. A single
|
|
274
|
+
object with *N* disjoint parts produces *N* inner lists.
|
|
275
|
+
|
|
276
|
+
When ``iscrowd`` is not in ``detections.data``, masks with holes
|
|
277
|
+
or multiple disjoint segments are auto-encoded as RLE
|
|
278
|
+
(``iscrowd=1``); simple single-region masks use polygon format
|
|
279
|
+
(``iscrowd=0``). Supply ``data={"iscrowd": np.array([0])}`` to
|
|
280
|
+
force polygon output regardless of mask topology.
|
|
281
|
+
|
|
282
|
+
Examples:
|
|
283
|
+
```python
|
|
284
|
+
import numpy as np
|
|
285
|
+
from supervision import Detections
|
|
286
|
+
from supervision.dataset.formats.coco import (
|
|
287
|
+
detections_to_coco_annotations,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
detections = Detections(
|
|
291
|
+
xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
|
|
292
|
+
class_id=np.array([0], dtype=int),
|
|
293
|
+
)
|
|
294
|
+
annotations, next_id = detections_to_coco_annotations(
|
|
295
|
+
detections=detections, image_id=1, annotation_id=1
|
|
296
|
+
)
|
|
297
|
+
annotations[0]["category_id"]
|
|
298
|
+
# 1
|
|
299
|
+
```
|
|
300
|
+
"""
|
|
301
|
+
coco_annotations: list[CocoDict] = []
|
|
302
|
+
for xyxy, mask, _, class_id, _, data in detections:
|
|
303
|
+
if class_id is None:
|
|
304
|
+
raise ValueError("Detections must include class_id for COCO export.")
|
|
305
|
+
box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1]
|
|
306
|
+
segmentation: list[list[float]] | dict[str, list[int]] = []
|
|
307
|
+
if mask is not None:
|
|
308
|
+
mask_bool = mask
|
|
309
|
+
if "iscrowd" in data:
|
|
310
|
+
iscrowd = int(np.asarray(data["iscrowd"]).item())
|
|
311
|
+
else:
|
|
312
|
+
iscrowd = int(
|
|
313
|
+
contains_holes(mask=mask_bool)
|
|
314
|
+
or contains_multiple_segments(mask=mask_bool)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if iscrowd:
|
|
318
|
+
segmentation = {
|
|
319
|
+
"counts": cast(
|
|
320
|
+
list[int], mask_to_rle(mask=mask_bool, compressed=False)
|
|
321
|
+
),
|
|
322
|
+
"size": list(mask.shape[:2]),
|
|
323
|
+
}
|
|
324
|
+
else:
|
|
325
|
+
polygons = approximate_mask_with_polygons(
|
|
326
|
+
mask=mask_bool,
|
|
327
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
328
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
329
|
+
approximation_percentage=approximation_percentage,
|
|
330
|
+
)
|
|
331
|
+
# Small/noisy masks can be filtered out by approximation settings.
|
|
332
|
+
# Guard against empty output and keep a valid COCO annotation record.
|
|
333
|
+
if polygons:
|
|
334
|
+
# Export ALL polygons so disjoint mask components are preserved.
|
|
335
|
+
segmentation = [list(p.flatten()) for p in polygons]
|
|
336
|
+
else:
|
|
337
|
+
warnings.warn(
|
|
338
|
+
"Skipping COCO polygon segmentation for annotation "
|
|
339
|
+
f"id={annotation_id} because mask approximation "
|
|
340
|
+
"returned no polygons.",
|
|
341
|
+
stacklevel=2,
|
|
342
|
+
)
|
|
343
|
+
else:
|
|
344
|
+
iscrowd = int(np.asarray(data.get("iscrowd", 0)).item())
|
|
345
|
+
# When masks were not decoded during loading, fall back to the raw
|
|
346
|
+
# polygon/RLE stored in data["segmentation"] for a lossless round-trip.
|
|
347
|
+
raw_seg = data.get(COCO_RAW_SEGMENTATION)
|
|
348
|
+
if raw_seg is not None and bool(raw_seg):
|
|
349
|
+
if isinstance(raw_seg, dict):
|
|
350
|
+
# RLE format — pass through unchanged
|
|
351
|
+
segmentation = raw_seg
|
|
352
|
+
elif (
|
|
353
|
+
isinstance(raw_seg, list)
|
|
354
|
+
and raw_seg
|
|
355
|
+
and not isinstance(raw_seg[0], (list, tuple))
|
|
356
|
+
):
|
|
357
|
+
# Flat list shorthand [x1,y1,...] — wrap to list-of-lists
|
|
358
|
+
segmentation = [list(raw_seg)]
|
|
359
|
+
else:
|
|
360
|
+
segmentation = list(raw_seg)
|
|
361
|
+
|
|
362
|
+
stored_area = None
|
|
363
|
+
if "area" in data:
|
|
364
|
+
stored_area = float(np.asarray(data["area"]).item())
|
|
365
|
+
|
|
366
|
+
if stored_area is not None and np.isfinite(stored_area):
|
|
367
|
+
area = stored_area
|
|
368
|
+
elif mask is not None:
|
|
369
|
+
area = float(np.count_nonzero(mask))
|
|
370
|
+
else:
|
|
371
|
+
area = float(box_width * box_height)
|
|
372
|
+
coco_annotation = {
|
|
373
|
+
"id": annotation_id,
|
|
374
|
+
"image_id": image_id,
|
|
375
|
+
"category_id": int(class_id) + 1,
|
|
376
|
+
"bbox": [xyxy[0], xyxy[1], box_width, box_height],
|
|
377
|
+
"area": area,
|
|
378
|
+
"segmentation": segmentation,
|
|
379
|
+
"iscrowd": iscrowd,
|
|
380
|
+
}
|
|
381
|
+
coco_annotations.append(coco_annotation)
|
|
382
|
+
annotation_id += 1
|
|
383
|
+
return coco_annotations, annotation_id
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_coco_class_index_mapping(annotations_path: str) -> dict[int, int]:
|
|
387
|
+
"""
|
|
388
|
+
Generates a mapping from sequential class indices to original COCO class ids.
|
|
389
|
+
|
|
390
|
+
This function is essential when working with models that expect class ids to be
|
|
391
|
+
zero-indexed and sequential (0 to 79), as opposed to the original COCO
|
|
392
|
+
dataset where category ids are non-contiguous ranging from 1 to 90 but skipping some
|
|
393
|
+
ids.
|
|
394
|
+
|
|
395
|
+
Use Cases:
|
|
396
|
+
- Evaluating models trained with COCO-style annotations where class ids
|
|
397
|
+
are sequential ranging from 0 to 79.
|
|
398
|
+
- Ensuring consistent class indexing across training, inference and evaluation,
|
|
399
|
+
when using different tools or datasets with COCO format.
|
|
400
|
+
- Reproducing results from models that assume sequential class ids (0 to 79).
|
|
401
|
+
|
|
402
|
+
How it Works:
|
|
403
|
+
- Reads the COCO annotation file in its original format (`annotations_path`).
|
|
404
|
+
- Extracts and sorts all class names by their original COCO id (1 to 90).
|
|
405
|
+
- Builds a mapping from COCO class ids (not sequential with skipped ids) to
|
|
406
|
+
new class ids (sequential ranging from 0 to 79).
|
|
407
|
+
- Returns a dictionary mapping: `{new_class_id: original_COCO_class_id}`.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
annotations_path: Path to COCO JSON annotations file
|
|
411
|
+
(e.g., `instances_val2017.json`).
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
A mapping from new class id (sequential ranging from 0 to 79)
|
|
415
|
+
to original COCO class id (1 to 90 with skipped ids).
|
|
416
|
+
|
|
417
|
+
Examples:
|
|
418
|
+
>>> import json
|
|
419
|
+
>>> import os
|
|
420
|
+
>>> import tempfile
|
|
421
|
+
>>> from supervision.dataset.formats.coco import get_coco_class_index_mapping
|
|
422
|
+
>>> coco_data = {
|
|
423
|
+
... "categories": [
|
|
424
|
+
... {"id": 1, "name": "person"},
|
|
425
|
+
... {"id": 3, "name": "car"},
|
|
426
|
+
... ],
|
|
427
|
+
... "images": [],
|
|
428
|
+
... "annotations": [],
|
|
429
|
+
... }
|
|
430
|
+
>>> annotations_path = None
|
|
431
|
+
>>> try:
|
|
432
|
+
... with tempfile.NamedTemporaryFile(
|
|
433
|
+
... mode="w", suffix=".json", delete=False
|
|
434
|
+
... ) as f:
|
|
435
|
+
... annotations_path = f.name
|
|
436
|
+
... json.dump(coco_data, f)
|
|
437
|
+
... mapping = get_coco_class_index_mapping(annotations_path)
|
|
438
|
+
... print(mapping)
|
|
439
|
+
... finally:
|
|
440
|
+
... if annotations_path is not None:
|
|
441
|
+
... os.remove(annotations_path)
|
|
442
|
+
{0: 1, 1: 3}
|
|
443
|
+
>>> os.path.exists(annotations_path)
|
|
444
|
+
False
|
|
445
|
+
"""
|
|
446
|
+
coco_data = read_json_file(annotations_path)
|
|
447
|
+
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
|
|
448
|
+
class_mapping = build_coco_class_index_mapping(
|
|
449
|
+
coco_categories=coco_data["categories"], target_classes=classes
|
|
450
|
+
)
|
|
451
|
+
return {v: k for k, v in class_mapping.items()}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def load_coco_annotations(
|
|
455
|
+
images_directory_path: str,
|
|
456
|
+
annotations_path: str,
|
|
457
|
+
force_masks: bool = False,
|
|
458
|
+
use_iscrowd: bool = True,
|
|
459
|
+
show_progress: bool = False,
|
|
460
|
+
) -> tuple[list[str], list[str], dict[str, Detections]]:
|
|
461
|
+
"""
|
|
462
|
+
Load COCO annotations and convert them to `Detections`.
|
|
463
|
+
|
|
464
|
+
If `force_masks` is `False`, masks are still loaded for images whose annotations
|
|
465
|
+
include a `segmentation` field. This keeps mask handling consistent with other
|
|
466
|
+
dataset loaders that infer masks from annotation content.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
images_directory_path: Path to the image directory.
|
|
470
|
+
annotations_path: Path to COCO JSON annotations.
|
|
471
|
+
force_masks: If `True`, always attempt to load masks.
|
|
472
|
+
use_iscrowd: If `True`, include `iscrowd` and `area` in detection data.
|
|
473
|
+
show_progress: If `True`, display a progress bar during loading.
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
A tuple of `(classes, image_paths, annotations)` where image paths are
|
|
477
|
+
canonicalized resolved paths inside ``images_directory_path``.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
ValueError: If any annotation's ``file_name`` resolves to the images
|
|
481
|
+
directory itself, to a path outside the images directory (e.g. via
|
|
482
|
+
``../`` traversal or an absolute path), or to a subdirectory instead
|
|
483
|
+
of a regular image file.
|
|
484
|
+
ValueError: If two image entries resolve to the same canonical path.
|
|
485
|
+
|
|
486
|
+
Note:
|
|
487
|
+
Each annotation's ``file_name`` is validated against
|
|
488
|
+
``images_directory_path`` before loading. Annotations that reference
|
|
489
|
+
paths outside the directory are rejected to prevent path-traversal
|
|
490
|
+
attacks when loading user-supplied annotation files. Symlinked images
|
|
491
|
+
pointing outside the resolved images directory are also rejected.
|
|
492
|
+
"""
|
|
493
|
+
coco_data = read_json_file(file_path=annotations_path)
|
|
494
|
+
classes = coco_categories_to_classes(coco_categories=coco_data["categories"])
|
|
495
|
+
|
|
496
|
+
class_index_mapping = build_coco_class_index_mapping(
|
|
497
|
+
coco_categories=coco_data["categories"], target_classes=classes
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
coco_images = coco_data["images"]
|
|
501
|
+
coco_annotations_groups = group_coco_annotations_by_image_id(
|
|
502
|
+
coco_annotations=coco_data["annotations"]
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
images = []
|
|
506
|
+
annotations = {}
|
|
507
|
+
images_directory_resolved = Path(images_directory_path).resolve()
|
|
508
|
+
|
|
509
|
+
for coco_image in tqdm(
|
|
510
|
+
coco_images,
|
|
511
|
+
total=len(coco_images),
|
|
512
|
+
desc="Loading COCO annotations",
|
|
513
|
+
disable=not show_progress,
|
|
514
|
+
):
|
|
515
|
+
image_name, image_width, image_height = (
|
|
516
|
+
coco_image["file_name"],
|
|
517
|
+
coco_image["width"],
|
|
518
|
+
coco_image["height"],
|
|
519
|
+
)
|
|
520
|
+
image_annotations = coco_annotations_groups.get(coco_image["id"], [])
|
|
521
|
+
image_path = str(Path(images_directory_path) / Path(image_name))
|
|
522
|
+
try:
|
|
523
|
+
resolved_image_path = Path(image_path).resolve()
|
|
524
|
+
except (OSError, ValueError) as exc:
|
|
525
|
+
raise ValueError(
|
|
526
|
+
f"COCO annotation refers to image {image_name!r}, which "
|
|
527
|
+
f"produces an invalid path: {exc}"
|
|
528
|
+
) from exc
|
|
529
|
+
if resolved_image_path == images_directory_resolved:
|
|
530
|
+
raise ValueError(
|
|
531
|
+
f"COCO annotation refers to image {image_name!r}, which "
|
|
532
|
+
f"resolves to the images directory itself "
|
|
533
|
+
f"({images_directory_resolved}). Expected a path to an "
|
|
534
|
+
"image file."
|
|
535
|
+
)
|
|
536
|
+
if images_directory_resolved not in resolved_image_path.parents:
|
|
537
|
+
raise ValueError(
|
|
538
|
+
f"COCO annotation refers to image {image_name!r}, which "
|
|
539
|
+
f"resolves to {resolved_image_path} — outside the images "
|
|
540
|
+
f"directory {images_directory_resolved}."
|
|
541
|
+
)
|
|
542
|
+
if resolved_image_path.is_dir():
|
|
543
|
+
raise ValueError(
|
|
544
|
+
f"COCO annotation refers to image {image_name!r}, which "
|
|
545
|
+
f"resolves to directory {resolved_image_path}. Expected a "
|
|
546
|
+
"path to an image file."
|
|
547
|
+
)
|
|
548
|
+
image_path = str(resolved_image_path)
|
|
549
|
+
if image_path in annotations:
|
|
550
|
+
raise ValueError(
|
|
551
|
+
f"COCO annotation file contains duplicate entries for image "
|
|
552
|
+
f"{image_name!r}. Each image must appear at most once."
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
with_masks = force_masks or any(
|
|
556
|
+
_with_seg_mask(annotation) for annotation in image_annotations
|
|
557
|
+
)
|
|
558
|
+
annotation = coco_annotations_to_detections(
|
|
559
|
+
image_annotations=image_annotations,
|
|
560
|
+
resolution_wh=(image_width, image_height),
|
|
561
|
+
with_masks=with_masks,
|
|
562
|
+
use_iscrowd=use_iscrowd,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
annotation = map_detections_class_id(
|
|
566
|
+
source_to_target_mapping=class_index_mapping,
|
|
567
|
+
detections=annotation,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
images.append(image_path)
|
|
571
|
+
annotations[image_path] = annotation
|
|
572
|
+
|
|
573
|
+
return classes, images, annotations
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _with_seg_mask(annotation: dict[str, Any]) -> bool:
|
|
577
|
+
return bool(annotation.get("segmentation"))
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def save_coco_annotations(
|
|
581
|
+
dataset: DetectionDataset,
|
|
582
|
+
annotation_path: str,
|
|
583
|
+
min_image_area_percentage: float = 0.0,
|
|
584
|
+
max_image_area_percentage: float = 1.0,
|
|
585
|
+
approximation_percentage: float = 0.0,
|
|
586
|
+
starting_image_id: int = 1,
|
|
587
|
+
starting_annotation_id: int = 1,
|
|
588
|
+
show_progress: bool = False,
|
|
589
|
+
) -> tuple[int, int]:
|
|
590
|
+
"""Save a DetectionDataset to a COCO-format ``annotations.json`` file.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
dataset: The DetectionDataset to write.
|
|
594
|
+
annotation_path: Output path for the COCO ``annotations.json``.
|
|
595
|
+
min_image_area_percentage: Lower bound on detection area / image area;
|
|
596
|
+
used only for segmentation datasets.
|
|
597
|
+
max_image_area_percentage: Upper bound on detection area / image area;
|
|
598
|
+
used only for segmentation datasets.
|
|
599
|
+
approximation_percentage: Polygon-simplification ratio in ``[0, 1)``;
|
|
600
|
+
used only for segmentation datasets.
|
|
601
|
+
starting_image_id: First image id to assign in the exported file.
|
|
602
|
+
Defaults to ``1``. Override when exporting multiple splits into
|
|
603
|
+
a coordinated COCO collection so ids remain unique across the set.
|
|
604
|
+
starting_annotation_id: First annotation id to assign in the exported
|
|
605
|
+
file. Defaults to ``1``. Override for the same multi-split reason
|
|
606
|
+
as ``starting_image_id``.
|
|
607
|
+
show_progress: If ``True``, display a progress bar during saving.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
A ``(next_image_id, next_annotation_id)`` tuple. The returned values
|
|
611
|
+
are one greater than the highest ids written, so they can be fed
|
|
612
|
+
directly back into ``starting_image_id`` and ``starting_annotation_id``
|
|
613
|
+
when exporting another split into a coordinated COCO collection
|
|
614
|
+
(see ``DetectionDataset.as_coco`` for the chaining pattern). When the
|
|
615
|
+
dataset is empty the starting ids are returned unchanged.
|
|
616
|
+
|
|
617
|
+
.. note::
|
|
618
|
+
This function ensures globally unique integer ``id`` values across
|
|
619
|
+
splits. It rejects duplicate image basenames before writing because
|
|
620
|
+
``file_name`` is set to the bare image basename, so two input paths
|
|
621
|
+
that differ only by directory would otherwise collapse to the same
|
|
622
|
+
COCO image record.
|
|
623
|
+
|
|
624
|
+
Raises:
|
|
625
|
+
ValueError: If two image paths share the same basename and would map to
|
|
626
|
+
the same COCO ``file_name``.
|
|
627
|
+
|
|
628
|
+
Example:
|
|
629
|
+
```python
|
|
630
|
+
import supervision as sv
|
|
631
|
+
from supervision.dataset.formats.coco import save_coco_annotations
|
|
632
|
+
|
|
633
|
+
ds = sv.DetectionDataset.from_yolo(
|
|
634
|
+
images_directory_path="train/images",
|
|
635
|
+
annotations_directory_path="train/labels",
|
|
636
|
+
data_yaml_path="data.yaml",
|
|
637
|
+
)
|
|
638
|
+
next_img_id, next_ann_id = save_coco_annotations(
|
|
639
|
+
dataset=ds, annotation_path="out/train/annotations.json"
|
|
640
|
+
)
|
|
641
|
+
# next_img_id and next_ann_id are the first unused ids — pass them
|
|
642
|
+
# to the next split to keep ids globally unique across files.
|
|
643
|
+
```
|
|
644
|
+
"""
|
|
645
|
+
if starting_image_id < 1 or starting_annotation_id < 1:
|
|
646
|
+
raise ValueError(
|
|
647
|
+
"starting_image_id and starting_annotation_id must be >= 1 "
|
|
648
|
+
"(COCO spec requires 1-indexed ids); "
|
|
649
|
+
f"got {starting_image_id=}, {starting_annotation_id=}"
|
|
650
|
+
)
|
|
651
|
+
check_no_basename_collisions(
|
|
652
|
+
image_paths=dataset.image_paths,
|
|
653
|
+
key=lambda image_path: Path(image_path).name,
|
|
654
|
+
output_kind="COCO image",
|
|
655
|
+
)
|
|
656
|
+
Path(annotation_path).parent.mkdir(parents=True, exist_ok=True)
|
|
657
|
+
licenses = [
|
|
658
|
+
{
|
|
659
|
+
"id": 1,
|
|
660
|
+
"url": "https://creativecommons.org/licenses/by/4.0/",
|
|
661
|
+
"name": "CC BY 4.0",
|
|
662
|
+
}
|
|
663
|
+
]
|
|
664
|
+
|
|
665
|
+
coco_annotations = []
|
|
666
|
+
coco_images = []
|
|
667
|
+
coco_categories = classes_to_coco_categories(classes=dataset.classes)
|
|
668
|
+
|
|
669
|
+
image_id, annotation_id = starting_image_id, starting_annotation_id
|
|
670
|
+
for image_path, image, annotation in tqdm(
|
|
671
|
+
dataset,
|
|
672
|
+
total=len(dataset),
|
|
673
|
+
desc="Saving COCO annotations",
|
|
674
|
+
disable=not show_progress,
|
|
675
|
+
):
|
|
676
|
+
image_height, image_width, _ = image.shape
|
|
677
|
+
image_name = f"{Path(image_path).stem}{Path(image_path).suffix}"
|
|
678
|
+
coco_image = {
|
|
679
|
+
"id": image_id,
|
|
680
|
+
"license": 1,
|
|
681
|
+
"file_name": image_name,
|
|
682
|
+
"height": image_height,
|
|
683
|
+
"width": image_width,
|
|
684
|
+
"date_captured": datetime.now().strftime("%m/%d/%Y,%H:%M:%S"),
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
coco_images.append(coco_image)
|
|
688
|
+
coco_annotation, annotation_id = detections_to_coco_annotations(
|
|
689
|
+
detections=annotation,
|
|
690
|
+
image_id=image_id,
|
|
691
|
+
annotation_id=annotation_id,
|
|
692
|
+
min_image_area_percentage=min_image_area_percentage,
|
|
693
|
+
max_image_area_percentage=max_image_area_percentage,
|
|
694
|
+
approximation_percentage=approximation_percentage,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
coco_annotations.extend(coco_annotation)
|
|
698
|
+
image_id += 1
|
|
699
|
+
|
|
700
|
+
annotation_dict = {
|
|
701
|
+
"info": {},
|
|
702
|
+
"licenses": licenses,
|
|
703
|
+
"categories": coco_categories,
|
|
704
|
+
"images": coco_images,
|
|
705
|
+
"annotations": coco_annotations,
|
|
706
|
+
}
|
|
707
|
+
save_json_file(annotation_dict, file_path=annotation_path)
|
|
708
|
+
return image_id, annotation_id
|