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
1478 lines
46 KiB
Python
1478 lines
46 KiB
Python
from contextlib import ExitStack as DoesNotRaise
|
|
from contextlib import nullcontext as does_not_raise
|
|
|
|
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 supervision.detection.vlm import (
|
|
VLM,
|
|
from_florence_2,
|
|
from_google_gemini_2_0,
|
|
from_google_gemini_2_5,
|
|
from_moondream,
|
|
from_paligemma,
|
|
from_qwen_2_5_vl,
|
|
from_qwen_3_vl,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("result", "resolution_wh", "classes", "expected_xyxy", "expected_class_name"),
|
|
[
|
|
pytest.param(
|
|
'```json\n[{"bbox_2d": [100, 200, 300, 400], "label": "cat"}]\n```',
|
|
(640, 480),
|
|
None,
|
|
np.array([[64.0, 96.0, 192.0, 192.0]]),
|
|
np.array(["cat"], dtype=str),
|
|
id="single-detection-scales-from-1000x1000",
|
|
),
|
|
pytest.param(
|
|
"```json\n[]\n```",
|
|
(640, 480),
|
|
None,
|
|
np.empty((0, 4)),
|
|
np.empty(0, dtype=str),
|
|
id="empty-json-array-returns-empty",
|
|
),
|
|
pytest.param(
|
|
"```json\n"
|
|
'[{"bbox_2d": [0, 0, 500, 500], "label": "dog"},'
|
|
' {"bbox_2d": [500, 500, 1000, 1000], "label": "cat"}]\n```',
|
|
(640, 480),
|
|
["cat"],
|
|
np.array([[320.0, 240.0, 640.0, 480.0]]),
|
|
np.array(["cat"], dtype=str),
|
|
id="classes-filter-keeps-only-matching",
|
|
),
|
|
],
|
|
)
|
|
def test_from_qwen_3_vl(
|
|
result: str,
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_xyxy: np.ndarray,
|
|
expected_class_name: np.ndarray,
|
|
) -> None:
|
|
"""from_qwen_3_vl scales from implicit 1000x1000 input space to resolution_wh."""
|
|
xyxy, _class_id, class_name = from_qwen_3_vl(
|
|
result=result,
|
|
resolution_wh=resolution_wh,
|
|
classes=classes,
|
|
)
|
|
|
|
np.testing.assert_allclose(xyxy, expected_xyxy)
|
|
np.testing.assert_array_equal(class_name, expected_class_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "resolution_wh", "classes", "expected_results"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
"",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # empty text
|
|
(
|
|
does_not_raise(),
|
|
"",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # empty text, classes
|
|
(
|
|
does_not_raise(),
|
|
"\n",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # newline only
|
|
(
|
|
does_not_raise(),
|
|
"the quick brown fox jumps over the lazy dog.",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # random text, no location
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0768><loc0768> cat",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # partial location
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # extra loc
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768>",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
|
|
), # no class
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> catt",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(np.empty((0, 4)), np.empty(0), np.empty(0).astype(str)),
|
|
), # invalid class
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
None,
|
|
np.array(["cat"]).astype(str),
|
|
),
|
|
), # single box, no classes
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> black cat",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
None,
|
|
np.array(["black cat"]).astype(np.dtype("U")),
|
|
),
|
|
), # class with space
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> black-cat",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
None,
|
|
np.array(["black-cat"]).astype(np.dtype("U")),
|
|
),
|
|
), # class with hyphen
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> black_cat",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
None,
|
|
np.array(["black_cat"]).astype(np.dtype("U")),
|
|
),
|
|
), # class with underscore
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat ;",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
np.array([0]),
|
|
np.array(["cat"]).astype(str),
|
|
),
|
|
), # correct class filter
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
|
"<loc0256><loc0256><loc0768><loc0768> dog",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0], [250.0, 250.0, 750.0, 750.0]]),
|
|
np.array([0, 1]),
|
|
np.array(["cat", "dog"]).astype(np.dtype("U")),
|
|
),
|
|
), # multiple correct boxes, classes
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
|
"<loc0256><loc0256><loc0768> cat",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
np.array([0]),
|
|
np.array(["cat"]).astype(str),
|
|
),
|
|
), # partial valid boxes
|
|
(
|
|
does_not_raise(),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat ; "
|
|
"<loc0256><loc0256><loc0768><loc0768><loc0768> cat",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[250.0, 250.0, 750.0, 750.0]]),
|
|
np.array([0]),
|
|
np.array(["cat"]).astype(str),
|
|
),
|
|
), # partial valid again
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(0, 1000\)"
|
|
),
|
|
),
|
|
"<loc0256><loc0256><loc0768><loc0768> cat",
|
|
(0, 1000),
|
|
None,
|
|
None,
|
|
), # zero width -> ValueError
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(1000, -200\)"
|
|
),
|
|
),
|
|
"<loc0256><loc0256><loc0768><loc0768> dog",
|
|
(1000, -200),
|
|
None,
|
|
None,
|
|
), # negative height -> ValueError
|
|
],
|
|
)
|
|
def test_from_paligemma(
|
|
exception,
|
|
result: str,
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_results: tuple[np.ndarray, np.ndarray | None, np.ndarray],
|
|
) -> None:
|
|
with exception:
|
|
result = from_paligemma(
|
|
result=result, resolution_wh=resolution_wh, classes=classes
|
|
)
|
|
np.testing.assert_array_equal(result[0], expected_results[0])
|
|
np.testing.assert_array_equal(result[1], expected_results[1])
|
|
np.testing.assert_array_equal(result[2], expected_results[2])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "input_wh", "resolution_wh", "classes", "expected_results"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
"some random text without triple backticks",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
|
), # no snippet
|
|
(
|
|
does_not_raise(),
|
|
"```json\nnot valid json\n```",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
|
), # invalid JSON
|
|
(
|
|
does_not_raise(),
|
|
"```json\n[]\n```",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
|
), # empty list
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 10, 100, 100]},
|
|
{"label": "missing box"},
|
|
{"bbox_2d": [50, 60, 110, 120], "unused": "something"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
|
|
), # missing keys
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(
|
|
np.array([[20.0, 22.5, 220.0, 135.0]]),
|
|
None,
|
|
np.array(["cat"], dtype=str),
|
|
),
|
|
), # single box no classes
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [0, 0, 64, 64], "label": "dog"},
|
|
{"bbox_2d": [100, 200, 300, 400], "label": "cat"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(640, 640),
|
|
None,
|
|
(
|
|
np.array([[0, 0, 64, 64], [100, 200, 300, 400]], dtype=float),
|
|
None,
|
|
np.array(["dog", "cat"], dtype=str),
|
|
),
|
|
), # multiple no classes
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "bird"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(1280, 720),
|
|
["cat", "dog"],
|
|
(np.empty((0, 4)), np.empty(0, dtype=int), np.empty(0, dtype=str)),
|
|
), # class mismatch
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "cat"},
|
|
{"bbox_2d": [50, 100, 150, 200], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(640, 480),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[10.0, 15.0, 110.0, 90.0], [50.0, 75.0, 150.0, 150.0]]),
|
|
np.array([0, 1], dtype=int),
|
|
np.array(["cat", "dog"], dtype=str),
|
|
),
|
|
), # partial filtering
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [-10, 0, 700, 700], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(
|
|
np.array([[-20.0, 0.0, 1400.0, 787.5]]),
|
|
None,
|
|
np.array(["dog"], dtype=str),
|
|
),
|
|
), # out-of-bounds box
|
|
(
|
|
does_not_raise(),
|
|
"""[
|
|
{'bbox_2d': [10, 20, 110, 120], 'label': 'cat'}
|
|
]""",
|
|
(640, 640),
|
|
(1280, 720),
|
|
None,
|
|
(
|
|
np.array([[20.0, 22.5, 220.0, 135.0]]),
|
|
None,
|
|
np.array(["cat"], dtype=str),
|
|
),
|
|
), # python-style list, single quotes, no fences
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [0, 0, 64, 64], "label": "dog"},
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "cat"},
|
|
{"bbox_2d": [30, 40, 130, 140], "label":
|
|
""",
|
|
(640, 640),
|
|
(640, 640),
|
|
None,
|
|
(
|
|
np.array(
|
|
[
|
|
[0.0, 0.0, 64.0, 64.0],
|
|
[10.0, 20.0, 110.0, 120.0],
|
|
],
|
|
dtype=float,
|
|
),
|
|
None,
|
|
np.array(["dog", "cat"], dtype=str),
|
|
),
|
|
), # truncated response, last object unfinished, previous ones recovered
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(0, 640\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(0, 640),
|
|
(1280, 720),
|
|
None,
|
|
None, # invalid input_wh
|
|
),
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(1280, -100\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"bbox_2d": [10, 20, 110, 120], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 640),
|
|
(1280, -100),
|
|
None,
|
|
None, # invalid resolution_wh
|
|
),
|
|
],
|
|
)
|
|
def test_from_qwen_2_5_vl(
|
|
exception,
|
|
result: str,
|
|
input_wh: tuple[int, int],
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_results,
|
|
) -> None:
|
|
with exception:
|
|
xyxy, class_id, class_name = from_qwen_2_5_vl(
|
|
result=result,
|
|
input_wh=input_wh,
|
|
resolution_wh=resolution_wh,
|
|
classes=classes,
|
|
)
|
|
if expected_results is not None:
|
|
np.testing.assert_array_equal(xyxy, expected_results[0])
|
|
np.testing.assert_array_equal(class_id, expected_results[1])
|
|
np.testing.assert_array_equal(class_name, expected_results[2])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "resolution_wh", "classes", "expected_results"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
"random text",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)),
|
|
), # random text without JSON format
|
|
(
|
|
does_not_raise(),
|
|
"```json\ninvalid json\n```",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)),
|
|
), # invalid JSON within code blocks
|
|
(
|
|
does_not_raise(),
|
|
"```json\n[]\n```",
|
|
(1000, 1000),
|
|
None,
|
|
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)),
|
|
), # empty JSON array
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [100, 200, 300, 400], "label": "cat"}
|
|
]
|
|
```""",
|
|
(1000, 500),
|
|
None,
|
|
(
|
|
np.array([[200.0, 50.0, 400.0, 150.0]]),
|
|
None,
|
|
np.array(["cat"], dtype=str),
|
|
),
|
|
), # single valid box with coordinate scaling
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
None,
|
|
(
|
|
np.array([[12.8, 4.8, 76.8, 52.8], [64.0, 24.0, 128.0, 72.0]]),
|
|
None,
|
|
np.array(["cat", "dog"], dtype=str),
|
|
),
|
|
), # multiple valid boxes without class filtering
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["dog", "person"],
|
|
(np.empty((0, 4)), np.empty(0, dtype=int), np.empty(0, dtype=str)),
|
|
), # class mismatch with filter
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["person", "dog"],
|
|
(
|
|
np.array([[64.0, 24.0, 128.0, 72.0]]),
|
|
np.array([1]),
|
|
np.array(["dog"], dtype=str),
|
|
),
|
|
), # partial class filtering
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog"}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[12.8, 4.8, 76.8, 52.8], [64.0, 24.0, 128.0, 72.0]]),
|
|
np.array([0, 1]),
|
|
np.array(["cat", "dog"]),
|
|
),
|
|
), # complete class filtering with multiple boxes
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(0, 480\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(0, 480),
|
|
None,
|
|
None,
|
|
), # zero resolution width -> ValueError
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(640, -100\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(640, -100),
|
|
None,
|
|
None,
|
|
), # negative resolution height -> ValueError
|
|
],
|
|
)
|
|
def test_from_google_gemini(
|
|
exception,
|
|
result: str,
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_results: tuple[np.ndarray, np.ndarray | None, np.ndarray],
|
|
) -> None:
|
|
with exception:
|
|
xyxy, class_id, class_name = from_google_gemini_2_0(
|
|
result=result, resolution_wh=resolution_wh, classes=classes
|
|
)
|
|
if expected_results is not None:
|
|
np.testing.assert_array_equal(xyxy, expected_results[0])
|
|
np.testing.assert_array_equal(class_id, expected_results[1])
|
|
np.testing.assert_array_equal(class_name, expected_results[2])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "resolution_wh", "expected_results"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
{},
|
|
(640, 480),
|
|
np.empty((0, 4)),
|
|
), # empty dict
|
|
(
|
|
does_not_raise(),
|
|
{"objects": []},
|
|
(640, 480),
|
|
np.empty((0, 4)),
|
|
), # empty objects list
|
|
(
|
|
does_not_raise(),
|
|
{"objects": "not a list"},
|
|
(640, 480),
|
|
np.empty((0, 4)),
|
|
), # objects is not a list
|
|
(
|
|
does_not_raise(),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
|
|
]
|
|
},
|
|
(640, 480),
|
|
np.array([[64.0, 96.0, 192.0, 192.0]]),
|
|
), # single box
|
|
(
|
|
does_not_raise(),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
|
|
{"x_min": 0.5, "y_min": 0.6, "x_max": 0.7, "y_max": 0.8},
|
|
]
|
|
},
|
|
(640, 480),
|
|
np.array([[64.0, 96.0, 192.0, 192.0], [320.0, 288.0, 448.0, 384.0]]),
|
|
), # multiple boxes
|
|
(
|
|
does_not_raise(),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.1, "y_min": 0.2}, # missing x_max, y_max
|
|
{"x_min": 0.5, "y_min": 0.6, "x_max": 0.7, "y_max": 0.8},
|
|
]
|
|
},
|
|
(640, 480),
|
|
np.array([[320.0, 288.0, 448.0, 384.0]]),
|
|
), # partial valid boxes
|
|
(
|
|
does_not_raise(),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.0, "y_min": 0.0, "x_max": 1.0, "y_max": 1.0},
|
|
]
|
|
},
|
|
(1000, 800),
|
|
np.array([[0.0, 0.0, 1000.0, 800.0]]),
|
|
), # full image box
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution_wh must be positive\. "
|
|
r"Got \(0, 480\)"
|
|
),
|
|
),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
|
|
]
|
|
},
|
|
(0, 480),
|
|
None,
|
|
), # zero width -> ValueError
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution_wh must be positive\. "
|
|
r"Got \(640, -100\)"
|
|
),
|
|
),
|
|
{
|
|
"objects": [
|
|
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
|
|
]
|
|
},
|
|
(640, -100),
|
|
None,
|
|
), # negative height -> ValueError
|
|
],
|
|
)
|
|
def test_from_moondream(
|
|
exception,
|
|
result: dict,
|
|
resolution_wh: tuple[int, int],
|
|
expected_results,
|
|
) -> None:
|
|
with exception:
|
|
xyxy = from_moondream(
|
|
result=result,
|
|
resolution_wh=resolution_wh,
|
|
)
|
|
if expected_results is not None:
|
|
np.testing.assert_array_equal(xyxy, expected_results)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("florence_result", "resolution_wh", "expected_results", "exception"),
|
|
[
|
|
( # Object detection: empty
|
|
{"<OD>": {"bboxes": [], "labels": []}},
|
|
(10, 10),
|
|
(np.array([], dtype=np.float32), np.array([]), None, None),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Object detection: two detections
|
|
{
|
|
"<OD>": {
|
|
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
|
"labels": ["car", "door"],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
|
np.array(["car", "door"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Caption: unsupported
|
|
{"<CAPTION>": "A green car parked in front of a yellow building."},
|
|
(10, 10),
|
|
None,
|
|
pytest.raises(ValueError, match="<CAPTION> not supported"),
|
|
),
|
|
( # Detailed Caption: unsupported
|
|
{
|
|
"<DETAILED_CAPTION>": "The image shows a blue Volkswagen Beetle parked "
|
|
"in front of a yellow building with two brown doors, surrounded by "
|
|
"trees and a clear blue sky."
|
|
},
|
|
(10, 10),
|
|
None,
|
|
pytest.raises(ValueError, match="<DETAILED_CAPTION> not supported"),
|
|
),
|
|
( # More Detailed Caption: unsupported
|
|
{
|
|
"<MORE_DETAILED_CAPTION>": "The image shows a vintage Volkswagen "
|
|
"Beetle car parked on a "
|
|
"cobblestone street in front of a yellow building with two wooden "
|
|
"doors. The car is painted in a bright turquoise color and has a "
|
|
"white stripe running along the side. It has two doors on either side "
|
|
"of the car, one on top of the other, and a small window on the "
|
|
"front. The building appears to be old and dilapidated, with peeling "
|
|
"paint and crumbling walls. The sky is blue and there are trees in "
|
|
"the background."
|
|
},
|
|
(10, 10),
|
|
None,
|
|
pytest.raises(ValueError, match="<MORE_DETAILED_CAPTION> not supported"),
|
|
),
|
|
( # Caption to Phrase Grounding: empty
|
|
{"<CAPTION_TO_PHRASE_GROUNDING>": {"bboxes": [], "labels": []}},
|
|
(10, 10),
|
|
(np.array([], dtype=np.float32), np.array([]), None, None),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Caption to Phrase Grounding: two detections
|
|
{
|
|
"<CAPTION_TO_PHRASE_GROUNDING>": {
|
|
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
|
"labels": ["a green car", "a yellow building"],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
|
np.array(["a green car", "a yellow building"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Dense Region caption: empty
|
|
{"<DENSE_REGION_CAPTION>": {"bboxes": [], "labels": []}},
|
|
(10, 10),
|
|
(np.array([], dtype=np.float32), np.array([]), None, None),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Caption to Phrase Grounding: two detections
|
|
{
|
|
"<DENSE_REGION_CAPTION>": {
|
|
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
|
"labels": ["a green car", "a yellow building"],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
|
np.array(["a green car", "a yellow building"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Region proposal
|
|
{
|
|
"<REGION_PROPOSAL>": {
|
|
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
|
"labels": ["", ""],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
|
None,
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Referring Expression Segmentation
|
|
{
|
|
"<REFERRING_EXPRESSION_SEGMENTATION>": {
|
|
"polygons": [[[1, 1, 2, 1, 2, 2, 1, 2]]],
|
|
"labels": [""],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[1.0, 1.0, 2.0, 2.0]], dtype=np.float32),
|
|
None,
|
|
np.array(
|
|
[
|
|
[
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
]
|
|
],
|
|
dtype=bool,
|
|
),
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # OCR: unsupported
|
|
{"<OCR>": "A"},
|
|
(10, 10),
|
|
None,
|
|
pytest.raises(ValueError, match="<OCR> not supported"),
|
|
),
|
|
( # OCR with Region: obb boxes
|
|
{
|
|
"<OCR_WITH_REGION>": {
|
|
"quad_boxes": [[2, 2, 6, 4, 5, 6, 1, 5], [4, 4, 5, 5, 4, 6, 3, 5]],
|
|
"labels": ["some text", "other text"],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[1, 2, 6, 6], [3, 4, 5, 6]], dtype=np.float32),
|
|
np.array(["some text", "other text"]),
|
|
None,
|
|
np.array(
|
|
[[[2, 2], [6, 4], [5, 6], [1, 5]], [[4, 4], [5, 5], [4, 6], [3, 5]]]
|
|
),
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Open Vocabulary Detection
|
|
{
|
|
"<OPEN_VOCABULARY_DETECTION>": {
|
|
"bboxes": [[4, 4, 6, 6], [5, 5, 7, 7]],
|
|
"bboxes_labels": ["cat", "cat"],
|
|
"polygon": [],
|
|
"polygons_labels": [],
|
|
}
|
|
},
|
|
(10, 10),
|
|
(
|
|
np.array([[4, 4, 6, 6], [5, 5, 7, 7]], dtype=np.float32),
|
|
np.array(["cat", "cat"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Region to Category: empty
|
|
{"<REGION_TO_CATEGORY>": "No object detected."},
|
|
(10, 10),
|
|
(np.empty((0, 4), dtype=np.float32), np.array([]), None, None),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Region to Category: detected
|
|
{"<REGION_TO_CATEGORY>": "some object<loc_300><loc_400><loc_500><loc_600>"},
|
|
(10, 10),
|
|
(
|
|
np.array([[3, 4, 5, 6]], dtype=np.float32),
|
|
np.array(["some object"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Region to Description: empty
|
|
{"<REGION_TO_DESCRIPTION>": "No object detected."},
|
|
(10, 10),
|
|
(np.empty((0, 4), dtype=np.float32), np.array([]), None, None),
|
|
DoesNotRaise(),
|
|
),
|
|
( # Region to Description: detected
|
|
{"<REGION_TO_DESCRIPTION>": "descr<loc_300><loc_400><loc_500><loc_600>"},
|
|
(10, 10),
|
|
(
|
|
np.array([[3, 4, 5, 6]], dtype=np.float32),
|
|
np.array(["descr"]),
|
|
None,
|
|
None,
|
|
),
|
|
DoesNotRaise(),
|
|
),
|
|
],
|
|
)
|
|
def test_florence_2(
|
|
florence_result: dict,
|
|
resolution_wh: tuple[int, int],
|
|
expected_results: tuple[
|
|
np.ndarray, np.ndarray | None, np.ndarray | None, np.ndarray | None
|
|
],
|
|
exception: Exception,
|
|
) -> None:
|
|
with exception:
|
|
result = from_florence_2(florence_result, resolution_wh)
|
|
np.testing.assert_array_equal(result[0], expected_results[0])
|
|
if expected_results[1] is None:
|
|
assert result[1] is None
|
|
else:
|
|
np.testing.assert_array_equal(result[1], expected_results[1])
|
|
if expected_results[2] is None:
|
|
assert result[2] is None
|
|
else:
|
|
np.testing.assert_array_equal(result[2], expected_results[2])
|
|
if expected_results[3] is None:
|
|
assert result[3] is None
|
|
else:
|
|
np.testing.assert_array_equal(result[3], expected_results[3])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("florence_result", "match"),
|
|
[
|
|
pytest.param(
|
|
{
|
|
"<REGION_TO_CATEGORY>": (
|
|
"some object<loc_300><loc_400><loc_500><loc_600>"
|
|
),
|
|
"<REGION_TO_DESCRIPTION>": "other",
|
|
},
|
|
"single element",
|
|
id="multiple-top-level-tasks",
|
|
),
|
|
pytest.param(
|
|
{"<REGION_TO_CATEGORY>": 123},
|
|
"Expected string as <REGION_TO_CATEGORY> result",
|
|
id="non-string-region-result",
|
|
),
|
|
pytest.param(
|
|
{"<REGION_TO_CATEGORY>": "some object"},
|
|
"Expected string to end in location tags",
|
|
id="missing-location-tags",
|
|
),
|
|
],
|
|
)
|
|
def test_florence_2_invalid_payloads_raise_value_error(
|
|
florence_result: dict[str, object], match: str
|
|
) -> None:
|
|
"""Malformed Florence 2 region payloads raise `ValueError`."""
|
|
with pytest.raises(ValueError, match=match):
|
|
from_florence_2(florence_result, (10, 10))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "resolution_wh", "classes", "expected_results"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
"random text",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.empty((0, 4)),
|
|
np.empty(0, dtype=int),
|
|
np.empty(0, dtype=str),
|
|
np.empty(0, dtype=float),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"```json\ninvalid json\n```",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.empty((0, 4)),
|
|
np.empty(0, dtype=int),
|
|
np.empty(0, dtype=str),
|
|
np.empty(0, dtype=float),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"```json\n[]\n```",
|
|
(1000, 1000),
|
|
None,
|
|
(
|
|
np.empty((0, 4)),
|
|
np.empty(0, dtype=int),
|
|
np.empty(0, dtype=str),
|
|
np.empty(0, dtype=float),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [100, 200, 300, 400], "label": "cat", "confidence": 0.8}
|
|
]
|
|
```""",
|
|
(1000, 500),
|
|
None,
|
|
(
|
|
np.array([[200.0, 50.0, 400.0, 150.0]]),
|
|
np.array([0]),
|
|
np.array(["cat"], dtype=str),
|
|
np.array([0.8]),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat", "confidence": 0.8},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog", "confidence": 0.9}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
None,
|
|
(
|
|
np.array([[12.8, 4.8, 76.8, 52.8], [64.0, 24.0, 128.0, 72.0]]),
|
|
np.array([0, 1]),
|
|
np.array(["cat", "dog"], dtype=str),
|
|
np.array([0.8, 0.9]),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat", "confidence": 0.8}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["dog", "person"],
|
|
(
|
|
np.empty((0, 4)),
|
|
np.empty(0, dtype=int),
|
|
np.empty(0, dtype=str),
|
|
np.empty(0, dtype=float),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat", "confidence": 0.8},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog", "confidence": 0.9}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["person", "dog"],
|
|
(
|
|
np.array([[64.0, 24.0, 128.0, 72.0]]),
|
|
np.array([1]),
|
|
np.array(["dog"], dtype=str),
|
|
np.array([0.9]),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat", "confidence": 0.8},
|
|
{"box_2d": [50, 100, 150, 200], "label": "dog", "confidence": 0.9}
|
|
]
|
|
```""",
|
|
(640, 480),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[12.8, 4.8, 76.8, 52.8], [64.0, 24.0, 128.0, 72.0]]),
|
|
np.array([0, 1]),
|
|
np.array(["cat", "dog"]),
|
|
np.array([0.8, 0.9]),
|
|
None,
|
|
),
|
|
),
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(0, 480\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(0, 480),
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
pytest.raises(
|
|
ValueError,
|
|
match=(
|
|
r"Both dimensions in resolution must be positive\. "
|
|
r"Got \(640, -100\)"
|
|
),
|
|
),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "label": "cat"}
|
|
]
|
|
```""",
|
|
(640, -100),
|
|
None,
|
|
None,
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [10, 20, 110, 120], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAADElEQVR4nGNgoCcAAABuAAFIXXpjAAAAAElFTkSuQmCC", "label": "cat"}
|
|
]
|
|
```""", # noqa E501 // docs
|
|
(10, 10),
|
|
["cat"],
|
|
(
|
|
np.array([[0.2, 0.1, 1.2, 1.1]]),
|
|
np.array([0]),
|
|
np.array(["cat"]),
|
|
None,
|
|
np.array([np.zeros((10, 10), dtype=bool)]),
|
|
),
|
|
),
|
|
(
|
|
does_not_raise(),
|
|
"""```json
|
|
[
|
|
{"box_2d": [100, 100, 200, 200], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAADElEQVR4nGNgoCcAAABuAAFIXXpjAAAAAElFTkSuQmCC", "label": "cat", "confidence": 0.8},
|
|
{"box_2d": [300, 300, 400, 400], "mask": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAAAAACoWZBhAAAADElEQVR4nGNgoCcAAABuAAFIXXpjAAAAAElFTkSuQmCC", "label": "dog", "confidence": 0.9}
|
|
]
|
|
```""", # noqa E501 // docs
|
|
(10, 10),
|
|
["cat", "dog"],
|
|
(
|
|
np.array([[1.0, 1.0, 2.0, 2.0], [3.0, 3.0, 4.0, 4.0]]),
|
|
np.array([0, 1]),
|
|
np.array(["cat", "dog"]),
|
|
np.array([0.8, 0.9]),
|
|
np.array(
|
|
[np.zeros((10, 10), dtype=bool), np.zeros((10, 10), dtype=bool)],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
def test_from_google_gemini_2_5(
|
|
exception,
|
|
result: str,
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_results: None
|
|
| (tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]),
|
|
) -> None:
|
|
with exception:
|
|
(
|
|
xyxy,
|
|
class_id,
|
|
class_name,
|
|
confidence,
|
|
masks,
|
|
) = from_google_gemini_2_5(
|
|
result=result, resolution_wh=resolution_wh, classes=classes
|
|
)
|
|
|
|
if expected_results is None:
|
|
return
|
|
|
|
assert xyxy.shape == expected_results[0].shape
|
|
assert np.allclose(xyxy, expected_results[0])
|
|
|
|
assert class_id.shape == expected_results[1].shape
|
|
assert np.array_equal(class_id, expected_results[1])
|
|
|
|
assert class_name.shape == expected_results[2].shape
|
|
assert np.array_equal(class_name, expected_results[2])
|
|
|
|
if confidence is None:
|
|
assert expected_results[3] is None
|
|
else:
|
|
assert expected_results[3] is not None
|
|
assert confidence.shape == expected_results[3].shape
|
|
assert np.allclose(confidence, expected_results[3])
|
|
|
|
if masks is None:
|
|
assert expected_results[4] is None
|
|
else:
|
|
assert masks is not None
|
|
assert masks.shape == expected_results[4].shape
|
|
assert np.array_equal(masks, expected_results[4])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "result", "resolution_wh", "classes", "expected_detections"),
|
|
[
|
|
(
|
|
does_not_raise(),
|
|
"",
|
|
(100, 100),
|
|
None,
|
|
Detections.empty(),
|
|
), # empty text -> empty detections (aligned with other VLM parsers)
|
|
(
|
|
does_not_raise(),
|
|
"random text",
|
|
(100, 100),
|
|
None,
|
|
Detections.empty(),
|
|
), # random text -> empty detections
|
|
(
|
|
does_not_raise(),
|
|
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
|
(1000, 1000),
|
|
None,
|
|
Detections(
|
|
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
|
class_id=np.array([0]),
|
|
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
|
),
|
|
), # single box, no classes
|
|
(
|
|
does_not_raise(),
|
|
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
Detections(
|
|
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
|
class_id=np.array([0]),
|
|
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
|
),
|
|
), # single box, with classes
|
|
(
|
|
does_not_raise(),
|
|
"<|ref|>person<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>",
|
|
(1000, 1000),
|
|
["cat", "dog"],
|
|
Detections.empty(),
|
|
), # single box, wrong class
|
|
(
|
|
does_not_raise(),
|
|
(
|
|
"<|ref|>cat<|/ref|><|det|>[[100, 200, 300, 400]]<|/det|>"
|
|
"<|ref|>dog<|/ref|><|det|>[[500, 600, 700, 800]]<|/det|>"
|
|
),
|
|
(1000, 1000),
|
|
["cat"],
|
|
Detections(
|
|
xyxy=np.array([[100.1, 200.2, 300.3, 400.4]]),
|
|
class_id=np.array([0]),
|
|
data={CLASS_NAME_DATA_FIELD: np.array(["cat"])},
|
|
),
|
|
), # multiple boxes, one class correct
|
|
(
|
|
pytest.raises(ValueError, match="ref tags \\(1\\)"),
|
|
"<|ref|>cat<|/ref|>",
|
|
(100, 100),
|
|
None,
|
|
None,
|
|
), # only ref
|
|
(
|
|
pytest.raises(ValueError, match="ref tags \\(0\\)"),
|
|
"<|det|>[[100, 200, 300, 400]]<|/det|>",
|
|
(100, 100),
|
|
None,
|
|
None,
|
|
), # only det
|
|
],
|
|
)
|
|
def test_from_deepseek_vl_2(
|
|
exception,
|
|
result: str,
|
|
resolution_wh: tuple[int, int],
|
|
classes: list[str] | None,
|
|
expected_detections: Detections,
|
|
) -> None:
|
|
with exception:
|
|
detections = Detections.from_vlm(
|
|
vlm=VLM.DEEPSEEK_VL_2,
|
|
result=result,
|
|
resolution_wh=resolution_wh,
|
|
classes=classes,
|
|
)
|
|
|
|
if expected_detections is None:
|
|
return
|
|
|
|
assert len(detections) == len(expected_detections)
|
|
|
|
if len(detections) == 0:
|
|
return
|
|
|
|
assert np.allclose(detections.xyxy, expected_detections.xyxy, atol=1e-1)
|
|
assert np.array_equal(detections.class_id, expected_detections.class_id)
|
|
assert np.array_equal(
|
|
detections.data[CLASS_NAME_DATA_FIELD],
|
|
expected_detections.data[CLASS_NAME_DATA_FIELD],
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("result", "classes"),
|
|
[
|
|
pytest.param("", None, id="empty_string"),
|
|
pytest.param("no tags here", None, id="no_tags"),
|
|
pytest.param("", ["cat"], id="empty_string_with_classes"),
|
|
],
|
|
)
|
|
def test_from_deepseek_vl_2_empty_parse_returns_empty_detections(
|
|
result: str, classes: list[str] | None
|
|
) -> None:
|
|
"""A result with no ref/det pairs yields empty Detections instead of raising."""
|
|
detections = Detections.from_vlm(
|
|
vlm=VLM.DEEPSEEK_VL_2,
|
|
result=result,
|
|
resolution_wh=(1000, 1000),
|
|
classes=classes,
|
|
)
|
|
|
|
assert len(detections) == 0
|
|
assert detections.xyxy.shape == (0, 4)
|
|
|
|
|
|
def test_from_google_gemini_2_5_malformed_mask_keeps_confidence_aligned():
|
|
"""A non-data-URI mask must not skip the item's confidence and desync arrays."""
|
|
result = (
|
|
'[{"box_2d": [10, 10, 100, 100], "label": "cat", "mask": "bad", '
|
|
'"confidence": 0.8}, {"box_2d": [20, 20, 120, 120], "label": "dog", '
|
|
'"mask": "bad", "confidence": 0.9}]'
|
|
)
|
|
|
|
xyxy, _, _, confidence, masks = from_google_gemini_2_5(
|
|
result=result, resolution_wh=(640, 480)
|
|
)
|
|
|
|
assert xyxy.shape == (2, 4)
|
|
assert confidence is not None
|
|
assert confidence.shape == (2,)
|
|
assert np.allclose(confidence, [0.8, 0.9])
|
|
assert masks is not None
|
|
assert masks.shape == (2, 480, 640)
|
|
|
|
|
|
def test_from_vlm_unsupported_future_enum_raises(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Unknown VLM members should raise instead of returning empty detections."""
|
|
|
|
class FakeVLM:
|
|
PALIGEMMA = object()
|
|
FLORENCE_2 = object()
|
|
QWEN_2_5_VL = object()
|
|
QWEN_3_VL = object()
|
|
DEEPSEEK_VL_2 = object()
|
|
GOOGLE_GEMINI_2_0 = object()
|
|
GOOGLE_GEMINI_2_5 = object()
|
|
MOONDREAM = object()
|
|
FUTURE = object()
|
|
|
|
monkeypatch.setattr(detection_core, "VLM", FakeVLM)
|
|
monkeypatch.setattr(
|
|
detection_core,
|
|
"_validate_vlm_parameters",
|
|
lambda vlm, result, kwargs: vlm,
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Unsupported VLM value"):
|
|
Detections.from_vlm(vlm=FakeVLM.FUTURE, result="ignored")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("parser", "result"),
|
|
[
|
|
pytest.param(
|
|
from_google_gemini_2_0, "[1, 2, 3]", id="gemini_2_0_non_dict_items"
|
|
),
|
|
pytest.param(from_google_gemini_2_0, "42", id="gemini_2_0_non_list"),
|
|
pytest.param(
|
|
from_google_gemini_2_5, "[1, 2, 3]", id="gemini_2_5_non_dict_items"
|
|
),
|
|
pytest.param(from_google_gemini_2_5, "42", id="gemini_2_5_non_list"),
|
|
pytest.param(from_qwen_2_5_vl, "[1, 2, 3]", id="qwen_2_5_non_dict_items"),
|
|
pytest.param(from_qwen_2_5_vl, "42", id="qwen_2_5_non_list"),
|
|
],
|
|
)
|
|
def test_vlm_parsers_degrade_on_malformed_json(parser, result):
|
|
"""Valid JSON of the wrong shape should yield empty results, not raise."""
|
|
kwargs: dict = {"resolution_wh": (640, 480)}
|
|
if parser is from_qwen_2_5_vl:
|
|
kwargs["input_wh"] = (640, 480)
|
|
|
|
xyxy = parser(result=result, **kwargs)[0]
|
|
|
|
assert xyxy.shape == (0, 4)
|