improve `InferenceSlicer` implementation lowering the count of generated boxes

This commit is contained in:
SkalskiP 2025-11-14 16:42:10 +01:00
parent ce28ab7fc7
commit 8d464aa1b5
2 changed files with 148 additions and 71 deletions

View File

@ -200,40 +200,58 @@ class InferenceSlicer:
slice dimensions, and pixel overlap.
Args:
resolution_wh (Tuple[int, int]): A tuple representing the width and height
of the image to be sliced.
slice_wh (Tuple[int, int]): Dimensions of each slice measured in pixels. The
tuple should be in the format `(width, height)`.
overlap_wh (Tuple[int, int]): A tuple representing the desired
overlap for width and height between consecutive slices measured in
pixels. Each value must be greater than or equal to 0.
resolution_wh (Tuple[int, int]): Width and height of the image to be sliced.
slice_wh (Tuple[int, int]): Dimensions of each slice in pixels (width, height).
overlap_wh (Tuple[int, int]): Overlap in pixels (overlap_w, overlap_h).
Returns:
np.ndarray: An array of shape `(n, 4)` containing coordinates for each
slice in the format `[xmin, ymin, xmax, ymax]`.
Note:
The function ensures that slices do not exceed the boundaries of the
original image. As a result, the final slices in the row and column
dimensions might be smaller than the specified slice dimensions if the
image's width or height is not a multiple of the slice's width or
height minus the overlap.
np.ndarray: Array of shape (n, 4) with [x_min, y_min, x_max, y_max] slices.
"""
slice_width, slice_height = slice_wh
image_width, image_height = resolution_wh
overlap_width, overlap_height = overlap_wh
width_stride = slice_width - overlap_width
height_stride = slice_height - overlap_height
stride_x = slice_width - overlap_width
stride_y = slice_height - overlap_height
ws = np.arange(0, image_width, width_stride)
hs = np.arange(0, image_height, height_stride)
def _compute_axis_starts(
image_size: int,
slice_size: int,
stride: int,
) -> list[int]:
if image_size <= slice_size:
return [0]
xmin, ymin = np.meshgrid(ws, hs)
xmax = np.clip(xmin + slice_width, 0, image_width)
ymax = np.clip(ymin + slice_height, 0, image_height)
# No overlap case, preserve original behavior, no overlapping tiles
if stride == slice_size:
return np.arange(0, image_size, stride).tolist()
offsets = np.stack([xmin, ymin, xmax, ymax], axis=-1).reshape(-1, 4)
# Overlap case, ensure last tile touches the border without redundancy
last_start = image_size - slice_size
starts = np.arange(0, last_start, stride).tolist()
if not starts or starts[-1] != last_start:
starts.append(last_start)
return starts
x_starts = _compute_axis_starts(
image_size=image_width,
slice_size=slice_width,
stride=stride_x,
)
y_starts = _compute_axis_starts(
image_size=image_height,
slice_size=slice_height,
stride=stride_y,
)
x_min, y_min = np.meshgrid(x_starts, y_starts)
x_max = np.clip(x_min + slice_width, 0, image_width)
y_max = np.clip(y_min + slice_height, 0, image_height)
offsets = np.stack(
[x_min, y_min, x_max, y_max],
axis=-1,
).reshape(-1, 4)
return offsets

View File

@ -1,13 +1,10 @@
from __future__ import annotations
from contextlib import ExitStack as DoesNotRaise
import numpy as np
import pytest
from supervision.detection.core import Detections
from supervision.detection.tools.inference_slicer import InferenceSlicer
from supervision.detection.utils.iou_and_nms import OverlapFilter
@pytest.fixture
@ -22,7 +19,7 @@ def mock_callback():
@pytest.mark.parametrize(
"resolution_wh, slice_wh, overlap_wh, expected_offsets",
[
# Case 1: No overlap, exact slices fit within image dimensions
# Case 1: Square image, square slices, no overlap
(
(256, 256),
(128, 128),
@ -36,7 +33,7 @@ def mock_callback():
]
),
),
# Case 2: Overlap of 64 pixels in both directions
# Case 2: Square image, square slices, non-zero overlap
(
(256, 256),
(128, 128),
@ -46,75 +43,137 @@ def mock_callback():
[0, 0, 128, 128],
[64, 0, 192, 128],
[128, 0, 256, 128],
[192, 0, 256, 128],
[0, 64, 128, 192],
[64, 64, 192, 192],
[128, 64, 256, 192],
[192, 64, 256, 192],
[0, 128, 128, 256],
[64, 128, 192, 256],
[128, 128, 256, 256],
[192, 128, 256, 256],
[0, 192, 128, 256],
[64, 192, 192, 256],
[128, 192, 256, 256],
[192, 192, 256, 256],
]
),
),
# Case 3: Image not perfectly divisible by slice size (no overlap)
# Case 3: Rectangle image (horizontal), square slices, no overlap
(
(300, 300),
(128, 128),
(192, 128),
(64, 64),
(0, 0),
np.array(
[
[0, 0, 128, 128],
[128, 0, 256, 128],
[256, 0, 300, 128],
[0, 128, 128, 256],
[128, 128, 256, 256],
[256, 128, 300, 256],
[0, 256, 128, 300],
[128, 256, 256, 300],
[256, 256, 300, 300],
[0, 0, 64, 64],
[64, 0, 128, 64],
[128, 0, 192, 64],
[0, 64, 64, 128],
[64, 64, 128, 128],
[128, 64, 192, 128],
]
),
),
# Case 4: Overlap of 32 pixels, image not perfectly divisible by slice size
# Case 4: Rectangle image (horizontal), square slices, non-zero overlap
(
(300, 300),
(128, 128),
(192, 128),
(64, 64),
(32, 32),
np.array(
[
[0, 0, 128, 128],
[96, 0, 224, 128],
[192, 0, 300, 128],
[288, 0, 300, 128],
[0, 96, 128, 224],
[96, 96, 224, 224],
[192, 96, 300, 224],
[288, 96, 300, 224],
[0, 192, 128, 300],
[96, 192, 224, 300],
[192, 192, 300, 300],
[288, 192, 300, 300],
[0, 288, 128, 300],
[96, 288, 224, 300],
[192, 288, 300, 300],
[288, 288, 300, 300],
[0, 0, 64, 64],
[32, 0, 96, 64],
[64, 0, 128, 64],
[96, 0, 160, 64],
[128, 0, 192, 64],
[0, 32, 64, 96],
[32, 32, 96, 96],
[64, 32, 128, 96],
[96, 32, 160, 96],
[128, 32, 192, 96],
[0, 64, 64, 128],
[32, 64, 96, 128],
[64, 64, 128, 128],
[96, 64, 160, 128],
[128, 64, 192, 128],
]
),
),
# Case 5: Image smaller than slice size (no overlap)
# Case 5: Rectangle image (vertical), square slices, no overlap
(
(100, 100),
(128, 128),
(128, 192),
(64, 64),
(0, 0),
np.array(
[
[0, 0, 100, 100],
[0, 0, 64, 64],
[64, 0, 128, 64],
[0, 64, 64, 128],
[64, 64, 128, 128],
[0, 128, 64, 192],
[64, 128, 128, 192],
]
),
),
# Case 6: Rectangle image (vertical), square slices, non-zero overlap
(
(128, 192),
(64, 64),
(32, 32),
np.array(
[
[0, 0, 64, 64],
[32, 0, 96, 64],
[64, 0, 128, 64],
[0, 32, 64, 96],
[32, 32, 96, 96],
[64, 32, 128, 96],
[0, 64, 64, 128],
[32, 64, 96, 128],
[64, 64, 128, 128],
[0, 96, 64, 160],
[32, 96, 96, 160],
[64, 96, 128, 160],
[0, 128, 64, 192],
[32, 128, 96, 192],
[64, 128, 128, 192],
]
),
),
# Case 7: Square image, rectangular slices (horizontal), no overlap
(
(160, 160),
(80, 40),
(0, 0),
np.array(
[
[0, 0, 80, 40],
[80, 0, 160, 40],
[0, 40, 80, 80],
[80, 40, 160, 80],
[0, 80, 80, 120],
[80, 80, 160, 120],
[0, 120, 80, 160],
[80, 120, 160, 160],
]
),
),
# Case 8: Square image, rectangular slices (vertical), non-zero overlap
(
(160, 160),
(40, 80),
(10, 20),
np.array(
[
[0, 0, 40, 80],
[30, 0, 70, 80],
[60, 0, 100, 80],
[90, 0, 130, 80],
[120, 0, 160, 80],
[0, 60, 40, 140],
[30, 60, 70, 140],
[60, 60, 100, 140],
[90, 60, 130, 140],
[120, 60, 160, 140],
[0, 80, 40, 160],
[30, 80, 70, 160],
[60, 80, 100, 160],
[90, 80, 130, 160],
[120, 80, 160, 160],
]
),
),