From c96dd21233ae76a4118f20b253c601ef7ef8484d Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 22 May 2022 18:28:34 +0000 Subject: [PATCH 01/49] Implement GridPatch and RandGridPatch Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 146 +++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 69f98aa2a0..d30a81f8ae 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -12,6 +12,7 @@ A collection of "vanilla" transforms for spatial operations https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design """ +from pstats import SortKey import warnings from copy import deepcopy from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union @@ -22,7 +23,14 @@ from monai.config import USE_COMPILED, DtypeLike from monai.config.type_definitions import NdarrayOrTensor -from monai.data.utils import AFFINE_TOL, compute_shape_offset, reorient_spatial_axes, to_affine_nd, zoom_affine +from monai.data.utils import ( + AFFINE_TOL, + compute_shape_offset, + iter_patch, + reorient_spatial_axes, + to_affine_nd, + zoom_affine, +) from monai.networks.layers import AffineTransform, GaussianFilter, grid_pull from monai.networks.utils import meshgrid_ij, normalize_transform from monai.transforms.croppad.array import CenterSpatialCrop, Pad @@ -68,7 +76,8 @@ "Flip", "GridDistortion", "GridSplit", - "Resize", + "GridPatch", + "RandGridPatch" "Resize", "Rotate", "Zoom", "Rotate90", @@ -2605,3 +2614,136 @@ def _get_params( ) return size, steps + + +class GridPatch(Transform): + """ + Return all (or a subset of) the patches sweeping the entire image + + Args: + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: 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. + max_num_patches: maximum number of patches to return. No limit by default. + overlap: amount of overlap between patches in each dimension. Default to 0.0. + sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", + which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions + and channels. Also "random" creates a random order of patches. + By default no sorting is being done and patches are returned in a row-major order. + pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + One of the listed string values or a user supplied function. Defaults to ``"wrap"``. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html + pad_opts: padding options, see `numpy.pad` + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + patch_size: Sequence[int], + start_pos: Sequence[int] = (), + max_num_patches: Optional[int] = None, + overlap: float = 0.0, + sort_key: Optional[Union[Callable, str]] = None, + pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.WRAP, + pad_opts: Optional[Dict] = None, + ): + self.patch_size = (None,) + ensure_tuple(patch_size) # expand to have the channel dim + self.start_pos = (0,) + ensure_tuple(start_pos) + self.pad_mode: NumpyPadMode = look_up_option(pad_mode, NumpyPadMode) + self.pad_opts = {} if pad_opts is None else pad_opts + self.overlap = overlap + self.max_num_patches = max_num_patches + self.num_pacthes = max_num_patches + if isinstance(sort_key, str): + if sort_key == "random": + self.sort_key = np.random.random() + if sort_key == "min": + self.sort_key = lambda x: x[0].sum() + if sort_key == "max": + self.sort_key = lambda x: -x[0].sum() + else: + ValueError(f'sort_key should be either "min", "max", or "random", "{sort_key}" was given.') + else: + self.sort_key = sort_key + + def __call__(self, array: NdarrayOrTensor): + # create the patch iterator which sweeps the image row-by-row + patch_iterator = iter_patch( + array, + patch_size=self.patch_size, + start_pos=self.start_pos, + overlap=self.overlap, + copy_back=False, + mode=self.pad_mode, + **self.pad_opts, + ) + if self.sort_key is not None: + output = sorted(patch_iterator, key=self.sort_key) + else: + # Get all the patches (it's required to have a defined length) + output = list(patch_iterator) + # keep up to max_num_patches + output = output[: self.max_num_patches] + self.num_pacthes = len(output) + return output + + +class RandGridPatch(RandomizableTransform, GridPatch): + """ + Return all (or a subset of) the patches sweeping the entire image with a random starting position. + + Args: + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: 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. + max_num_patches: maximum number of patches to return. No limit by default. + overlap: amount of overlap between patches in each dimension. Default to 0.0. + sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", + which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions + and channels. Also "random" creates a random order of patches. + By default no sorting is being done and patches are returned in a row-major order. + pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + One of the listed string values or a user supplied function. Defaults to ``"wrap"``. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html + pad_opts: padding options, see `numpy.pad` + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + patch_size: Sequence[int], + min_start_pos: Optional[Union[Sequence[int], int]] = None, + max_start_pos: Optional[Union[Sequence[int], int]] = None, + max_num_patches: Optional[int] = None, + overlap: float = 0.0, + sort_key: Optional[Union[Callable, str]] = None, + pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + **pad_opts: Dict, + ): + super().__init__( + patch_size=patch_size, + start_pos=(), + max_num_patches=max_num_patches, + overlap=overlap, + sort_key=sort_key, + pad_mode=pad_mode, + **pad_opts, + ) + self.min_start_pos = min_start_pos + self.max_start_pos = max_start_pos + + def __call__(self, array: NdarrayOrTensor): + if self.min_start_pos is None: + min_start_pos = (0,) * (len(self.patch_size) - 1) + if self.max_start_pos is None: + max_start_pos = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size[1:])) + self.start_pos = tuple(self.R.randint(low=low, high=high) for low, high in zip(min_start_pos, max_start_pos)) + return super().__call__(array) From 3df8dbbc9761a72cffae4c6175195dd4024aa0b1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 22 May 2022 18:29:35 +0000 Subject: [PATCH 02/49] Add unittests for GridPatch Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patch.py | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_grid_patch.py diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py new file mode 100644 index 0000000000..5a4807329a --- /dev/null +++ b/tests/test_grid_patch.py @@ -0,0 +1,64 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.spatial.array import GridPatch +from tests.utils import TEST_NDARRAYS, assert_allclose + +A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) +A11 = A[:, :2, :2] +A12 = A[:, :2, 2:] +A21 = A[:, 2:, :2] +A22 = A[:, 2:, 2:] + +TEST_CASE_0 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "max_num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "max_num_patches": 5}, A, [A11, A12, A21, A22]] +TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, A, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, A, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "max_num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "max_num_patches": 4, "sort_key": "min"}, A, [A11, A12, A21, A22]] + + +TEST_SINGLE = [] +for p in TEST_NDARRAYS: + TEST_SINGLE.append([p, *TEST_CASE_0]) + TEST_SINGLE.append([p, *TEST_CASE_1]) + TEST_SINGLE.append([p, *TEST_CASE_2]) + TEST_SINGLE.append([p, *TEST_CASE_3]) + TEST_SINGLE.append([p, *TEST_CASE_4]) + TEST_SINGLE.append([p, *TEST_CASE_5]) + TEST_SINGLE.append([p, *TEST_CASE_6]) + TEST_SINGLE.append([p, *TEST_CASE_7]) + TEST_SINGLE.append([p, *TEST_CASE_8]) + TEST_SINGLE.append([p, *TEST_CASE_9]) + + +class TestSlidingPatch(unittest.TestCase): + @parameterized.expand(TEST_SINGLE) + def test_split_patch_single_call(self, in_type, input_parameters, image, expected): + input_image = in_type(image) + splitter = GridPatch(**input_parameters) + output = list(splitter(input_image)) + self.assertEqual(len(output), len(expected)) + for output_patch, expected_patch in zip(output, expected): + assert_allclose(output_patch[0], expected_patch, type_test=False) + + +if __name__ == "__main__": + unittest.main() From ec2e6cd8be2689a9eaee1c3d63e1f55ef545f963 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:02:06 +0000 Subject: [PATCH 03/49] Update pad mode Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index d30a81f8ae..44290a0a0a 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2648,7 +2648,7 @@ def __init__( max_num_patches: Optional[int] = None, overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, - pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.WRAP, + pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, ): self.patch_size = (None,) + ensure_tuple(patch_size) # expand to have the channel dim From 8cae266ba4d7677599ee2edc4532faae2f5f309d Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:02:31 +0000 Subject: [PATCH 04/49] Add several test cases Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patch.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 5a4807329a..dcdfcd480e 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -33,6 +33,21 @@ TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] TEST_CASE_8 = [{"patch_size": (2, 2), "max_num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] TEST_CASE_9 = [{"patch_size": (2, 2), "max_num_patches": 4, "sort_key": "min"}, A, [A11, A12, A21, A22]] +TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "max_num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_11 = [ + {"patch_size": (3, 3), "max_num_patches": 2, "pad_opts": {"constant_values": 255}}, + 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), + "start_pos": (-2, -2), + "max_num_patches": 2, + }, + A, + [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], +] TEST_SINGLE = [] @@ -47,6 +62,9 @@ TEST_SINGLE.append([p, *TEST_CASE_7]) TEST_SINGLE.append([p, *TEST_CASE_8]) TEST_SINGLE.append([p, *TEST_CASE_9]) + TEST_SINGLE.append([p, *TEST_CASE_10]) + TEST_SINGLE.append([p, *TEST_CASE_11]) + TEST_SINGLE.append([p, *TEST_CASE_12]) class TestSlidingPatch(unittest.TestCase): From 232fd6e087adcef555fde251265e2540c88d07f8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:03:03 +0000 Subject: [PATCH 05/49] Implement GridPatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 54 +++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index d00dd4463c..7e6b5b232d 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -17,7 +17,7 @@ from copy import deepcopy from enum import Enum -from typing import Any, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -34,6 +34,7 @@ AffineGrid, Flip, GridDistortion, + GridPatch, GridSplit, Orientation, Rand2DElastic, @@ -63,6 +64,7 @@ ensure_tuple, ensure_tuple_rep, fall_back_tuple, + first, ) from monai.utils.deprecate_utils import deprecated_arg from monai.utils.enums import PostFix, TraceKeys @@ -133,6 +135,9 @@ "GridSplitd", "GridSplitD", "GridSplitDict", + "GridPatchd", + "GridPatchD", + "GridPatchDict", ] GridSampleModeSequence = Union[Sequence[Union[GridSampleMode, str]], GridSampleMode, str] @@ -2194,6 +2199,52 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashab return output +class GridPatchd(MapTransform): + """ """ + + backend = GridSplit.backend + + def __init__( + self, + keys: KeysCollection, + patch_size: Sequence[int], + start_pos: Sequence[int] = (), + max_num_patches: Optional[int] = None, + overlap: float = 0.0, + sort_key: Optional[Union[Callable, str]] = None, + pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_opts: Optional[Dict] = None, + allow_missing_keys: bool = False, + ): + super().__init__(keys, allow_missing_keys) + self.patcher = GridPatch( + patch_size=patch_size, + start_pos=start_pos, + max_num_patches=max_num_patches, + overlap=overlap, + sort_key=sort_key, + pad_mode=pad_mode, + pad_opts=pad_opts, + ) + self.max_num_patches = max_num_patches + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashable, NdarrayOrTensor]]: + d = dict(data) + original_spatial_shape = d[first(self.keys)].shape[1:] + for patch in zip(*[self.patcher(d[key]) for key in self.keys]): + new_dict = {k: v[0] for k, v in zip(self.keys, patch)} + # fill in the extra keys with unmodified data + for k in set(d.keys()).difference(set(self.keys)): + new_dict[k] = deepcopy(d[k]) + # fill additional metadata + new_dict["original_spatial_shape"] = original_spatial_shape + # use the coordinate of the first item + location = patch[0][1] + new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} + new_dict["start_pos"] = self.patcher.start_pos + yield new_dict + + SpatialResampleD = SpatialResampleDict = SpatialResampled ResampleToMatchD = ResampleToMatchDict = ResampleToMatchd SpacingD = SpacingDict = Spacingd @@ -2215,3 +2266,4 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashab ZoomD = ZoomDict = Zoomd RandZoomD = RandZoomDict = RandZoomd GridSplitD = GridSplitDict = GridSplitd +GridPatchD = GridPatchDict = GridPatchd From d66377a5c23c046fb66848ebdead8d6bd7910847 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:03:22 +0000 Subject: [PATCH 06/49] Update module imports Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index c2385499b3..6b891ce34b 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -311,6 +311,7 @@ AffineGrid, Flip, GridDistortion, + GridPatch, GridSplit, Orientation, Rand2DElastic, @@ -343,6 +344,9 @@ GridDistortiond, GridDistortionD, GridDistortionDict, + GridPatchd, + GridPatchD, + GridPatchDict, GridSplitd, GridSplitD, GridSplitDict, From cf6c72a5a99359f30e66e895b8610a70926569d9 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:14:34 +0000 Subject: [PATCH 07/49] Add docs Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index a93c48984c..66aa3e39c4 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -737,6 +737,13 @@ Spatial :members: :special-members: __call__ +`GridPatch` +""""""""""" +.. autoclass:: GridPatch + :members: + :special-members: __call__ + + `GridSplit` """"""""""" .. autoclass:: GridSplit @@ -1513,6 +1520,12 @@ Spatial (Dict) :members: :special-members: __call__ +`GridPatchd` +"""""""""""" +.. autoclass:: GridPatchd + :members: + :special-members: __call__ + `GridSplitd` """""""""""" .. autoclass:: GridSplitd From a01faa749e68cd5f4d2ee5ebce532cd80cd6485f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 13:47:48 +0000 Subject: [PATCH 08/49] Deprecate SplitOnGrid and TileOnGrid Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/apps/pathology/transforms/spatial/array.py | 5 +++-- monai/apps/pathology/transforms/spatial/dictionary.py | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/monai/apps/pathology/transforms/spatial/array.py b/monai/apps/pathology/transforms/spatial/array.py index a44dce1e3f..4bc2eb1f3b 100644 --- a/monai/apps/pathology/transforms/spatial/array.py +++ b/monai/apps/pathology/transforms/spatial/array.py @@ -17,12 +17,13 @@ from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.transform import Randomizable, Transform -from monai.utils import convert_data_type, convert_to_dst_type +from monai.utils import convert_data_type, convert_to_dst_type, deprecated from monai.utils.enums import TransformBackends __all__ = ["SplitOnGrid", "TileOnGrid"] +@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridSplit` instead.") class SplitOnGrid(Transform): """ Split the image into patches based on the provided grid shape. @@ -106,7 +107,7 @@ def get_params(self, image_size): return patch_size, steps - +@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridPatch` or `monai.transforms.RandGridPatch` instead.") class TileOnGrid(Randomizable, Transform): """ Tile the 2D image into patches on a grid and maintain a subset of it. diff --git a/monai/apps/pathology/transforms/spatial/dictionary.py b/monai/apps/pathology/transforms/spatial/dictionary.py index d5c34a0840..78f3836c4b 100644 --- a/monai/apps/pathology/transforms/spatial/dictionary.py +++ b/monai/apps/pathology/transforms/spatial/dictionary.py @@ -15,12 +15,14 @@ from monai.config import KeysCollection from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.transform import MapTransform, Randomizable +from monai.utils import deprecated from .array import SplitOnGrid, TileOnGrid __all__ = ["SplitOnGridd", "SplitOnGridD", "SplitOnGridDict", "TileOnGridd", "TileOnGridD", "TileOnGridDict"] +@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridSplitd` instead.") class SplitOnGridd(MapTransform): """ Split the image into patches based on the provided grid shape. @@ -55,6 +57,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d +@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridPatchd` or `monai.transforms.RandGridPatchd` instead.") class TileOnGridd(Randomizable, MapTransform): """ Tile the 2D image into patches on a grid and maintain a subset of it. From c2afba0803fd12bd30f87ee1f3b2cb55e604188c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 14:19:35 +0000 Subject: [PATCH 09/49] Fix formatting Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 11 ++++++----- monai/transforms/spatial/dictionary.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 44290a0a0a..44e97f238b 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -12,7 +12,6 @@ A collection of "vanilla" transforms for spatial operations https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design """ -from pstats import SortKey import warnings from copy import deepcopy from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union @@ -77,7 +76,8 @@ "GridDistortion", "GridSplit", "GridPatch", - "RandGridPatch" "Resize", + "RandGridPatch", + "Resize", "Rotate", "Zoom", "Rotate90", @@ -2658,9 +2658,10 @@ def __init__( self.overlap = overlap self.max_num_patches = max_num_patches self.num_pacthes = max_num_patches + self.sort_key: Optional[Callable] if isinstance(sort_key, str): if sort_key == "random": - self.sort_key = np.random.random() + self.sort_key = np.random.random if sort_key == "min": self.sort_key = lambda x: x[0].sum() if sort_key == "max": @@ -2673,7 +2674,7 @@ def __init__( def __call__(self, array: NdarrayOrTensor): # create the patch iterator which sweeps the image row-by-row patch_iterator = iter_patch( - array, + array, # type: ignore patch_size=self.patch_size, start_pos=self.start_pos, overlap=self.overlap, @@ -2692,7 +2693,7 @@ def __call__(self, array: NdarrayOrTensor): return output -class RandGridPatch(RandomizableTransform, GridPatch): +class RandGridPatch(GridPatch, RandomizableTransform): """ Return all (or a subset of) the patches sweeping the entire image with a random starting position. diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 7e6b5b232d..aa233862b8 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -17,7 +17,7 @@ from copy import deepcopy from enum import Enum -from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Generator, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -2228,7 +2228,7 @@ def __init__( ) self.max_num_patches = max_num_patches - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashable, NdarrayOrTensor]]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): From a8e68ccbf4103c1c27449ced0b3149c55400bc19 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 15:03:52 +0000 Subject: [PATCH 10/49] Update WSIReader value error message Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/wsi_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py index b29ac3848f..48b82a9cc8 100644 --- a/monai/data/wsi_reader.py +++ b/monai/data/wsi_reader.py @@ -266,7 +266,7 @@ def __init__(self, backend="cucim", level: int = 0, **kwargs): elif self.backend == "openslide": self.reader = OpenSlideWSIReader(level=level, **kwargs) else: - raise ValueError("The supported backends are: cucim") + raise ValueError(f"The supported backends are cucim and openslide, {self.backend} was given.") self.supported_suffixes = self.reader.supported_suffixes def get_level_count(self, wsi) -> int: From 13fbd8d7fd402c3ac0de6ea53c83b9f7171af3c1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 15:13:03 +0000 Subject: [PATCH 11/49] Implement RandGridPatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/wsi_reader.py | 2 +- monai/transforms/spatial/dictionary.py | 53 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py index 48b82a9cc8..fc9b475567 100644 --- a/monai/data/wsi_reader.py +++ b/monai/data/wsi_reader.py @@ -266,7 +266,7 @@ def __init__(self, backend="cucim", level: int = 0, **kwargs): elif self.backend == "openslide": self.reader = OpenSlideWSIReader(level=level, **kwargs) else: - raise ValueError(f"The supported backends are cucim and openslide, {self.backend} was given.") + raise ValueError(f"The supported backends are cucim and openslide, '{self.backend}' was given.") self.supported_suffixes = self.reader.supported_suffixes def get_level_count(self, wsi) -> int: diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index aa233862b8..b3d55bfcf9 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -43,6 +43,7 @@ RandAxisFlip, RandFlip, RandGridDistortion, + RandGridPatch, RandRotate, RandZoom, ResampleToMatch, @@ -138,6 +139,9 @@ "GridPatchd", "GridPatchD", "GridPatchDict", + "RandGridPatchd", + "RandGridPatchD", + "RandGridPatchDict", ] GridSampleModeSequence = Union[Sequence[Union[GridSampleMode, str]], GridSampleMode, str] @@ -2245,6 +2249,54 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, yield new_dict +class RandGridPatchd(MapTransform, RandomizableTransform): + """ """ + + backend = GridSplit.backend + + def __init__( + self, + keys: KeysCollection, + patch_size: Sequence[int], + min_start_pos: Optional[Union[Sequence[int], int]] = None, + max_start_pos: Optional[Union[Sequence[int], int]] = None, + max_num_patches: Optional[int] = None, + overlap: float = 0.0, + sort_key: Optional[Union[Callable, str]] = None, + pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_opts: Optional[Dict] = None, + allow_missing_keys: bool = False, + ): + super().__init__(keys, allow_missing_keys) + self.patcher = RandGridPatch( + patch_size=patch_size, + min_start_pos=min_start_pos, + max_start_pos=max_start_pos, + max_num_patches=max_num_patches, + overlap=overlap, + sort_key=sort_key, + pad_mode=pad_mode, + pad_opts=pad_opts, + ) + self.max_num_patches = max_num_patches + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: + d = dict(data) + original_spatial_shape = d[first(self.keys)].shape[1:] + for patch in zip(*[self.patcher(d[key]) for key in self.keys]): + new_dict = {k: v[0] for k, v in zip(self.keys, patch)} + # fill in the extra keys with unmodified data + for k in set(d.keys()).difference(set(self.keys)): + new_dict[k] = deepcopy(d[k]) + # fill additional metadata + new_dict["original_spatial_shape"] = original_spatial_shape + # use the coordinate of the first item + location = patch[0][1] + new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} + new_dict["start_pos"] = self.patcher.start_pos + yield new_dict + + SpatialResampleD = SpatialResampleDict = SpatialResampled ResampleToMatchD = ResampleToMatchDict = ResampleToMatchd SpacingD = SpacingDict = Spacingd @@ -2267,3 +2319,4 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, RandZoomD = RandZoomDict = RandZoomd GridSplitD = GridSplitDict = GridSplitd GridPatchD = GridPatchDict = GridPatchd +RandGridPatchD = RandGridPatchDict = RandGridPatchd From 4a1b8dae496740d304da3eb174aba9435a0f3dcc Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 15:14:06 +0000 Subject: [PATCH 12/49] Update init Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 6b891ce34b..f39f314e90 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -371,6 +371,9 @@ RandGridDistortiond, RandGridDistortionD, RandGridDistortionDict, + RandGridPatchd, + RandGridPatchD, + RandGridPatchDict, RandRotate90d, RandRotate90D, RandRotate90Dict, From 0b6fe585ce87afac1d79ea6178651d52592a331f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 15:48:01 +0000 Subject: [PATCH 13/49] Add unittests for GridPatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 28 ++++++++- tests/test_grid_patchd.py | 87 ++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 tests/test_grid_patchd.py diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index b3d55bfcf9..098d15c558 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2204,9 +2204,31 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashab class GridPatchd(MapTransform): - """ """ + """ + Generate all (or a subset of) the patches sweeping the entire image - backend = GridSplit.backend + Args: + keys: keys of the corresponding items to be transformed. + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: 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. + max_num_patches: maximum number of patches to return. No limit by default. + overlap: amount of overlap between patches in each dimension. Default to 0.0. + sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", + which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions + and channels. Also "random" creates a random order of patches. + By default no sorting is being done and patches are returned in a row-major order. + pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + One of the listed string values or a user supplied function. Defaults to ``"wrap"``. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html + pad_opts: padding options, see `numpy.pad` + allow_missing_keys: don't raise exception if key is missing. + + """ + + backend = GridPatch.backend def __init__( self, @@ -2252,7 +2274,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, class RandGridPatchd(MapTransform, RandomizableTransform): """ """ - backend = GridSplit.backend + backend = RandGridPatch.backend def __init__( self, diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py new file mode 100644 index 0000000000..370a3c7eeb --- /dev/null +++ b/tests/test_grid_patchd.py @@ -0,0 +1,87 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.spatial.dictionary import GridPatchd +from tests.utils import TEST_NDARRAYS, assert_allclose + +A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) +A11 = A[:, :2, :2] +A12 = A[:, :2, 2:] +A21 = A[:, 2:, :2] +A22 = A[:, 2:, 2:] + +TEST_CASE_0 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "max_num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "max_num_patches": 5}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, {"image": A}, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, {"image": A}, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, {"image": A}, [A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "max_num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "max_num_patches": 4, "sort_key": "min"}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "max_num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_11 = [ + {"patch_size": (3, 3), "max_num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"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), + "start_pos": (-2, -2), + "max_num_patches": 2, + }, + {"image": A}, + [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], +] + + +TEST_SINGLE = [] +for p in TEST_NDARRAYS: + TEST_SINGLE.append([p, *TEST_CASE_0]) + TEST_SINGLE.append([p, *TEST_CASE_1]) + TEST_SINGLE.append([p, *TEST_CASE_2]) + TEST_SINGLE.append([p, *TEST_CASE_3]) + TEST_SINGLE.append([p, *TEST_CASE_4]) + TEST_SINGLE.append([p, *TEST_CASE_5]) + TEST_SINGLE.append([p, *TEST_CASE_6]) + TEST_SINGLE.append([p, *TEST_CASE_7]) + TEST_SINGLE.append([p, *TEST_CASE_8]) + TEST_SINGLE.append([p, *TEST_CASE_9]) + TEST_SINGLE.append([p, *TEST_CASE_10]) + TEST_SINGLE.append([p, *TEST_CASE_11]) + TEST_SINGLE.append([p, *TEST_CASE_12]) + + +class TestSlidingPatch(unittest.TestCase): + @parameterized.expand(TEST_SINGLE) + def test_split_patch_single_call(self, in_type, input_parameters, image_dict, expected): + image_key = "image" + input_dict = {} + for k, v in image_dict.items(): + input_dict[k] = v + if k == image_key: + input_dict[k] = in_type(v) + splitter = GridPatchd(keys=image_key, **input_parameters) + output = list(splitter(input_dict)) + self.assertEqual(len(output), len(expected)) + for output_patch, expected_patch in zip(output, expected): + assert_allclose(output_patch[image_key], expected_patch, type_test=False) + + +if __name__ == "__main__": + unittest.main() From 4105006729ff6c2074daf8e8f41cf45ff66176ca Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 17:30:42 +0000 Subject: [PATCH 14/49] Add unittests for RandGridPatch Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 9 ++- tests/test_rand_grid_patch.py | 91 +++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 tests/test_rand_grid_patch.py diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 44e97f238b..c9e1444818 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2744,7 +2744,14 @@ def __init__( def __call__(self, array: NdarrayOrTensor): if self.min_start_pos is None: min_start_pos = (0,) * (len(self.patch_size) - 1) + else: + min_start_pos = ensure_tuple_rep(self.min_start_pos, len(self.patch_size[1:])) if self.max_start_pos is None: max_start_pos = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size[1:])) - self.start_pos = tuple(self.R.randint(low=low, high=high) for low, high in zip(min_start_pos, max_start_pos)) + else: + max_start_pos = ensure_tuple_rep(self.max_start_pos, len(self.patch_size[1:])) + + self.start_pos = (0,) + tuple( + self.R.randint(low=low, high=high + 1) for low, high in zip(min_start_pos, max_start_pos) + ) return super().__call__(array) diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py new file mode 100644 index 0000000000..eeb2a82aa1 --- /dev/null +++ b/tests/test_rand_grid_patch.py @@ -0,0 +1,91 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.spatial.array import RandGridPatch +from tests.utils import TEST_NDARRAYS, assert_allclose + +A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) +A11 = A[:, :2, :2] +A12 = A[:, :2, 2:] +A21 = A[:, 2:, :2] +A22 = A[:, 2:, 2:] + +TEST_CASE_0 = [ + { + "patch_size": (2, 2), + "min_start_pos": 0, + "max_start_pos": 0, + }, + A, + [A11, A12, A21, A22], +] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_2 = [ + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, + A, + [A11, A12, A21, A22], +] +TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "min_start_pos": 2, "max_start_pos": 2}, A, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, A, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, A, [A22]] +TEST_CASE_8 = [ + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "max_num_patches": 1, "sort_key": "max"}, + A, + [A[:, 1:3, 1:3]], +] +TEST_CASE_9 = [ + { + "patch_size": (3, 3), + "min_start_pos": -3, + "max_start_pos": -1, + "sort_key": "min", + "max_num_patches": 1, + "pad_opts": {"constant_values": 255}, + }, + A, + [np.pad(A[:, :2, 1:], ((0, 0), (1, 0), (0, 0)), mode="constant", constant_values=255)], +] + +TEST_SINGLE = [] +for p in TEST_NDARRAYS: + TEST_SINGLE.append([p, *TEST_CASE_0]) + TEST_SINGLE.append([p, *TEST_CASE_1]) + TEST_SINGLE.append([p, *TEST_CASE_2]) + TEST_SINGLE.append([p, *TEST_CASE_3]) + TEST_SINGLE.append([p, *TEST_CASE_4]) + TEST_SINGLE.append([p, *TEST_CASE_5]) + TEST_SINGLE.append([p, *TEST_CASE_6]) + TEST_SINGLE.append([p, *TEST_CASE_7]) + TEST_SINGLE.append([p, *TEST_CASE_8]) + TEST_SINGLE.append([p, *TEST_CASE_9]) + + +class TestSlidingPatch(unittest.TestCase): + @parameterized.expand(TEST_SINGLE) + def test_split_patch_single_call(self, in_type, input_parameters, image, expected): + input_image = in_type(image) + splitter = RandGridPatch(**input_parameters) + splitter.R = np.random.RandomState(1234) + output = list(splitter(input_image)) + self.assertEqual(len(output), len(expected)) + for output_patch, expected_patch in zip(output, expected): + assert_allclose(output_patch[0], expected_patch, type_test=False) + + +if __name__ == "__main__": + unittest.main() From b7e22595a0b5790f60df6df6032aa47b9d55f3f3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 17:59:49 +0000 Subject: [PATCH 15/49] Add unittests for RandGridPatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 11 ++- tests/test_rand_grid_patch.py | 5 +- tests/test_rand_grid_patchd.py | 99 ++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 tests/test_rand_grid_patchd.py diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 098d15c558..b4b4dff232 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2271,7 +2271,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, yield new_dict -class RandGridPatchd(MapTransform, RandomizableTransform): +class RandGridPatchd(RandomizableTransform, MapTransform): """ """ backend = RandGridPatch.backend @@ -2289,7 +2289,7 @@ def __init__( pad_opts: Optional[Dict] = None, allow_missing_keys: bool = False, ): - super().__init__(keys, allow_missing_keys) + MapTransform.__init__(self, keys, allow_missing_keys) self.patcher = RandGridPatch( patch_size=patch_size, min_start_pos=min_start_pos, @@ -2302,6 +2302,13 @@ def __init__( ) self.max_num_patches = max_num_patches + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "RandGridPatchd": + self.patcher.set_random_state(seed, state) + super().set_random_state(seed, state) + return self + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index eeb2a82aa1..186c9ddc56 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -15,8 +15,11 @@ from parameterized import parameterized from monai.transforms.spatial.array import RandGridPatch +from monai.utils import set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose +set_determinism(1234) + A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) A11 = A[:, :2, :2] A12 = A[:, :2, 2:] @@ -80,7 +83,7 @@ class TestSlidingPatch(unittest.TestCase): def test_split_patch_single_call(self, in_type, input_parameters, image, expected): input_image = in_type(image) splitter = RandGridPatch(**input_parameters) - splitter.R = np.random.RandomState(1234) + splitter.set_random_state(state=np.random.RandomState(1234)) output = list(splitter(input_image)) self.assertEqual(len(output), len(expected)) for output_patch, expected_patch in zip(output, expected): diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py new file mode 100644 index 0000000000..ab5e121fef --- /dev/null +++ b/tests/test_rand_grid_patchd.py @@ -0,0 +1,99 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.spatial.dictionary import RandGridPatchd +from monai.utils import set_determinism +from tests.utils import TEST_NDARRAYS, assert_allclose + +set_determinism(1234) + +A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) +A11 = A[:, :2, :2] +A12 = A[:, :2, 2:] +A21 = A[:, 2:, :2] +A22 = A[:, 2:, 2:] + +TEST_CASE_0 = [ + { + "patch_size": (2, 2), + "min_start_pos": 0, + "max_start_pos": 0, + }, + {"image": A}, + [A11, A12, A21, A22], +] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_2 = [ + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, + {"image": A}, + [A11, A12, A21, A22], +] +TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "min_start_pos": 2, "max_start_pos": 2}, {"image": A}, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, {"image": A}, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, {"image": A}, [A22]] +TEST_CASE_8 = [ + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "max_num_patches": 1, "sort_key": "max"}, + {"image": A}, + [A[:, 1:3, 1:3]], +] +TEST_CASE_9 = [ + { + "patch_size": (3, 3), + "min_start_pos": -3, + "max_start_pos": -1, + "sort_key": "min", + "max_num_patches": 1, + "pad_opts": {"constant_values": 255}, + }, + {"image": A}, + [np.pad(A[:, :2, 1:], ((0, 0), (1, 0), (0, 0)), mode="constant", constant_values=255)], +] + +TEST_SINGLE = [] +for p in TEST_NDARRAYS: + TEST_SINGLE.append([p, *TEST_CASE_0]) + TEST_SINGLE.append([p, *TEST_CASE_1]) + TEST_SINGLE.append([p, *TEST_CASE_2]) + TEST_SINGLE.append([p, *TEST_CASE_3]) + TEST_SINGLE.append([p, *TEST_CASE_4]) + TEST_SINGLE.append([p, *TEST_CASE_5]) + TEST_SINGLE.append([p, *TEST_CASE_6]) + TEST_SINGLE.append([p, *TEST_CASE_7]) + TEST_SINGLE.append([p, *TEST_CASE_8]) + TEST_SINGLE.append([p, *TEST_CASE_9]) + + +class TestSlidingPatch(unittest.TestCase): + @parameterized.expand(TEST_SINGLE) + def test_split_patch_single_call(self, in_type, input_parameters, image_dict, expected): + image_key = "image" + input_dict = {} + for k, v in image_dict.items(): + input_dict[k] = v + if k == image_key: + input_dict[k] = in_type(v) + splitter = RandGridPatchd(keys=image_key, **input_parameters) + splitter.set_random_state(state=np.random.RandomState(1234)) + output = list(splitter(input_dict)) + self.assertEqual(len(output), len(expected)) + for output_patch, expected_patch in zip(output, expected): + assert_allclose(output_patch[image_key], expected_patch, type_test=False) + + +if __name__ == "__main__": + unittest.main() From 173f6169239da3b8deb001b91a288454c4772b98 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 18:10:24 +0000 Subject: [PATCH 16/49] Gen to list Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/utils.py | 65 +++++++++++++++++++++++--- monai/transforms/spatial/dictionary.py | 12 +++-- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index ca91006817..d42aac9708 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -123,7 +123,10 @@ def get_random_patch( def iter_patch_slices( - dims: Sequence[int], patch_size: Union[Sequence[int], int], start_pos: Sequence[int] = () + image_size: Sequence[int], + patch_size: Union[Sequence[int], int], + start_pos: Sequence[int] = (), + overlap: float = 0.0, ) -> Generator[Tuple[slice, ...], None, None]: """ Yield successive tuples of slices defining patches of size `patch_size` from an array of dimensions `dims`. The @@ -131,21 +134,25 @@ def iter_patch_slices( patch is chosen in a contiguous grid using a first dimension as least significant ordering. Args: - dims: dimensions of array to iterate over + image_size: dimensions of array to iterate over patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: starting position in the array, default is 0 for each dimension + overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. Yields: Tuples of slice objects defining each patch """ # ensure patchSize and startPos are the right length - ndim = len(dims) - patch_size_ = get_valid_patch_size(dims, patch_size) + ndim = len(image_size) + patch_size_ = get_valid_patch_size(image_size, patch_size) start_pos = ensure_tuple_size(start_pos, ndim) + # calculate steps, which depends on the amount of overlap + steps = tuple(round(p * (1.0 - overlap)) for p in patch_size_) + # collect the ranges to step over each dimension - ranges = tuple(starmap(range, zip(start_pos, dims, patch_size_))) + ranges = tuple(starmap(range, zip(start_pos, image_size, steps))) # choose patches by applying product to the ranges for position in product(*ranges): @@ -196,6 +203,7 @@ def iter_patch( arr: np.ndarray, patch_size: Union[Sequence[int], int] = 0, start_pos: Sequence[int] = (), + overlap: float = 0.0, copy_back: bool = True, mode: Union[NumpyPadMode, str] = NumpyPadMode.WRAP, **pad_opts: Dict, @@ -209,6 +217,7 @@ def iter_patch( arr: array to iterate over patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: starting position in the array, default is 0 for each dimension + overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. copy_back: if True data from the yielded patches is copied back to `arr` once the generator completes mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} @@ -243,7 +252,7 @@ def iter_patch( # patches which are only in the padded regions iter_size = tuple(s + p for s, p in zip(arr.shape, patch_size_)) - for slices in iter_patch_slices(iter_size, patch_size_, start_pos_padded): + for slices in iter_patch_slices(iter_size, patch_size_, start_pos_padded, overlap): # compensate original image padding coords_no_pad = tuple((coord.start - p, coord.stop - p) for coord, p in zip(slices, patch_size_)) yield arrpad[slices], np.asarray(coords_no_pad) # data and coords (in numpy; works with torch loader) @@ -254,6 +263,50 @@ def iter_patch( arr[...] = arrpad[slices] +def iter_wsi_patch_location( + image_size: Sequence[int], + patch_size: Union[Sequence[int], int], + start_pos: Sequence[int] = (), + overlap: float = 0.0, + downsample: float = 1.0, + padded: bool = False, +): + """ + Yield successive tuple of locations defining a patch of size `patch_size` from an image of size `image_size`, + with the relative overalpping of `overlap`. The patch is in the resolution level related to `downsample` ratio. + The iteration starts from position `start_pos` in the whole slide image, or starting at the origin if this isn't + provided. + + Args: + image_size: dimensions of image + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: starting position in the image, default is 0 for each dimension + overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. + downsample: the downsample ratio + padded: if the image is padded so the patches can go beyond the borders. Defaults to False. + Note that the padding depends on the functionality of the underlying whole slide imaging reader, + and is not guranteed for all images. + + Yields: + Tuple of patch location + """ + ndim = len(image_size) + patch_size = get_valid_patch_size(image_size, patch_size) + start_pos = ensure_tuple_size(start_pos, ndim) + + # get the patch size at level=0 + patch_size_ = tuple(p * downsample for p in patch_size) + + # calculate steps, which depends on the amount of overlap + steps = tuple(round(p * (1.0 - overlap)) for p in patch_size_) + + # calculate the last starting location (depending on the padding) + end_pos = image_size if padded else tuple(s - round(p) + 1 for s, p in zip(image_size, patch_size_)) + + # evaluate the starting locations for patches + return product(*tuple(starmap(range, zip(start_pos, end_pos, steps)))) + + def get_valid_patch_size(image_size: Sequence[int], patch_size: Union[Sequence[int], int]) -> Tuple[int, ...]: """ Given an image of dimensions `image_size`, return a patch size tuple taking the dimension from `patch_size` if this is diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index b4b4dff232..861059bc50 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2254,9 +2254,10 @@ def __init__( ) self.max_num_patches = max_num_patches - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] + dict_list = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2268,7 +2269,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, location = patch[0][1] new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} new_dict["start_pos"] = self.patcher.start_pos - yield new_dict + dict_list.append(new_dict) + return dict_list class RandGridPatchd(RandomizableTransform, MapTransform): @@ -2309,9 +2311,10 @@ def set_random_state( super().set_random_state(seed, state) return self - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] + dict_list = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2323,7 +2326,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, location = patch[0][1] new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} new_dict["start_pos"] = self.patcher.start_pos - yield new_dict + dict_list.append(new_dict) + return dict_list SpatialResampleD = SpatialResampleDict = SpatialResampled From 3600bf02145120ecdb76f29c662a1a9d93dfcb8c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 18:11:42 +0000 Subject: [PATCH 17/49] Fix List Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 861059bc50..70a2544a5f 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2254,7 +2254,7 @@ def __init__( ) self.max_num_patches = max_num_patches - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] dict_list = [] @@ -2311,7 +2311,7 @@ def set_random_state( super().set_random_state(seed, state) return self - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] dict_list = [] From 55cf543e08a4a1689710fddda2f91865c0bc2549 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 18:50:22 +0000 Subject: [PATCH 18/49] Convert lambda to method Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index c9e1444818..19cd161a1d 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2663,14 +2663,22 @@ def __init__( if sort_key == "random": self.sort_key = np.random.random if sort_key == "min": - self.sort_key = lambda x: x[0].sum() + self.sort_key = self._get_array_sum if sort_key == "max": - self.sort_key = lambda x: -x[0].sum() + self.sort_key = self._get_negative_array_sum else: ValueError(f'sort_key should be either "min", "max", or "random", "{sort_key}" was given.') else: self.sort_key = sort_key + @staticmethod + def _get_array_sum(x): + return x[0].sum() + + @staticmethod + def _get_negative_array_sum(x): + return -x[0].sum() + def __call__(self, array: NdarrayOrTensor): # create the patch iterator which sweeps the image row-by-row patch_iterator = iter_patch( From 65a3a153d650e24c5ea1374be60a1b4bedcd3246 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 18:55:51 +0000 Subject: [PATCH 19/49] change array to patch Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 19cd161a1d..7def069f73 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2663,20 +2663,20 @@ def __init__( if sort_key == "random": self.sort_key = np.random.random if sort_key == "min": - self.sort_key = self._get_array_sum + self.sort_key = self._get_patch_sum if sort_key == "max": - self.sort_key = self._get_negative_array_sum + self.sort_key = self._get_negative_patch_sum else: ValueError(f'sort_key should be either "min", "max", or "random", "{sort_key}" was given.') else: self.sort_key = sort_key @staticmethod - def _get_array_sum(x): + def _get_patch_sum(x): return x[0].sum() @staticmethod - def _get_negative_array_sum(x): + def _get_negative_patch_sum(x): return -x[0].sum() def __call__(self, array: NdarrayOrTensor): From f948afd5d519da8fa4e28983a56ee8e716901fa3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 19:38:20 +0000 Subject: [PATCH 20/49] Remove first patch_size dim Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 70a2544a5f..eb8db34af3 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2324,7 +2324,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict["original_spatial_shape"] = original_spatial_shape # use the coordinate of the first item location = patch[0][1] - new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} + new_dict["patch"] = {"location": location, "size": self.patcher.patch_size[1:]} new_dict["start_pos"] = self.patcher.start_pos dict_list.append(new_dict) return dict_list From 60deeeac965eab1d0727eda2da6b85be2408f3e6 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 23 May 2022 23:21:36 +0000 Subject: [PATCH 21/49] Remove first patch size dim and return gen Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 22 +++++++++++----------- monai/transforms/spatial/dictionary.py | 12 ++++-------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 7def069f73..19e47d09ce 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2651,8 +2651,8 @@ def __init__( pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, ): - self.patch_size = (None,) + ensure_tuple(patch_size) # expand to have the channel dim - self.start_pos = (0,) + ensure_tuple(start_pos) + self.patch_size = ensure_tuple(patch_size) + self.start_pos = ensure_tuple(start_pos) self.pad_mode: NumpyPadMode = look_up_option(pad_mode, NumpyPadMode) self.pad_opts = {} if pad_opts is None else pad_opts self.overlap = overlap @@ -2683,8 +2683,8 @@ def __call__(self, array: NdarrayOrTensor): # create the patch iterator which sweeps the image row-by-row patch_iterator = iter_patch( array, # type: ignore - patch_size=self.patch_size, - start_pos=self.start_pos, + patch_size=(None,) + self.patch_size, # expand to have the channel dim + start_pos=(0,) + self.start_pos, # expand to have the channel dim overlap=self.overlap, copy_back=False, mode=self.pad_mode, @@ -2735,7 +2735,7 @@ def __init__( overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, - **pad_opts: Dict, + pad_opts: Optional[Dict] = None, ): super().__init__( patch_size=patch_size, @@ -2744,22 +2744,22 @@ def __init__( overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, - **pad_opts, + pad_opts=pad_opts, ) self.min_start_pos = min_start_pos self.max_start_pos = max_start_pos def __call__(self, array: NdarrayOrTensor): if self.min_start_pos is None: - min_start_pos = (0,) * (len(self.patch_size) - 1) + min_start_pos = (0,) * len(self.patch_size) else: - min_start_pos = ensure_tuple_rep(self.min_start_pos, len(self.patch_size[1:])) + min_start_pos = ensure_tuple_rep(self.min_start_pos, len(self.patch_size)) if self.max_start_pos is None: - max_start_pos = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size[1:])) + max_start_pos = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size)) else: - max_start_pos = ensure_tuple_rep(self.max_start_pos, len(self.patch_size[1:])) + max_start_pos = ensure_tuple_rep(self.max_start_pos, len(self.patch_size)) - self.start_pos = (0,) + tuple( + self.start_pos = tuple( self.R.randint(low=low, high=high + 1) for low, high in zip(min_start_pos, max_start_pos) ) return super().__call__(array) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index eb8db34af3..85df15031f 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2254,10 +2254,9 @@ def __init__( ) self.max_num_patches = max_num_patches - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] - dict_list = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2269,8 +2268,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: location = patch[0][1] new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} new_dict["start_pos"] = self.patcher.start_pos - dict_list.append(new_dict) - return dict_list + yield new_dict class RandGridPatchd(RandomizableTransform, MapTransform): @@ -2311,10 +2309,9 @@ def set_random_state( super().set_random_state(seed, state) return self - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] - dict_list = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2326,8 +2323,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: location = patch[0][1] new_dict["patch"] = {"location": location, "size": self.patcher.patch_size[1:]} new_dict["start_pos"] = self.patcher.start_pos - dict_list.append(new_dict) - return dict_list + yield new_dict SpatialResampleD = SpatialResampleDict = SpatialResampled From 69c6e7982230cd4d9d15fdfd12422be51c402018 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 24 May 2022 00:02:50 +0000 Subject: [PATCH 22/49] Separate overlap for each dimension Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/apps/pathology/transforms/spatial/array.py | 1 + monai/transforms/spatial/array.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/monai/apps/pathology/transforms/spatial/array.py b/monai/apps/pathology/transforms/spatial/array.py index 4bc2eb1f3b..562a94952e 100644 --- a/monai/apps/pathology/transforms/spatial/array.py +++ b/monai/apps/pathology/transforms/spatial/array.py @@ -107,6 +107,7 @@ def get_params(self, image_size): return patch_size, steps + @deprecated(since="0.9", msg_suffix="use `monai.transforms.GridPatch` or `monai.transforms.RandGridPatch` instead.") class TileOnGrid(Randomizable, Transform): """ diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 19e47d09ce..d8b18cd3f4 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2625,7 +2625,8 @@ class GridPatch(Transform): start_pos: 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. max_num_patches: maximum number of patches to return. No limit by default. - overlap: amount of overlap between patches in each dimension. Default to 0.0. + 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions @@ -2646,7 +2647,7 @@ def __init__( patch_size: Sequence[int], start_pos: Sequence[int] = (), max_num_patches: Optional[int] = None, - overlap: float = 0.0, + overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, @@ -2710,7 +2711,8 @@ class RandGridPatch(GridPatch, RandomizableTransform): start_pos: 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. max_num_patches: maximum number of patches to return. No limit by default. - overlap: amount of overlap between patches in each dimension. Default to 0.0. + 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions @@ -2732,7 +2734,7 @@ def __init__( min_start_pos: Optional[Union[Sequence[int], int]] = None, max_start_pos: Optional[Union[Sequence[int], int]] = None, max_num_patches: Optional[int] = None, - overlap: float = 0.0, + overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, From 2b506a5c67d4ab22c794f02c7259acf239c215e3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 24 May 2022 00:04:41 +0000 Subject: [PATCH 23/49] Remove trailing comma Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_grid_patch.py | 6 +----- tests/test_grid_patchd.py | 6 +----- tests/test_rand_grid_patch.py | 6 +----- tests/test_rand_grid_patchd.py | 6 +----- 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index dcdfcd480e..7a839e7f86 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -40,11 +40,7 @@ [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), - "start_pos": (-2, -2), - "max_num_patches": 2, - }, + {"patch_size": (3, 3), "start_pos": (-2, -2), "max_num_patches": 2}, A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 370a3c7eeb..b3ff95f773 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -40,11 +40,7 @@ [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), - "start_pos": (-2, -2), - "max_num_patches": 2, - }, + {"patch_size": (3, 3), "start_pos": (-2, -2), "max_num_patches": 2}, {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index 186c9ddc56..a0900ea4b2 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -27,11 +27,7 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [ - { - "patch_size": (2, 2), - "min_start_pos": 0, - "max_start_pos": 0, - }, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22], ] diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index ab5e121fef..72c2eb43ea 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -27,11 +27,7 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [ - { - "patch_size": (2, 2), - "min_start_pos": 0, - "max_start_pos": 0, - }, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22], ] From 35c747e80c9683ffa0588d8e5113359afa031796 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 24 May 2022 00:56:49 +0000 Subject: [PATCH 24/49] Add num_patches Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 2 +- monai/transforms/spatial/dictionary.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index d8b18cd3f4..95f39b009c 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2698,7 +2698,7 @@ def __call__(self, array: NdarrayOrTensor): output = list(patch_iterator) # keep up to max_num_patches output = output[: self.max_num_patches] - self.num_pacthes = len(output) + self.num_patches = len(output) return output diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 85df15031f..c786dc5e84 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2266,7 +2266,11 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, new_dict["original_spatial_shape"] = original_spatial_shape # use the coordinate of the first item location = patch[0][1] - new_dict["patch"] = {"location": location, "size": self.patcher.patch_size} + new_dict["patch"] = { + "location": location, + "size": self.patcher.patch_size, + "num_patches": self.patcher.num_patches, + } new_dict["start_pos"] = self.patcher.start_pos yield new_dict @@ -2321,7 +2325,11 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, new_dict["original_spatial_shape"] = original_spatial_shape # use the coordinate of the first item location = patch[0][1] - new_dict["patch"] = {"location": location, "size": self.patcher.patch_size[1:]} + new_dict["patch"] = { + "location": location, + "size": self.patcher.patch_size, + "num_patches": self.patcher.num_patches, + } new_dict["start_pos"] = self.patcher.start_pos yield new_dict From 770fa113f00c06504ebf59a58f36f43632d83218 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 24 May 2022 01:01:12 +0000 Subject: [PATCH 25/49] Remove trailing comma Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_rand_grid_patch.py | 6 +----- tests/test_rand_grid_patchd.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index a0900ea4b2..1579103027 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -26,11 +26,7 @@ A21 = A[:, 2:, :2] A22 = A[:, 2:, 2:] -TEST_CASE_0 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, - A, - [A11, A12, A21, A22], -] +TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, A, [A11, A12, A21]] TEST_CASE_2 = [ {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index 72c2eb43ea..0f4008c12f 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -26,11 +26,7 @@ A21 = A[:, 2:, :2] A22 = A[:, 2:, 2:] -TEST_CASE_0 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, - {"image": A}, - [A11, A12, A21, A22], -] +TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, {"image": A}, [A11, A12, A21]] TEST_CASE_2 = [ {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, From fe04994fe6ba39a4f8effbc90dc65a2544915fdf Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 00:07:38 +0000 Subject: [PATCH 26/49] Add seed and update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 3 +++ monai/transforms/spatial/dictionary.py | 29 +++++++++++++++++++++++++- tests/test_rand_grid_patch.py | 6 +----- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 95f39b009c..ad3813c09a 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2723,6 +2723,7 @@ class RandGridPatch(GridPatch, RandomizableTransform): One of the listed string values or a user supplied function. Defaults to ``"wrap"``. See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html pad_opts: padding options, see `numpy.pad` + seed: random seed to generate offsets """ @@ -2738,6 +2739,7 @@ def __init__( sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, + seed: int = 0, ): super().__init__( patch_size=patch_size, @@ -2748,6 +2750,7 @@ def __init__( pad_mode=pad_mode, pad_opts=pad_opts, ) + self.set_random_state(seed) self.min_start_pos = min_start_pos self.max_start_pos = max_start_pos diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index c786dc5e84..042429faf6 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2276,7 +2276,32 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, class RandGridPatchd(RandomizableTransform, MapTransform): - """ """ + """ + Return all (or a subset of) the patches sweeping the entire image with a random starting position. + + Args: + keys: keys of the corresponding items to be transformed. + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: 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. + max_num_patches: maximum number of patches to return. No limit by default. + 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", + which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions + and channels. Also "random" creates a random order of patches. + By default no sorting is being done and patches are returned in a row-major order. + pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, + ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} + One of the listed string values or a user supplied function. Defaults to ``"wrap"``. + See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html + pad_opts: padding options, see `numpy.pad` + seed: random seed to generate offsets + allow_missing_keys: don't raise exception if key is missing. + + + """ backend = RandGridPatch.backend @@ -2291,6 +2316,7 @@ def __init__( sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, + seed: int = 0, allow_missing_keys: bool = False, ): MapTransform.__init__(self, keys, allow_missing_keys) @@ -2303,6 +2329,7 @@ def __init__( sort_key=sort_key, pad_mode=pad_mode, pad_opts=pad_opts, + seed=seed, ) self.max_num_patches = max_num_patches diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index 1579103027..715dd2adda 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -15,11 +15,8 @@ from parameterized import parameterized from monai.transforms.spatial.array import RandGridPatch -from monai.utils import set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose -set_determinism(1234) - A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) A11 = A[:, :2, :2] A12 = A[:, :2, 2:] @@ -74,8 +71,7 @@ class TestSlidingPatch(unittest.TestCase): @parameterized.expand(TEST_SINGLE) def test_split_patch_single_call(self, in_type, input_parameters, image, expected): input_image = in_type(image) - splitter = RandGridPatch(**input_parameters) - splitter.set_random_state(state=np.random.RandomState(1234)) + splitter = RandGridPatch(seed=1234, **input_parameters) output = list(splitter(input_image)) self.assertEqual(len(output), len(expected)) for output_patch, expected_patch in zip(output, expected): From 148243f68a46ba70b046fc2af41acbd32f5ff4e9 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 14:48:19 +0000 Subject: [PATCH 27/49] Add required files form #4239 Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/grid_dataset.py | 1 + monai/data/utils.py | 122 ++++++++++++++++++------------------- 2 files changed, 61 insertions(+), 62 deletions(-) diff --git a/monai/data/grid_dataset.py b/monai/data/grid_dataset.py index 33497b5a68..2e389d9e0b 100644 --- a/monai/data/grid_dataset.py +++ b/monai/data/grid_dataset.py @@ -73,6 +73,7 @@ def __call__(self, array: np.ndarray): array, patch_size=self.patch_size, # type: ignore start_pos=self.start_pos, + overlap=0.0, copy_back=False, mode=self.mode, **self.pad_opts, diff --git a/monai/data/utils.py b/monai/data/utils.py index d42aac9708..e56d8b86e9 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -67,6 +67,7 @@ "get_valid_patch_size", "is_supported_format", "iter_patch", + "iter_patch_position", "iter_patch_slices", "json_hashing", "list_data_collate", @@ -126,36 +127,33 @@ def iter_patch_slices( image_size: Sequence[int], patch_size: Union[Sequence[int], int], start_pos: Sequence[int] = (), - overlap: float = 0.0, + overlap: Union[Sequence[float], float] = 0.0, + padded: bool = True, ) -> Generator[Tuple[slice, ...], None, None]: """ - Yield successive tuples of slices defining patches of size `patch_size` from an array of dimensions `dims`. The - iteration starts from position `start_pos` in the array, or starting at the origin if this isn't provided. Each - patch is chosen in a contiguous grid using a first dimension as least significant ordering. + Yield successive tuples of slices defining patches of size `patch_size` from an array of dimensions `image_size`. + The iteration starts from position `start_pos` in the array, or starting at the origin if this isn't provided. Each + patch is chosen in a contiguous grid using a rwo-major ordering. Args: image_size: dimensions of array to iterate over patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: starting position in the array, default is 0 for each dimension - overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. + 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. + padded: if the image is padded so the patches can go beyond the borders. Defaults to False. Yields: Tuples of slice objects defining each patch """ - # ensure patchSize and startPos are the right length - ndim = len(image_size) + # ensure patch_size has the right length patch_size_ = get_valid_patch_size(image_size, patch_size) - start_pos = ensure_tuple_size(start_pos, ndim) - # calculate steps, which depends on the amount of overlap - steps = tuple(round(p * (1.0 - overlap)) for p in patch_size_) - - # collect the ranges to step over each dimension - ranges = tuple(starmap(range, zip(start_pos, image_size, steps))) - - # choose patches by applying product to the ranges - for position in product(*ranges): + # create slices based on start position of each patch + for position in iter_patch_position( + image_size=image_size, patch_size=patch_size_, start_pos=start_pos, overlap=overlap, padded=padded + ): yield tuple(slice(s, s + p) for s, p in zip(position, patch_size_)) @@ -199,11 +197,54 @@ def dense_patch_slices( return [tuple(slice(s, s + patch_size[d]) for d, s in enumerate(x)) for x in out] +def iter_patch_position( + image_size: Sequence[int], + patch_size: Union[Sequence[int], int], + start_pos: Sequence[int] = (), + overlap: Union[Sequence[float], float] = 0.0, + padded: bool = False, +): + """ + Yield successive tuples of upper left corner of patches of size `patch_size` from an array of dimensions `image_size`. + The iteration starts from position `start_pos` in the array, or starting at the origin if this isn't provided. Each + patch is chosen in a contiguous grid using a rwo-major ordering. + + Args: + image_size: dimensions of array to iterate over + patch_size: size of patches to generate slices for, 0 or None selects whole dimension + start_pos: starting position in the array, default is 0 for each dimension + 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. + padded: if the image is padded so the patches can go beyond the borders. Defaults to False. + + Yields: + Tuples of positions defining the upper left corner of each patch + """ + + # ensure patchSize and startPos are the right length + ndim = len(image_size) + patch_size_ = get_valid_patch_size(image_size, patch_size) + start_pos = ensure_tuple_size(start_pos, ndim) + overlap = ensure_tuple_rep(overlap, ndim) + + # calculate steps, which depends on the amount of overlap + steps = tuple(round(p * (1.0 - o)) for p, o in zip(patch_size_, overlap)) + + # calculate the last starting location (depending on the padding) + end_pos = image_size if padded else tuple(s - round(p) + 1 for s, p in zip(image_size, patch_size_)) + + # collect the ranges to step over each dimension + ranges = starmap(range, zip(start_pos, end_pos, steps)) + + # choose patches by applying product to the ranges + return product(*ranges) + + def iter_patch( arr: np.ndarray, patch_size: Union[Sequence[int], int] = 0, start_pos: Sequence[int] = (), - overlap: float = 0.0, + overlap: Union[Sequence[float], float] = 0.0, copy_back: bool = True, mode: Union[NumpyPadMode, str] = NumpyPadMode.WRAP, **pad_opts: Dict, @@ -217,7 +258,8 @@ def iter_patch( arr: array to iterate over patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: starting position in the array, default is 0 for each dimension - overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. + 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. copy_back: if True data from the yielded patches is copied back to `arr` once the generator completes mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} @@ -263,50 +305,6 @@ def iter_patch( arr[...] = arrpad[slices] -def iter_wsi_patch_location( - image_size: Sequence[int], - patch_size: Union[Sequence[int], int], - start_pos: Sequence[int] = (), - overlap: float = 0.0, - downsample: float = 1.0, - padded: bool = False, -): - """ - Yield successive tuple of locations defining a patch of size `patch_size` from an image of size `image_size`, - with the relative overalpping of `overlap`. The patch is in the resolution level related to `downsample` ratio. - The iteration starts from position `start_pos` in the whole slide image, or starting at the origin if this isn't - provided. - - Args: - image_size: dimensions of image - patch_size: size of patches to generate slices for, 0 or None selects whole dimension - start_pos: starting position in the image, default is 0 for each dimension - overlap: the amount of overlap between patches, which is between 0.0 and 1.0. Defaults to 0.0. - downsample: the downsample ratio - padded: if the image is padded so the patches can go beyond the borders. Defaults to False. - Note that the padding depends on the functionality of the underlying whole slide imaging reader, - and is not guranteed for all images. - - Yields: - Tuple of patch location - """ - ndim = len(image_size) - patch_size = get_valid_patch_size(image_size, patch_size) - start_pos = ensure_tuple_size(start_pos, ndim) - - # get the patch size at level=0 - patch_size_ = tuple(p * downsample for p in patch_size) - - # calculate steps, which depends on the amount of overlap - steps = tuple(round(p * (1.0 - overlap)) for p in patch_size_) - - # calculate the last starting location (depending on the padding) - end_pos = image_size if padded else tuple(s - round(p) + 1 for s, p in zip(image_size, patch_size_)) - - # evaluate the starting locations for patches - return product(*tuple(starmap(range, zip(start_pos, end_pos, steps)))) - - def get_valid_patch_size(image_size: Sequence[int], patch_size: Union[Sequence[int], int]) -> Tuple[int, ...]: """ Given an image of dimensions `image_size`, return a patch size tuple taking the dimension from `patch_size` if this is From d984eb30bad7169ea9ba952d1055799098fd0207 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 18:44:09 +0000 Subject: [PATCH 28/49] Update docs Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 66aa3e39c4..67749a75fe 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -743,6 +743,11 @@ Spatial :members: :special-members: __call__ +`GridPatch` +""""""""""" +.. autoclass:: RandGridPatch + :members: + :special-members: __call__ `GridSplit` """"""""""" @@ -1526,6 +1531,12 @@ Spatial (Dict) :members: :special-members: __call__ +`RandGridPatchd` +"""""""""""" +.. autoclass:: GridPatchd + :members: + :special-members: __call__ + `GridSplitd` """""""""""" .. autoclass:: GridSplitd From 62f542c6f5dd71ff4b2b09d0c6fb32e4510b17ea Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 18:46:59 +0000 Subject: [PATCH 29/49] Update init Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index f39f314e90..18459c1b7b 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -322,6 +322,7 @@ RandDeformGrid, RandFlip, RandGridDistortion, + RandGridPatch, RandRotate, RandRotate90, RandZoom, From 7ea478c4b432a74950e70dab259e18879eae4b56 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 19:03:52 +0000 Subject: [PATCH 30/49] Convert to tensor Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 8 ++++++-- monai/transforms/spatial/dictionary.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 550c52a37c..a6ff42de80 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -51,6 +51,7 @@ InterpolateMode, NumpyPadMode, PytorchPadMode, + convert_to_dst_type, ensure_tuple, ensure_tuple_rep, ensure_tuple_size, @@ -63,7 +64,7 @@ from monai.utils.enums import 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, convert_to_dst_type +from monai.utils.type_conversion import convert_data_type nib, has_nib = optional_import("nibabel") @@ -2684,8 +2685,9 @@ def _get_negative_patch_sum(x): def __call__(self, array: NdarrayOrTensor): # create the patch iterator which sweeps the image row-by-row + array_np, *_ = convert_data_type(array, np.ndarray) patch_iterator = iter_patch( - array, # type: ignore + array_np, patch_size=(None,) + self.patch_size, # expand to have the channel dim start_pos=(0,) + self.start_pos, # expand to have the channel dim overlap=self.overlap, @@ -2701,6 +2703,8 @@ def __call__(self, array: NdarrayOrTensor): # keep up to max_num_patches output = output[: self.max_num_patches] self.num_patches = len(output) + if not isinstance(array, np.ndarray): + output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] return output diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 58f31d9167..71af92e113 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2331,6 +2331,7 @@ def __init__( pad_opts=pad_opts, seed=seed, ) + self.set_random_state(seed) self.max_num_patches = max_num_patches def set_random_state( From f7895994c5b8cfcdc76ff4ccd9e2290d9ceeb749 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 19:26:24 +0000 Subject: [PATCH 31/49] Update docs Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 67749a75fe..9b27e196e7 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -743,7 +743,7 @@ Spatial :members: :special-members: __call__ -`GridPatch` +`RandGridPatch` """"""""""" .. autoclass:: RandGridPatch :members: @@ -1533,7 +1533,7 @@ Spatial (Dict) `RandGridPatchd` """""""""""" -.. autoclass:: GridPatchd +.. autoclass:: RandGridPatchd :members: :special-members: __call__ From 83809e3d152d5724523caa73024ea6a93e8868b2 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 02:07:18 +0000 Subject: [PATCH 32/49] Fix number of patches Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 27 ++++++++++++--------- monai/transforms/spatial/dictionary.py | 33 ++++++++++++++------------ tests/test_grid_patch.py | 14 +++++------ tests/test_grid_patchd.py | 14 +++++------ tests/test_rand_grid_patch.py | 10 ++++---- tests/test_rand_grid_patchd.py | 10 ++++---- 6 files changed, 58 insertions(+), 50 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index a6ff42de80..146cb6cc91 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2627,7 +2627,7 @@ class GridPatch(Transform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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. - max_num_patches: maximum number of patches to return. No limit by default. + num_patches: number of patches to return. Defaults to None, which return 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2649,7 +2649,7 @@ def __init__( self, patch_size: Sequence[int], start_pos: Sequence[int] = (), - max_num_patches: Optional[int] = None, + num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2660,8 +2660,7 @@ def __init__( self.pad_mode: NumpyPadMode = look_up_option(pad_mode, NumpyPadMode) self.pad_opts = {} if pad_opts is None else pad_opts self.overlap = overlap - self.max_num_patches = max_num_patches - self.num_pacthes = max_num_patches + self.num_patches = num_patches self.sort_key: Optional[Callable] if isinstance(sort_key, str): if sort_key == "random": @@ -2698,11 +2697,17 @@ def __call__(self, array: NdarrayOrTensor): if self.sort_key is not None: output = sorted(patch_iterator, key=self.sort_key) else: - # Get all the patches (it's required to have a defined length) output = list(patch_iterator) - # keep up to max_num_patches - output = output[: self.max_num_patches] - self.num_patches = len(output) + if self.num_patches: + output = output[: self.num_patches] + if len(output) < self.num_patches: + if self.pad_opts.get("constant_values"): + patch = np.ones((array.shape[0], *self.patch_size)) * self.pad_opts["constant_values"] + else: + patch = np.zeros((array.shape[0], *self.patch_size)) + location = (None,) * len(self.patch_size) + output += [(patch, location)] * (self.num_patches - len(output)) + if not isinstance(array, np.ndarray): output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] return output @@ -2716,7 +2721,7 @@ class RandGridPatch(GridPatch, RandomizableTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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. - max_num_patches: maximum number of patches to return. No limit by default. + num_patches: number of patches to return. Defaults to None, which return 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2740,7 +2745,7 @@ def __init__( patch_size: Sequence[int], min_start_pos: Optional[Union[Sequence[int], int]] = None, max_start_pos: Optional[Union[Sequence[int], int]] = None, - max_num_patches: Optional[int] = None, + num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2750,7 +2755,7 @@ def __init__( super().__init__( patch_size=patch_size, start_pos=(), - max_num_patches=max_num_patches, + num_patches=num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 71af92e113..28b91d7a10 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -17,7 +17,7 @@ from copy import deepcopy from enum import Enum -from typing import Any, Callable, Dict, Generator, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -2212,7 +2212,7 @@ class GridPatchd(MapTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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. - max_num_patches: maximum number of patches to return. No limit by default. + num_patches: number of patches to return. Defaults to None, which return all the available patches. overlap: amount of overlap between patches in each dimension. Default to 0.0. sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", @@ -2235,7 +2235,7 @@ def __init__( keys: KeysCollection, patch_size: Sequence[int], start_pos: Sequence[int] = (), - max_num_patches: Optional[int] = None, + num_patches: Optional[int] = None, overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2246,17 +2246,18 @@ def __init__( self.patcher = GridPatch( patch_size=patch_size, start_pos=start_pos, - max_num_patches=max_num_patches, + num_patches=num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, pad_opts=pad_opts, ) - self.max_num_patches = max_num_patches + self.num_patches = num_patches - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] + output = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2272,7 +2273,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, "num_patches": self.patcher.num_patches, } new_dict["start_pos"] = self.patcher.start_pos - yield new_dict + output.append(new_dict) + return output class RandGridPatchd(RandomizableTransform, MapTransform): @@ -2284,7 +2286,7 @@ class RandGridPatchd(RandomizableTransform, MapTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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. - max_num_patches: maximum number of patches to return. No limit by default. + num_patches: number of patches to return. Defaults to None, which return 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2311,7 +2313,7 @@ def __init__( patch_size: Sequence[int], min_start_pos: Optional[Union[Sequence[int], int]] = None, max_start_pos: Optional[Union[Sequence[int], int]] = None, - max_num_patches: Optional[int] = None, + num_patches: Optional[int] = None, overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2324,7 +2326,7 @@ def __init__( patch_size=patch_size, min_start_pos=min_start_pos, max_start_pos=max_start_pos, - max_num_patches=max_num_patches, + num_patches=num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, @@ -2332,7 +2334,7 @@ def __init__( seed=seed, ) self.set_random_state(seed) - self.max_num_patches = max_num_patches + self.num_patches = num_patches def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -2341,9 +2343,10 @@ def set_random_state( super().set_random_state(seed, state) return self - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, None, None]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] + output = [] for patch in zip(*[self.patcher(d[key]) for key in self.keys]): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2351,15 +2354,15 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Generator[Dict, new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - # use the coordinate of the first item - location = patch[0][1] + location = patch[0][1] # use the coordinate of the first item new_dict["patch"] = { "location": location, "size": self.patcher.patch_size, "num_patches": self.patcher.num_patches, } new_dict["start_pos"] = self.patcher.start_pos - yield new_dict + output.append(new_dict) + return output SpatialResampleD = SpatialResampleDict = SpatialResampled diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 7a839e7f86..c8fd0ac93a 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "max_num_patches": 3}, A, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "max_num_patches": 5}, A, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, A, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "max_num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "max_num_patches": 4, "sort_key": "min"}, A, [A11, A12, A21, A22]] -TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "max_num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_key": "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), "max_num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "num_patches": 2, "pad_opts": {"constant_values": 255}}, 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), "start_pos": (-2, -2), "max_num_patches": 2}, + {"patch_size": (3, 3), "start_pos": (-2, -2), "num_patches": 2}, A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index b3ff95f773..ed3823e14d 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "max_num_patches": 3}, {"image": A}, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "max_num_patches": 5}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, {"image": A}, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, {"image": A}, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "max_num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "max_num_patches": 4, "sort_key": "min"}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "max_num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_key": "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), "max_num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "num_patches": 2, "pad_opts": {"constant_values": 255}}, {"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), "start_pos": (-2, -2), "max_num_patches": 2}, + {"patch_size": (3, 3), "start_pos": (-2, -2), "num_patches": 2}, {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index 715dd2adda..b34f1714ea 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -24,11 +24,11 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "num_patches": 3}, A, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "num_patches": 5}, A, - [A11, A12, A21, A22], + [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] @@ -36,7 +36,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, A, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "max_num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "num_patches": 1, "sort_key": "max"}, A, [A[:, 1:3, 1:3]], ] @@ -46,7 +46,7 @@ "min_start_pos": -3, "max_start_pos": -1, "sort_key": "min", - "max_num_patches": 1, + "num_patches": 1, "pad_opts": {"constant_values": 255}, }, A, diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index 0f4008c12f..c01464a161 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -27,11 +27,11 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "num_patches": 3}, {"image": A}, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "max_num_patches": 5}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "num_patches": 5}, {"image": A}, - [A11, A12, A21, A22], + [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] @@ -39,7 +39,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, {"image": A}, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "max_num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "num_patches": 1, "sort_key": "max"}, {"image": A}, [A[:, 1:3, 1:3]], ] @@ -49,7 +49,7 @@ "min_start_pos": -3, "max_start_pos": -1, "sort_key": "min", - "max_num_patches": 1, + "num_patches": 1, "pad_opts": {"constant_values": 255}, }, {"image": A}, From 5eca9a678a3ac765456f72a77635fa47feb9401e Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 02:14:07 +0000 Subject: [PATCH 33/49] Make RandGridPatchd to inherit GridPatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 28b91d7a10..920ecf061a 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2252,7 +2252,6 @@ def __init__( pad_mode=pad_mode, pad_opts=pad_opts, ) - self.num_patches = num_patches def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) @@ -2277,7 +2276,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: return output -class RandGridPatchd(RandomizableTransform, MapTransform): +class RandGridPatchd(RandomizableTransform, GridPatchd, MapTransform): """ Return all (or a subset of) the patches sweeping the entire image with a random starting position. @@ -2334,7 +2333,6 @@ def __init__( seed=seed, ) self.set_random_state(seed) - self.num_patches = num_patches def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -2343,27 +2341,6 @@ def set_random_state( super().set_random_state(seed, state) return self - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: - d = dict(data) - original_spatial_shape = d[first(self.keys)].shape[1:] - output = [] - for patch in zip(*[self.patcher(d[key]) for key in self.keys]): - new_dict = {k: v[0] for k, v in zip(self.keys, patch)} - # fill in the extra keys with unmodified data - for k in set(d.keys()).difference(set(self.keys)): - new_dict[k] = deepcopy(d[k]) - # fill additional metadata - new_dict["original_spatial_shape"] = original_spatial_shape - location = patch[0][1] # use the coordinate of the first item - new_dict["patch"] = { - "location": location, - "size": self.patcher.patch_size, - "num_patches": self.patcher.num_patches, - } - new_dict["start_pos"] = self.patcher.start_pos - output.append(new_dict) - return output - SpatialResampleD = SpatialResampleDict = SpatialResampled ResampleToMatchD = ResampleToMatchDict = ResampleToMatchd From 4af3bbebb94a4931a5fe7f7d38009ec0f4edd4ec Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 02:48:33 +0000 Subject: [PATCH 34/49] Change the location type Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 4 ++-- monai/transforms/spatial/dictionary.py | 11 ++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 146cb6cc91..7a431727de 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2705,11 +2705,11 @@ def __call__(self, array: NdarrayOrTensor): patch = np.ones((array.shape[0], *self.patch_size)) * self.pad_opts["constant_values"] else: patch = np.zeros((array.shape[0], *self.patch_size)) - location = (None,) * len(self.patch_size) + location = np.zeros((3, len(self.patch_size))) output += [(patch, location)] * (self.num_patches - len(output)) if not isinstance(array, np.ndarray): - output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] + output = [(convert_to_dst_type(src=patch, dst=array)[0], loc) for patch, loc in output] return output diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 28b91d7a10..d3ad7e6fe6 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2265,8 +2265,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - # use the coordinate of the first item - location = patch[0][1] + location = patch[0][1] # use the coordinate of the first item new_dict["patch"] = { "location": location, "size": self.patcher.patch_size, @@ -2355,11 +2354,9 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape location = patch[0][1] # use the coordinate of the first item - new_dict["patch"] = { - "location": location, - "size": self.patcher.patch_size, - "num_patches": self.patcher.num_patches, - } + new_dict["location"]= location, + new_dict["patch_size"] = self.patcher.patch_size, + new_dict["num_patches"] = self.patcher.num_patches, new_dict["start_pos"] = self.patcher.start_pos output.append(new_dict) return output From c31c546d8231f29421b6226ed9c246c9968192f4 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 12:48:04 +0000 Subject: [PATCH 35/49] Update fix num patches and separate randgridpatchd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 23 +++++++++------- monai/transforms/spatial/dictionary.py | 38 ++++++++++++++++++++------ tests/test_grid_patch.py | 14 +++++----- tests/test_grid_patchd.py | 14 +++++----- tests/test_rand_grid_patch.py | 8 +++--- tests/test_rand_grid_patchd.py | 8 +++--- 6 files changed, 65 insertions(+), 40 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 146cb6cc91..5ddf670dd5 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2627,7 +2627,7 @@ class GridPatch(Transform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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 return all the available patches. + fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2649,7 +2649,7 @@ def __init__( self, patch_size: Sequence[int], start_pos: Sequence[int] = (), - num_patches: Optional[int] = None, + fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2660,7 +2660,8 @@ def __init__( self.pad_mode: NumpyPadMode = look_up_option(pad_mode, NumpyPadMode) self.pad_opts = {} if pad_opts is None else pad_opts self.overlap = overlap - self.num_patches = num_patches + self.num_patches = 0 + self.fix_num_patches = fix_num_patches self.sort_key: Optional[Callable] if isinstance(sort_key, str): if sort_key == "random": @@ -2698,18 +2699,20 @@ def __call__(self, array: NdarrayOrTensor): output = sorted(patch_iterator, key=self.sort_key) else: output = list(patch_iterator) - if self.num_patches: - output = output[: self.num_patches] - if len(output) < self.num_patches: + if self.fix_num_patches: + output = output[: self.fix_num_patches] + if len(output) < self.fix_num_patches: if self.pad_opts.get("constant_values"): patch = np.ones((array.shape[0], *self.patch_size)) * self.pad_opts["constant_values"] else: patch = np.zeros((array.shape[0], *self.patch_size)) location = (None,) * len(self.patch_size) - output += [(patch, location)] * (self.num_patches - len(output)) + output += [(patch, location)] * (self.fix_num_patches - len(output)) if not isinstance(array, np.ndarray): output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] + + self.num_patches = len(output) return output @@ -2721,7 +2724,7 @@ class RandGridPatch(GridPatch, RandomizableTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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 return all the available patches. + fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2745,7 +2748,7 @@ def __init__( patch_size: Sequence[int], min_start_pos: Optional[Union[Sequence[int], int]] = None, max_start_pos: Optional[Union[Sequence[int], int]] = None, - num_patches: Optional[int] = None, + fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2755,7 +2758,7 @@ def __init__( super().__init__( patch_size=patch_size, start_pos=(), - num_patches=num_patches, + fix_num_patches=fix_num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 920ecf061a..565add7869 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2212,7 +2212,7 @@ class GridPatchd(MapTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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 return all the available patches. + fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. overlap: amount of overlap between patches in each dimension. Default to 0.0. sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", @@ -2235,7 +2235,7 @@ def __init__( keys: KeysCollection, patch_size: Sequence[int], start_pos: Sequence[int] = (), - num_patches: Optional[int] = None, + fix_num_patches: Optional[int] = None, overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2246,7 +2246,7 @@ def __init__( self.patcher = GridPatch( patch_size=patch_size, start_pos=start_pos, - num_patches=num_patches, + fix_num_patches=fix_num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, @@ -2276,7 +2276,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: return output -class RandGridPatchd(RandomizableTransform, GridPatchd, MapTransform): +class RandGridPatchd(RandomizableTransform, MapTransform): """ Return all (or a subset of) the patches sweeping the entire image with a random starting position. @@ -2285,7 +2285,7 @@ class RandGridPatchd(RandomizableTransform, GridPatchd, MapTransform): patch_size: size of patches to generate slices for, 0 or None selects whole dimension start_pos: 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 return all the available patches. + fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2312,7 +2312,7 @@ def __init__( patch_size: Sequence[int], min_start_pos: Optional[Union[Sequence[int], int]] = None, max_start_pos: Optional[Union[Sequence[int], int]] = None, - num_patches: Optional[int] = None, + fix_num_patches: Optional[int] = None, overlap: float = 0.0, sort_key: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, @@ -2325,7 +2325,7 @@ def __init__( patch_size=patch_size, min_start_pos=min_start_pos, max_start_pos=max_start_pos, - num_patches=num_patches, + fix_num_patches=fix_num_patches, overlap=overlap, sort_key=sort_key, pad_mode=pad_mode, @@ -2338,9 +2338,31 @@ def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None ) -> "RandGridPatchd": self.patcher.set_random_state(seed, state) - super().set_random_state(seed, state) + RandomizableTransform.set_random_state(self, seed, state) return self + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: + d = dict(data) + original_spatial_shape = d[first(self.keys)].shape[1:] + output = [] + for patch in zip(*[self.patcher(d[key]) for key in self.keys]): + new_dict = {k: v[0] for k, v in zip(self.keys, patch)} + # fill in the extra keys with unmodified data + for k in set(d.keys()).difference(set(self.keys)): + new_dict[k] = deepcopy(d[k]) + # fill additional metadata + new_dict["original_spatial_shape"] = original_spatial_shape + # use the coordinate of the first item + location = patch[0][1] + new_dict["patch"] = { + "location": location, + "size": self.patcher.patch_size, + "num_patches": self.patcher.num_patches, + } + new_dict["start_pos"] = self.patcher.start_pos + output.append(new_dict) + return output + SpatialResampleD = SpatialResampleDict = SpatialResampled ResampleToMatchD = ResampleToMatchDict = ResampleToMatchd diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index c8fd0ac93a..7e282198b0 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, A, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] +TEST_CASE_1 = [{"patch_size": (2, 2), "fix_num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "fix_num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, A, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_key": "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_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_key": "min"}, A, [A11, A12, A21, A22]] +TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ - {"patch_size": (3, 3), "num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, 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), "start_pos": (-2, -2), "num_patches": 2}, + {"patch_size": (3, 3), "start_pos": (-2, -2), "fix_num_patches": 2}, A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index ed3823e14d..949328a4c0 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, {"image": A}, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] +TEST_CASE_1 = [{"patch_size": (2, 2), "fix_num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "fix_num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, {"image": A}, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, {"image": A}, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "num_patches": 4, "sort_key": "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_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_key": "min"}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ - {"patch_size": (3, 3), "num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, {"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), "start_pos": (-2, -2), "num_patches": 2}, + {"patch_size": (3, 3), "start_pos": (-2, -2), "fix_num_patches": 2}, {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index b34f1714ea..e32628f0c7 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -24,9 +24,9 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "fix_num_patches": 3}, A, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "num_patches": 5}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "fix_num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] @@ -36,7 +36,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, A, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_key": "max"}, A, [A[:, 1:3, 1:3]], ] @@ -46,7 +46,7 @@ "min_start_pos": -3, "max_start_pos": -1, "sort_key": "min", - "num_patches": 1, + "fix_num_patches": 1, "pad_opts": {"constant_values": 255}, }, A, diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index c01464a161..9dec26394a 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -27,9 +27,9 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "fix_num_patches": 3}, {"image": A}, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "num_patches": 5}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "fix_num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] @@ -39,7 +39,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, {"image": A}, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_key": "max"}, {"image": A}, [A[:, 1:3, 1:3]], ] @@ -49,7 +49,7 @@ "min_start_pos": -3, "max_start_pos": -1, "sort_key": "min", - "num_patches": 1, + "fix_num_patches": 1, "pad_opts": {"constant_values": 255}, }, {"image": A}, From fd71c903c3435792c120370cc1a785f14f5bb879 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 12:56:53 +0000 Subject: [PATCH 36/49] Minor fixes Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/dictionary.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 239dbef9eb..37abff9532 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2264,10 +2264,9 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - location = patch[0][1] # use the coordinate of the first item - new_dict["location"]= location, - new_dict["patch_size"] = self.patcher.patch_size, - new_dict["num_patches"] = self.patcher.num_patches, + new_dict["location"] = patch[0][1] # use the coordinate of the first item + new_dict["patch_size"] = self.patcher.patch_size + new_dict["num_patches"] = self.patcher.num_patches new_dict["start_pos"] = self.patcher.start_pos output.append(new_dict) return output @@ -2349,10 +2348,9 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - location = patch[0][1] # use the coordinate of the first item - new_dict["location"]= location, - new_dict["patch_size"] = self.patcher.patch_size, - new_dict["num_patches"] = self.patcher.num_patches, + new_dict["location"] = patch[0][1] # use the coordinate of the first item + new_dict["patch_size"] = self.patcher.patch_size + new_dict["num_patches"] = self.patcher.num_patches new_dict["start_pos"] = self.patcher.start_pos output.append(new_dict) return output From fa965a22c0416d3a926037f3ddb3802f2fecce44 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 13:41:11 +0000 Subject: [PATCH 37/49] Update docs Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 9b27e196e7..17a399fb78 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -744,7 +744,7 @@ Spatial :special-members: __call__ `RandGridPatch` -""""""""""" +""""""""""""""" .. autoclass:: RandGridPatch :members: :special-members: __call__ @@ -1532,7 +1532,7 @@ Spatial (Dict) :special-members: __call__ `RandGridPatchd` -"""""""""""" +""""""""""""""""" .. autoclass:: RandGridPatchd :members: :special-members: __call__ From a809445c9a79342383068a32ac3426dbe3d0d6b1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Fri, 27 May 2022 19:30:16 +0000 Subject: [PATCH 38/49] Address review comments Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 2 +- .../pathology/transforms/spatial/array.py | 4 +- .../transforms/spatial/dictionary.py | 4 +- monai/transforms/spatial/array.py | 52 ++++++++++--------- monai/transforms/spatial/dictionary.py | 24 +++++---- 5 files changed, 46 insertions(+), 40 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 17a399fb78..bf5ed2b180 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -1532,7 +1532,7 @@ Spatial (Dict) :special-members: __call__ `RandGridPatchd` -""""""""""""""""" +"""""""""""""""" .. autoclass:: RandGridPatchd :members: :special-members: __call__ diff --git a/monai/apps/pathology/transforms/spatial/array.py b/monai/apps/pathology/transforms/spatial/array.py index 562a94952e..34cabac50c 100644 --- a/monai/apps/pathology/transforms/spatial/array.py +++ b/monai/apps/pathology/transforms/spatial/array.py @@ -23,7 +23,7 @@ __all__ = ["SplitOnGrid", "TileOnGrid"] -@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridSplit` instead.") +@deprecated(since="0.8", msg_suffix="use `monai.transforms.GridSplit` instead.") class SplitOnGrid(Transform): """ Split the image into patches based on the provided grid shape. @@ -108,7 +108,7 @@ def get_params(self, image_size): return patch_size, steps -@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridPatch` or `monai.transforms.RandGridPatch` instead.") +@deprecated(since="0.8", msg_suffix="use `monai.transforms.GridPatch` or `monai.transforms.RandGridPatch` instead.") class TileOnGrid(Randomizable, Transform): """ Tile the 2D image into patches on a grid and maintain a subset of it. diff --git a/monai/apps/pathology/transforms/spatial/dictionary.py b/monai/apps/pathology/transforms/spatial/dictionary.py index 78f3836c4b..022d82a053 100644 --- a/monai/apps/pathology/transforms/spatial/dictionary.py +++ b/monai/apps/pathology/transforms/spatial/dictionary.py @@ -22,7 +22,7 @@ __all__ = ["SplitOnGridd", "SplitOnGridD", "SplitOnGridDict", "TileOnGridd", "TileOnGridD", "TileOnGridDict"] -@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridSplitd` instead.") +@deprecated(since="0.8", msg_suffix="use `monai.transforms.GridSplitd` instead.") class SplitOnGridd(MapTransform): """ Split the image into patches based on the provided grid shape. @@ -57,7 +57,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d -@deprecated(since="0.9", msg_suffix="use `monai.transforms.GridPatchd` or `monai.transforms.RandGridPatchd` instead.") +@deprecated(since="0.8", msg_suffix="use `monai.transforms.GridPatchd` or `monai.transforms.RandGridPatchd` instead.") class TileOnGridd(Randomizable, MapTransform): """ Tile the 2D image into patches on a grid and maintain a subset of it. diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index e51f0bf967..a382f7da11 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2623,7 +2623,8 @@ def _get_params( class GridPatch(Transform): """ - Return all (or a subset of) the patches sweeping the entire image + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps. + It can sort the patches and return all or a subset of them. Args: patch_size: size of patches to generate slices for, 0 or None selects whole dimension @@ -2632,7 +2633,7 @@ class GridPatch(Transform): fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + sort_fn: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. @@ -2653,7 +2654,7 @@ def __init__( start_pos: Sequence[int] = (), fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, - sort_key: Optional[Union[Callable, str]] = None, + sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, ): @@ -2664,21 +2665,21 @@ def __init__( self.overlap = overlap self.num_patches = 0 self.fix_num_patches = fix_num_patches - self.sort_key: Optional[Callable] - if isinstance(sort_key, str): - if sort_key == "random": - self.sort_key = np.random.random - if sort_key == "min": - self.sort_key = self._get_patch_sum - if sort_key == "max": - self.sort_key = self._get_negative_patch_sum + self.sort_fn: Optional[Callable] + if isinstance(sort_fn, str): + if sort_fn == "random": + self.sort_fn = np.random.random + if sort_fn == "min": + self.sort_fn = self.get_patch_sum + if sort_fn == "max": + self.sort_fn = self._get_negative_patch_sum else: - ValueError(f'sort_key should be either "min", "max", or "random", "{sort_key}" was given.') + ValueError(f'sort_fn should be either "min", "max", or "random", "{sort_fn}" was given.') else: - self.sort_key = sort_key + self.sort_fn = sort_fn @staticmethod - def _get_patch_sum(x): + def get_patch_sum(x): return x[0].sum() @staticmethod @@ -2697,8 +2698,8 @@ def __call__(self, array: NdarrayOrTensor): mode=self.pad_mode, **self.pad_opts, ) - if self.sort_key is not None: - output = sorted(patch_iterator, key=self.sort_key) + if self.sort_fn is not None: + output = sorted(patch_iterator, key=self.sort_fn) else: output = list(patch_iterator) if self.fix_num_patches: @@ -2711,25 +2712,26 @@ def __call__(self, array: NdarrayOrTensor): location = np.zeros((3, len(self.patch_size))) output += [(patch, location)] * (self.fix_num_patches - len(output)) - if not isinstance(array, np.ndarray): - output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] - + output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] self.num_patches = len(output) return output class RandGridPatch(GridPatch, RandomizableTransform): """ - Return all (or a subset of) the patches sweeping the entire image with a random starting position. + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps, + and with random offset for the starting position of upper left corner of the image. + It can sort the patches and return all or a subset of them. Args: patch_size: size of patches to generate slices for, 0 or None selects whole dimension - start_pos: 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. + min_start_pos: the minimum range of starting position to be selected randomly. Defaults to 0. + max_start_pos: the maximum range of starting position to be selected randomly. + Defaults to image size modulo patch size. fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + sort_fn: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. @@ -2752,7 +2754,7 @@ def __init__( max_start_pos: Optional[Union[Sequence[int], int]] = None, fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, - sort_key: Optional[Union[Callable, str]] = None, + sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, seed: int = 0, @@ -2762,7 +2764,7 @@ def __init__( start_pos=(), fix_num_patches=fix_num_patches, overlap=overlap, - sort_key=sort_key, + sort_fn=sort_fn, pad_mode=pad_mode, pad_opts=pad_opts, ) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 37abff9532..65a635da78 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2205,7 +2205,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict[Hashab class GridPatchd(MapTransform): """ - Generate all (or a subset of) the patches sweeping the entire image + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps. + It can sort the patches and return all or a subset of them. Args: keys: keys of the corresponding items to be transformed. @@ -2214,7 +2215,7 @@ class GridPatchd(MapTransform): np.random.randint(0, patch_size, 2) creates random start between 0 and `patch_size` for a 2D image. fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. overlap: amount of overlap between patches in each dimension. Default to 0.0. - sort_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + sort_fn: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. @@ -2237,7 +2238,7 @@ def __init__( start_pos: Sequence[int] = (), fix_num_patches: Optional[int] = None, overlap: float = 0.0, - sort_key: Optional[Union[Callable, str]] = None, + sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, allow_missing_keys: bool = False, @@ -2248,7 +2249,7 @@ def __init__( start_pos=start_pos, fix_num_patches=fix_num_patches, overlap=overlap, - sort_key=sort_key, + sort_fn=sort_fn, pad_mode=pad_mode, pad_opts=pad_opts, ) @@ -2274,17 +2275,20 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: class RandGridPatchd(RandomizableTransform, MapTransform): """ - Return all (or a subset of) the patches sweeping the entire image with a random starting position. + Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps, + and with random offset for the starting position of upper left corner of the image. + It can sort the patches and return all or a subset of them. Args: keys: keys of the corresponding items to be transformed. patch_size: size of patches to generate slices for, 0 or None selects whole dimension - start_pos: 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. + min_start_pos: the minimum range of starting position to be selected randomly. Defaults to 0. + max_start_pos: the maximum range of starting position to be selected randomly. + Defaults to image size modulo patch size. fix_num_patches: number of patches to return. 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_key: a callable or string that defines the order of the patches to be returned. If it is a callable, it + sort_fn: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. @@ -2310,7 +2314,7 @@ def __init__( max_start_pos: Optional[Union[Sequence[int], int]] = None, fix_num_patches: Optional[int] = None, overlap: float = 0.0, - sort_key: Optional[Union[Callable, str]] = None, + sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, seed: int = 0, @@ -2323,7 +2327,7 @@ def __init__( max_start_pos=max_start_pos, fix_num_patches=fix_num_patches, overlap=overlap, - sort_key=sort_key, + sort_fn=sort_fn, pad_mode=pad_mode, pad_opts=pad_opts, seed=seed, From 037ac0a17025fbab22c2c491b2f764d203641f56 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 00:03:33 +0000 Subject: [PATCH 39/49] Few minor fixes Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/wsi_datasets.py | 6 +++--- monai/transforms/spatial/array.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py index 6fe5435d57..e250a373fb 100644 --- a/monai/data/wsi_datasets.py +++ b/monai/data/wsi_datasets.py @@ -206,13 +206,13 @@ def __init__( elif isinstance(offset_limits[0], tuple): self.offset_limits = offset_limits else: - ValueError( + raise ValueError( "The offset limits should be either a tuple of integers or tuple of tuple of integers." ) else: - ValueError("The offset limits should be a tuple.") + raise ValueError("The offset limits should be a tuple.") else: - ValueError( + raise ValueError( f'Invalid string for offset "{offset}". It should be either "random" as a string,' "an integer, or a tuple of integers defining the offset." ) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index a382f7da11..c12557d8eb 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2592,7 +2592,7 @@ def __call__( # Flatten the first two dimensions strided_image = strided_image.reshape(-1, *strided_image.shape[2:]) # Make a list of contiguous patches - patches = [np.ascontiguousarray(p) for p in strided_image] + patches = [np.copy(p) for p in strided_image] else: raise ValueError(f"Input type [{type(image)}] is not supported.") From 688866564a831569954239a0e20f7483728667ea Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 00:31:58 +0000 Subject: [PATCH 40/49] Add PytorchPadMode and change sort_key to sort_fn Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 5 +++-- tests/test_grid_patch.py | 4 ++-- tests/test_grid_patchd.py | 4 ++-- tests/test_rand_grid_patch.py | 4 ++-- tests/test_rand_grid_patchd.py | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index a382f7da11..24079134f1 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -36,6 +36,7 @@ from monai.transforms.intensity.array import GaussianSmooth from monai.transforms.transform import Randomizable, RandomizableTransform, ThreadUnsafe, Transform from monai.transforms.utils import ( + convert_pad_mode, create_control_grid, create_grid, create_rotate, @@ -2655,12 +2656,12 @@ def __init__( fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_fn: Optional[Union[Callable, str]] = None, - pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, ): self.patch_size = ensure_tuple(patch_size) self.start_pos = ensure_tuple(start_pos) - self.pad_mode: NumpyPadMode = look_up_option(pad_mode, NumpyPadMode) + self.pad_mode: NumpyPadMode = convert_pad_mode(dst=np.zeros(1), mode=pad_mode) self.pad_opts = {} if pad_opts is None else pad_opts self.overlap = overlap self.num_patches = 0 diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 7e282198b0..c3cd9fbd55 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -31,8 +31,8 @@ TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, A, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_key": "max"}, A, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_key": "min"}, A, [A11, A12, A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_fn": "max"}, A, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_fn": "min"}, A, [A11, A12, A21, A22]] TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ {"patch_size": (3, 3), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 949328a4c0..f157b08573 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -31,8 +31,8 @@ TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, {"image": A}, [A22]] TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, {"image": A}, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_key": "max"}, {"image": A}, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_key": "min"}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_fn": "max"}, {"image": A}, [A22, A21, A12]] +TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_fn": "min"}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] TEST_CASE_11 = [ {"patch_size": (3, 3), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index e32628f0c7..ca7103482f 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -36,7 +36,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, A, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, A, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_fn": "max"}, A, [A[:, 1:3, 1:3]], ] @@ -45,7 +45,7 @@ "patch_size": (3, 3), "min_start_pos": -3, "max_start_pos": -1, - "sort_key": "min", + "sort_fn": "min", "fix_num_patches": 1, "pad_opts": {"constant_values": 255}, }, diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index 9dec26394a..c6402acd66 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -39,7 +39,7 @@ TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, {"image": A}, [A12, A22]] TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, {"image": A}, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_key": "max"}, + {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_fn": "max"}, {"image": A}, [A[:, 1:3, 1:3]], ] @@ -48,7 +48,7 @@ "patch_size": (3, 3), "min_start_pos": -3, "max_start_pos": -1, - "sort_key": "min", + "sort_fn": "min", "fix_num_patches": 1, "pad_opts": {"constant_values": 255}, }, From 02a1104879befb277f03bc0b8c98e5b7f1a90603 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 00:35:03 +0000 Subject: [PATCH 41/49] Update pad_mode Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 2 +- monai/transforms/spatial/dictionary.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index da9d0bc89d..c649f8dfb1 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2756,7 +2756,7 @@ def __init__( fix_num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_fn: Optional[Union[Callable, str]] = None, - pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, seed: int = 0, ): diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 65a635da78..21258809c3 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2239,7 +2239,7 @@ def __init__( fix_num_patches: Optional[int] = None, overlap: float = 0.0, sort_fn: Optional[Union[Callable, str]] = None, - pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, allow_missing_keys: bool = False, ): @@ -2315,7 +2315,7 @@ def __init__( fix_num_patches: Optional[int] = None, overlap: float = 0.0, sort_fn: Optional[Union[Callable, str]] = None, - pad_mode: Union[NumpyPadMode, str] = NumpyPadMode.CONSTANT, + pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, seed: int = 0, allow_missing_keys: bool = False, From 76338b42d02af1322ac0d49daf21014bcb0faf99 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 01:35:24 +0000 Subject: [PATCH 42/49] Remove seed Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 2 -- monai/transforms/spatial/dictionary.py | 5 +---- tests/test_rand_grid_patch.py | 6 +++++- tests/test_rand_grid_patchd.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index c649f8dfb1..692811d38f 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2758,7 +2758,6 @@ def __init__( sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, - seed: int = 0, ): super().__init__( patch_size=patch_size, @@ -2769,7 +2768,6 @@ def __init__( pad_mode=pad_mode, pad_opts=pad_opts, ) - self.set_random_state(seed) self.min_start_pos = min_start_pos self.max_start_pos = max_start_pos diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 21258809c3..346b45c1cb 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2317,7 +2317,6 @@ def __init__( sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, pad_opts: Optional[Dict] = None, - seed: int = 0, allow_missing_keys: bool = False, ): MapTransform.__init__(self, keys, allow_missing_keys) @@ -2330,15 +2329,13 @@ def __init__( sort_fn=sort_fn, pad_mode=pad_mode, pad_opts=pad_opts, - seed=seed, ) - self.set_random_state(seed) def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None ) -> "RandGridPatchd": self.patcher.set_random_state(seed, state) - RandomizableTransform.set_random_state(self, seed, state) + super().set_random_state(seed, state) return self def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index ca7103482f..2243165faf 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -15,8 +15,11 @@ from parameterized import parameterized from monai.transforms.spatial.array import RandGridPatch +from monai.utils import set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose +set_determinism(1234) + A = np.arange(16).repeat(3).reshape(4, 4, 3).transpose(2, 0, 1) A11 = A[:, :2, :2] A12 = A[:, :2, 2:] @@ -71,7 +74,8 @@ class TestSlidingPatch(unittest.TestCase): @parameterized.expand(TEST_SINGLE) def test_split_patch_single_call(self, in_type, input_parameters, image, expected): input_image = in_type(image) - splitter = RandGridPatch(seed=1234, **input_parameters) + splitter = RandGridPatch(**input_parameters) + splitter.set_random_state(1234) output = list(splitter(input_image)) self.assertEqual(len(output), len(expected)) for output_patch, expected_patch in zip(output, expected): diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index c6402acd66..e64b56ed7c 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -80,7 +80,7 @@ def test_split_patch_single_call(self, in_type, input_parameters, image_dict, ex if k == image_key: input_dict[k] = in_type(v) splitter = RandGridPatchd(keys=image_key, **input_parameters) - splitter.set_random_state(state=np.random.RandomState(1234)) + splitter.set_random_state(1234) output = list(splitter(input_dict)) self.assertEqual(len(output), len(expected)) for output_patch, expected_patch in zip(output, expected): From 9cf0b0cf6ebd6e008af7701b557d41b8423308bc Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 02:03:36 +0000 Subject: [PATCH 43/49] Make it thread safe Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 3 +-- monai/transforms/spatial/dictionary.py | 12 ++++++++---- tests/test_grid_patch.py | 4 ++-- tests/test_grid_patchd.py | 4 ++-- tests/test_rand_grid_patch.py | 4 ++-- tests/test_rand_grid_patchd.py | 4 ++-- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 692811d38f..5a0b317811 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2664,7 +2664,6 @@ def __init__( self.pad_mode: NumpyPadMode = convert_pad_mode(dst=np.zeros(1), mode=pad_mode) self.pad_opts = {} if pad_opts is None else pad_opts self.overlap = overlap - self.num_patches = 0 self.fix_num_patches = fix_num_patches self.sort_fn: Optional[Callable] if isinstance(sort_fn, str): @@ -2714,7 +2713,7 @@ def __call__(self, array: NdarrayOrTensor): output += [(patch, location)] * (self.fix_num_patches - len(output)) output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] - self.num_patches = len(output) + return output diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 346b45c1cb..0c4bd54132 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2258,7 +2258,9 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] output = [] - for patch in zip(*[self.patcher(d[key]) for key in self.keys]): + results = [self.patcher(d[key]) for key in self.keys] + num_patches = min([len(r) for r in results]) + for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data for k in set(d.keys()).difference(set(self.keys)): @@ -2267,7 +2269,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict["original_spatial_shape"] = original_spatial_shape new_dict["location"] = patch[0][1] # use the coordinate of the first item new_dict["patch_size"] = self.patcher.patch_size - new_dict["num_patches"] = self.patcher.num_patches + new_dict["num_patches"] = num_patches new_dict["start_pos"] = self.patcher.start_pos output.append(new_dict) return output @@ -2342,7 +2344,9 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] output = [] - for patch in zip(*[self.patcher(d[key]) for key in self.keys]): + results = [self.patcher(d[key]) for key in self.keys] + num_patches = min([len(r) for r in results]) + for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data for k in set(d.keys()).difference(set(self.keys)): @@ -2351,7 +2355,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict["original_spatial_shape"] = original_spatial_shape new_dict["location"] = patch[0][1] # use the coordinate of the first item new_dict["patch_size"] = self.patcher.patch_size - new_dict["num_patches"] = self.patcher.num_patches + new_dict["num_patches"] = num_patches new_dict["start_pos"] = self.patcher.start_pos output.append(new_dict) return output diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index c3cd9fbd55..502ce3f8f0 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -63,9 +63,9 @@ TEST_SINGLE.append([p, *TEST_CASE_12]) -class TestSlidingPatch(unittest.TestCase): +class TestGridPatch(unittest.TestCase): @parameterized.expand(TEST_SINGLE) - def test_split_patch_single_call(self, in_type, input_parameters, image, expected): + def test_grid_patch(self, in_type, input_parameters, image, expected): input_image = in_type(image) splitter = GridPatch(**input_parameters) output = list(splitter(input_image)) diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index f157b08573..409712972e 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -63,9 +63,9 @@ TEST_SINGLE.append([p, *TEST_CASE_12]) -class TestSlidingPatch(unittest.TestCase): +class TestGridPatchd(unittest.TestCase): @parameterized.expand(TEST_SINGLE) - def test_split_patch_single_call(self, in_type, input_parameters, image_dict, expected): + def test_grid_patchd(self, in_type, input_parameters, image_dict, expected): image_key = "image" input_dict = {} for k, v in image_dict.items(): diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index 2243165faf..042370610f 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -70,9 +70,9 @@ TEST_SINGLE.append([p, *TEST_CASE_9]) -class TestSlidingPatch(unittest.TestCase): +class TestRandGridPatch(unittest.TestCase): @parameterized.expand(TEST_SINGLE) - def test_split_patch_single_call(self, in_type, input_parameters, image, expected): + def test_rand_grid_patch(self, in_type, input_parameters, image, expected): input_image = in_type(image) splitter = RandGridPatch(**input_parameters) splitter.set_random_state(1234) diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index e64b56ed7c..dbbd50116c 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -70,9 +70,9 @@ TEST_SINGLE.append([p, *TEST_CASE_9]) -class TestSlidingPatch(unittest.TestCase): +class TestRandGridPatchd(unittest.TestCase): @parameterized.expand(TEST_SINGLE) - def test_split_patch_single_call(self, in_type, input_parameters, image_dict, expected): + def test_rand_grid_patchd(self, in_type, input_parameters, image_dict, expected): image_key = "image" input_dict = {} for k, v in image_dict.items(): From 6879290c5ed42a1de846183778fce0c506268325 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 28 May 2022 02:04:05 +0000 Subject: [PATCH 44/49] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/spatial/dictionary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 0c4bd54132..5b820e0562 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2259,7 +2259,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: original_spatial_shape = d[first(self.keys)].shape[1:] output = [] results = [self.patcher(d[key]) for key in self.keys] - num_patches = min([len(r) for r in results]) + num_patches = min(len(r) for r in results) for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data @@ -2345,7 +2345,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: original_spatial_shape = d[first(self.keys)].shape[1:] output = [] results = [self.patcher(d[key]) for key in self.keys] - num_patches = min([len(r) for r in results]) + num_patches = min(len(r) for r in results) for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data From dfb15b30e9f9014f936b01ce968fdcfc5b14ec00 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 30 May 2022 15:27:13 +0000 Subject: [PATCH 45/49] Address reviews Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/wsi_datasets.py | 4 +- monai/transforms/spatial/array.py | 104 ++++++++++++------------- monai/transforms/spatial/dictionary.py | 88 ++++++++++++--------- tests/test_grid_patch.py | 24 +++--- tests/test_grid_patchd.py | 24 +++--- tests/test_rand_grid_patch.py | 24 +++--- tests/test_rand_grid_patchd.py | 24 +++--- 7 files changed, 148 insertions(+), 144 deletions(-) diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py index e250a373fb..02a172d5b2 100644 --- a/monai/data/wsi_datasets.py +++ b/monai/data/wsi_datasets.py @@ -238,7 +238,7 @@ def _evaluate_patch_coordinates(self, sample): """Define the location for each patch based on sliding-window approach""" patch_size = self._get_size(sample) level = self._get_level(sample) - start_pos = self._get_offset(sample) + offset = self._get_offset(sample) wsi_obj = self._get_wsi_object(sample) wsi_size = self.wsi_reader.get_size(wsi_obj, 0) @@ -246,7 +246,7 @@ def _evaluate_patch_coordinates(self, sample): patch_size_ = tuple(p * downsample for p in patch_size) # patch size at level 0 locations = list( iter_patch_position( - image_size=wsi_size, patch_size=patch_size_, start_pos=start_pos, overlap=self.overlap, padded=False + image_size=wsi_size, patch_size=patch_size_, offset=offset, overlap=self.overlap, padded=False ) ) sample["size"] = patch_size diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 5a0b317811..242a0bf506 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2588,12 +2588,11 @@ def __call__( image, shape=(*self.grid, n_channels, split_size[0], split_size[1]), strides=(x_stride * x_step, y_stride * y_step, c_stride, x_stride, y_stride), - writeable=False, ) # Flatten the first two dimensions strided_image = strided_image.reshape(-1, *strided_image.shape[2:]) # Make a list of contiguous patches - patches = [np.copy(p) for p in strided_image] + patches = [np.ascontiguousarray(p) for p in strided_image] else: raise ValueError(f"Input type [{type(image)}] is not supported.") @@ -2629,9 +2628,8 @@ class GridPatch(Transform): Args: patch_size: size of patches to generate slices for, 0 or None selects whole dimension - start_pos: 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. - fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. + 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. 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: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2639,11 +2637,8 @@ class GridPatch(Transform): which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. By default no sorting is being done and patches are returned in a row-major order. - pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, - ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} - One of the listed string values or a user supplied function. Defaults to ``"wrap"``. - See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html - pad_opts: padding options, see `numpy.pad` + pad_mode: refer to NumpyPadMode and PytorchPadMode. Defaults to ``"constant"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. """ @@ -2652,19 +2647,19 @@ class GridPatch(Transform): def __init__( self, patch_size: Sequence[int], - start_pos: Sequence[int] = (), - fix_num_patches: Optional[int] = None, + offset: Sequence[int] = (), + num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, - pad_opts: Optional[Dict] = None, + **pad_kwargs, ): self.patch_size = ensure_tuple(patch_size) - self.start_pos = ensure_tuple(start_pos) + self.offset = ensure_tuple(offset) self.pad_mode: NumpyPadMode = convert_pad_mode(dst=np.zeros(1), mode=pad_mode) - self.pad_opts = {} if pad_opts is None else pad_opts + self.pad_kwargs = pad_kwargs self.overlap = overlap - self.fix_num_patches = fix_num_patches + self.num_patches = num_patches self.sort_fn: Optional[Callable] if isinstance(sort_fn, str): if sort_fn == "random": @@ -2672,7 +2667,7 @@ def __init__( if sort_fn == "min": self.sort_fn = self.get_patch_sum if sort_fn == "max": - self.sort_fn = self._get_negative_patch_sum + self.sort_fn = self.get_negative_patch_sum else: ValueError(f'sort_fn should be either "min", "max", or "random", "{sort_fn}" was given.') else: @@ -2683,7 +2678,7 @@ def get_patch_sum(x): return x[0].sum() @staticmethod - def _get_negative_patch_sum(x): + def get_negative_patch_sum(x): return -x[0].sum() def __call__(self, array: NdarrayOrTensor): @@ -2692,25 +2687,25 @@ def __call__(self, array: NdarrayOrTensor): patch_iterator = iter_patch( array_np, patch_size=(None,) + self.patch_size, # expand to have the channel dim - start_pos=(0,) + self.start_pos, # expand to have the channel dim + start_pos=(0,) + self.offset, # expand to have the channel dim overlap=self.overlap, copy_back=False, mode=self.pad_mode, - **self.pad_opts, + **self.pad_kwargs, ) if self.sort_fn is not None: output = sorted(patch_iterator, key=self.sort_fn) else: output = list(patch_iterator) - if self.fix_num_patches: - output = output[: self.fix_num_patches] - if len(output) < self.fix_num_patches: - if self.pad_opts.get("constant_values"): - patch = np.ones((array.shape[0], *self.patch_size)) * self.pad_opts["constant_values"] + if self.num_patches: + output = output[: self.num_patches] + if len(output) < self.num_patches: + if self.pad_kwargs.get("constant_values"): + patch = np.full((array.shape[0], *self.patch_size), self.pad_kwargs["constant_values"]) else: patch = np.zeros((array.shape[0], *self.patch_size)) - location = np.zeros((3, len(self.patch_size))) - output += [(patch, location)] * (self.fix_num_patches - len(output)) + slices = np.zeros((3, len(self.patch_size))) + output += [(patch, slices)] * (self.num_patches - len(output)) output = [convert_to_dst_type(src=patch, dst=array)[0] for patch in output] @@ -2720,15 +2715,15 @@ def __call__(self, array: NdarrayOrTensor): class RandGridPatch(GridPatch, RandomizableTransform): """ Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps, - and with random offset for the starting position of upper left corner of the image. + and with random offset for the minimal corner of the image, (0,0) for 2D and (0,0,0) for 3D. It can sort the patches and return all or a subset of them. Args: patch_size: size of patches to generate slices for, 0 or None selects whole dimension - min_start_pos: the minimum range of starting position to be selected randomly. Defaults to 0. - max_start_pos: the maximum range of starting position to be selected randomly. + 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. - fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches to return. 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: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2736,12 +2731,8 @@ class RandGridPatch(GridPatch, RandomizableTransform): which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. By default no sorting is being done and patches are returned in a row-major order. - pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, - ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} - One of the listed string values or a user supplied function. Defaults to ``"wrap"``. - See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html - pad_opts: padding options, see `numpy.pad` - seed: random seed to generate offsets + pad_mode: refer to NumpyPadMode and PytorchPadMode. Defaults to ``"constant"``. + pad_kwargs: other arguments for the `np.pad` or `torch.pad` function. """ @@ -2750,37 +2741,38 @@ class RandGridPatch(GridPatch, RandomizableTransform): def __init__( self, patch_size: Sequence[int], - min_start_pos: Optional[Union[Sequence[int], int]] = None, - max_start_pos: Optional[Union[Sequence[int], int]] = None, - fix_num_patches: Optional[int] = None, + min_offset: Optional[Union[Sequence[int], int]] = None, + max_offset: Optional[Union[Sequence[int], int]] = None, + num_patches: Optional[int] = None, overlap: Union[Sequence[float], float] = 0.0, sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, - pad_opts: Optional[Dict] = None, + **pad_kwargs, ): super().__init__( patch_size=patch_size, - start_pos=(), - fix_num_patches=fix_num_patches, + offset=(), + num_patches=num_patches, overlap=overlap, sort_fn=sort_fn, pad_mode=pad_mode, - pad_opts=pad_opts, + **pad_kwargs, ) - self.min_start_pos = min_start_pos - self.max_start_pos = max_start_pos + self.min_offset = min_offset + self.max_offset = max_offset - def __call__(self, array: NdarrayOrTensor): - if self.min_start_pos is None: - min_start_pos = (0,) * len(self.patch_size) + def randomize(self, array): + if self.min_offset is None: + min_offset = (0,) * len(self.patch_size) else: - min_start_pos = ensure_tuple_rep(self.min_start_pos, len(self.patch_size)) - if self.max_start_pos is None: - max_start_pos = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size)) + min_offset = ensure_tuple_rep(self.min_offset, len(self.patch_size)) + if self.max_offset is None: + max_offset = tuple(s % p for s, p in zip(array.shape[1:], self.patch_size)) else: - max_start_pos = ensure_tuple_rep(self.max_start_pos, len(self.patch_size)) + max_offset = ensure_tuple_rep(self.max_offset, len(self.patch_size)) - self.start_pos = tuple( - self.R.randint(low=low, high=high + 1) for low, high in zip(min_start_pos, max_start_pos) - ) + self.offset = tuple(self.R.randint(low=low, high=high + 1) for low, high in zip(min_offset, max_offset)) + + def __call__(self, array: NdarrayOrTensor): + self.randomize(array) return super().__call__(array) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 5b820e0562..9472b93a7f 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2211,22 +2211,27 @@ class GridPatchd(MapTransform): Args: keys: keys of the corresponding items to be transformed. patch_size: size of patches to generate slices for, 0 or None selects whole dimension - start_pos: starting position in the array, default is 0 for each 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. - fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches to return. Defaults to None, which returns all the available patches. overlap: amount of overlap between patches in each dimension. Default to 0.0. sort_fn: a callable or string that defines the order of the patches to be returned. If it is a callable, it will be passed directly to the `key` argument of `sorted` function. The string can be "min" or "max", which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. By default no sorting is being done and patches are returned in a row-major order. - pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, - ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} - One of the listed string values or a user supplied function. Defaults to ``"wrap"``. - See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html - pad_opts: padding options, see `numpy.pad` + pad_mode: refer to NumpyPadMode and PytorchPadMode. Defaults to ``"constant"``. 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: + + "slices": slices from the image that defines the patch, + "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) """ backend = GridPatch.backend @@ -2235,23 +2240,23 @@ def __init__( self, keys: KeysCollection, patch_size: Sequence[int], - start_pos: Sequence[int] = (), - fix_num_patches: Optional[int] = None, + offset: Sequence[int] = (), + num_patches: Optional[int] = None, overlap: float = 0.0, sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, - pad_opts: Optional[Dict] = None, allow_missing_keys: bool = False, + **pad_kwargs, ): super().__init__(keys, allow_missing_keys) self.patcher = GridPatch( patch_size=patch_size, - start_pos=start_pos, - fix_num_patches=fix_num_patches, + offset=offset, + num_patches=num_patches, overlap=overlap, sort_fn=sort_fn, pad_mode=pad_mode, - pad_opts=pad_opts, + **pad_kwargs, ) def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: @@ -2267,10 +2272,10 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - new_dict["location"] = patch[0][1] # use the coordinate of the first item + new_dict["slices"] = patch[0][1] # use the coordinate of the first item new_dict["patch_size"] = self.patcher.patch_size new_dict["num_patches"] = num_patches - new_dict["start_pos"] = self.patcher.start_pos + new_dict["offset"] = self.patcher.offset output.append(new_dict) return output @@ -2278,16 +2283,16 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: class RandGridPatchd(RandomizableTransform, MapTransform): """ Extract all the patches sweeping the entire image in a row-major sliding-window manner with possible overlaps, - and with random offset for the starting position of upper left corner of the image. + and with random offset for the minimal corner of the image, (0,0) for 2D and (0,0,0) for 3D. It can sort the patches and return all or a subset of them. Args: keys: keys of the corresponding items to be transformed. patch_size: size of patches to generate slices for, 0 or None selects whole dimension - min_start_pos: the minimum range of starting position to be selected randomly. Defaults to 0. - max_start_pos: the maximum range of starting position to be selected randomly. + 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. - fix_num_patches: number of patches to return. Defaults to None, which returns all the available patches. + num_patches: number of patches to return. 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: a callable or string that defines the order of the patches to be returned. If it is a callable, it @@ -2295,14 +2300,18 @@ class RandGridPatchd(RandomizableTransform, MapTransform): which are, respectively, the minimum and maximum of the sum of intensities of a patch across all dimensions and channels. Also "random" creates a random order of patches. By default no sorting is being done and patches are returned in a row-major order. - pad_mode: {``"constant"``, ``"edge"``, ``"linear_ramp"``, ``"maximum"``, ``"mean"``, - ``"median"``, ``"minimum"``, ``"reflect"``, ``"symmetric"``, ``"wrap"``, ``"empty"``} - One of the listed string values or a user supplied function. Defaults to ``"wrap"``. - See also: https://numpy.org/doc/stable/reference/generated/numpy.pad.html - pad_opts: padding options, see `numpy.pad` - seed: random seed to generate offsets + pad_mode: refer to NumpyPadMode and PytorchPadMode. Defaults to ``"constant"``. 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: + "slices": slices from the image that defines the patch, + "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) """ @@ -2312,39 +2321,42 @@ def __init__( self, keys: KeysCollection, patch_size: Sequence[int], - min_start_pos: Optional[Union[Sequence[int], int]] = None, - max_start_pos: Optional[Union[Sequence[int], int]] = None, - fix_num_patches: Optional[int] = None, + min_offset: Optional[Union[Sequence[int], int]] = None, + max_offset: Optional[Union[Sequence[int], int]] = None, + num_patches: Optional[int] = None, overlap: float = 0.0, sort_fn: Optional[Union[Callable, str]] = None, pad_mode: Union[NumpyPadMode, PytorchPadMode, str] = NumpyPadMode.CONSTANT, - pad_opts: Optional[Dict] = None, allow_missing_keys: bool = False, + **pad_kwargs, ): MapTransform.__init__(self, keys, allow_missing_keys) self.patcher = RandGridPatch( patch_size=patch_size, - min_start_pos=min_start_pos, - max_start_pos=max_start_pos, - fix_num_patches=fix_num_patches, + min_offset=min_offset, + max_offset=max_offset, + num_patches=num_patches, overlap=overlap, sort_fn=sort_fn, pad_mode=pad_mode, - pad_opts=pad_opts, + **pad_kwargs, ) def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None ) -> "RandGridPatchd": - self.patcher.set_random_state(seed, state) super().set_random_state(seed, state) + self.patcher.set_random_state(seed, state) return self def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] - output = [] - results = [self.patcher(d[key]) for key in self.keys] + output, results = [], [] + random_state = self.R # to use the same randomness for all the keys + for key in self.keys: + self.set_random_state(state=random_state) + results.append(self.patcher(d[key])) num_patches = min(len(r) for r in results) for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} @@ -2353,10 +2365,10 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: new_dict[k] = deepcopy(d[k]) # fill additional metadata new_dict["original_spatial_shape"] = original_spatial_shape - new_dict["location"] = patch[0][1] # use the coordinate of the first item + new_dict["slices"] = patch[0][1] # use the coordinate of the first item new_dict["patch_size"] = self.patcher.patch_size new_dict["num_patches"] = num_patches - new_dict["start_pos"] = self.patcher.start_pos + new_dict["offset"] = self.patcher.offset output.append(new_dict) return output diff --git a/tests/test_grid_patch.py b/tests/test_grid_patch.py index 502ce3f8f0..c1d73f262f 100644 --- a/tests/test_grid_patch.py +++ b/tests/test_grid_patch.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "fix_num_patches": 3}, A, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "fix_num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] -TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] -TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, A, [A11, A12, A21, A22]] -TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, A, [A22]] -TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, A, [A12, A22]] -TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, A, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_fn": "max"}, A, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_fn": "min"}, A, [A11, A12, A21, A22]] -TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, A, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] +TEST_CASE_3 = [{"patch_size": (2, 2), "offset": (0, 0)}, A, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2), "offset": (0, 0)}, A, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "offset": (2, 2)}, A, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "offset": (0, 2)}, A, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "offset": (2, 0)}, A, [A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_fn": "max"}, A, [A22, A21, A12]] +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), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255}, 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), "start_pos": (-2, -2), "fix_num_patches": 2}, + {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2}, A, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_grid_patchd.py b/tests/test_grid_patchd.py index 409712972e..a9eec8a2f6 100644 --- a/tests/test_grid_patchd.py +++ b/tests/test_grid_patchd.py @@ -24,23 +24,23 @@ A22 = A[:, 2:, 2:] TEST_CASE_0 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "fix_num_patches": 3}, {"image": A}, [A11, A12, A21]] -TEST_CASE_2 = [{"patch_size": (2, 2), "fix_num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] -TEST_CASE_3 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_4 = [{"patch_size": (2, 2), "start_pos": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_5 = [{"patch_size": (2, 2), "start_pos": (2, 2)}, {"image": A}, [A22]] -TEST_CASE_6 = [{"patch_size": (2, 2), "start_pos": (0, 2)}, {"image": A}, [A12, A22]] -TEST_CASE_7 = [{"patch_size": (2, 2), "start_pos": (2, 0)}, {"image": A}, [A21, A22]] -TEST_CASE_8 = [{"patch_size": (2, 2), "fix_num_patches": 3, "sort_fn": "max"}, {"image": A}, [A22, A21, A12]] -TEST_CASE_9 = [{"patch_size": (2, 2), "fix_num_patches": 4, "sort_fn": "min"}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_10 = [{"patch_size": (2, 2), "overlap": 0.5, "fix_num_patches": 3}, {"image": A}, [A11, A[:, :2, 1:3], A12]] +TEST_CASE_1 = [{"patch_size": (2, 2), "num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_2 = [{"patch_size": (2, 2), "num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))]] +TEST_CASE_3 = [{"patch_size": (2, 2), "offset": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_4 = [{"patch_size": (2, 2), "offset": (0, 0)}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "offset": (2, 2)}, {"image": A}, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "offset": (0, 2)}, {"image": A}, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "offset": (2, 0)}, {"image": A}, [A21, A22]] +TEST_CASE_8 = [{"patch_size": (2, 2), "num_patches": 3, "sort_fn": "max"}, {"image": A}, [A22, A21, A12]] +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), "fix_num_patches": 2, "pad_opts": {"constant_values": 255}}, + {"patch_size": (3, 3), "num_patches": 2, "constant_values": 255}, {"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), "start_pos": (-2, -2), "fix_num_patches": 2}, + {"patch_size": (3, 3), "offset": (-2, -2), "num_patches": 2}, {"image": A}, [np.zeros((3, 3, 3)), np.pad(A[:, :1, 1:4], ((0, 0), (2, 0), (0, 0)), mode="constant")], ] diff --git a/tests/test_rand_grid_patch.py b/tests/test_rand_grid_patch.py index 042370610f..36da899982 100644 --- a/tests/test_rand_grid_patch.py +++ b/tests/test_rand_grid_patch.py @@ -26,31 +26,31 @@ A21 = A[:, 2:, :2] A22 = A[:, 2:, 2:] -TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "fix_num_patches": 3}, A, [A11, A12, A21]] +TEST_CASE_0 = [{"patch_size": (2, 2), "min_offset": 0, "max_offset": 0}, A, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_offset": 0, "num_patches": 3}, A, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "fix_num_patches": 5}, + {"patch_size": (2, 2), "min_offset": 0, "max_offset": 0, "num_patches": 5}, A, [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] -TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, A, [A11, A12, A21, A22]] +TEST_CASE_3 = [{"patch_size": (2, 2), "min_offset": 0, "max_offset": 0}, A, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2)}, A, [A11, A12, A21, A22]] -TEST_CASE_5 = [{"patch_size": (2, 2), "min_start_pos": 2, "max_start_pos": 2}, A, [A22]] -TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, A, [A12, A22]] -TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, A, [A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "min_offset": 2, "max_offset": 2}, A, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "min_offset": (0, 2), "max_offset": (0, 2)}, A, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "min_offset": 1, "max_offset": 2}, A, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_fn": "max"}, + {"patch_size": (2, 2), "min_offset": 0, "max_offset": 1, "num_patches": 1, "sort_fn": "max"}, A, [A[:, 1:3, 1:3]], ] TEST_CASE_9 = [ { "patch_size": (3, 3), - "min_start_pos": -3, - "max_start_pos": -1, + "min_offset": -3, + "max_offset": -1, "sort_fn": "min", - "fix_num_patches": 1, - "pad_opts": {"constant_values": 255}, + "num_patches": 1, + "constant_values": 255, }, A, [np.pad(A[:, :2, 1:], ((0, 0), (1, 0), (0, 0)), mode="constant", constant_values=255)], diff --git a/tests/test_rand_grid_patchd.py b/tests/test_rand_grid_patchd.py index dbbd50116c..6f89a3d155 100644 --- a/tests/test_rand_grid_patchd.py +++ b/tests/test_rand_grid_patchd.py @@ -26,31 +26,31 @@ A21 = A[:, 2:, :2] A22 = A[:, 2:, 2:] -TEST_CASE_0 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_1 = [{"patch_size": (2, 2), "min_start_pos": 0, "fix_num_patches": 3}, {"image": A}, [A11, A12, A21]] +TEST_CASE_0 = [{"patch_size": (2, 2), "min_offset": 0, "max_offset": 0}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_1 = [{"patch_size": (2, 2), "min_offset": 0, "num_patches": 3}, {"image": A}, [A11, A12, A21]] TEST_CASE_2 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0, "fix_num_patches": 5}, + {"patch_size": (2, 2), "min_offset": 0, "max_offset": 0, "num_patches": 5}, {"image": A}, [A11, A12, A21, A22, np.zeros((3, 2, 2))], ] -TEST_CASE_3 = [{"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 0}, {"image": A}, [A11, A12, A21, A22]] +TEST_CASE_3 = [{"patch_size": (2, 2), "min_offset": 0, "max_offset": 0}, {"image": A}, [A11, A12, A21, A22]] TEST_CASE_4 = [{"patch_size": (2, 2)}, {"image": A}, [A11, A12, A21, A22]] -TEST_CASE_5 = [{"patch_size": (2, 2), "min_start_pos": 2, "max_start_pos": 2}, {"image": A}, [A22]] -TEST_CASE_6 = [{"patch_size": (2, 2), "min_start_pos": (0, 2), "max_start_pos": (0, 2)}, {"image": A}, [A12, A22]] -TEST_CASE_7 = [{"patch_size": (2, 2), "min_start_pos": 1, "max_start_pos": 2}, {"image": A}, [A22]] +TEST_CASE_5 = [{"patch_size": (2, 2), "min_offset": 2, "max_offset": 2}, {"image": A}, [A22]] +TEST_CASE_6 = [{"patch_size": (2, 2), "min_offset": (0, 2), "max_offset": (0, 2)}, {"image": A}, [A12, A22]] +TEST_CASE_7 = [{"patch_size": (2, 2), "min_offset": 1, "max_offset": 2}, {"image": A}, [A22]] TEST_CASE_8 = [ - {"patch_size": (2, 2), "min_start_pos": 0, "max_start_pos": 1, "fix_num_patches": 1, "sort_fn": "max"}, + {"patch_size": (2, 2), "min_offset": 0, "max_offset": 1, "num_patches": 1, "sort_fn": "max"}, {"image": A}, [A[:, 1:3, 1:3]], ] TEST_CASE_9 = [ { "patch_size": (3, 3), - "min_start_pos": -3, - "max_start_pos": -1, + "min_offset": -3, + "max_offset": -1, "sort_fn": "min", - "fix_num_patches": 1, - "pad_opts": {"constant_values": 255}, + "num_patches": 1, + "constant_values": 255, }, {"image": A}, [np.pad(A[:, :2, 1:], ((0, 0), (1, 0), (0, 0)), mode="constant", constant_values=255)], From 3d00f87eca50d7a2d57815915f45b924400e05f5 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 30 May 2022 15:38:18 +0000 Subject: [PATCH 46/49] Update additional patches Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 242a0bf506..705d95bfdf 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2700,10 +2700,7 @@ def __call__(self, array: NdarrayOrTensor): if self.num_patches: output = output[: self.num_patches] if len(output) < self.num_patches: - if self.pad_kwargs.get("constant_values"): - patch = np.full((array.shape[0], *self.patch_size), self.pad_kwargs["constant_values"]) - else: - patch = np.zeros((array.shape[0], *self.patch_size)) + patch = np.full((array.shape[0], *self.patch_size), self.pad_kwargs.get("constant_values", 0)) slices = np.zeros((3, len(self.patch_size))) output += [(patch, slices)] * (self.num_patches - len(output)) From 46b206993032c3e39b328159a87781030679c3b1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 30 May 2022 16:02:17 +0000 Subject: [PATCH 47/49] Add GridPatchSort Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 13 ++++++++----- monai/utils/__init__.py | 1 + monai/utils/enums.py | 11 +++++++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 705d95bfdf..e8e7a75720 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -62,7 +62,7 @@ pytorch_after, ) from monai.utils.deprecate_utils import deprecated_arg -from monai.utils.enums import TransformBackends +from monai.utils.enums import GridPatchSort, 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 @@ -2662,14 +2662,17 @@ def __init__( self.num_patches = num_patches self.sort_fn: Optional[Callable] if isinstance(sort_fn, str): - if sort_fn == "random": + if sort_fn == GridPatchSort.RANDOM.value: self.sort_fn = np.random.random - if sort_fn == "min": + elif sort_fn == GridPatchSort.MIN.value: self.sort_fn = self.get_patch_sum - if sort_fn == "max": + elif sort_fn == GridPatchSort.MAX.value: self.sort_fn = self.get_negative_patch_sum else: - ValueError(f'sort_fn should be either "min", "max", or "random", "{sort_fn}" was given.') + raise ValueError( + f'sort_fn should be one of the following values, "{sort_fn}" was given:', + [enum.value for enum in GridPatchSort], + ) else: self.sort_fn = sort_fn diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index e7ecab077d..cd8555d173 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -22,6 +22,7 @@ CommonKeys, DiceCEReduction, ForwardMode, + GridPatchSort, GridSampleMode, GridSamplePadMode, InterpolateMode, diff --git a/monai/utils/enums.py b/monai/utils/enums.py index af044f30fe..50b55560f9 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -37,6 +37,7 @@ "ForwardMode", "TransformBackends", "BoxModeName", + "GridPatchSort", ] @@ -329,3 +330,13 @@ class BoxModeName(Enum): XYZWHD = "xyzwhd" # [xmin, ymin, zmin, xsize, ysize, zsize] CCWH = "ccwh" # [xcenter, ycenter, xsize, ysize] CCCWHD = "cccwhd" # [xcenter, ycenter, zcenter, xsize, ysize, zsize] + + +class GridPatchSort(Enum): + """ + The sorting method for the generated patches in `GridPatch` + """ + + RANDOM = "random" + MIN = "min" + MAX = "max" From 3c70c65f93e193949fc4ebef915a6b8745f0a81f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 02:18:49 +0000 Subject: [PATCH 48/49] Fix an issue Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/wsi_datasets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py index 02a172d5b2..83b4fd9fe3 100644 --- a/monai/data/wsi_datasets.py +++ b/monai/data/wsi_datasets.py @@ -246,7 +246,7 @@ def _evaluate_patch_coordinates(self, sample): patch_size_ = tuple(p * downsample for p in patch_size) # patch size at level 0 locations = list( iter_patch_position( - image_size=wsi_size, patch_size=patch_size_, offset=offset, overlap=self.overlap, padded=False + image_size=wsi_size, patch_size=patch_size_, start_pos=offset, overlap=self.overlap, padded=False ) ) sample["size"] = patch_size From 5e8632ea14a76013038195220c2f4d34c4d91303 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 02:32:17 +0000 Subject: [PATCH 49/49] Update randomize Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/spatial/array.py | 5 +++-- monai/transforms/spatial/dictionary.py | 13 ++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index e8e7a75720..eb854c8d23 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -2773,6 +2773,7 @@ def randomize(self, array): self.offset = tuple(self.R.randint(low=low, high=high + 1) for low, high in zip(min_offset, max_offset)) - def __call__(self, array: NdarrayOrTensor): - self.randomize(array) + def __call__(self, array: NdarrayOrTensor, randomize: bool = True): + if randomize: + self.randomize(array) return super().__call__(array) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 9472b93a7f..4a38dbbb59 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -2352,12 +2352,15 @@ def set_random_state( def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> List[Dict]: d = dict(data) original_spatial_shape = d[first(self.keys)].shape[1:] - output, results = [], [] - random_state = self.R # to use the same randomness for all the keys - for key in self.keys: - self.set_random_state(state=random_state) - results.append(self.patcher(d[key])) + # all the keys share the same random noise + first_key: Union[Hashable, List] = self.first_key(d) + if first_key == []: + return [d] + self.patcher.randomize(d[first_key]) # type: ignore + results = [self.patcher(d[key], randomize=False) for key in self.keys] + num_patches = min(len(r) for r in results) + output = [] for patch in zip(*results): new_dict = {k: v[0] for k, v in zip(self.keys, patch)} # fill in the extra keys with unmodified data