Skip to content

Commit 8185496

Browse files
LingikaushikreddyBordacodex
authored
fix(cv2): match OpenCV's nearest-neighbour sampling grid in the resize fallback (#2722)
* fix(cv2): match OpenCV's nearest-neighbour sampling grid in the resize fallback * docs: add changelog entry for #2722 --------- Co-authored-by: Lingikaushikreddy <214906239+Lingikaushikreddy@users.noreply.github.com> Co-authored-by: jirka <6035284+borda@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com>
1 parent c92f8cf commit 8185496

3 files changed

Lines changed: 43 additions & 8 deletions

File tree

‎docs/changelog.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ date_modified: 2026-10-15
77

88
### Unreleased <small>upcoming</small>
99

10+
- The NumPy `resize` fallback now samples the same source pixel as OpenCV for `INTER_NEAREST` when `dsize` is given. It used the exact integer ratio `x * src // dst`, while OpenCV steps through the source with the reciprocal of the size ratio. At some whole-number positions of `x * src / dst`, reciprocal multiplication rounds below the integer: a 6-pixel ramp resized to 34 wide picks pixel 2 at `x = 17`, not 3. These differences also made `sv.CompactMask.resize`, which derives its grid from this call, give different masks with and without OpenCV installed. ([#2722](https://github.com/roboflow/supervision/pull/2722))
11+
1012
- `sv.process_video` now stops its reader when a callback or frame write fails, including callback interruption, instead of decoding the rest of the source before raising the error. Shutdown releases the source capture and waits for queued writes unless the writer itself fails; an active backend decode or write is allowed to finish. Decoding and callback errors retain their original exceptions when capture or output-writer release also fails; secondary cleanup failures are logged. ([#2720](https://github.com/roboflow/supervision/pull/2720))
1113

1214
- `sv.LabelAnnotator`, `sv.RichLabelAnnotator` and `sv.VertexLabelAnnotator` with `smart_position=True` now separate labels whose boxes share a center. `spread_out_boxes` pushes overlapping boxes away from each other's centers, and boxes with the same center got no direction, so they stayed exactly on top of each other and only the last label was visible: two detections with the same box and same-width labels, or two key points at the same position. Such boxes are now pushed apart vertically, the earlier one upward. ([#2718](https://github.com/roboflow/supervision/pull/2718))

‎src/supervision/_cv2/_image.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def _resize(
140140
fy: float = 0,
141141
interpolation: int = _INTER_LINEAR,
142142
) -> npt.NDArray[Any]:
143-
"""Resize with exact nearest or OpenCV-compatible linear sampling."""
143+
"""Resize with OpenCV-compatible nearest or linear sampling."""
144144
source_height, source_width = src.shape[:2]
145145
width, height = dsize if dsize is not None else (0, 0)
146146
# Like OpenCV, when the size comes from `fx`/`fy`, pixels are mapped with
@@ -156,15 +156,16 @@ def _resize(
156156
if scale_from_factors:
157157
scale_x, scale_y = 1 / fx, 1 / fy
158158
else:
159-
scale_x, scale_y = source_width / width, source_height / height
159+
# OpenCV steps through the source with the reciprocal of the size ratio
160+
# (`ifx = 1. / (dsize.width / ssize.width)`), whose rounding can put a
161+
# whole-number position such as 17 * 6 / 34 just below 3.0, so the
162+
# exact integer ratio would pick a different pixel there.
163+
scale_x = 1 / (width / source_width)
164+
scale_y = 1 / (height / source_height)
160165

161166
if interpolation == _INTER_NEAREST:
162-
if scale_from_factors:
163-
y_indices = np.floor(np.arange(height) * scale_y).astype(np.int64)
164-
x_indices = np.floor(np.arange(width) * scale_x).astype(np.int64)
165-
else:
166-
y_indices = np.arange(height) * source_height // height
167-
x_indices = np.arange(width) * source_width // width
167+
y_indices = np.floor(np.arange(height) * scale_y).astype(np.int64)
168+
x_indices = np.floor(np.arange(width) * scale_x).astype(np.int64)
168169
y_indices = np.minimum(y_indices, source_height - 1)
169170
x_indices = np.minimum(x_indices, source_width - 1)
170171
return np.ascontiguousarray(src[y_indices[:, np.newaxis], x_indices])

‎tests/cv2/test_image.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,38 @@ def test_fallback_resize_matches_opencv(interpolation: int, atol: int) -> None:
201201
np.testing.assert_allclose(actual, expected, atol=atol, rtol=0)
202202

203203

204+
@pytest.mark.parametrize(
205+
("source_width", "width"),
206+
[
207+
pytest.param(6, 34, id="six-to-thirty-four"),
208+
pytest.param(9, 51, id="nine-to-fifty-one"),
209+
pytest.param(10, 74, id="ten-to-seventy-four"),
210+
pytest.param(12, 34, id="twelve-to-thirty-four"),
211+
],
212+
)
213+
def test_fallback_nearest_resize_matches_opencv_at_whole_sample_positions(
214+
source_width: int, width: int
215+
) -> None:
216+
"""Pick OpenCV's source pixel where `x * source_width / width` is a whole number."""
217+
source = np.arange(source_width, dtype=np.uint8).reshape(1, -1)
218+
219+
actual = _resize(source, (width, 1), interpolation=cv2.INTER_NEAREST)
220+
expected = cv2.resize(source, (width, 1), interpolation=cv2.INTER_NEAREST)
221+
222+
np.testing.assert_array_equal(actual, expected)
223+
224+
225+
def test_fallback_nearest_resize_matches_opencv_on_both_axes() -> None:
226+
"""Match OpenCV's nearest sampling grid on rows and columns of a color image."""
227+
rng = np.random.default_rng(20261012)
228+
source = rng.integers(0, 256, (9, 6, 3), dtype=np.uint8)
229+
230+
actual = _resize(source, (34, 51), interpolation=cv2.INTER_NEAREST)
231+
expected = cv2.resize(source, (34, 51), interpolation=cv2.INTER_NEAREST)
232+
233+
np.testing.assert_array_equal(actual, expected)
234+
235+
204236
@pytest.mark.parametrize(
205237
("interpolation", "atol"),
206238
[

0 commit comments

Comments
 (0)