From 8fd2509919b432e5da746a790e1c2d106449adc7 Mon Sep 17 00:00:00 2001 From: nhatnm52 Date: Mon, 4 Mar 2024 08:48:05 +0000 Subject: [PATCH 1/7] allow to change downsample function --- ome_zarr/scale.py | 12 +++++++++--- ome_zarr/writer.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ome_zarr/scale.py b/ome_zarr/scale.py index f9cbcd8b..5a6922c6 100644 --- a/ome_zarr/scale.py +++ b/ome_zarr/scale.py @@ -77,9 +77,7 @@ def methods() -> Iterator[str]: def scale(self, input_array: str, output_directory: str) -> None: """Perform downsampling to disk.""" - func = getattr(self, self.method, None) - if not func: - raise Exception + func = self.func store = self.__check_store(output_directory) base = zarr.open_array(input_array) @@ -94,6 +92,14 @@ def scale(self, input_array: str, output_directory: str) -> None: print(f"copying attribute keys: {list(base.attrs.keys())}") grp.attrs.update(base.attrs) + @property + def func(self) -> Callable[[np.ndarray, int, int], np.ndarray]: + """Get downsample function.""" + func = getattr(self, self.method, None) + if not func: + raise Exception + return func + def __check_store(self, output_directory: str) -> MutableMapping: """Return a Zarr store if it doesn't already exist.""" assert not os.path.exists(output_directory) diff --git a/ome_zarr/writer.py b/ome_zarr/writer.py index 83a1d0ae..69efb4d0 100644 --- a/ome_zarr/writer.py +++ b/ome_zarr/writer.py @@ -906,7 +906,7 @@ def _create_mip( "Can't downsample if size of x or y dimension is 1. " "Shape: %s" % (image.shape,) ) - mip = scaler.nearest(image) + mip = scaler.func(image) else: LOGGER.debug("disabling pyramid") mip = [image] From 111d70a275b68687912794c925ebb84b6b17902f Mon Sep 17 00:00:00 2001 From: nhatnm52 Date: Mon, 4 Mar 2024 09:01:16 +0000 Subject: [PATCH 2/7] fix type hint for Callable --- ome_zarr/scale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome_zarr/scale.py b/ome_zarr/scale.py index 5a6922c6..b2ec2bbb 100644 --- a/ome_zarr/scale.py +++ b/ome_zarr/scale.py @@ -93,7 +93,7 @@ def scale(self, input_array: str, output_directory: str) -> None: grp.attrs.update(base.attrs) @property - def func(self) -> Callable[[np.ndarray, int, int], np.ndarray]: + def func(self) -> Callable[[np.ndarray], List[np.ndarray]]: """Get downsample function.""" func = getattr(self, self.method, None) if not func: From eacdca7907840057cd701a9adf91d515a4c91f0d Mon Sep 17 00:00:00 2001 From: nhatnm52 Date: Thu, 14 Mar 2024 15:01:02 +0000 Subject: [PATCH 3/7] test Scale through func property --- tests/test_scaler.py | 73 +++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/tests/test_scaler.py b/tests/test_scaler.py index 4d508613..1299e695 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -8,13 +8,19 @@ class TestScaler: @pytest.fixture( params=( - (1, 2, 1, 256, 256), - (3, 512, 512), - (256, 256), + [(1, 2, 1, 256, 256), True], + [(1, 2, 1, 256, 256), False], + [(3, 512, 512), True], + [(3, 512, 512), False], + [(256, 256), True], + [(256, 256), False], ), - ids=["5D", "3D", "2D"], + ids=["5D-directly", "5D-indirectly", + "3D-directly", "3D-indirectly", + "2D-directly", "2D-indirectly" + ], ) - def shape(self, request): + def test_case(self, request): return request.param def create_data(self, shape, dtype=np.uint8, mean_val=10): @@ -30,42 +36,68 @@ def check_downscaled(self, downscaled, shape, scale_factor=2): sh // scale_factor for sh in expected_shape[-2:] ) - def test_nearest(self, shape): + def test_nearest(self, test_case): + shape, directly = test_case data = self.create_data(shape) scaler = Scaler() - downscaled = scaler.nearest(data) + if directly: + downscaled = scaler.nearest(data) + else: + scaler.method = "nearest" + downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) # this fails because of wrong channel dimension; need to fix in follow-up PR @pytest.mark.xfail - def test_gaussian(self, shape): + def test_gaussian(self, test_case): + shape, directly = test_case data = self.create_data(shape) scaler = Scaler() - downscaled = scaler.gaussian(data) + if directly: + downscaled = scaler.gaussian(data) + else: + scaler.method = "gaussian" + downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) # this fails because of wrong channel dimension; need to fix in follow-up PR @pytest.mark.xfail - def test_laplacian(self, shape): + def test_laplacian(self, test_case): + shape, directly = test_case data = self.create_data(shape) scaler = Scaler() - downscaled = scaler.laplacian(data) + if directly: + downscaled = scaler.laplacian(data) + else: + scaler.method = "laplacian" + downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) - def test_local_mean(self, shape): + def test_local_mean(self, test_case): + shape, directly = test_case data = self.create_data(shape) scaler = Scaler() - downscaled = scaler.local_mean(data) + if directly: + downscaled = scaler.local_mean(data) + else: + scaler.method = "local_mean" + downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) @pytest.mark.skip(reason="This test does not terminate") - def test_zoom(self, shape): + def test_zoom(self, test_case): + shape, directly = test_case data = self.create_data(shape) scaler = Scaler() - downscaled = scaler.zoom(data) + if directly: + downscaled = scaler.zoom(data) + else: + scaler.method = "zoom" + downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) - def test_scale_dask(self, shape): + def test_scale_dask(self, test_case): + shape, directly = test_case data = self.create_data(shape) # chunk size gives odd-shaped chunks at the edges # tests https://github.com/ome/ome-zarr-py/pull/244 @@ -75,8 +107,13 @@ def test_scale_dask(self, shape): data_delayed = da.from_array(data, chunks=chunk_2d) scaler = Scaler() - resized_data = scaler.resize_image(data) - resized_dask = scaler.resize_image(data_delayed) + if directly: + resized_data = scaler.resize_image(data) + resized_dask = scaler.resize_image(data_delayed) + else: + scaler.method = "resize_image" + resized_data = scaler.func(data) + resized_dask = scaler.func(data_delayed) assert np.array_equal(resized_data, resized_dask) From 40f7aeef45eedc0784ed0d1efa51c138fb801f10 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:01:37 +0000 Subject: [PATCH 4/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_scaler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/test_scaler.py b/tests/test_scaler.py index 1299e695..a29ccbca 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -15,10 +15,14 @@ class TestScaler: [(256, 256), True], [(256, 256), False], ), - ids=["5D-directly", "5D-indirectly", - "3D-directly", "3D-indirectly", - "2D-directly", "2D-indirectly" - ], + ids=[ + "5D-directly", + "5D-indirectly", + "3D-directly", + "3D-indirectly", + "2D-directly", + "2D-indirectly", + ], ) def test_case(self, request): return request.param From 96e29b2b6169b4b1c0147783844901328db6a942 Mon Sep 17 00:00:00 2001 From: nhatnm52 Date: Wed, 20 Mar 2024 06:24:03 +0000 Subject: [PATCH 5/7] test func property by pixel-wise --- tests/test_scaler.py | 121 ++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 59 deletions(-) diff --git a/tests/test_scaler.py b/tests/test_scaler.py index a29ccbca..bf588f53 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -8,23 +8,13 @@ class TestScaler: @pytest.fixture( params=( - [(1, 2, 1, 256, 256), True], - [(1, 2, 1, 256, 256), False], - [(3, 512, 512), True], - [(3, 512, 512), False], - [(256, 256), True], - [(256, 256), False], + (1, 2, 1, 256, 256), + (3, 512, 512), + (256, 256), ), - ids=[ - "5D-directly", - "5D-indirectly", - "3D-directly", - "3D-indirectly", - "2D-directly", - "2D-indirectly", - ], + ids=["5D", "3D", "2D"], ) - def test_case(self, request): + def shape(self, request): return request.param def create_data(self, shape, dtype=np.uint8, mean_val=10): @@ -40,68 +30,72 @@ def check_downscaled(self, downscaled, shape, scale_factor=2): sh // scale_factor for sh in expected_shape[-2:] ) - def test_nearest(self, test_case): - shape, directly = test_case + def test_nearest(self, shape): data = self.create_data(shape) scaler = Scaler() - if directly: - downscaled = scaler.nearest(data) - else: - scaler.method = "nearest" - downscaled = scaler.func(data) + downscaled = scaler.nearest(data) self.check_downscaled(downscaled, shape) + def test_nearest_via_method(self, shape): + data = self.create_data(shape) + + scaler = Scaler() + expected_downscaled = scaler.nearest(data) + + scaler.method = "nearest" + downscaled = scaler.func(data) + self.check_downscaled(downscaled, shape) + + assert np.sum([ + not np.array_equal(downscaled[i], expected_downscaled[i]) + for i in range(len(downscaled)) + ]) == 0 + # this fails because of wrong channel dimension; need to fix in follow-up PR @pytest.mark.xfail - def test_gaussian(self, test_case): - shape, directly = test_case + def test_gaussian(self, shape): data = self.create_data(shape) scaler = Scaler() - if directly: - downscaled = scaler.gaussian(data) - else: - scaler.method = "gaussian" - downscaled = scaler.func(data) + downscaled = scaler.gaussian(data) self.check_downscaled(downscaled, shape) # this fails because of wrong channel dimension; need to fix in follow-up PR @pytest.mark.xfail - def test_laplacian(self, test_case): - shape, directly = test_case + def test_laplacian(self, shape): data = self.create_data(shape) scaler = Scaler() - if directly: - downscaled = scaler.laplacian(data) - else: - scaler.method = "laplacian" - downscaled = scaler.func(data) + downscaled = scaler.laplacian(data) self.check_downscaled(downscaled, shape) - def test_local_mean(self, test_case): - shape, directly = test_case + def test_nearest(self, shape): data = self.create_data(shape) scaler = Scaler() - if directly: - downscaled = scaler.local_mean(data) - else: - scaler.method = "local_mean" - downscaled = scaler.func(data) + downscaled = scaler.local_mean(data) self.check_downscaled(downscaled, shape) + def test_local_mean_via_method(self, shape): + data = self.create_data(shape) + + scaler = Scaler() + expected_downscaled = scaler.local_mean(data) + + scaler.method = "local_mean" + downscaled = scaler.func(data) + self.check_downscaled(downscaled, shape) + + assert np.sum([ + not np.array_equal(downscaled[i], expected_downscaled[i]) + for i in range(len(downscaled)) + ]) == 0 + @pytest.mark.skip(reason="This test does not terminate") - def test_zoom(self, test_case): - shape, directly = test_case + def test_zoom(self, shape): data = self.create_data(shape) scaler = Scaler() - if directly: - downscaled = scaler.zoom(data) - else: - scaler.method = "zoom" - downscaled = scaler.func(data) + downscaled = scaler.zoom(data) self.check_downscaled(downscaled, shape) - def test_scale_dask(self, test_case): - shape, directly = test_case + def test_scale_dask(self, shape): data = self.create_data(shape) # chunk size gives odd-shaped chunks at the edges # tests https://github.com/ome/ome-zarr-py/pull/244 @@ -111,16 +105,25 @@ def test_scale_dask(self, test_case): data_delayed = da.from_array(data, chunks=chunk_2d) scaler = Scaler() - if directly: - resized_data = scaler.resize_image(data) - resized_dask = scaler.resize_image(data_delayed) - else: - scaler.method = "resize_image" - resized_data = scaler.func(data) - resized_dask = scaler.func(data_delayed) + resized_data = scaler.resize_image(data) + resized_dask = scaler.resize_image(data_delayed) assert np.array_equal(resized_data, resized_dask) + def test_scale_dask_via_method(self, shape): + data = self.create_data(shape) + + chunk_size = [100, 100] + chunk_2d = (*(1,) * (data.ndim - 2), *chunk_size) + data_delayed = da.from_array(data, chunks=chunk_2d) + + scaler = Scaler() + expected_downscaled = scaler.resize_image(data) + + scaler.method = "resize_image" + assert np.array_equal(expected_downscaled, scaler.func(data)) + assert np.array_equal(expected_downscaled, scaler.func(data_delayed)) + def test_big_dask_pyramid(self, tmpdir): # from https://github.com/ome/omero-cli-zarr/pull/134 shape = (6675, 9560) From ad72115adf981071048e1ce9295ac4495e46c4e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 06:24:27 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_scaler.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_scaler.py b/tests/test_scaler.py index bf588f53..a3982a68 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -46,10 +46,15 @@ def test_nearest_via_method(self, shape): downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) - assert np.sum([ - not np.array_equal(downscaled[i], expected_downscaled[i]) - for i in range(len(downscaled)) - ]) == 0 + assert ( + np.sum( + [ + not np.array_equal(downscaled[i], expected_downscaled[i]) + for i in range(len(downscaled)) + ] + ) + == 0 + ) # this fails because of wrong channel dimension; need to fix in follow-up PR @pytest.mark.xfail @@ -83,10 +88,15 @@ def test_local_mean_via_method(self, shape): downscaled = scaler.func(data) self.check_downscaled(downscaled, shape) - assert np.sum([ - not np.array_equal(downscaled[i], expected_downscaled[i]) - for i in range(len(downscaled)) - ]) == 0 + assert ( + np.sum( + [ + not np.array_equal(downscaled[i], expected_downscaled[i]) + for i in range(len(downscaled)) + ] + ) + == 0 + ) @pytest.mark.skip(reason="This test does not terminate") def test_zoom(self, shape): From 93428aa7555e59c351e8dda3baa5dbfab60c6a6a Mon Sep 17 00:00:00 2001 From: nhatnm52 Date: Wed, 20 Mar 2024 06:32:43 +0000 Subject: [PATCH 7/7] fix bug: error function name --- tests/test_scaler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scaler.py b/tests/test_scaler.py index a3982a68..93ddc726 100644 --- a/tests/test_scaler.py +++ b/tests/test_scaler.py @@ -72,7 +72,7 @@ def test_laplacian(self, shape): downscaled = scaler.laplacian(data) self.check_downscaled(downscaled, shape) - def test_nearest(self, shape): + def test_local_mean(self, shape): data = self.create_data(shape) scaler = Scaler() downscaled = scaler.local_mean(data)