Skip to content

Commit 934da12

Browse files
Bordacodex
andauthored
chore(typing): Add explicit selection helpers (roboflow#2373)
* Add explicit selection helpers * improve typing in detection metrics and update pre-commit dependencies - Add explicit type annotation for `panel_array` in `_draw_panel` function. - Update `.pre-commit-config.yaml` to include `tomli>=2.0.1` as an additional dependency for `pyproject-fmt`. --------- Co-authored-by: Codex <codex@openai.com>
1 parent ad2c750 commit 934da12

8 files changed

Lines changed: 243 additions & 83 deletions

File tree

.pre-commit-config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ repos:
4040
rev: v2.25.0
4141
hooks:
4242
- id: pyproject-fmt
43+
additional_dependencies:
44+
- "tomli>=2.0.1"
4345

4446
- repo: https://github.com/abravalheri/validate-pyproject
4547
rev: v0.25

src/supervision/detection/core.py

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
process_transformers_v4_segmentation_result,
2020
process_transformers_v5_segmentation_result,
2121
)
22-
from supervision.detection.utils._typing import _DetectionDataType, _MetadataType
22+
from supervision.detection.utils._typing import (
23+
_DetectionDataType,
24+
_DetectionDataValueType,
25+
_MetadataType,
26+
)
2327
from supervision.detection.utils.boxes import obb_polygon_area, xyxyxyxy_to_xyxy
2428
from supervision.detection.utils.converters import (
2529
mask_to_xyxy,
@@ -2297,8 +2301,76 @@ def coordinates(
22972301

22982302
raise ValueError(f"{anchor} is not supported.")
22992303

2304+
def get_data(self, key: str) -> _DetectionDataValueType | None:
2305+
"""Get a value from the detection data dictionary.
2306+
2307+
Args:
2308+
key: Data field name.
2309+
2310+
Returns:
2311+
The stored data value, or `None` when the key is absent.
2312+
2313+
Example:
2314+
>>> import numpy as np
2315+
>>> from supervision import Detections
2316+
>>> detections = Detections(
2317+
... xyxy=np.array([[0, 0, 1, 1]]),
2318+
... data={"class_name": np.array(["cat"])},
2319+
... )
2320+
>>> detections.get_data("class_name").tolist()
2321+
['cat']
2322+
"""
2323+
return self.data.get(key)
2324+
2325+
def select(
2326+
self,
2327+
index: int | np.integer[Any] | slice | list[int] | npt.NDArray[np.generic],
2328+
) -> Detections:
2329+
"""Get a subset of the Detections object.
2330+
2331+
Args:
2332+
index: Row index, indices, slice, or boolean mask selecting detections.
2333+
2334+
Returns:
2335+
A `Detections` instance containing the selected rows. Empty detections
2336+
are returned unchanged.
2337+
2338+
Example:
2339+
>>> import numpy as np
2340+
>>> from supervision import Detections
2341+
>>> detections = Detections(xyxy=np.array([[0, 0, 1, 1], [1, 1, 2, 2]]))
2342+
>>> detections.select([1]).xyxy.tolist()
2343+
[[1, 1, 2, 2]]
2344+
"""
2345+
if len(self) == 0:
2346+
return self
2347+
if isinstance(index, (int, np.integer)):
2348+
index = [int(index)]
2349+
array_index = cast(
2350+
slice | list[int] | npt.NDArray[np.integer | np.bool_], index
2351+
)
2352+
return Detections(
2353+
xyxy=self.xyxy[array_index],
2354+
mask=self.mask[cast(Any, array_index)] if self.mask is not None else None,
2355+
confidence=(
2356+
self.confidence[array_index] if self.confidence is not None else None
2357+
),
2358+
class_id=self.class_id[array_index] if self.class_id is not None else None,
2359+
tracker_id=(
2360+
self.tracker_id[array_index] if self.tracker_id is not None else None
2361+
),
2362+
data=get_data_item(self.data, array_index),
2363+
metadata=self.metadata,
2364+
)
2365+
23002366
def __getitem__(
2301-
self, index: int | slice | list[int] | npt.NDArray[np.generic] | str
2367+
self,
2368+
index: int
2369+
| np.integer[Any]
2370+
| slice
2371+
| list[int]
2372+
| npt.NDArray[np.generic]
2373+
| str,
23022374
) -> Detections | list[Any] | npt.NDArray[np.generic] | None:
23032375
"""
23042376
Get a subset of the Detections object or access an item from its data field.
@@ -2331,27 +2403,8 @@ def __getitem__(
23312403
```
23322404
"""
23332405
if isinstance(index, str):
2334-
return self.data.get(index)
2335-
if len(self) == 0:
2336-
return self
2337-
if isinstance(index, int):
2338-
index = [index]
2339-
array_index = cast(
2340-
slice | list[int] | npt.NDArray[np.integer | np.bool_], index
2341-
)
2342-
return Detections(
2343-
xyxy=self.xyxy[array_index],
2344-
mask=self.mask[cast(Any, array_index)] if self.mask is not None else None,
2345-
confidence=(
2346-
self.confidence[array_index] if self.confidence is not None else None
2347-
),
2348-
class_id=self.class_id[array_index] if self.class_id is not None else None,
2349-
tracker_id=(
2350-
self.tracker_id[array_index] if self.tracker_id is not None else None
2351-
),
2352-
data=get_data_item(self.data, array_index),
2353-
metadata=self.metadata,
2354-
)
2406+
return self.get_data(index)
2407+
return self.select(index)
23552408

23562409
def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> None:
23572410
"""
@@ -2568,7 +2621,7 @@ def with_nms(
25682621
overlap_metric=overlap_metric,
25692622
)
25702623

2571-
return cast(Detections, self[indices])
2624+
return self.select(indices)
25722625

25732626
def with_nmm(
25742627
self,
@@ -2665,7 +2718,7 @@ def with_nmm(
26652718

26662719
result: list[Detections] = []
26672720
for merge_group in merge_groups:
2668-
group = [cast(Detections, self[i]) for i in merge_group]
2721+
group = [self.select(i) for i in merge_group]
26692722
result.append(_merge_detection_group(group))
26702723

26712724
return Detections.merge(result)

src/supervision/detection/tools/smoother.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import warnings
22
from collections import defaultdict, deque
33
from copy import deepcopy
4-
from typing import cast
54

65
import numpy as np
76

@@ -116,7 +115,7 @@ def update_with_detections(self, detections: Detections) -> Detections:
116115
tracker_id_value = detections.tracker_id[detection_idx]
117116
tracker_id = int(tracker_id_value)
118117

119-
self.tracks[tracker_id].append(cast(Detections, detections[detection_idx]))
118+
self.tracks[tracker_id].append(detections.select(detection_idx))
120119

121120
for track_id in self.tracks.keys():
122121
if track_id not in detections.tracker_id:
@@ -152,7 +151,7 @@ def get_track(self, track_id: int) -> Detections | None:
152151
return None
153152

154153
ret = deepcopy(valid[0])
155-
ret.xyxy = np.mean([d.xyxy for d in valid], axis=0)
154+
ret.xyxy = np.mean(np.stack([d.xyxy for d in valid], axis=0), axis=0)
156155
# Average confidence only over frames that carry it; frames with
157156
# confidence=None contribute nothing to the mean. Retain None when
158157
# no frame in the window carries confidence.

src/supervision/key_points/core.py

Lines changed: 84 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
from supervision.config import CLASS_NAME_DATA_FIELD
1212
from supervision.detection.core import Detections
13-
from supervision.detection.utils._typing import _DetectionDataType
13+
from supervision.detection.utils._typing import (
14+
_DetectionDataType,
15+
_DetectionDataValueType,
16+
)
1417
from supervision.detection.utils.internal import get_data_item, is_data_equal
1518
from supervision.detection.utils.iou_and_nms import (
1619
OverlapMetric,
@@ -908,51 +911,49 @@ def _get_by_2d_bool_mask(self, mask: npt.NDArray[np.bool_]) -> KeyPoints:
908911
data=data_selected,
909912
)
910913

911-
def __getitem__(
912-
self,
913-
index: Index1D | Index2D | str,
914-
) -> KeyPoints | npt.NDArray[np.generic] | list[Any] | None:
915-
"""
916-
Get a subset of the KeyPoints object or access an item from its data field.
917-
918-
Supports detection-level (skeleton) filtering, keypoint-level (anchor)
919-
filtering, combined tuple indexing, and data field access by string key.
914+
def get_data(self, key: str) -> _DetectionDataValueType | None:
915+
"""Get a value from the keypoint data dictionary.
920916
921917
Args:
922-
index: The index, indices, or key to access a subset of the KeyPoints
923-
or an item from the data.
918+
key: Data field name.
924919
925920
Returns:
926-
A subset of the KeyPoints object or an item from the data field.
927-
928-
Examples:
929-
```python
930-
import supervision as sv
921+
The stored data value, or `None` when the key is absent.
931922
932-
key_points = sv.KeyPoints(...)
923+
Example:
924+
>>> import numpy as np
925+
>>> from supervision import KeyPoints
926+
>>> key_points = KeyPoints(
927+
... xy=np.array([[[0, 0]]]),
928+
... data={"class_name": np.array(["person"])},
929+
... )
930+
>>> key_points.get_data("class_name").tolist()
931+
['person']
932+
"""
933+
return self.data.get(key)
933934

934-
# detection-level filtering (returns KeyPoints)
935-
high_conf = key_points[key_points.detection_confidence > 0.5]
936-
class_0 = key_points[key_points.class_id == 0]
935+
def select(
936+
self,
937+
index: Index1D | Index2D,
938+
) -> KeyPoints:
939+
"""Get a subset of the KeyPoints object.
937940
938-
# keypoint-level filtering (returns KeyPoints)
939-
visible = key_points[key_points.keypoint_confidence > 0.3]
941+
Supports detection-level (skeleton) filtering, keypoint-level (anchor)
942+
filtering, and combined tuple indexing.
940943
941-
# indexing
942-
first = key_points[0]
943-
first_two = key_points[0:2]
944-
subset = key_points[[0, 2]]
944+
Args:
945+
index: Index, indices, slice, or boolean mask selecting key points.
945946
946-
# anchor selection (uniform across all skeletons)
947-
nose_and_eyes = key_points[:, [0, 1, 2]]
947+
Returns:
948+
A new `KeyPoints` instance containing the selected rows or anchors.
948949
949-
# data field access
950-
class_names = key_points['class_name']
951-
```
950+
Example:
951+
>>> import numpy as np
952+
>>> from supervision import KeyPoints
953+
>>> key_points = KeyPoints(xy=np.array([[[0, 1]], [[2, 3]]]))
954+
>>> key_points.select([1]).xy.tolist()
955+
[[[2, 3]]]
952956
"""
953-
if isinstance(index, str):
954-
return self.data.get(index)
955-
956957
if isinstance(index, np.ndarray) and index.ndim == 2 and index.dtype == bool:
957958
return self._get_by_2d_bool_mask(cast(npt.NDArray[np.bool_], index))
958959

@@ -1045,6 +1046,52 @@ def __getitem__(
10451046
data=data_selected,
10461047
)
10471048

1049+
def __getitem__(
1050+
self,
1051+
index: Index1D | Index2D | str,
1052+
) -> KeyPoints | npt.NDArray[np.generic] | list[Any] | None:
1053+
"""
1054+
Get a subset of the KeyPoints object or access an item from its data field.
1055+
1056+
Supports detection-level (skeleton) filtering, keypoint-level (anchor)
1057+
filtering, combined tuple indexing, and data field access by string key.
1058+
1059+
Args:
1060+
index: The index, indices, or key to access a subset of the KeyPoints
1061+
or an item from the data.
1062+
1063+
Returns:
1064+
A subset of the KeyPoints object or an item from the data field.
1065+
1066+
Examples:
1067+
```python
1068+
import supervision as sv
1069+
1070+
key_points = sv.KeyPoints(...)
1071+
1072+
# detection-level filtering (returns KeyPoints)
1073+
high_conf = key_points[key_points.detection_confidence > 0.5]
1074+
class_0 = key_points[key_points.class_id == 0]
1075+
1076+
# keypoint-level filtering (returns KeyPoints)
1077+
visible = key_points[key_points.keypoint_confidence > 0.3]
1078+
1079+
# indexing
1080+
first = key_points[0]
1081+
first_two = key_points[0:2]
1082+
subset = key_points[[0, 2]]
1083+
1084+
# anchor selection (uniform across all skeletons)
1085+
nose_and_eyes = key_points[:, [0, 1, 2]]
1086+
1087+
# data field access
1088+
class_names = key_points['class_name']
1089+
```
1090+
"""
1091+
if isinstance(index, str):
1092+
return self.get_data(index)
1093+
return self.select(index)
1094+
10481095
def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> None:
10491096
"""
10501097
Set a value in the data dictionary of the `sv.KeyPoints` object.
@@ -1205,7 +1252,7 @@ def with_nms(
12051252
overlap_metric=overlap_metric,
12061253
)
12071254

1208-
return cast(KeyPoints, self[keep])
1255+
return self.select(keep)
12091256

12101257
def as_detections(
12111258
self, selected_keypoint_indices: Iterable[int] | None = None
@@ -1275,6 +1322,6 @@ def as_detections(
12751322
detections = Detections(xyxy=xyxy, confidence=confidence)
12761323
detections.class_id = self.class_id
12771324
detections.data = self.data
1278-
detections = cast(Detections, detections[cast(Any, detections.area) > 0])
1325+
detections = detections.select(cast(Any, detections.area) > 0)
12791326

12801327
return detections

src/supervision/metrics/detection.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,8 @@ def _split_detections_by_outcome(
239239
filtered_predictions = predictions
240240
else:
241241
prediction_confidence = np.asarray(predictions.confidence, dtype=np.float32)
242-
filtered_predictions = cast(
243-
Detections,
244-
predictions[prediction_confidence >= conf_threshold],
242+
filtered_predictions = predictions.select(
243+
prediction_confidence >= conf_threshold
245244
)
246245

247246
filtered_prediction_class_ids = filtered_predictions.class_id
@@ -258,17 +257,17 @@ def _split_detections_by_outcome(
258257
if prediction_count == 0:
259258
fn_indices = list(range(target_count))
260259
return (
261-
cast(Detections, filtered_predictions[tp_indices]),
262-
cast(Detections, filtered_predictions[fp_indices]),
263-
cast(Detections, targets[fn_indices]),
260+
filtered_predictions.select(tp_indices),
261+
filtered_predictions.select(fp_indices),
262+
targets.select(fn_indices),
264263
)
265264

266265
if target_count == 0:
267266
fp_indices = list(range(prediction_count))
268267
return (
269-
cast(Detections, filtered_predictions[tp_indices]),
270-
cast(Detections, filtered_predictions[fp_indices]),
271-
cast(Detections, targets[fn_indices]),
268+
filtered_predictions.select(tp_indices),
269+
filtered_predictions.select(fp_indices),
270+
targets.select(fn_indices),
272271
)
273272

274273
# IoU computation mirrors evaluate_detection_batch — keep in sync if either changes.
@@ -341,9 +340,9 @@ def _split_detections_by_outcome(
341340
fn_indices.extend(cross_class_target_indices)
342341

343342
return (
344-
cast(Detections, filtered_predictions[tp_indices]),
345-
cast(Detections, filtered_predictions[fp_indices]),
346-
cast(Detections, targets[fn_indices]),
343+
filtered_predictions.select(tp_indices),
344+
filtered_predictions.select(fp_indices),
345+
targets.select(fn_indices),
347346
)
348347

349348

@@ -483,7 +482,8 @@ def _annotate_detection_panel(
483482
title_thickness,
484483
cv2.LINE_AA,
485484
)
486-
return cast(npt.NDArray[np.uint8], panel)
485+
panel_array: npt.NDArray[np.uint8] = panel
486+
return panel_array
487487

488488

489489
def _save_detection_validation_visualization(

0 commit comments

Comments
 (0)