fix: return empty int ndarray instead of None for class_id on empty VLM parse (#2239)

When from_paligemma or from_google_gemini_2_0 find no detections (no regex
matches, JSON decode error, or empty bounding-box list), they previously
returned None for class_id. All other early-exit and filter paths already
return a zero-length ndarray of dtype int. This inconsistency causes
downstream AttributeError when callers unconditionally call .shape or
iterate over the result.

Affected paths:
- from_paligemma: matches.shape[0] == 0 branch
- from_google_gemini_2_0: JSONDecodeError branch and len(xyxy) == 0 branch

---------

Co-authored-by: YousefZahran1 <youssefzahran.y@gmail.com>
Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
This commit is contained in:
Youssef Ibrahim 2026-05-19 15:31:44 +03:00 committed by GitHub
parent f4b0767a88
commit 01ec36b31d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 14 additions and 14 deletions

View File

@ -202,7 +202,7 @@ def validate_vlm_parameters(vlm: VLM | str, result: Any, kwargs: dict[str, Any])
def from_paligemma(
result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None
) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]:
) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any]]:
"""
Parse bounding boxes from paligemma-formatted text, scale them to the specified
resolution, and optionally filter by classes.
@ -229,7 +229,7 @@ def from_paligemma(
matches = np.array(matches) if matches else np.empty((0, 5))
if matches.shape[0] == 0:
return np.empty((0, 4)), None, np.empty(0, dtype=str)
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)
xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4]
xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h])
@ -626,7 +626,7 @@ def from_google_gemini_2_0(
try:
data = json.loads(result)
except json.JSONDecodeError:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)
labels = []
xyxy = []
@ -640,7 +640,7 @@ def from_google_gemini_2_0(
xyxy.append([box[1], box[0], box[3], box[2]])
if len(xyxy) == 0:
return np.empty((0, 4)), None, np.empty((0,), dtype=str)
return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)
xyxy = denormalize_boxes(
np.array(xyxy, dtype=np.float64),

View File

@ -27,49 +27,49 @@ from supervision.detection.vlm import (
"",
(1000, 1000),
None,
(np.empty((0, 4)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(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)), None, np.empty(0).astype(str)),
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0).astype(str)),
), # no class
(
does_not_raise(),
@ -436,21 +436,21 @@ def test_from_qwen_2_5_vl(
"random text",
(1000, 1000),
None,
(np.empty((0, 4)), None, np.empty(0, dtype=str)),
(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)), None, np.empty(0, dtype=str)),
(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)), None, np.empty(0, dtype=str)),
(np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str)),
), # empty JSON array
(
does_not_raise(),