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:
parent
a32323d5bd
commit
8f576b02a8
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
Loading…
Reference in New Issue