From 5b50df44da7150a8417097f927ecefe8add61394 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:03:05 -0500 Subject: [PATCH 01/13] Update count and threashold filtering Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 32 ++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 1924953b06..ac9fe89e5c 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3139,8 +3139,10 @@ class GridPatch(Transform, MultiSampleTrait): Args: patch_size: size of patches to generate slices for, 0 or None selects whole dimension offset: offset of starting position in the array, default is 0 for each dimension. - num_patches: number of patches to return. Defaults to None, which returns all the available patches. - If the required patches are more than the available patches, padding will be applied. + num_patches: number of patches (or maximum number of patches) to return. + If the requested number of patches is greater than the number of available patches, + padding will be applied to provide exactly `num_patches` patches unless `threshold` is set. + Defaults to None, which returns all the available patches. overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), @@ -3230,22 +3232,22 @@ def __call__(self, array: NdarrayOrTensor): patched_image = np.array(patches[0]) locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location - # Filter patches - if self.num_patches: - patched_image, locations = self.filter_count(patched_image, locations) - elif self.threshold: + if self.threshold: patched_image, locations = self.filter_threshold(patched_image, locations) - # Pad the patch list to have the requested number of patches if self.num_patches: - padding = self.num_patches - len(patched_image) - if padding > 0: - patched_image = np.pad( - patched_image, - [[0, padding], [0, 0]] + [[0, 0]] * len(self.patch_size), - constant_values=self.pad_kwargs.get("constant_values", 0), - ) - locations = np.pad(locations, [[0, padding], [0, 0]], constant_values=0) + # Limit number of patches + patched_image, locations = self.filter_count(patched_image, locations) + # Pad the patch list to have the requested number of patches + if not self.threshold: + padding = self.num_patches - len(patched_image) + if padding > 0: + patched_image = np.pad( + patched_image, + [[0, padding], [0, 0]] + [[0, 0]] * len(self.patch_size), + constant_values=self.pad_kwargs.get("constant_values", 0), + ) + locations = np.pad(locations, [[0, padding], [0, 0]], constant_values=0) # Convert to MetaTensor metadata = array.meta if isinstance(array, MetaTensor) else MetaTensor.get_default_meta() From 81a12f91a9bbfd9b35aaaf7a4d15faa185b8dcb8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:15:48 -0500 Subject: [PATCH 02/13] Add unittests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patch.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 05e773929e..bd54dbc34f 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -47,16 +47,22 @@ A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] +# Only threshold filtering TEST_CASE_13 = [{"patch_size": (2, 2), "threshold": 50.0}, A, [A11]] +TEST_CASE_14 = [{"patch_size": (2, 2), "threshold": 150.0}, A, [A11, A12, A21]] +# threshold filtering with num_patches more than available patches (no effect) +TEST_CASE_15 = [{"patch_size": (2, 2), "threshold": 50.0, "num_patches": 3}, A, [A11]] +# threshold filtering with num_patches less than available patches (count filtering) +TEST_CASE_16 = [{"patch_size": (2, 2), "threshold": 150.0, "num_patches": 2}, A, [A11, A12]] -TEST_CASE_MEAT_0 = [ +TEST_CASE_META_0 = [ {"patch_size": (2, 2)}, A, [A11, A12, A21, A22], [{"location": [0, 0]}, {"location": [0, 2]}, {"location": [2, 0]}, {"location": [2, 2]}], ] -TEST_CASE_MEAT_1 = [ +TEST_CASE_META_1 = [ {"patch_size": (2, 2)}, MetaTensor(x=A, meta={"path": "path/to/file"}), [A11, A12, A21, A22], @@ -84,6 +90,9 @@ TEST_CASES.append([p, *TEST_CASE_11]) TEST_CASES.append([p, *TEST_CASE_12]) TEST_CASES.append([p, *TEST_CASE_13]) + TEST_CASES.append([p, *TEST_CASE_14]) + TEST_CASES.append([p, *TEST_CASE_15]) + TEST_CASES.append([p, *TEST_CASE_16]) class TestGridPatch(unittest.TestCase): @@ -96,7 +105,7 @@ def test_grid_patch(self, in_type, input_parameters, image, expected): for output_patch, expected_patch in zip(output, expected): assert_allclose(output_patch, expected_patch, type_test=False) - @parameterized.expand([TEST_CASE_MEAT_0, TEST_CASE_MEAT_1]) + @parameterized.expand([TEST_CASE_META_0, TEST_CASE_META_1]) @SkipIfBeforePyTorchVersion((1, 9, 1)) def test_grid_patch_meta(self, input_parameters, image, expected, expected_meta): set_track_meta(True) From d677aaba6142aca161098c4cee955734dad8c69c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:19:50 -0500 Subject: [PATCH 03/13] Add unittests for d Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patchd.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index a19e26a16d..3fb6cac072 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -46,7 +46,13 @@ {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] +# Only threshold filtering TEST_CASE_13 = [{"patch_size": (2, 2), "threshold": 50.0}, {"image": A}, [A11]] +TEST_CASE_14 = [{"patch_size": (2, 2), "threshold": 150.0}, {"image": A}, [A11, A12, A21]] +# threshold filtering with num_patches more than available patches (no effect) +TEST_CASE_15 = [{"patch_size": (2, 2), "threshold": 50.0, "num_patches": 3}, {"image": A}, [A11]] +# threshold filtering with num_patches less than available patches (count filtering) +TEST_CASE_16 = [{"patch_size": (2, 2), "threshold": 150.0, "num_patches": 2}, {"image": A}, [A11, A12]] TEST_SINGLE = [] for p in TEST_NDARRAYS: @@ -64,6 +70,9 @@ TEST_SINGLE.append([p, *TEST_CASE_11]) TEST_SINGLE.append([p, *TEST_CASE_12]) TEST_SINGLE.append([p, *TEST_CASE_13]) + TEST_SINGLE.append([p, *TEST_CASE_14]) + TEST_SINGLE.append([p, *TEST_CASE_15]) + TEST_SINGLE.append([p, *TEST_CASE_16]) class TestGridPatchd(unittest.TestCase): From 460f48f8e397fdcd8443cbda0ae9d200c64c6b11 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 11:24:42 -0500 Subject: [PATCH 04/13] minor changes Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index ac9fe89e5c..631bcf46f7 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3186,11 +3186,10 @@ def filter_threshold(self, image_np: np.ndarray, locations: np.ndarray): image_np: a numpy.ndarray representing a stack of patches locations: a numpy.ndarray representing the stack of location of each patch """ - if self.threshold is not None: - n_dims = len(image_np.shape) - idx = np.argwhere(image_np.sum(axis=tuple(range(1, n_dims))) < self.threshold).reshape(-1) - image_np = image_np[idx] - locations = locations[idx] + n_dims = len(image_np.shape) + idx = np.argwhere(image_np.sum(axis=tuple(range(1, n_dims))) < self.threshold).reshape(-1) + image_np = image_np[idx] + locations = locations[idx] return image_np, locations def filter_count(self, image_np: np.ndarray, locations: np.ndarray): @@ -3232,14 +3231,14 @@ def __call__(self, array: NdarrayOrTensor): patched_image = np.array(patches[0]) locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location - if self.threshold: + if self.threshold is not None: patched_image, locations = self.filter_threshold(patched_image, locations) if self.num_patches: # Limit number of patches patched_image, locations = self.filter_count(patched_image, locations) # Pad the patch list to have the requested number of patches - if not self.threshold: + if self.threshold is None: padding = self.num_patches - len(patched_image) if padding > 0: patched_image = np.pad( From 403857b89965bb69f1cedccc32af2e5302ad3935 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:07:14 -0500 Subject: [PATCH 05/13] add threshold_first Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 631bcf46f7..cb522fc8f7 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3149,6 +3149,8 @@ class GridPatch(Transform, MultiSampleTrait): lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. + threshold_first: whether to apply threshold filtering before limiting the number of patches to `num_patches`. + Defaults to True. pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. @@ -3167,6 +3169,7 @@ def __init__( overlap: Sequence[float] | float = 0.0, sort_fn: str | None = None, threshold: float | None = None, + threshold_first: bool = True, pad_mode: str = PytorchPadMode.CONSTANT, **pad_kwargs, ): @@ -3178,6 +3181,7 @@ def __init__( self.num_patches = num_patches self.sort_fn = sort_fn.lower() if sort_fn else None self.threshold = threshold + self.threshold_first = threshold_first def filter_threshold(self, image_np: np.ndarray, locations: np.ndarray): """ @@ -3231,7 +3235,8 @@ def __call__(self, array: NdarrayOrTensor): patched_image = np.array(patches[0]) locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location - if self.threshold is not None: + # Apply threshold filter before filtering by count (if threshold_first is set) + if self.threshold_first and self.threshold is not None: patched_image, locations = self.filter_threshold(patched_image, locations) if self.num_patches: @@ -3248,6 +3253,10 @@ def __call__(self, array: NdarrayOrTensor): ) locations = np.pad(locations, [[0, padding], [0, 0]], constant_values=0) + # Apply threshold filter after filtering by count (if threshold_first is not set) + if not self.threshold_first and self.threshold is not None: + patched_image, locations = self.filter_threshold(patched_image, locations) + # Convert to MetaTensor metadata = array.meta if isinstance(array, MetaTensor) else MetaTensor.get_default_meta() metadata[WSIPatchKeys.LOCATION] = locations.T From 44027310022991a09df8e1be44901d51a0eaf2f2 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:07:29 -0500 Subject: [PATCH 06/13] add unittests for threshold_first Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patch.py | 10 ++++++++-- tests/test_grid_patchd.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index bd54dbc34f..0ce09dea65 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -51,9 +51,13 @@ TEST_CASE_13 = [{"patch_size": (2, 2), "threshold": 50.0}, A, [A11]] TEST_CASE_14 = [{"patch_size": (2, 2), "threshold": 150.0}, A, [A11, A12, A21]] # threshold filtering with num_patches more than available patches (no effect) -TEST_CASE_15 = [{"patch_size": (2, 2), "threshold": 50.0, "num_patches": 3}, A, [A11]] +TEST_CASE_15 = [{"patch_size": (2, 2), "num_patches": 3, "threshold": 50.0}, A, [A11]] # threshold filtering with num_patches less than available patches (count filtering) -TEST_CASE_16 = [{"patch_size": (2, 2), "threshold": 150.0, "num_patches": 2}, A, [A11, A12]] +TEST_CASE_16 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": 150.0}, A, [A11, A12]] +# threshold filtering before count filtering +TEST_CASE_17 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": True}, -A, [-A12, -A21]] +# threshold filtering after count filtering (causes desirable or undesirable data reduction) +TEST_CASE_18 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": False}, -A, [-A12]] TEST_CASE_META_0 = [ {"patch_size": (2, 2)}, @@ -93,6 +97,8 @@ TEST_CASES.append([p, *TEST_CASE_14]) TEST_CASES.append([p, *TEST_CASE_15]) TEST_CASES.append([p, *TEST_CASE_16]) + TEST_CASES.append([p, *TEST_CASE_17]) + TEST_CASES.append([p, *TEST_CASE_18]) class TestGridPatch(unittest.TestCase): diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 3fb6cac072..4d520d0dda 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -53,6 +53,18 @@ TEST_CASE_15 = [{"patch_size": (2, 2), "threshold": 50.0, "num_patches": 3}, {"image": A}, [A11]] # threshold filtering with num_patches less than available patches (count filtering) TEST_CASE_16 = [{"patch_size": (2, 2), "threshold": 150.0, "num_patches": 2}, {"image": A}, [A11, A12]] +# threshold filtering before count filtering +TEST_CASE_17 = [ + {"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": True}, + {"image": -A}, + [-A12, -A21], +] +# threshold filtering after count filtering (causes desirable or undesirable data reduction) +TEST_CASE_18 = [ + {"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": False}, + {"image": -A}, + [-A12], +] TEST_SINGLE = [] for p in TEST_NDARRAYS: @@ -73,6 +85,8 @@ TEST_SINGLE.append([p, *TEST_CASE_14]) TEST_SINGLE.append([p, *TEST_CASE_15]) TEST_SINGLE.append([p, *TEST_CASE_16]) + TEST_SINGLE.append([p, *TEST_CASE_17]) + TEST_SINGLE.append([p, *TEST_CASE_18]) class TestGridPatchd(unittest.TestCase): From fc6bd013346c595fd5ac827daeac5bfe12f62509 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:53:54 -0500 Subject: [PATCH 07/13] memeory and docstring enhancements Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 41 +++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index cb522fc8f7..2a2c415cca 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -68,7 +68,7 @@ optional_import, ) from monai.utils.deprecate_utils import deprecated_arg -from monai.utils.enums import GridPatchSort, PytorchPadMode, TraceKeys, TransformBackends, WSIPatchKeys +from monai.utils.enums import GridPatchSort, PatchKeys, PytorchPadMode, TraceKeys, TransformBackends from monai.utils.misc import ImageMetaKey as Key from monai.utils.module import look_up_option from monai.utils.type_conversion import convert_data_type, get_equivalent_dtype, get_torch_dtype_from_string @@ -3185,23 +3185,26 @@ def __init__( def filter_threshold(self, image_np: np.ndarray, locations: np.ndarray): """ - Filter the patches and their locations according to a threshold + Filter the patches and their locations according to a threshold. + Args: - image_np: a numpy.ndarray representing a stack of patches - locations: a numpy.ndarray representing the stack of location of each patch + image_np: a numpy.ndarray representing a stack of patches. + locations: a numpy.ndarray representing the stack of location of each patch. + + Returns: + tuple[numpy.ndarray, numpy.ndarray]: tuple of filtered patches and locations. """ n_dims = len(image_np.shape) idx = np.argwhere(image_np.sum(axis=tuple(range(1, n_dims))) < self.threshold).reshape(-1) - image_np = image_np[idx] - locations = locations[idx] - return image_np, locations + return image_np[idx], locations[idx] def filter_count(self, image_np: np.ndarray, locations: np.ndarray): """ Sort the patches based on the sum of their intensity, and just keep `self.num_patches` of them. + Args: - image_np: a numpy.ndarray representing a stack of patches - locations: a numpy.ndarray representing the stack of location of each patch + image_np: a numpy.ndarray representing a stack of patches. + locations: a numpy.ndarray representing the stack of location of each patch. """ if self.sort_fn is None: image_np = image_np[: self.num_patches] @@ -3219,7 +3222,17 @@ def filter_count(self, image_np: np.ndarray, locations: np.ndarray): locations = locations[idx] return image_np, locations - def __call__(self, array: NdarrayOrTensor): + def __call__(self, array: NdarrayOrTensor) -> MetaTensor: + """ + Extract the patches (sweeping the entire image in a row-major sliding-window manner with possible overlaps). + + Args: + array: a input image as `numpy.ndarray` or `torch.Tensor` + + Return: + MetaTensor: the extracted patches as a single tensor (with patch dimension as the first dimension), + with defined `PatchKeys.LOCATION` and `PatchKeys.COUNT` metadata. + """ # create the patch iterator which sweeps the image row-by-row array_np, *_ = convert_data_type(array, np.ndarray) patch_iterator = iter_patch( @@ -3233,7 +3246,9 @@ def __call__(self, array: NdarrayOrTensor): ) patches = list(zip(*patch_iterator)) patched_image = np.array(patches[0]) - locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location + del patches[0] + locations = np.array(patches[0])[:, 1:, 0] # only keep the starting location + del patches[0] # Apply threshold filter before filtering by count (if threshold_first is set) if self.threshold_first and self.threshold is not None: @@ -3259,8 +3274,8 @@ def __call__(self, array: NdarrayOrTensor): # Convert to MetaTensor metadata = array.meta if isinstance(array, MetaTensor) else MetaTensor.get_default_meta() - metadata[WSIPatchKeys.LOCATION] = locations.T - metadata[WSIPatchKeys.COUNT] = len(locations) + metadata[PatchKeys.LOCATION] = locations.T + metadata[PatchKeys.COUNT] = len(locations) metadata["spatial_shape"] = np.tile(np.array(self.patch_size), (len(locations), 1)).T output = MetaTensor(x=patched_image, meta=metadata) output.is_batch = True From 75e6f1e2f80a98e3c9470df94dc38335779ec071 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:13:53 -0500 Subject: [PATCH 08/13] remove threshold first Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 13 +++---------- tests/test_grid_patch.py | 7 +------ tests/test_grid_patchd.py | 15 +-------------- 3 files changed, 5 insertions(+), 30 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 2a2c415cca..18b6da73a0 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3149,8 +3149,6 @@ class GridPatch(Transform, MultiSampleTrait): lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - threshold_first: whether to apply threshold filtering before limiting the number of patches to `num_patches`. - Defaults to True. pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. @@ -3169,7 +3167,6 @@ def __init__( overlap: Sequence[float] | float = 0.0, sort_fn: str | None = None, threshold: float | None = None, - threshold_first: bool = True, pad_mode: str = PytorchPadMode.CONSTANT, **pad_kwargs, ): @@ -3181,7 +3178,6 @@ def __init__( self.num_patches = num_patches self.sort_fn = sort_fn.lower() if sort_fn else None self.threshold = threshold - self.threshold_first = threshold_first def filter_threshold(self, image_np: np.ndarray, locations: np.ndarray): """ @@ -3250,10 +3246,11 @@ def __call__(self, array: NdarrayOrTensor) -> MetaTensor: locations = np.array(patches[0])[:, 1:, 0] # only keep the starting location del patches[0] - # Apply threshold filter before filtering by count (if threshold_first is set) - if self.threshold_first and self.threshold is not None: + # Apply threshold filtering + if self.threshold is not None: patched_image, locations = self.filter_threshold(patched_image, locations) + # Apply count filtering if self.num_patches: # Limit number of patches patched_image, locations = self.filter_count(patched_image, locations) @@ -3268,10 +3265,6 @@ def __call__(self, array: NdarrayOrTensor) -> MetaTensor: ) locations = np.pad(locations, [[0, padding], [0, 0]], constant_values=0) - # Apply threshold filter after filtering by count (if threshold_first is not set) - if not self.threshold_first and self.threshold is not None: - patched_image, locations = self.filter_threshold(patched_image, locations) - # Convert to MetaTensor metadata = array.meta if isinstance(array, MetaTensor) else MetaTensor.get_default_meta() metadata[PatchKeys.LOCATION] = locations.T diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 0ce09dea65..3c63087289 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -54,10 +54,7 @@ TEST_CASE_15 = [{"patch_size": (2, 2), "num_patches": 3, "threshold": 50.0}, A, [A11]] # threshold filtering with num_patches less than available patches (count filtering) TEST_CASE_16 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": 150.0}, A, [A11, A12]] -# threshold filtering before count filtering -TEST_CASE_17 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": True}, -A, [-A12, -A21]] -# threshold filtering after count filtering (causes desirable or undesirable data reduction) -TEST_CASE_18 = [{"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": False}, -A, [-A12]] + TEST_CASE_META_0 = [ {"patch_size": (2, 2)}, @@ -97,8 +94,6 @@ TEST_CASES.append([p, *TEST_CASE_14]) TEST_CASES.append([p, *TEST_CASE_15]) TEST_CASES.append([p, *TEST_CASE_16]) - TEST_CASES.append([p, *TEST_CASE_17]) - TEST_CASES.append([p, *TEST_CASE_18]) class TestGridPatch(unittest.TestCase): diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 4d520d0dda..9673728b45 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -53,18 +53,7 @@ TEST_CASE_15 = [{"patch_size": (2, 2), "threshold": 50.0, "num_patches": 3}, {"image": A}, [A11]] # threshold filtering with num_patches less than available patches (count filtering) TEST_CASE_16 = [{"patch_size": (2, 2), "threshold": 150.0, "num_patches": 2}, {"image": A}, [A11, A12]] -# threshold filtering before count filtering -TEST_CASE_17 = [ - {"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": True}, - {"image": -A}, - [-A12, -A21], -] -# threshold filtering after count filtering (causes desirable or undesirable data reduction) -TEST_CASE_18 = [ - {"patch_size": (2, 2), "num_patches": 2, "threshold": -50.0, "threshold_first": False}, - {"image": -A}, - [-A12], -] + TEST_SINGLE = [] for p in TEST_NDARRAYS: @@ -85,8 +74,6 @@ TEST_SINGLE.append([p, *TEST_CASE_14]) TEST_SINGLE.append([p, *TEST_CASE_15]) TEST_SINGLE.append([p, *TEST_CASE_16]) - TEST_SINGLE.append([p, *TEST_CASE_17]) - TEST_SINGLE.append([p, *TEST_CASE_18]) class TestGridPatchd(unittest.TestCase): From c5b35082c5ce16aff2571736afa3d9bf374a788c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:25:27 -0500 Subject: [PATCH 09/13] Chagne default pad_mode to None Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 12 +++++++----- tests/test_grid_patch.py | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 18b6da73a0..d140c35173 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3149,7 +3149,10 @@ class GridPatch(Transform, MultiSampleTrait): lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_mode: the padding mode for padding to be applied to the input image by patch_size. + Defaults to None, which means no padding will be applied. + Refer to `NumpyPadMode` and `PytorchPadMode` for acceptable values. + For more details please see: https://numpy.org/doc/1.20/reference/generated/numpy.pad.html pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. Returns: @@ -3167,7 +3170,7 @@ def __init__( overlap: Sequence[float] | float = 0.0, sort_fn: str | None = None, threshold: float | None = None, - pad_mode: str = PytorchPadMode.CONSTANT, + pad_mode: str | None = None, **pad_kwargs, ): self.patch_size = ensure_tuple(patch_size) @@ -3242,9 +3245,8 @@ def __call__(self, array: NdarrayOrTensor) -> MetaTensor: ) patches = list(zip(*patch_iterator)) patched_image = np.array(patches[0]) - del patches[0] - locations = np.array(patches[0])[:, 1:, 0] # only keep the starting location - del patches[0] + locations = np.array(patches[1])[:, 1:, 0] # only keep the starting location + del patches # it will free up some memory if padding is used. # Apply threshold filtering if self.threshold is not None: diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 3c63087289..0bc69df689 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -18,6 +18,7 @@ from monai.data import MetaTensor, set_track_meta from monai.transforms.spatial.array import GridPatch +from monai.utils.enums import NumpyPadMode, PytorchPadMode from tests.utils import TEST_NDARRAYS, SkipIfBeforePyTorchVersion, assert_allclose A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) @@ -38,12 +39,12 @@ TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_fn": "min"}, A, [A11, A12, A21, A22]] TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ - {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255}, + {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255, "pad_mode": "constant"}, A, [A[:, :3, :3], np.pad(A[:, :3, 3:], ((0, 0), (0, 0), (0, 2)), mode="constant", constant_values=255)], ] TEST_CASE_12 = [ - {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2}, + {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2, "pad_mode": "constant"}, A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] From 3d311751dd472fa0aa1a702b95c0d865125f3312 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Feb 2023 21:26:39 +0000 Subject: [PATCH 10/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_grid_patch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 0bc69df689..22c2218afd 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -18,7 +18,6 @@ from monai.data import MetaTensor, set_track_meta from monai.transforms.spatial.array import GridPatch -from monai.utils.enums import NumpyPadMode, PytorchPadMode from tests.utils import TEST_NDARRAYS, SkipIfBeforePyTorchVersion, assert_allclose A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) From 295a3414806cc119dc4a45d27ca173985d6866e0 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 28 Feb 2023 16:47:26 -0500 Subject: [PATCH 11/13] update docstrings Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 39 ++++++++++++++++----- monai/transforms/spatial/dictionary.py | 48 +++++++++++++++++--------- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 2196d0302a..8e3e0ee83d 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -3028,21 +3028,29 @@ class GridPatch(Transform, MultiSampleTrait): num_patches: number of patches (or maximum number of patches) to return. If the requested number of patches is greater than the number of available patches, padding will be applied to provide exactly `num_patches` patches unless `threshold` is set. - Defaults to None, which returns all the available patches. + When `threshold` is set, this value is treated as the maximum number of patches. + Defaults to None, which does not limit number of the patches. overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - pad_mode: the padding mode for padding to be applied to the input image by patch_size. + pad_mode: the mode for padding the input image by `patch_size` to include patches that cross boundaries. Defaults to None, which means no padding will be applied. - Refer to `NumpyPadMode` and `PytorchPadMode` for acceptable values. - For more details please see: https://numpy.org/doc/1.20/reference/generated/numpy.pad.html + Available modes:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``}. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. Returns: - MetaTensor: A MetaTensor consisting of a batch of all the patches with associated metadata + MetaTensor: the extracted patches as a single tensor (with patch dimension as the first dimension), + with following metadata: + + - `PatchKeys.LOCATION`: the starting location of the patch in the image, + - `PatchKeys.COUNT`: total number of patches in the image, + - "spatial_shape": spatial size of the extracted patch, and + - "offset": the amount of offset for the patches in the image (starting position of the first patch) """ @@ -3158,6 +3166,7 @@ def __call__(self, array: NdarrayOrTensor) -> MetaTensor: metadata[PatchKeys.LOCATION] = locations.T metadata[PatchKeys.COUNT] = len(locations) metadata["spatial_shape"] = np.tile(np.array(self.patch_size), (len(locations), 1)).T + metadata["offset"] = self.offset output = MetaTensor(x=patched_image, meta=metadata) output.is_batch = True @@ -3175,18 +3184,32 @@ class RandGridPatch(GridPatch, RandomizableTransform, MultiSampleTrait): min_offset: the minimum range of offset to be selected randomly. Defaults to 0. max_offset: the maximum range of offset to be selected randomly. Defaults to image size modulo patch size. - num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches (or maximum number of patches) to return. + If the requested number of patches is greater than the number of available patches, + padding will be applied to provide exactly `num_patches` patches unless `threshold` is set. + When `threshold` is set, this value is treated as the maximum number of patches. + Defaults to None, which does not limit number of the patches. overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_mode: the mode for padding the input image by `patch_size` to include patches that cross boundaries. + Defaults to None, which means no padding will be applied. + Available modes:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``}. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. Returns: - MetaTensor: A MetaTensor consisting of a batch of all the patches with associated metadata + MetaTensor: the extracted patches as a single tensor (with patch dimension as the first dimension), + with following metadata: + + - `PatchKeys.LOCATION`: the starting location of the patch in the image, + - `PatchKeys.COUNT`: total number of patches in the image, + - "spatial_shape": spatial size of the extracted patch, and + - "offset": the amount of offset for the patches in the image (starting position of the first patch) """ diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index cea89dc76d..4c1fe4f268 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -1836,24 +1836,32 @@ class GridPatchd(MapTransform, MultiSampleTrait): patch_size: size of patches to generate slices for, 0 or None selects whole dimension offset: starting position in the array, default is 0 for each dimension. np.random.randint(0, patch_size, 2) creates random start between 0 and `patch_size` for a 2D image. - num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches (or maximum number of patches) to return. + If the requested number of patches is greater than the number of available patches, + padding will be applied to provide exactly `num_patches` patches unless `threshold` is set. + When `threshold` is set, this value is treated as the maximum number of patches. + Defaults to None, which does not limit number of the patches. overlap: amount of overlap between patches in each dimension. Default to 0.0. sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_mode: the mode for padding the input image by `patch_size` to include patches that cross boundaries. + Defaults to None, which means no padding will be applied. + Available modes:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``}. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html allow_missing_keys: don't raise exception if key is missing. pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. Returns: - a list of dictionaries, each of which contains the all the original key/value with the values for `keys` - replaced by the patches. It also add the following new keys: + dictionary, contains the all the original key/value with the values for `keys` + replaced by the patches, a MetaTensor with following metadata: - "patch_location": the starting location of the patch in the image, - "patch_size": size of the extracted patch - "num_patches": total number of patches in the image - "offset": the amount of offset for the patches in the image (starting position of upper left patch) + - `PatchKeys.LOCATION`: the starting location of the patch in the image, + - `PatchKeys.COUNT`: total number of patches in the image, + - "spatial_shape": spatial size of the extracted patch, and + - "offset": the amount of offset for the patches in the image (starting position of the first patch) """ backend = GridPatch.backend @@ -1902,25 +1910,33 @@ class RandGridPatchd(RandomizableTransform, MapTransform, MultiSampleTrait): min_offset: the minimum range of starting position to be selected randomly. Defaults to 0. max_offset: the maximum range of starting position to be selected randomly. Defaults to image size modulo patch size. - num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches (or maximum number of patches) to return. + If the requested number of patches is greater than the number of available patches, + padding will be applied to provide exactly `num_patches` patches unless `threshold` is set. + When `threshold` is set, this value is treated as the maximum number of patches. + Defaults to None, which does not limit number of the patches. overlap: the amount of overlap of neighboring patches in each dimension (a value between 0.0 and 1.0). If only one float number is given, it will be applied to all dimensions. Defaults to 0.0. sort_fn: when `num_patches` is provided, it determines if keep patches with highest values (`"max"`), lowest values (`"min"`), or in their default order (`None`). Default to None. threshold: a value to keep only the patches whose sum of intensities are less than the threshold. Defaults to no filtering. - pad_mode: refer to NumpyPadMode and PytorchPadMode. If None, no padding will be applied. Defaults to ``"constant"``. + pad_mode: the mode for padding the input image by `patch_size` to include patches that cross boundaries. + Defaults to None, which means no padding will be applied. + Available modes:{``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``}. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html allow_missing_keys: don't raise exception if key is missing. pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. Returns: - a list of dictionaries, each of which contains the all the original key/value with the values for `keys` - replaced by the patches. It also add the following new keys: + dictionary, contains the all the original key/value with the values for `keys` + replaced by the patches, a MetaTensor with following metadata: - "patch_location": the starting location of the patch in the image, - "patch_size": size of the extracted patch - "num_patches": total number of patches in the image - "offset": the amount of offset for the patches in the image (starting position of the first patch) + - `PatchKeys.LOCATION`: the starting location of the patch in the image, + - `PatchKeys.COUNT`: total number of patches in the image, + - "spatial_shape": spatial size of the extracted patch, and + - "offset": the amount of offset for the patches in the image (starting position of the first patch) """ From ca1033f4bd0290a43491c8542a9f72a4bded8038 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:11:11 -0500 Subject: [PATCH 12/13] remove deprecated arg in measure.regionprops Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/apps/pathology/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/apps/pathology/utils.py b/monai/apps/pathology/utils.py index 76386ce070..d3ebe0a7a6 100644 --- a/monai/apps/pathology/utils.py +++ b/monai/apps/pathology/utils.py @@ -52,7 +52,7 @@ def compute_isolated_tumor_cells(tumor_mask: np.ndarray, threshold: float) -> li A region with the longest diameter less than this threshold is considered as an ITC. """ max_label = np.amax(tumor_mask) - properties = measure.regionprops(tumor_mask, coordinates="rc") + properties = measure.regionprops(tumor_mask) itc_list = [i + 1 for i in range(max_label) if properties[i].major_axis_length < threshold] return itc_list From f12b77a507a5731d926281ed28718640241cf6dc Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 1 Mar 2023 09:24:33 -0500 Subject: [PATCH 13/13] update gridpatchd tests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patchd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 9673728b45..5629c0e871 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -37,12 +37,12 @@ TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_fn": "min"}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ - {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255}, + {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255, "pad_mode": "constant"}, {"image": A}, [A[:, :3, :3], np.pad(A[:, :3, 3:], ((0, 0), (0, 0), (0, 2)), mode="constant", constant_values=255)], ] TEST_CASE_12 = [ - {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2}, + {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2, "pad_mode": "constant"}, {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ]