Skip to content

Commit 75316fa

Browse files
authored
fix(workflows): translate oriented bounding box corners in dynamic_crop (#2430)
`crop_image` shifts every other detection geometry to crop-local coords (xyxy via move_boxes, mask by slicing, keypoints and polygon by explicit subtraction) but skipped `data['xyxyxyxy']`. OBB workflows that fed the block (e.g. detect aerial vehicles, crop each one, run downstream OBB analysis) ended up with corners still in the original image frame next to a crop-local xyxy. Same shape and pattern as the detections_stitch fix in #2427.
1 parent fd07bec commit 75316fa

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

  • inference/core/workflows/core_steps/transformations/dynamic_crop
  • tests/workflows/unit_tests/core_steps/transformations

inference/core/workflows/core_steps/transformations/dynamic_crop/v1.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import numpy as np
66
import supervision as sv
77
from pydantic import AliasChoices, ConfigDict, Field
8+
from supervision.config import ORIENTED_BOX_COORDINATES
89

910
from inference.core.workflows.execution_engine.constants import (
1011
DETECTION_ID_KEY,
@@ -256,6 +257,14 @@ def crop_image(
256257
translated_detection[POLYGON_KEY_IN_SV_DETECTIONS] = selected_detection[
257258
POLYGON_KEY_IN_SV_DETECTIONS
258259
] - np.array([x_min, y_min])
260+
if ORIENTED_BOX_COORDINATES in detections.data:
261+
# OBB corners (shape (1, 4, 2) on a single-detection slice) get the
262+
# same crop-local offset applied to xyxy / keypoints / polygon. Without
263+
# this, OBB-aware downstream blocks see corners in the original image
264+
# frame next to a crop-local xyxy.
265+
translated_detection[ORIENTED_BOX_COORDINATES] = selected_detection[
266+
ORIENTED_BOX_COORDINATES
267+
] - np.array([x_min, y_min])
259268

260269
crops.append(
261270
{

tests/workflows/unit_tests/core_steps/transformations/test_crop.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
import supervision as sv
77
from pydantic import ValidationError
8+
from supervision.config import ORIENTED_BOX_COORDINATES
89

910
from inference.core.workflows.core_steps.transformations.dynamic_crop.v1 import (
1011
BlockManifest,
@@ -450,3 +451,68 @@ def test_crop_image_when_background_removal_requested_and_mask_found() -> None:
450451
assert (
451452
result[2]["crops"].numpy_image == expected_third_crop
452453
).all(), "Image must have expected size and color"
454+
455+
456+
@pytest.mark.parametrize(
457+
"crop_offset",
458+
[
459+
pytest.param((250, 300), id="interior-crop"),
460+
pytest.param((0, 0), id="top-left-crop"),
461+
pytest.param((100, 0), id="x-only-crop"),
462+
],
463+
)
464+
def test_crop_image_translates_oriented_bounding_box_corners(
465+
crop_offset: Tuple[int, int],
466+
) -> None:
467+
"""OBB corners stored in `data['xyxyxyxy']` must follow the same `-(x_min, y_min)`
468+
translation already applied to `xyxy`, `mask`, `keypoints`, and `polygon`.
469+
Without this, downstream OBB-aware blocks would see corners in the original
470+
image frame next to a crop-local `xyxy`."""
471+
# given
472+
x_min, y_min = crop_offset
473+
side = 40
474+
np_image = np.zeros((1000, 1000, 3), dtype=np.uint8)
475+
image = WorkflowImageData(
476+
parent_metadata=ImageParentMetadata(parent_id="origin_image"),
477+
numpy_image=np_image,
478+
)
479+
# OBB centered inside the crop region, rotated 45-deg (diamond corners).
480+
image_corners = np.array(
481+
[
482+
[
483+
[x_min + side / 2, y_min],
484+
[x_min + side, y_min + side / 2],
485+
[x_min + side / 2, y_min + side],
486+
[x_min, y_min + side / 2],
487+
]
488+
],
489+
dtype=np.float32,
490+
)
491+
detections = sv.Detections(
492+
xyxy=np.array([[x_min, y_min, x_min + side, y_min + side]], dtype=np.float64),
493+
class_id=np.array([0]),
494+
confidence=np.array([0.9], dtype=np.float64),
495+
data={
496+
"detection_id": np.array(["one"]),
497+
"class_name": np.array(["thing"]),
498+
ORIENTED_BOX_COORDINATES: image_corners,
499+
},
500+
)
501+
502+
# when
503+
result = crop_image(
504+
image=image,
505+
detections=detections,
506+
mask_opacity=0.0,
507+
background_color=(0, 0, 0),
508+
)
509+
510+
# then
511+
assert len(result) == 1
512+
translated = result[0]["predictions"]
513+
expected_corners = image_corners - np.array([x_min, y_min], dtype=np.float32)
514+
assert np.allclose(translated.data[ORIENTED_BOX_COORDINATES], expected_corners)
515+
# Cross-check: AABB of the translated corners sits inside [0, side]^2.
516+
translated_corners = translated.data[ORIENTED_BOX_COORDINATES][0]
517+
assert translated_corners.min() >= 0.0
518+
assert translated_corners.max() <= side

0 commit comments

Comments
 (0)