Merge branch 'develop' into feat/moondream-vlm-object-detection

This commit is contained in:
Onuralp SEZER 2025-07-11 21:13:34 +03:00
commit eb330ac1c1
No known key found for this signature in database
GPG Key ID: CF0835DFDF14CA38
22 changed files with 2293 additions and 1306 deletions

View File

@ -51,7 +51,7 @@ jobs:
- name: 🏗️ Install dependencies
run: |
uv pip install -r pyproject.toml --extra docs
uv pip install -r pyproject.toml --group docs
# Install mkdocs-material-insiders using the GitHub App token
uv pip install "git+https://roboflow:${{ steps.mkdocs_token.outputs.token }}@github.com/roboflow/mkdocs-material-insiders.git@9.5.49-insiders-4.53.14#egg=mkdocs-material[imaging]"

View File

@ -37,7 +37,7 @@ jobs:
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --extra build
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*

View File

@ -35,7 +35,7 @@ jobs:
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --extra build
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*

View File

@ -32,7 +32,7 @@ jobs:
- name: 🏗️ Build source and wheel distributions
run: |
uv pip install -r pyproject.toml --extra build
uv pip install -r pyproject.toml --group build
uv build
uv run twine check --strict dist/*

View File

@ -29,11 +29,9 @@ jobs:
python-version: ${{ matrix.python-version }}
activate-environment: true
- name: 🏗️ Install dependencies
run: |
uv pip install -r pyproject.toml --extra docs
uv pip install -r pyproject.toml --group docs
- name: 🧪 Test Docs Build
run: uv run mkdocs build --verbose

View File

@ -26,7 +26,7 @@ jobs:
- name: 🚀 Install Packages
run: uv pip install -r pyproject.toml --extra dev --extra docs --extra metrics
run: uv pip install -r pyproject.toml --group dev --group docs --extra metrics
- name: 🧪 Run the Import test
run: uv run python -c "import supervision; from supervision import assets; from supervision import metrics; print(supervision.__version__)"

View File

@ -107,6 +107,7 @@ status: new
:::supervision.detection.utils.denormalize_boxes
<div class="md-typeset">
<h2><a href="#supervision.detection.utils.xyxy_to_xcycarh">xyxy_to_xcycarh</a></h2>
</div>

View File

@ -93,7 +93,7 @@
},
"outputs": [],
"source": [
"!pip install -q inference-gpu \"supervision[assets]\""
"!pip install -q inference-gpu \"supervision\""
]
},
{
@ -156,7 +156,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@ -207,7 +207,6 @@
],
"source": [
"import supervision as sv\n",
"from supervision.assets import download_assets, VideoAssets\n",
"from inference.models.utils import get_roboflow_model\n",
"\n",
"\n",

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"metadata": {
"vscode": {
"languageId": "shellscript"
@ -25,7 +25,7 @@
},
"outputs": [],
"source": [
"!pip install -q \"supervision[assets]\""
"pip install -q \"supervision\""
]
},
{

View File

@ -101,7 +101,7 @@
},
"outputs": [],
"source": [
"!pip install -q torch diffusers accelerate inference-gpu[yolo-world] dill git+https://github.com/openai/CLIP.git supervision==0.19.0rc5"
"!pip install -q torch diffusers accelerate inference-gpu[yolo-world] dill git+https://github.com/openai/CLIP.git supervision"
]
},
{

View File

@ -92,7 +92,7 @@
},
"outputs": [],
"source": [
"!pip install -q inference-gpu \"supervision[assets]\""
"!pip install -q inference-gpu \"supervision\""
]
},
{

View File

@ -72,7 +72,7 @@
},
"outputs": [],
"source": [
"!pip install roboflow supervision==0.19.0 -q"
"!pip install roboflow supervision -q"
]
},
{

View File

@ -300,7 +300,7 @@
},
"outputs": [],
"source": [
"!pip install -q \"ultralytics<=8.3.40\""
"!pip install -q ultralytics"
]
},
{
@ -495,7 +495,7 @@
"source": [
"from ultralytics import YOLO\n",
"\n",
"model = YOLO(\"yolov8x.pt\")\n",
"model = YOLO(\"yolo11x.pt\")\n",
"result = model(image, verbose=False)[0]\n",
"detections = sv.Detections.from_ultralytics(result)"
]
@ -594,7 +594,7 @@
},
{
"cell_type": "code",
"execution_count": 33,
"execution_count": null,
"metadata": {
"id": "yM6dmicTRGl6"
},
@ -602,7 +602,7 @@
"source": [
"from ultralytics import YOLO\n",
"\n",
"model = YOLO(\"yolov8x-seg.pt\")\n",
"model = YOLO(\"yolo11x-seg.pt\")\n",
"result = model(image, verbose=False)[0]\n",
"detections = sv.Detections.from_ultralytics(result)"
]
@ -926,7 +926,7 @@
},
"outputs": [],
"source": [
"!pip install -q supervision[assets]"
"!pip install -q supervision"
]
},
{

File diff suppressed because one or more lines are too long

View File

@ -32,7 +32,7 @@
},
"outputs": [],
"source": [
"!pip install -q inference requests tqdm supervision==0.21.0"
"!pip install -q inference requests tqdm supervision"
]
},
{
@ -695,12 +695,12 @@
"source": [
"###\u00a0Annotate Image with Detections\n",
"\n",
"Finally, we can annotate the image with the predictions. Since we are working with an object detection model, we will use the [`sv.BoundingBoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoundingBoxAnnotator) and [`sv.LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) classes."
"Finally, we can annotate the image with the predictions. Since we are working with an object detection model, we will use the [`sv.BoxAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.BoxAnnotator) and [`sv.LabelAnnotator`](https://supervision.roboflow.com/latest/detection/annotators/#supervision.annotators.core.LabelAnnotator) classes."
]
},
{
"cell_type": "code",
"execution_count": 49,
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
@ -722,7 +722,7 @@
}
],
"source": [
"bounding_box_annotator = sv.BoundingBoxAnnotator()\n",
"bounding_box_annotator = sv.BoxAnnotator()\n",
"label_annotator = sv.LabelAnnotator()\n",
"\n",
"annotated_frame = frame.copy()\n",

View File

@ -12,15 +12,15 @@
</div>
<div class="custom-grid">
<a href="/develop/notebooks/quickstart"> <p class="card repo-card" data-name="Supervision Quickstart" data-labels="ANNOTATOR,DETECTION,SAM"
data-version="v0.18.0" data-author="SkalskiP,onuralpszr"></p>
data-version="v0.26.0" data-author="SkalskiP,onuralpszr"></p>
</a>
<a href="/develop/notebooks/count-objects-crossing-the-line">
<p class="card repo-card" data-name="Count Objects Crossing the Line"
data-labels="ANNOTATORS,LINE ZONE,TRACKING" data-version="v0.18.0" data-author="SkalskiP"></p>
data-labels="ANNOTATORS,LINE ZONE,TRACKING" data-version="v0.26.0" data-author="SkalskiP"></p>
</a>
<a href="/develop/notebooks/zero-shot-object-detection-with-yolo-world">
<p class="card repo-card" data-name="Zero-Shot Object Detection with YOLO-World"
data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.19.0" data-author="SkalskiP"></p>
data-labels="ANNOTATORS,DETECTION,INFERENCE" data-version="v0.26.0" data-author="SkalskiP"></p>
</a>
<a href="/develop/notebooks/download-supervision-assets">
<p class="card repo-card" data-name="Downloading Supervision Assets" data-labels="ASSETS" data-version="v0.18.0"
@ -28,7 +28,7 @@
</a>
<a href="/develop/notebooks/annotate-video-with-detections">
<p class="card repo-card" data-name="Annotate Video with Detections" data-labels="INFERENCE,YOLOV8"
data-version="v0.18.0" data-author="nickherrig"></p>
data-version="v0.26.0" data-author="nickherrig"></p>
</a>
<a href="/develop/notebooks/object-tracking">
<p class="card repo-card" data-name="Object Tracking" data-labels="TRACKING, ANNOTATOR" data-version="v0.18.0"
@ -36,23 +36,23 @@
</a>
<a href="/develop/notebooks/occupancy_analytics">
<p class="card repo-card" data-name="Analyzing Zone Occupancy" data-labels="ANNOTATOR,DETECTION,ZONES"
data-version="v0.19.0" data-author="stellasphere"></p>
data-version="v0.26.0" data-author="stellasphere"></p>
</a>
<a href="/develop/notebooks/evaluating-alignment-of-text-to-image-diffusion-models">
<p class="card repo-card" data-name="Evaluating Alignment of Text-to-image Diffusion Models"
data-labels="ANNOTATORS,YOLO WORLD" data-version="v0.19.0rc5" data-author="iamhatesz"></p>
data-labels="ANNOTATORS,YOLO WORLD" data-version="v0.26.0" data-author="iamhatesz"></p>
</a>
<a href="/develop/notebooks/serialise-detections-to-csv">
<p class="card repo-card" data-name="Serialise Detections to a CSV File"
data-labels="DETECTIONS,CSV SINK,INFERENCE" data-version="v0.21.0" data-author="onuralpszr"></p>
data-labels="DETECTIONS,CSV SINK,INFERENCE" data-version="v0.26.0" data-author="onuralpszr"></p>
</a>
<a href="/develop/notebooks/serialise-detections-to-json">
<p class="card repo-card" data-name="Serialise Detections to a JSON File"
data-labels="DETECTIONS,JSON SINK,INFERENCE" data-version="v0.21.0" data-author="onuralpszr"></p>
data-labels="DETECTIONS,JSON SINK,INFERENCE" data-version="v0.26.0" data-author="onuralpszr"></p>
</a>
<a href="/develop/notebooks/small-object-detection-with-sahi">
<p class="card repo-card" data-name="Small Object Detection with SAHI"
data-labels="DETECTIONS,SAHI,SMALL,OBJECT,INFERENCE" data-version="v0.23.0" data-author="ediardo"></p>
data-labels="DETECTIONS,SAHI,SMALL,OBJECT,INFERENCE" data-version="v0.26.0" data-author="ediardo"></p>
</a>
</div>
</div>

View File

@ -58,6 +58,8 @@ Documentation = "https://supervision.roboflow.com/latest/"
metrics = [
"pandas>=2.0.0",
]
[dependency-groups]
dev = [
"pytest>=7.2.2,<9.0.0",
"tox>=4.11.4",

View File

@ -864,6 +864,7 @@ class Detections:
qwen_2_5_vl_result,
input_wh=(1000, 1000),
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[139., 768., 315., 954.], [366., 679., 536., 849.]])
@ -873,6 +874,9 @@ class Detections:
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U10')}
detections.class_id
# array([0, 1])
```
Examples:
@ -890,6 +894,7 @@ class Detections:
sv.LMM.GOOGLE_GEMINI_2_0,
gemini_response_text,
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
@ -900,6 +905,9 @@ class Detections:
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U26')}
detections.class_id
# array([0, 1])
```
Examples:
@ -1022,6 +1030,7 @@ class Detections:
qwen_2_5_vl_result,
input_wh=(1000, 1000),
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
# array([[139., 768., 315., 954.], [366., 679., 536., 849.]])
@ -1031,6 +1040,9 @@ class Detections:
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U10')}
detections.class_id
# array([0, 1])
```
Examples:
@ -1048,6 +1060,7 @@ class Detections:
sv.VLM.GOOGLE_GEMINI_2_0,
gemini_response_text,
resolution_wh=(1000, 1000),
classes=['cat', 'dog'],
)
detections.xyxy
@ -1058,6 +1071,9 @@ class Detections:
detections.data
# {'class_name': array(['cat', 'dog'], dtype='<U26')}
detections.class_id
# array([0, 1])
```
Examples:
@ -1119,10 +1135,13 @@ class Detections:
return cls(xyxy=xyxy, mask=mask, data=data)
if vlm == VLM.GOOGLE_GEMINI_2_0 or vlm == VLM.GOOGLE_GEMINI_2_5:
xyxy, class_name = from_google_gemini(result, **kwargs)
if (
vlm == VLM.GOOGLE_GEMINI_2_0
or vlm == VLM.GOOGLE_GEMINI_2_5
):
xyxy, class_id, class_name = from_google_gemini(result, **kwargs)
data = {CLASS_NAME_DATA_FIELD: class_name}
return cls(xyxy=xyxy, data=data)
return cls(xyxy=xyxy, class_id=class_id, data=data)
if vlm == VLM.MOONDREAM:
xyxy = from_moondream(result, **kwargs)

View File

@ -56,9 +56,8 @@ ALLOWED_ARGUMENTS: Dict[VLM, List[str]] = {
VLM.PALIGEMMA: ["resolution_wh", "classes"],
VLM.FLORENCE_2: ["resolution_wh"],
VLM.QWEN_2_5_VL: ["input_wh", "resolution_wh", "classes"],
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh"],
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh"],
VLM.MOONDREAM: ["resolution_wh"],
VLM.GOOGLE_GEMINI_2_0: ["resolution_wh", "classes"],
VLM.GOOGLE_GEMINI_2_5: ["resolution_wh", "classes"],
}
SUPPORTED_TASKS_FLORENCE_2 = [
@ -339,7 +338,8 @@ def from_florence_2(
def from_google_gemini(
result: str,
resolution_wh: Tuple[int, int],
) -> Tuple[np.ndarray, np.ndarray]:
classes: Optional[List[str]] = None,
) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]:
"""
Parse and scale bounding boxes from Google Gemini style
[JSON output](https://ai.google.dev/gemini-api/docs/vision?lang=python).
@ -363,11 +363,16 @@ def from_google_gemini(
Args:
result: String containing the JSON snippet enclosed by triple backticks.
resolution_wh: (output_width, output_height) to which we rescale the boxes.
classes: Optional list of valid class names. If provided, returned boxes/labels
are filtered to only those classes found here.
Returns:
xyxy (np.ndarray): An array of shape `(n, 4)` containing
the bounding boxes coordinates in format `[x1, y1, x2, y2]`
labels: (np.ndarray): An array of shape `(n,)` containing
class_id (Optional[np.ndarray]): An array of shape `(n,)` containing
the class indices for each bounding box (or None if `classes` is not
provided)
class_name (np.ndarray): An array of shape `(n,)` containing
the class labels for each bounding box
"""
@ -388,7 +393,7 @@ def from_google_gemini(
try:
data = json.loads(result)
except json.JSONDecodeError:
return np.empty((0, 4)), np.empty((0,), dtype=str)
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
labels = []
xyxy = []
@ -407,9 +412,20 @@ def from_google_gemini(
)
if not xyxy:
return np.empty((0, 4)), np.empty((0,), dtype=str)
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
xyxy = np.array(xyxy)
class_name = np.array(labels)
class_id = None
if classes is not None:
mask = np.array([name in classes for name in class_name], dtype=bool)
xyxy = xyxy[mask]
class_name = class_name[mask]
class_id = np.array([classes.index(name) for name in class_name])
return xyxy, class_id, class_name
return np.array(xyxy), np.array(labels)
def from_moondream(

View File

@ -360,119 +360,140 @@ def test_from_qwen_2_5_vl(
np.testing.assert_array_equal(class_name, expected_results[2])
def test_from_google_gemini() -> None:
result = """```json
[
{"box_2d": [10, 20, 110, 120], "label": "cat"},
{"box_2d": [50, 100, 150, 200], "label": "dog"}
]
```"""
resolution_wh = (640, 480)
xyxy, class_name = from_google_gemini(
result=result,
resolution_wh=resolution_wh,
)
np.testing.assert_array_equal(
xyxy, np.array([[12.8, 4.8, 76.8, 52.8], [64.0, 24.0, 128.0, 72.0]])
)
np.testing.assert_array_equal(class_name, np.array(["cat", "dog"]))
@pytest.mark.parametrize(
"exception, result, resolution_wh, expected_results",
"exception, result, resolution_wh, classes, expected_results",
[
(
does_not_raise(),
{},
(640, 480),
np.empty((0, 4)),
), # empty dict
"random text",
(1000, 1000),
None,
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
), # random text without JSON format
(
does_not_raise(),
{"objects": []},
(640, 480),
np.empty((0, 4)),
), # empty objects list
"```json\ninvalid json\n```",
(1000, 1000),
None,
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
), # invalid JSON within code blocks
(
does_not_raise(),
{"objects": "not a list"},
(640, 480),
np.empty((0, 4)),
), # objects is not a list
"```json\n[]\n```",
(1000, 1000),
None,
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
), # empty JSON array
(
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
"""```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(),
{
"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},
]
},
"""```json
[
{"box_2d": [10, 20, 110, 120], "label": "cat"},
{"box_2d": [50, 100, 150, 200], "label": "dog"}
]
```""",
(640, 480),
np.array([[64.0, 96.0, 192.0, 192.0], [320.0, 288.0, 448.0, 384.0]]),
), # multiple boxes
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(),
{
"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},
]
},
"""```json
[
{"box_2d": [10, 20, 110, 120], "label": "cat"}
]
```""",
(640, 480),
np.array([[320.0, 288.0, 448.0, 384.0]]),
), # partial valid boxes
["dog", "person"],
(np.empty((0, 4)), np.empty(0, dtype=int), np.empty(0, dtype=str)),
), # class mismatch with filter
(
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
"""```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),
{
"objects": [
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
]
},
"""```json
[
{"box_2d": [10, 20, 110, 120], "label": "cat"}
]
```""",
(0, 480),
None,
), # zero width -> ValueError
None,
), # zero resolution width -> ValueError
(
pytest.raises(ValueError),
{
"objects": [
{"x_min": 0.1, "y_min": 0.2, "x_max": 0.3, "y_max": 0.4},
]
},
"""```json
[
{"box_2d": [10, 20, 110, 120], "label": "cat"}
]
```""",
(640, -100),
None,
), # negative height -> ValueError
None,
), # negative resolution height -> ValueError
],
)
def test_from_moondream(
def test_from_google_gemini(
exception,
result: dict,
result: str,
resolution_wh: Tuple[int, int],
expected_results,
classes: Optional[List[str]],
expected_results: Tuple[np.ndarray, Optional[np.ndarray], np.ndarray],
) -> None:
with exception:
xyxy = from_moondream(
result=result,
resolution_wh=resolution_wh,
xyxy, class_id, class_name = from_google_gemini(
result=result, resolution_wh=resolution_wh, classes=classes
)
if expected_results is not None:
np.testing.assert_array_equal(xyxy, expected_results)
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])

3223
uv.lock

File diff suppressed because it is too large Load Diff