From dcce0e480744cf989370fe36a79432deed9e9efa Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 2 Feb 2026 07:56:04 +0100 Subject: [PATCH] test(detection): add adapter tests and ignore local data directory (#2116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(detection): add adapter tests and ignore local data directory * fix(pre_commit): 🎨 auto format pre-commit hooks * chore: retrigger ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .gitignore | 3 + test/detection/test_from_adapters.py | 113 +++++++++++++++++++++++++++ test/helpers.py | 69 ++++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 test/detection/test_from_adapters.py diff --git a/.gitignore b/.gitignore index 52bf2110..a380012e 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,6 @@ Desktop.ini *~ .directory .Trash-* + +# local data +data/ diff --git a/test/detection/test_from_adapters.py b/test/detection/test_from_adapters.py new file mode 100644 index 00000000..f31988ea --- /dev/null +++ b/test/detection/test_from_adapters.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import numpy as np +import pytest + +import supervision.detection.core as detection_core +from supervision.config import CLASS_NAME_DATA_FIELD +from supervision.detection.core import Detections +from test.helpers import ( + _FakeUltralyticsBoxes, + _FakeUltralyticsResults, + _FakeYoloNasPrediction, + _FakeYoloNasResults, + _FakeYOLOv5Results, +) + + +def test_from_yolov5_maps_columns_correctly() -> None: + pred = np.array( + [ + [10, 20, 30, 40, 0.9, 2], + [1, 2, 3, 4, 0.1, 7], + ], + dtype=np.float32, + ) + results = _FakeYOLOv5Results(pred0=pred) + + det = Detections.from_yolov5(results) + + assert isinstance(det, Detections) + np.testing.assert_allclose(det.xyxy, pred[:, :4]) + np.testing.assert_allclose(det.confidence, pred[:, 4]) + np.testing.assert_array_equal(det.class_id, pred[:, 5].astype(int)) + + +def test_from_ultralytics_boxes_branch_maps_fields_and_class_names() -> None: + xyxy = np.array([[0, 0, 10, 10], [5, 6, 7, 8]], dtype=np.float32) + conf = np.array([0.8, 0.2], dtype=np.float32) + cls = np.array([1, 0], dtype=np.float32) + names = {0: "cat", 1: "dog"} + + boxes = _FakeUltralyticsBoxes(xyxy=xyxy, conf=conf, cls=cls, id_=None) + results = _FakeUltralyticsResults(boxes=boxes, names=names) + + det = Detections.from_ultralytics(results) + + np.testing.assert_allclose(det.xyxy, xyxy) + np.testing.assert_allclose(det.confidence, conf) + np.testing.assert_array_equal(det.class_id, cls.astype(int)) + assert det.tracker_id is None + + assert CLASS_NAME_DATA_FIELD in det.data + expected_names = np.array([names[i] for i in cls.astype(int)]) + np.testing.assert_array_equal(det.data[CLASS_NAME_DATA_FIELD], expected_names) + + +def test_from_ultralytics_segmentation_only_branch_uses_masks_and_arange( + monkeypatch: pytest.MonkeyPatch, +) -> None: + results = _FakeUltralyticsResults(boxes=None, names={}, length=3) + + fake_masks = np.zeros((3, 10, 10), dtype=bool) + fake_xyxy = np.array([[0, 0, 1, 1], [2, 2, 3, 3], [4, 4, 5, 5]], dtype=np.float32) + + monkeypatch.setattr( + detection_core, "extract_ultralytics_masks", lambda _: fake_masks + ) + monkeypatch.setattr(detection_core, "mask_to_xyxy", lambda masks: fake_xyxy) + + det = Detections.from_ultralytics(results) + + np.testing.assert_allclose(det.xyxy, fake_xyxy) + np.testing.assert_array_equal(det.mask, fake_masks) + np.testing.assert_array_equal(det.class_id, np.arange(len(results))) + + +@pytest.mark.parametrize( + ("bboxes", "conf", "labels", "expected_len"), + [ + ( + np.empty((0, 4), dtype=np.float32), + np.empty((0,), dtype=np.float32), + np.empty((0,), dtype=np.int64), + 0, + ), + ( + np.array([[1, 2, 3, 4], [10, 20, 30, 40]], dtype=np.float32), + np.array([0.3, 0.9], dtype=np.float32), + np.array([5, 6], dtype=np.int64), + 2, + ), + ], +) +def test_from_yolo_nas_handles_empty_and_non_empty( + bboxes: np.ndarray, + conf: np.ndarray, + labels: np.ndarray, + expected_len: int, +) -> None: + pred = _FakeYoloNasPrediction( + bboxes_xyxy=bboxes, + confidence=conf, + labels=labels, + ) + results = _FakeYoloNasResults(prediction=pred) + + det = Detections.from_yolo_nas(results) + + assert len(det) == expected_len + if expected_len > 0: + np.testing.assert_allclose(det.xyxy, bboxes) + np.testing.assert_allclose(det.confidence, conf) + np.testing.assert_array_equal(det.class_id, labels.astype(int)) diff --git a/test/helpers.py b/test/helpers.py index bff7c867..0c8fc349 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -238,3 +238,72 @@ def assert_image_mostly_same( # Check that the image is not completely identical assert not np.array_equal(original, annotated), "Images are completely identical" + + +class _FakeTensor: + """Minimal tensor wrapper for cpu().numpy() and int().""" + + def __init__(self, arr: np.ndarray): + self._arr = np.asarray(arr) + + def cpu(self) -> _FakeTensor: + return self + + def numpy(self) -> np.ndarray: + return self._arr + + def int(self) -> _FakeTensor: + return _FakeTensor(self._arr.astype(int)) + + +class _FakeYOLOv5Results: + """YOLOv5-like results exposing pred list.""" + + def __init__(self, pred0: np.ndarray): + self.pred = [_FakeTensor(pred0)] + + +class _FakeUltralyticsBoxes: + """Ultralytics-like Boxes exposing xyxy/conf/cls and optional id.""" + + def __init__( + self, + xyxy: np.ndarray, + conf: np.ndarray, + cls: np.ndarray, + id_: np.ndarray | None = None, + ): + self.xyxy = _FakeTensor(xyxy) + self.conf = _FakeTensor(conf) + self.cls = _FakeTensor(cls) + self.id = _FakeTensor(id_) if id_ is not None else None + + +class _FakeUltralyticsResults: + """Ultralytics-like results container used by from_ultralytics.""" + + def __init__(self, boxes, names: dict[int, str], length: int = 0): + self.boxes = boxes + self.names = names + self.obb = None + self.masks = None + self._length = length + + def __len__(self) -> int: + return self._length + + +class _FakeYoloNasPrediction: + """YOLO-NAS-like prediction struct.""" + + def __init__(self, bboxes_xyxy, confidence, labels): + self.bboxes_xyxy = bboxes_xyxy + self.confidence = confidence + self.labels = labels + + +class _FakeYoloNasResults: + """YOLO-NAS-like results exposing prediction.""" + + def __init__(self, prediction: _FakeYoloNasPrediction): + self.prediction = prediction