From 092085efb110337592f4259c9643a377ca65c164 Mon Sep 17 00:00:00 2001 From: Anirudh Date: Sat, 14 Dec 2024 14:59:58 -0500 Subject: [PATCH 1/3] fix: allow negative offsets in move_masks function (#1715) --- supervision/detection/utils.py | 51 ++++++++++++++++++----- test/detection/test_utils.py | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 11 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index a2cbd87b..48340a32 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -720,25 +720,54 @@ def move_masks( masks (npt.NDArray[np.bool_]): A 3D array of binary masks corresponding to the predictions. Shape: `(N, H, W)`, where N is the number of predictions, and H, W are the dimensions of each mask. - offset (npt.NDArray[np.int32]): An array of shape `(2,)` containing non-negative - int values `[dx, dy]`. + offset (npt.NDArray[np.int32]): An array of shape `(2,)` containing int values + `[dx, dy]`. Supports both positive and negative values for bidirectional + movement. resolution_wh (Tuple[int, int]): The width and height of the desired mask resolution. Returns: (npt.NDArray[np.bool_]) repositioned masks, optionally padded to the specified shape. + + Examples: + ```python + import numpy as np + import supervision as sv + + # Create a sample mask + mask = np.zeros((1, 4, 4), dtype=bool) + mask[0, 1:3, 1:3] = True + + # Move mask left and up + offset = np.array([-1, -1]) + moved_mask = sv.move_masks(mask, offset, resolution_wh=(4, 4)) + + # Move mask right and down + offset = np.array([1, 1]) + moved_mask = sv.move_masks(mask, offset, resolution_wh=(4, 4)) + ``` """ - - if offset[0] < 0 or offset[1] < 0: - raise ValueError(f"Offset values must be non-negative integers. Got: {offset}") - mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False) - mask_array[ - :, - offset[1] : masks.shape[1] + offset[1], - offset[0] : masks.shape[2] + offset[0], - ] = masks + + # For negative offsets, copying starting portion of mask + if offset[0] <= 0 and offset[1] <= 0: + w = min( + masks.shape[2], resolution_wh[0] + offset[0] + ) # How much to copy horizontally + h = min( + masks.shape[1], resolution_wh[1] + offset[1] + ) # How much to copy vertically + + mask_array[:, 0:h, 0:w] = masks[:, 0:h, 0:w] + else: + # For positive offsets, using original logic + w = min(masks.shape[2], resolution_wh[0] - offset[0]) + h = min(masks.shape[1], resolution_wh[1] - offset[1]) + + mask_array[:, offset[1] : offset[1] + h, offset[0] : offset[0] + w] = masks[ + :, 0:h, 0:w + ] return mask_array diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 87e50f6a..812d0b28 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -16,6 +16,7 @@ from supervision.detection.utils import ( merge_data, merge_metadata, move_boxes, + move_masks, process_roboflow_result, scale_boxes, xcycwh_to_xyxy, @@ -442,6 +443,80 @@ def test_move_boxes( assert np.array_equal(result, expected_result) +@pytest.mark.parametrize( + "masks, offset, resolution_wh, expected_result, exception", + [ + # Test case 1: Positive offset (existing behavior) + ( + np.array([[[True, True], [True, True]]]), + np.array([1, 1]), + (3, 3), + np.array( + [[[False, False, False], [False, True, True], [False, True, True]]] + ), + DoesNotRaise(), + ), + # Test case 2: Negative offset (new behavior) + ( + np.array([[[True, True], [True, True]]]), + np.array([-1, -1]), + (3, 3), + np.array( + [[[True, True, False], [True, True, False], [False, False, False]]] + ), + DoesNotRaise(), + ), + # Test case 3: Zero offset + ( + np.array([[[True, True], [True, True]]]), + np.array([0, 0]), + (2, 2), + np.array([[[True, True], [True, True]]]), + DoesNotRaise(), + ), + # Test case 4: Partial out of bounds (positive) + ( + np.array([[[True, True], [True, True]]]), + np.array([1, 1]), + (2, 2), + np.array([[[False, False], [False, True]]]), + DoesNotRaise(), + ), + # Test case 5: Partial out of bounds (negative) + ( + np.array([[[True, True], [True, True]]]), + np.array([-1, -1]), + (2, 2), + np.array([[[True, False], [False, False]]]), + DoesNotRaise(), + ), + # Test case 6: Multiple masks + ( + np.array([[[True, True], [True, True]], [[True, False], [False, True]]]), + np.array([1, 1]), + (3, 3), + np.array( + [ + [[False, False, False], [False, True, True], [False, True, True]], + [[False, False, False], [False, True, False], [False, False, True]], + ] + ), + DoesNotRaise(), + ), + ], +) +def test_move_masks( + masks: np.ndarray, + offset: np.ndarray, + resolution_wh: Tuple[int, int], + expected_result: np.ndarray, + exception: Exception, +) -> None: + with exception: + result = move_masks(masks=masks, offset=offset, resolution_wh=resolution_wh) + np.testing.assert_array_equal(result, expected_result) + + @pytest.mark.parametrize( "xyxy, factor, expected_result, exception", [ From b46607638313b60668d99a555e8292e6cde8f239 Mon Sep 17 00:00:00 2001 From: SkalskiP Date: Mon, 16 Dec 2024 14:27:13 +0100 Subject: [PATCH 2/3] making sure code works with mixed offset like [2, -2]; dostring updates --- supervision/detection/utils.py | 65 ++++++++------ test/detection/test_utils.py | 154 +++++++++++++++++++++++---------- 2 files changed, 148 insertions(+), 71 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 48340a32..3f5e7233 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -735,39 +735,54 @@ def move_masks( import numpy as np import supervision as sv - # Create a sample mask - mask = np.zeros((1, 4, 4), dtype=bool) - mask[0, 1:3, 1:3] = True + mask = np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool) - # Move mask left and up - offset = np.array([-1, -1]) - moved_mask = sv.move_masks(mask, offset, resolution_wh=(4, 4)) - - # Move mask right and down offset = np.array([1, 1]) - moved_mask = sv.move_masks(mask, offset, resolution_wh=(4, 4)) + sv.move_masks(mask, offset, resolution_wh=(4, 4)) + # array([[[False, False, False, False], + # [False, False, False, False], + # [False, False, True, True], + # [False, False, True, True]]], dtype=bool) + + offset = np.array([-2, 2]) + sv.move_masks(mask, offset, resolution_wh=(4, 4)) + # array([[[False, False, False, False], + # [False, False, False, False], + # [False, False, False, False], + # [True, False, False, False]]], dtype=bool) ``` """ mask_array = np.full((masks.shape[0], resolution_wh[1], resolution_wh[0]), False) - # For negative offsets, copying starting portion of mask - if offset[0] <= 0 and offset[1] <= 0: - w = min( - masks.shape[2], resolution_wh[0] + offset[0] - ) # How much to copy horizontally - h = min( - masks.shape[1], resolution_wh[1] + offset[1] - ) # How much to copy vertically - - mask_array[:, 0:h, 0:w] = masks[:, 0:h, 0:w] + if offset[0] < 0: + source_x_start = -offset[0] + source_x_end = min(masks.shape[2], resolution_wh[0] - offset[0]) + destination_x_start = 0 + destination_x_end = min(resolution_wh[0], masks.shape[2] + offset[0]) else: - # For positive offsets, using original logic - w = min(masks.shape[2], resolution_wh[0] - offset[0]) - h = min(masks.shape[1], resolution_wh[1] - offset[1]) + source_x_start = 0 + source_x_end = min(masks.shape[2], resolution_wh[0] - offset[0]) + destination_x_start = offset[0] + destination_x_end = offset[0] + source_x_end - source_x_start - mask_array[:, offset[1] : offset[1] + h, offset[0] : offset[0] + w] = masks[ - :, 0:h, 0:w - ] + if offset[1] < 0: + source_y_start = -offset[1] + source_y_end = min(masks.shape[1], resolution_wh[1] - offset[1]) + destination_y_start = 0 + destination_y_end = min(resolution_wh[1], masks.shape[1] + offset[1]) + else: + source_y_start = 0 + source_y_end = min(masks.shape[1], resolution_wh[1] - offset[1]) + destination_y_start = offset[1] + destination_y_end = offset[1] + source_y_end - source_y_start + + if source_x_end > source_x_start and source_y_end > source_y_start: + mask_array[:, destination_y_start:destination_y_end, + destination_x_start:destination_x_end] = \ + masks[:, source_y_start:source_y_end, source_x_start:source_x_end] return mask_array diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 812d0b28..721fb2ac 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -446,61 +446,123 @@ def test_move_boxes( @pytest.mark.parametrize( "masks, offset, resolution_wh, expected_result, exception", [ - # Test case 1: Positive offset (existing behavior) ( - np.array([[[True, True], [True, True]]]), - np.array([1, 1]), - (3, 3), - np.array( - [[[False, False, False], [False, True, True], [False, True, True]]] - ), - DoesNotRaise(), - ), - # Test case 2: Negative offset (new behavior) - ( - np.array([[[True, True], [True, True]]]), - np.array([-1, -1]), - (3, 3), - np.array( - [[[True, True, False], [True, True, False], [False, False, False]]] - ), - DoesNotRaise(), - ), - # Test case 3: Zero offset - ( - np.array([[[True, True], [True, True]]]), + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), np.array([0, 0]), - (2, 2), - np.array([[[True, True], [True, True]]]), + (4, 4), + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), DoesNotRaise(), ), - # Test case 4: Partial out of bounds (positive) ( - np.array([[[True, True], [True, True]]]), - np.array([1, 1]), - (2, 2), - np.array([[[False, False], [False, True]]]), - DoesNotRaise(), - ), - # Test case 5: Partial out of bounds (negative) - ( - np.array([[[True, True], [True, True]]]), + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), np.array([-1, -1]), - (2, 2), - np.array([[[True, False], [False, False]]]), + (4, 4), + np.array([[[ True, True, False, False], + [ True, True, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), DoesNotRaise(), ), - # Test case 6: Multiple masks ( - np.array([[[True, True], [True, True]], [[True, False], [False, True]]]), - np.array([1, 1]), - (3, 3), - np.array( - [ - [[False, False, False], [False, True, True], [False, True, True]], - [[False, False, False], [False, True, False], [False, False, True]], - ] - ), + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([-2, -2]), + (4, 4), + np.array([[[ True, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([-3, -3]), + (4, 4), + np.array([[[False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([-2, -1]), + (4, 4), + np.array([[[ True, False, False, False], + [ True, False, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([-1, -2]), + (4, 4), + np.array([[[ True, True, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([-2, 2]), + (4, 4), + np.array([[[False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [ True, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([3, 3]), + (4, 4), + np.array([[[False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False]]], dtype=bool), + DoesNotRaise(), + ), + ( + np.array([[[False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False]]], dtype=bool), + np.array([3, 3]), + (6, 6), + np.array([[[False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, True, True], + [False, False, False, False, True, True]]], dtype=bool), DoesNotRaise(), ), ], From 63392ab6403e248e3c1c60d6eb59ef59649f4153 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:27:36 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto=20?= =?UTF-8?q?format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/detection/utils.py | 10 +- test/detection/test_utils.py | 274 ++++++++++++++++++++++++--------- 2 files changed, 206 insertions(+), 78 deletions(-) diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 3f5e7233..0d5ec475 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -747,7 +747,7 @@ def move_masks( # [False, False, True, True], # [False, False, True, True]]], dtype=bool) - offset = np.array([-2, 2]) + offset = np.array([-2, 2]) sv.move_masks(mask, offset, resolution_wh=(4, 4)) # array([[[False, False, False, False], # [False, False, False, False], @@ -780,9 +780,11 @@ def move_masks( destination_y_end = offset[1] + source_y_end - source_y_start if source_x_end > source_x_start and source_y_end > source_y_start: - mask_array[:, destination_y_start:destination_y_end, - destination_x_start:destination_x_end] = \ - masks[:, source_y_start:source_y_end, source_x_start:source_x_end] + mask_array[ + :, + destination_y_start:destination_y_end, + destination_x_start:destination_x_end, + ] = masks[:, source_y_start:source_y_end, source_x_start:source_x_end] return mask_array diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index 721fb2ac..d93c72c8 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -447,122 +447,248 @@ def test_move_boxes( "masks, offset, resolution_wh, expected_result, exception", [ ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([0, 0]), (4, 4), - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-1, -1]), (4, 4), - np.array([[[ True, True, False, False], - [ True, True, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [True, True, False, False], + [True, True, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-2, -2]), (4, 4), - np.array([[[ True, False, False, False], - [False, False, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [True, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-3, -3]), (4, 4), - np.array([[[False, False, False, False], - [False, False, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-2, -1]), (4, 4), - np.array([[[ True, False, False, False], - [ True, False, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [True, False, False, False], + [True, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-1, -2]), (4, 4), - np.array([[[ True, True, False, False], - [False, False, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [True, True, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([-2, 2]), (4, 4), - np.array([[[False, False, False, False], - [False, False, False, False], - [False, False, False, False], - [ True, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [True, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([3, 3]), (4, 4), - np.array([[[False, False, False, False], - [False, False, False, False], - [False, False, False, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ( - np.array([[[False, False, False, False], - [False, True, True, False], - [False, True, True, False], - [False, False, False, False]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False], + [False, True, True, False], + [False, True, True, False], + [False, False, False, False], + ] + ], + dtype=bool, + ), np.array([3, 3]), (6, 6), - np.array([[[False, False, False, False, False, False], - [False, False, False, False, False, False], - [False, False, False, False, False, False], - [False, False, False, False, False, False], - [False, False, False, False, True, True], - [False, False, False, False, True, True]]], dtype=bool), + np.array( + [ + [ + [False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, False, False, True, True], + [False, False, False, False, True, True], + ] + ], + dtype=bool, + ), DoesNotRaise(), ), ],