fix(detection): scale `from_tensorflow` boxes by correct axes (#2360)

`Detections.from_tensorflow` scaled the normalized box coordinates by the
wrong image dimensions: the y coordinates (ymin/ymax, columns 0 and 2) were
multiplied by width and the x coordinates (xmin/xmax, columns 1 and 3) by
height. Tensorflow Hub object-detection models emit `detection_boxes` as
normalized `[ymin, xmin, ymax, xmax]`, so y must scale by height and x by
width.

The bug is masked on square images (width == height) but corrupts every
coordinate on the common non-square case — e.g. a box normalized to
`[0.1, 0.2, 0.5, 0.6]` on a 1000x500 image came out as
`[100, 100, 300, 500]` instead of the correct `[200, 50, 600, 250]`.

Swap the two multipliers so y scales by `resolution_wh[1]` (height) and x by
`resolution_wh[0]` (width). Adds a non-square regression test (the connector
was previously untested).

- Expand tensorflow_results arg to document required dict keys and tensor
  shapes so callers know what to pass before getting a KeyError
- Add Note: section documenting the [ymin, xmin, ymax, xmax] normalized
  box format; the inline comment was only visible to code readers
- Fix SOURCE_IMAGE_PATH undefined identifier → "<SOURCE_IMAGE_PATH>"
  string placeholder (consistent with other connector examples in file)

---------

Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Ruben 2026-07-01 22:44:01 +02:00 committed by GitHub
parent a32323d5bd
commit 8f576b02a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 4 deletions

View File

@ -407,7 +407,12 @@ class Detections:
inference result.
Args:
tensorflow_results: The output results from Tensorflow Hub.
tensorflow_results: Raw output dict from a TensorFlow Hub
object-detection model. Must contain:
``"detection_boxes"`` (shape ``[1, N, 4]``, normalized
``[ymin, xmin, ymax, xmax]``), ``"detection_scores"``
(shape ``[1, N]``), and ``"detection_classes"``
(shape ``[1, N]``).
resolution_wh: The input image resolution as `(width, height)`.
Bounding boxes from Tensorflow are normalized and are scaled
to absolute coordinates using this resolution.
@ -415,6 +420,13 @@ class Detections:
Returns:
A new Detections object.
Note:
TensorFlow Hub object-detection models return bounding boxes
normalized as ``[ymin, xmin, ymax, xmax]``. This method rescales
them to absolute pixel coordinates and reorders them to ``xyxy``
(``[xmin, ymin, xmax, ymax]``) before constructing the
:class:`Detections` object.
Example:
```python
import tensorflow as tf
@ -424,7 +436,7 @@ class Detections:
module_handle = "https://tfhub.dev/tensorflow/centernet/hourglass_512x512_kpts/1"
model = hub.load(module_handle)
img = np.array(cv2.imread(SOURCE_IMAGE_PATH))
img = np.array(cv2.imread("<SOURCE_IMAGE_PATH>"))
result = model(img)
detections = sv.Detections.from_tensorflow(
result, resolution_wh=(img.shape[1], img.shape[0])
@ -432,9 +444,11 @@ class Detections:
```
"""
# Tensorflow returns normalized boxes as [ymin, xmin, ymax, xmax], so the
# y coordinates (cols 0, 2) scale by height and x (cols 1, 3) by width.
boxes = tensorflow_results["detection_boxes"][0].numpy()
boxes[:, [0, 2]] *= resolution_wh[0]
boxes[:, [1, 3]] *= resolution_wh[1]
boxes[:, [0, 2]] *= resolution_wh[1]
boxes[:, [1, 3]] *= resolution_wh[0]
boxes = boxes[:, [1, 0, 3, 2]]
return cls(
xyxy=boxes,

View File

@ -5,6 +5,7 @@ import supervision.detection.core as detection_core
from supervision.config import CLASS_NAME_DATA_FIELD
from supervision.detection.core import Detections
from tests.helpers import (
_FakeTensor,
_FakeUltralyticsBoxes,
_FakeUltralyticsResults,
_FakeYoloNasPrediction,
@ -109,3 +110,21 @@ def test_from_yolo_nas_handles_empty_and_non_empty(
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])