Files
wehub-resource-sync 9194ef5abd
Docs/Test Workflow / Test docs build (push) Failing after 0s
Check links & references / links-check (push) Failing after 1s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.10) (push) Failing after 0s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.11) (push) Failing after 0s
PR Conflict Labeler / main (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.12) (push) Failing after 2s
Pytest/Test Workflow / Import Test and Pytest Run (ubuntu-latest, 3.13) (push) Failing after 0s
Pytest/Test Workflow / Build this Package (push) Failing after 5s
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (macos-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.10) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.11) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.12) (push) Has been cancelled
Pytest/Test Workflow / Import Test and Pytest Run (windows-latest, 3.13) (push) Has been cancelled
Pytest/Test Workflow / testing-guardian (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:06:10 +08:00

794 lines
28 KiB
Python

import numpy as np
import pytest
import supervision.detection.core as detection_core
from supervision.config import CLASS_NAME_DATA_FIELD, ORIENTED_BOX_COORDINATES
from supervision.detection.core import LMM, Detections
from supervision.detection.vlm import VLM
from supervision.utils.internal import SupervisionWarnings
from tests.helpers import (
_FakeDeepSparseResults,
_FakeDetachTensor,
_FakeDetectron2Instances,
_FakeMMDetPredInstances,
_FakeMMDetResults,
_FakeNCNNObject,
_FakeTensor,
_FakeUltralyticsBoxes,
_FakeUltralyticsResults,
_FakeYoloNasPrediction,
_FakeYoloNasResults,
_FakeYOLOv5Results,
make_panoptic_png,
)
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)))
def test_from_ultralytics_segmentation_only_without_masks_returns_empty() -> None:
"""Segmentation-only Ultralytics results without masks return empty detections."""
results = _FakeUltralyticsResults(boxes=None, names={}, length=0)
det = Detections.from_ultralytics(results)
assert len(det) == 0
assert det.xyxy.shape == (0, 4)
assert det.mask is None
np.testing.assert_array_equal(
det.data[CLASS_NAME_DATA_FIELD], np.array([], dtype=str)
)
@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))
def test_from_tensorflow_scales_axes_on_non_square_image() -> None:
"""Non-square image exposes swapped scaling: y uses height, x uses width."""
results = {
"detection_boxes": [
_FakeTensor(np.array([[0.1, 0.2, 0.5, 0.6]], dtype=np.float32))
],
"detection_scores": [_FakeTensor(np.array([0.9], dtype=np.float32))],
"detection_classes": [_FakeTensor(np.array([1], dtype=np.float32))],
}
det = Detections.from_tensorflow(results, resolution_wh=(1000, 500))
# xmin=0.2*1000, ymin=0.1*500, xmax=0.6*1000, ymax=0.5*500
np.testing.assert_allclose(det.xyxy, [[200.0, 50.0, 600.0, 250.0]])
np.testing.assert_allclose(det.confidence, [0.9])
np.testing.assert_array_equal(det.class_id, [1])
def test_from_tensorflow_does_not_mutate_source_boxes() -> None:
"""Scaling must copy the tensor buffer, leaving the caller's boxes untouched."""
source_boxes = np.array([[0.1, 0.2, 0.5, 0.6]], dtype=np.float32)
original = source_boxes.copy()
results = {
"detection_boxes": [_FakeTensor(source_boxes)],
"detection_scores": [_FakeTensor(np.array([0.9], dtype=np.float32))],
"detection_classes": [_FakeTensor(np.array([1], dtype=np.float32))],
}
det = Detections.from_tensorflow(results, resolution_wh=(1000, 500))
np.testing.assert_array_equal(source_boxes, original)
np.testing.assert_allclose(det.xyxy, [[200.0, 50.0, 600.0, 250.0]])
class TestFromLMMMapping:
"""`from_lmm` must map every LMM member to a VLM without raising KeyError."""
@pytest.mark.parametrize(
"lmm_member",
[pytest.param(member, id=member.name.lower()) for member in LMM],
)
def test_from_lmm_maps_every_member_to_vlm(
self, lmm_member: LMM, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Each LMM member dispatches to the VLM sharing its value with args intact."""
captured: dict[str, object] = {}
def fake_from_vlm(vlm: VLM, result: str, **kwargs: object) -> Detections:
captured["vlm"] = vlm
captured["result"] = result
captured["kwargs"] = kwargs
return Detections.empty()
monkeypatch.setattr(Detections, "from_vlm", staticmethod(fake_from_vlm))
Detections.from_lmm(lmm_member, result="sentinel", resolution_wh=(10, 10))
assert isinstance(captured["vlm"], VLM)
assert captured["vlm"].value == lmm_member.value # type: ignore[union-attr]
assert captured["result"] == "sentinel"
assert captured["kwargs"]["resolution_wh"] == (10, 10) # type: ignore[index]
# ---------------------------------------------------------------------------
# from_transformers
# ---------------------------------------------------------------------------
class TestFromTransformers:
"""from_transformers routes detection/segmentation inputs to the right processor."""
def test_detection_path_maps_boxes_labels_scores(self) -> None:
"""Detection result with boxes+labels+scores sets xyxy, class_id, confidence."""
xyxy = np.array([[10, 20, 30, 40], [5, 6, 7, 8]], dtype=np.float32)
labels = np.array([1, 0], dtype=np.int64)
scores = np.array([0.9, 0.5], dtype=np.float32)
result = {
"boxes": _FakeDetachTensor(xyxy),
"labels": _FakeDetachTensor(labels),
"scores": _FakeDetachTensor(scores),
}
det = Detections.from_transformers(result)
np.testing.assert_allclose(det.xyxy, xyxy)
np.testing.assert_array_equal(det.class_id, labels.astype(int))
np.testing.assert_allclose(det.confidence, scores)
def test_detection_path_empty_returns_zero_detections(self) -> None:
"""Detection path with zero-row tensors yields an empty Detections."""
result = {
"boxes": _FakeDetachTensor(np.empty((0, 4), dtype=np.float32)),
"labels": _FakeDetachTensor(np.empty(0, dtype=np.int64)),
"scores": _FakeDetachTensor(np.empty(0, dtype=np.float32)),
}
det = Detections.from_transformers(result)
assert len(det) == 0
def test_detection_path_with_id2label_populates_class_names(self) -> None:
"""id2label mapping adds class name strings to data dict."""
labels = np.array([0, 1], dtype=np.int64)
result = {
"boxes": _FakeDetachTensor(np.zeros((2, 4), dtype=np.float32)),
"labels": _FakeDetachTensor(labels),
"scores": _FakeDetachTensor(np.array([0.8, 0.7], dtype=np.float32)),
}
det = Detections.from_transformers(result, id2label={0: "cat", 1: "dog"})
np.testing.assert_array_equal(det.data[CLASS_NAME_DATA_FIELD], ["cat", "dog"])
def test_v4_segmentation_masks_without_boxes_yield_correct_shape(self) -> None:
"""V4 segmentation with masks-only produces mask of shape (N, H, W)."""
masks_bool = np.zeros((2, 4, 4), dtype=bool)
masks_bool[0, 0:2, 0:2] = True
masks_bool[1, 2:4, 2:4] = True
result = {
"masks": _FakeDetachTensor(masks_bool.astype(np.uint8)),
"labels": _FakeDetachTensor(np.array([0, 1], dtype=np.int64)),
"scores": _FakeDetachTensor(np.array([0.9, 0.8], dtype=np.float32)),
}
det = Detections.from_transformers(result)
assert len(det) == 2
assert det.mask is not None
assert det.mask.shape == (2, 4, 4)
def test_v4_panoptic_png_string_extracts_masks_from_red_channel(self) -> None:
"""V4 panoptic: png_string+segments_info produce masks keyed by segment id."""
seg_map = np.zeros((4, 4), dtype=np.uint8)
seg_map[0:2, 0:2] = 1
seg_map[2:4, 2:4] = 2
png_bytes = make_panoptic_png(seg_map)
result = {
"png_string": png_bytes,
"segments_info": [
{"id": 1, "category_id": 3},
{"id": 2, "category_id": 7},
],
}
det = Detections.from_transformers(result)
assert len(det) == 2
np.testing.assert_array_equal(det.class_id, [3, 7])
assert det.mask is not None
def test_v5_segmentation_key_routes_to_semantic_instance_processor(self) -> None:
"""'segmentation' key routes through v5 semantic/instance segmentation path."""
seg_arr = np.zeros((4, 4), dtype=np.int64)
seg_arr[2:4, :] = 1
segments_info = [
{"id": 0, "label_id": 0, "score": 0.9},
{"id": 1, "label_id": 1, "score": 0.8},
]
result = {
"segmentation": _FakeDetachTensor(seg_arr),
"segments_info": segments_info,
}
det = Detections.from_transformers(result)
assert len(det) == 2
np.testing.assert_array_equal(det.class_id, [0, 1])
np.testing.assert_allclose(det.confidence, [0.9, 0.8])
def test_unrecognised_keys_raise_value_error(self) -> None:
"""Dict with no valid keys (no boxes/masks/segmentation) raises ValueError."""
with pytest.raises(ValueError, match="valid fields"):
Detections.from_transformers({})
# ---------------------------------------------------------------------------
# from_detectron2
# ---------------------------------------------------------------------------
class TestFromDetectron2:
"""from_detectron2 maps Detectron2 pred_instances to Detections fields."""
def test_two_detections_without_masks_maps_fields(self) -> None:
"""N=2, no pred_masks: xyxy, confidence, class_id extracted correctly."""
xyxy = np.array([[0, 0, 10, 10], [5, 5, 15, 15]], dtype=np.float32)
scores = np.array([0.9, 0.7], dtype=np.float32)
class_ids = np.array([1, 2], dtype=np.int64)
instances = _FakeDetectron2Instances(xyxy, scores, class_ids)
result = {"instances": instances}
det = Detections.from_detectron2(result)
np.testing.assert_allclose(det.xyxy, xyxy)
np.testing.assert_allclose(det.confidence, scores)
np.testing.assert_array_equal(det.class_id, class_ids.astype(int))
assert det.mask is None
def test_single_detection_without_masks(self) -> None:
"""N=1 detection without masks returns one-element Detections."""
xyxy = np.array([[1, 2, 3, 4]], dtype=np.float32)
instances = _FakeDetectron2Instances(
xyxy, np.array([0.5], dtype=np.float32), np.array([0])
)
det = Detections.from_detectron2({"instances": instances})
assert len(det) == 1
def test_with_pred_masks_sets_mask_field(self) -> None:
"""When pred_masks present, mask is populated with boolean array."""
xyxy = np.array([[0, 0, 4, 4]], dtype=np.float32)
masks = np.ones((1, 4, 4), dtype=bool)
instances = _FakeDetectron2Instances(
xyxy, np.array([0.8], dtype=np.float32), np.array([0]), masks=masks
)
det = Detections.from_detectron2({"instances": instances})
assert det.mask is not None
assert det.mask.shape == (1, 4, 4)
def test_empty_instances_returns_zero_length(self) -> None:
"""Empty pred_instances arrays produce a zero-length Detections."""
instances = _FakeDetectron2Instances(
np.empty((0, 4), dtype=np.float32),
np.empty(0, dtype=np.float32),
np.empty(0, dtype=np.int64),
)
det = Detections.from_detectron2({"instances": instances})
assert len(det) == 0
# ---------------------------------------------------------------------------
# from_mmdetection
# ---------------------------------------------------------------------------
class TestFromMMDetection:
"""from_mmdetection maps MMDet pred_instances to Detections fields."""
def test_two_detections_without_masks(self) -> None:
"""N=2, no masks attribute: xyxy, confidence, class_id set; mask is None."""
xyxy = np.array([[0, 0, 10, 10], [5, 5, 20, 20]], dtype=np.float32)
scores = np.array([0.85, 0.6], dtype=np.float32)
labels = np.array([0, 3], dtype=np.int64)
pred_instances = _FakeMMDetPredInstances(xyxy, scores, labels)
result = _FakeMMDetResults(pred_instances)
det = Detections.from_mmdetection(result)
np.testing.assert_allclose(det.xyxy, xyxy)
np.testing.assert_allclose(det.confidence, scores)
np.testing.assert_array_equal(det.class_id, labels.astype(int))
assert det.mask is None
def test_single_detection_without_masks(self) -> None:
"""N=1 detection without masks returns one-element Detections."""
pred_instances = _FakeMMDetPredInstances(
np.array([[2, 4, 6, 8]], dtype=np.float32),
np.array([0.75], dtype=np.float32),
np.array([1]),
)
det = Detections.from_mmdetection(_FakeMMDetResults(pred_instances))
assert len(det) == 1
def test_with_masks_populates_mask_field(self) -> None:
"""When masks present in pred_instances, mask field is set."""
xyxy = np.array([[0, 0, 4, 4]], dtype=np.float32)
masks = np.ones((1, 4, 4), dtype=bool)
pred_instances = _FakeMMDetPredInstances(
xyxy, np.array([0.9], dtype=np.float32), np.array([0]), masks=masks
)
det = Detections.from_mmdetection(_FakeMMDetResults(pred_instances))
assert det.mask is not None
assert det.mask.shape == (1, 4, 4)
def test_empty_pred_instances_returns_zero_length(self) -> None:
"""Empty bboxes/scores/labels arrays yield zero-length Detections."""
pred_instances = _FakeMMDetPredInstances(
np.empty((0, 4), dtype=np.float32),
np.empty(0, dtype=np.float32),
np.empty(0, dtype=np.int64),
)
det = Detections.from_mmdetection(_FakeMMDetResults(pred_instances))
assert len(det) == 0
# ---------------------------------------------------------------------------
# from_paddledet
# ---------------------------------------------------------------------------
class TestFromPaddleDet:
"""from_paddledet extracts xyxy, confidence, class_id from the bbox column array."""
def test_empty_bbox_returns_empty_detections(self) -> None:
"""Empty (0,6) bbox array yields zero-length Detections."""
det = Detections.from_paddledet({"bbox": np.empty((0, 6), dtype=np.float32)})
assert len(det) == 0
@pytest.mark.parametrize(
("bbox_array", "expected_len"),
[
pytest.param(
np.array(
[[0, 0.9, 10, 20, 30, 40], [1, 0.7, 5, 6, 7, 8]],
dtype=np.float32,
),
2,
id="two-detections",
),
pytest.param(
np.array([[2, 0.5, 1, 2, 3, 4]], dtype=np.float32),
1,
id="single-detection",
),
],
)
def test_maps_bbox_columns_to_detections(
self, bbox_array: np.ndarray, expected_len: int
) -> None:
"""bbox[:,0]=class_id, [:,1]=confidence, [:,2:6]=xyxy are extracted."""
result = {"bbox": bbox_array}
det = Detections.from_paddledet(result)
assert len(det) == expected_len
np.testing.assert_allclose(det.xyxy, bbox_array[:, 2:6])
np.testing.assert_allclose(det.confidence, bbox_array[:, 1])
np.testing.assert_array_equal(det.class_id, bbox_array[:, 0].astype(int))
# ---------------------------------------------------------------------------
# from_deepsparse
# ---------------------------------------------------------------------------
class TestFromDeepSparse:
"""from_deepsparse maps DeepSparse boxes/scores/labels to Detections."""
def test_empty_results_return_empty_detections(self) -> None:
"""Empty boxes/scores/labels arrays yield zero-length Detections."""
result = _FakeDeepSparseResults(
boxes=[np.empty((0, 4), dtype=np.float32)],
scores=[np.empty(0, dtype=np.float32)],
labels=[np.empty(0, dtype=np.float32)],
)
det = Detections.from_deepsparse(result)
assert len(det) == 0
@pytest.mark.parametrize(
("boxes", "scores", "labels", "expected_len"),
[
pytest.param(
np.array([[0, 0, 10, 10], [5, 5, 15, 15]], dtype=np.float32),
np.array([0.95, 0.8], dtype=np.float32),
np.array([0, 1], dtype=np.float32),
2,
id="two-detections",
),
pytest.param(
np.array([[1, 2, 3, 4]], dtype=np.float32),
np.array([0.6], dtype=np.float32),
np.array([3], dtype=np.float32),
1,
id="single-detection",
),
],
)
def test_maps_boxes_scores_labels_to_detections(
self,
boxes: np.ndarray,
scores: np.ndarray,
labels: np.ndarray,
expected_len: int,
) -> None:
"""boxes[0], scores[0], labels[0] are extracted into Detections fields."""
result = _FakeDeepSparseResults(boxes=[boxes], scores=[scores], labels=[labels])
det = Detections.from_deepsparse(result)
assert len(det) == expected_len
np.testing.assert_allclose(det.xyxy, boxes)
np.testing.assert_allclose(det.confidence, scores)
np.testing.assert_array_equal(det.class_id, labels.astype(int))
# ---------------------------------------------------------------------------
# from_easyocr
# ---------------------------------------------------------------------------
class TestFromEasyOCR:
"""from_easyocr converts EasyOCR polygon-corner results to Detections."""
def test_two_detections_produces_correct_xyxy_and_text(self) -> None:
"""Two detections with rectangular corners produce correct xyxy and text."""
bbox_a = [[10, 10], [30, 10], [30, 20], [10, 20]]
bbox_b = [[50, 5], [80, 5], [80, 25], [50, 25]]
results = [
(bbox_a, "hello", 0.95),
(bbox_b, "world", 0.80),
]
det = Detections.from_easyocr(results)
assert len(det) == 2
np.testing.assert_allclose(det.xyxy[0], [10, 10, 30, 20])
np.testing.assert_allclose(det.xyxy[1], [50, 5, 80, 25])
np.testing.assert_array_equal(
det.data[CLASS_NAME_DATA_FIELD], ["hello", "world"]
)
def test_single_detection_returns_one_element(self) -> None:
"""N=1 result returns a single-detection Detections."""
results = [([[0, 0], [10, 0], [10, 5], [0, 5]], "ok", 0.7)]
det = Detections.from_easyocr(results)
assert len(det) == 1
def test_empty_list_returns_empty_detections(self) -> None:
"""Empty input returns an empty Detections."""
det = Detections.from_easyocr([])
assert len(det) == 0
def test_missing_confidence_defaults_to_zero(self) -> None:
"""Two-element tuples (no confidence) default confidence to 0."""
results = [([[0, 0], [5, 0], [5, 3], [0, 3]], "hi")]
det = Detections.from_easyocr(results)
assert len(det) == 1
assert float(det.confidence[0]) == pytest.approx(0.0)
def test_preserves_oriented_corners_in_data(self) -> None:
"""Quadrilateral EasyOCR boxes must be preserved in the data payload."""
bbox = [[0, 0], [8, 1], [7, 5], [1, 4]]
results = [(bbox, "text", 0.9)]
det = Detections.from_easyocr(results)
assert ORIENTED_BOX_COORDINATES in det.data
np.testing.assert_allclose(det.data[ORIENTED_BOX_COORDINATES], np.array([bbox]))
def test_detail_zero_results_raise_clear_error(self) -> None:
"""detail=0 EasyOCR results must fail with a descriptive ValueError."""
with pytest.raises(ValueError, match="detail=1"):
Detections.from_easyocr(["text"])
# ---------------------------------------------------------------------------
# from_azure_analyze_image
# ---------------------------------------------------------------------------
def _make_azure_result(
detections: list[dict],
) -> dict:
"""Build a minimal Azure Image Analysis response dict."""
return {"objectsResult": {"values": detections}}
def _make_azure_detection(x: int, y: int, w: int, h: int, tags: list[dict]) -> dict:
"""Build one Azure detection entry."""
return {
"boundingBox": {"x": x, "y": y, "w": w, "h": h},
"tags": tags,
}
class TestFromAzureAnalyzeImage:
"""from_azure_analyze_image converts Azure object detection results."""
def test_dynamic_class_mapping_assigns_ids_in_order(self) -> None:
"""Without class_map, unique class names get monotonically increasing IDs."""
result = _make_azure_result(
[
_make_azure_detection(
0, 0, 10, 10, [{"name": "cat", "confidence": 0.9}]
),
_make_azure_detection(
20, 20, 10, 10, [{"name": "dog", "confidence": 0.7}]
),
]
)
det = Detections.from_azure_analyze_image(result)
assert len(det) == 2
# cat gets id=0 (first seen), dog gets id=1
np.testing.assert_array_equal(det.class_id, [0, 1])
np.testing.assert_allclose(det.confidence, [0.9, 0.7])
np.testing.assert_allclose(det.xyxy[0], [0, 0, 10, 10])
def test_explicit_class_map_filters_unknown_classes(self) -> None:
"""With class_map, the highest-confidence mapped tag is selected."""
class_map = {5: "cat"}
result = _make_azure_result(
[
_make_azure_detection(
0,
0,
10,
10,
[
{"name": "unknown", "confidence": 0.95},
{"name": "cat", "confidence": 0.9},
],
),
]
)
det = Detections.from_azure_analyze_image(result, class_map=class_map)
assert len(det) == 1
assert int(det.class_id[0]) == 5
np.testing.assert_allclose(det.confidence, [0.9])
def test_unmapped_tags_warn_and_skip_detection(self) -> None:
"""With class_map, completely unmapped tags should warn before skipping."""
class_map = {5: "cat"}
result = _make_azure_result(
[
_make_azure_detection(
0,
0,
10,
10,
[
{"name": "unknown", "confidence": 0.95},
{"name": "other", "confidence": 0.9},
],
),
]
)
with pytest.warns(SupervisionWarnings, match="none of its tags matched"):
det = Detections.from_azure_analyze_image(result, class_map=class_map)
assert len(det) == 0
def test_empty_values_list_returns_empty_detections(self) -> None:
"""Zero detections in values list produce an empty Detections."""
result = _make_azure_result([])
det = Detections.from_azure_analyze_image(result)
assert len(det) == 0
def test_error_key_raises_value_error(self) -> None:
"""Response containing 'error' key raises ValueError."""
result = {"error": {"message": "service unavailable"}}
with pytest.raises(ValueError, match="service unavailable"):
Detections.from_azure_analyze_image(result)
# ---------------------------------------------------------------------------
# from_ncnn
# ---------------------------------------------------------------------------
class TestFromNCNN:
"""from_ncnn converts ncnn rect objects (xywh) to xyxy Detections."""
def test_empty_objects_return_empty_detections(self) -> None:
"""Empty object list yields zero-length Detections."""
det = Detections.from_ncnn([])
assert len(det) == 0
@pytest.mark.parametrize(
("objects", "expected_len"),
[
pytest.param(
[
_FakeNCNNObject(10, 20, 30, 40, 0.9, 0),
_FakeNCNNObject(5, 5, 10, 10, 0.7, 1),
],
2,
id="two-detections",
),
pytest.param(
[_FakeNCNNObject(0, 0, 20, 20, 0.5, 2)],
1,
id="single-detection",
),
],
)
def test_maps_xywh_rect_to_xyxy(self, objects: list, expected_len: int) -> None:
"""rect xywh converts to xyxy; prob and label map to confidence/class_id."""
det = Detections.from_ncnn(objects)
assert len(det) == expected_len
first = objects[0]
expected_x2 = first.rect.x + first.rect.w
expected_y2 = first.rect.y + first.rect.h
np.testing.assert_allclose(det.xyxy[0, 2], expected_x2)
np.testing.assert_allclose(det.xyxy[0, 3], expected_y2)
assert float(det.confidence[0]) == pytest.approx(first.prob)
assert int(det.class_id[0]) == first.label
# ---------------------------------------------------------------------------
# from_lmm end-to-end
# ---------------------------------------------------------------------------
class TestFromLMMEndToEnd:
"""from_lmm end-to-end: deprecated dispatcher produces correct Detections."""
def test_paligemma_result_produces_correct_xyxy(self) -> None:
"""PaliGemma loc-token string is correctly parsed through the legacy API."""
result = "<loc0256><loc0256><loc0768><loc0768> cat"
with pytest.warns(SupervisionWarnings):
det = Detections.from_lmm(
LMM.PALIGEMMA,
result,
resolution_wh=(1000, 1000),
classes=["cat"],
)
assert len(det) == 1
np.testing.assert_allclose(det.xyxy, [[250.0, 250.0, 750.0, 750.0]])
assert int(det.class_id[0]) == 0
def test_string_lmm_name_is_accepted_and_dispatches(self) -> None:
"""Passing LMM name as lowercase string works identically to the enum."""
with pytest.warns(SupervisionWarnings):
det = Detections.from_lmm(
"paligemma",
"",
resolution_wh=(1000, 1000),
)
assert len(det) == 0
def test_lmm_values_are_subset_of_vlm_values() -> None:
"""Every LMM value exists in VLM — required for VLM(lmm.value) to succeed."""
assert {m.value for m in LMM} <= {m.value for m in VLM}