Skip to content

Commit af0fba5

Browse files
authored
Merge pull request #516 from jo-mueller/clean-scalers
deprecate scaler class
2 parents 87f142f + a081d53 commit af0fba5

File tree

11 files changed

+927
-404
lines changed

11 files changed

+927
-404
lines changed

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ numpy
77
rangehttpserver
88
scipy
99
scikit-image
10+
Deprecated

docs/source/cli.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,36 @@ Specify a different output directory::
5656

5757
ome_zarr download https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0062A/6001240_labels.zarr --output image_dir
5858

59+
scale
60+
=====
61+
62+
Use the `ome_zarr scale` command to generate a multiscale pyramid from a Zarr array.
63+
This creates downsampled resolutions of the input image using the specified downsampling method.
64+
65+
Basic usage::
66+
67+
ome_zarr scale input.zarr output.zarr zyx
68+
69+
This reads the input Zarr array with dimensions 'zyx' and writes a multiscale pyramid to the output directory.
70+
71+
Options:
72+
73+
- `--downscale`: The downsampling factor (default: 2). For example, `--downscale 2` creates scale factors of 2, 4, 8, etc.
74+
- `--max_layer`: Maximum number of pyramid levels to generate (default: 4)
75+
- `--method`: Downsampling method to use (default: "resize"). Options are:
76+
- `nearest`: Nearest neighbor
77+
- `resize`: Resize with anti-aliasing (default)
78+
- `laplacian`: Laplacian pyramid
79+
- `local_mean`: Local mean
80+
- `zoom`: Scipy zoom
81+
- `--copy-metadata`: If specified, copies the input array metadata to the output group
82+
83+
Example with custom options::
84+
85+
ome_zarr scale input.zarr output.zarr tczyx --downscale 3 --max_layer 5 --method nearest --copy-metadata
86+
87+
88+
5989
create
6090
======
6191

docs/source/python.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,47 @@ This image can be viewed in `napari` using the
3838

3939
$ napari test_ngff_image.zarr
4040

41+
Building a pyramid
42+
------------------
43+
44+
Multi-resolution pyramids are an integral part of ome-zarr image data
45+
and enable fast rendering of large images.
46+
The entrypoints to writing ome-zarr images in ome-zarr-py (`write_image` and `write_labels`)
47+
build these pyramids under the hood as delayed dask arrays based on the settings for the scaling functions and scale factors.
48+
49+
The scale factors can be passed as a list of integers or a list of dicts::
50+
51+
from ome_zarr.writer import write_image
52+
53+
scale_factors = [2, 4, 8]
54+
write_image(
55+
your_data
56+
path,
57+
axes="zyx",
58+
scale_factors=scale_factors,
59+
)
60+
61+
In this example, the downsampling will be applied in all spatial dimensions *except the z dimension*, which will be left at a scale factor of 1.
62+
To apply equal or custom downsampling factors along all spatial dimensions, pass the scale factors as a list of dicts, e.g.::
63+
64+
from ome_zarr.writer import write_image
65+
66+
scale_factors = [
67+
{"z": 2, "y": 2, "x": 2},
68+
{"z": 4, "y": 4, "x": 4},
69+
{"z": 8, "y": 8, "x": 8}
70+
]
71+
write_image(
72+
your_data
73+
path,
74+
axes="zyx",
75+
scale_factors=scale_factors,
76+
)
77+
78+
If you have already built a pyramid representation by other means,
79+
you can pass it directly to the :py:func:`ome_zarr.writer.write_multiscale` or use :py:func:`ome_zarr.writer.write_multiscale_labels`,
80+
which do not perform any down-sampling but just write the passed pyramid to disk with the correct metadata.
81+
4182
Rendering settings
4283
------------------
4384
Rendering settings can be added to an existing zarr group::

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
disallow_untyped_defs = False
33
ignore_missing_imports = True
44

5+
[mypy-deprecated.*]
6+
ignore_missing_imports = True
7+
58
[mypy-ome_zarr]
69
disallow_untyped_defs = True

ome_zarr/cli.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from .csv import csv_to_zarr
88
from .data import astronaut, coins, create_zarr
99
from .format import CurrentFormat, Format, format_from_version
10-
from .scale import Scaler
1110
from .utils import download as zarr_download
1211
from .utils import finder as bff_finder
1312
from .utils import info as zarr_info
@@ -72,16 +71,30 @@ def create(args: argparse.Namespace) -> None:
7271

7372

7473
def scale(args: argparse.Namespace) -> None:
75-
"""Wrap the :func:`~ome_zarr.scale.Scaler.scale` method."""
76-
scaler = Scaler(
77-
copy_metadata=args.copy_metadata,
78-
downscale=args.downscale,
79-
in_place=args.in_place,
80-
labeled=args.labeled,
81-
max_layer=args.max_layer,
74+
import dask.array as da
75+
import zarr
76+
77+
from .writer import write_image
78+
79+
"""Wrap the :func:`~ome_zarr.scale._build_pyramid` method."""
80+
base = zarr.open_array(args.input_array, mode="r")
81+
scale_factors = tuple(args.downscale**i for i in range(1, args.max_layer + 1))
82+
83+
data = da.from_zarr(args.input_array)
84+
85+
write_image(
86+
data,
87+
args.output_directory,
88+
axes=str(args.axes),
8289
method=args.method,
90+
scale_factors=scale_factors,
8391
)
84-
scaler.scale(args.input_array, args.output_directory)
92+
93+
grp = zarr.open_group(args.output_directory, mode="a")
94+
95+
if args.copy_metadata:
96+
print(f"copying attribute keys: {list(base.attrs.keys())}")
97+
grp.attrs.update(base.attrs)
8598

8699

87100
def csv_to_labels(args: argparse.Namespace) -> None:
@@ -167,23 +180,24 @@ def main(args: list[str] | None = None) -> None:
167180
parser_scale.add_argument("input_array")
168181
parser_scale.add_argument("output_directory")
169182
parser_scale.add_argument(
170-
"--labeled",
171-
action="store_true",
172-
help="assert that the list of unique pixel values doesn't change",
183+
"axes", type=str, help="Dimensions of input data, i.e. 'zyx' or 'tczyx'."
173184
)
174185
parser_scale.add_argument(
175186
"--copy-metadata",
176187
action="store_true",
177188
help="copies the array metadata to the new group",
178189
)
179190
parser_scale.add_argument(
180-
"--method", choices=list(Scaler.methods()), default="nearest"
191+
"--method",
192+
choices=["nearest", "resize", "laplacian", "local_mean", "zoom"],
193+
default="resize",
181194
)
182195
parser_scale.add_argument(
183196
"--in-place", action="store_true", help="if true, don't write the base array"
184197
)
185198
parser_scale.add_argument("--downscale", type=int, default=2)
186199
parser_scale.add_argument("--max_layer", type=int, default=4)
200+
187201
parser_scale.set_defaults(func=scale)
188202

189203
# csv to label properties

ome_zarr/dask_utils.py

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections.abc import Sequence
12
from typing import Any
23

34
import numpy as np
@@ -8,15 +9,37 @@
89
# See https://github.com/toloudis/ome-zarr-py/pull/1
910

1011

12+
def _better_chunksize(
13+
image: da.Array, factors: np.ndarray
14+
) -> tuple[Sequence[int], Sequence[int]]:
15+
better_chunksize = tuple(
16+
np.maximum(1, np.round(np.array(image.chunksize) * factors) / factors).astype(
17+
int
18+
)
19+
)
20+
21+
# If E.g. we resize image from 6675 by 0.5 to 3337, factor is 0.49992509 so each
22+
# chunk of size e.g. 1000 will resize to 499. When assumbled into a new array, the
23+
# array will now be of size 3331 instead of 3337 because each of 6 chunks was
24+
# smaller by 1. When we compute() this, dask will read 6 chunks of 1000 and expect
25+
# last chunk to be 337 but instead it will only be 331.
26+
# So we use ceil() here (and in resize_block) to round 499.925 up to chunk of 500
27+
block_output_shape = tuple(
28+
np.ceil(np.array(better_chunksize) * factors).astype(int)
29+
)
30+
31+
return better_chunksize, block_output_shape
32+
33+
1134
def resize(
12-
image: da.Array, output_shape: tuple[int, ...], *args: Any, **kwargs: Any
35+
image: da.Array, output_shape: Sequence[int], *args: Any, **kwargs: Any
1336
) -> da.Array:
1437
r"""
1538
Wrapped copy of "skimage.transform.resize"
1639
Resize image to match a certain size.
1740
:type image: :class:`dask.array`
1841
:param image: The dask array to resize
19-
:type output_shape: tuple
42+
:type output_shape: Sequence[int]
2043
:param output_shape: The shape of the resize array
2144
:type \*args: list
2245
:param \*args: Arguments of skimage.transform.resize
@@ -27,23 +50,9 @@ def resize(
2750
factors = np.array(output_shape) / np.array(image.shape).astype(float)
2851
# Rechunk the input blocks so that the factors achieve an output
2952
# blocks size of full numbers.
30-
better_chunksize = tuple(
31-
np.maximum(1, np.round(np.array(image.chunksize) * factors) / factors).astype(
32-
int
33-
)
34-
)
53+
better_chunksize, block_output_shape = _better_chunksize(image, factors)
3554
image_prepared = image.rechunk(better_chunksize)
3655

37-
# If E.g. we resize image from 6675 by 0.5 to 3337, factor is 0.49992509 so each
38-
# chunk of size e.g. 1000 will resize to 499. When assumbled into a new array, the
39-
# array will now be of size 3331 instead of 3337 because each of 6 chunks was
40-
# smaller by 1. When we compute() this, dask will read 6 chunks of 1000 and expect
41-
# last chunk to be 337 but instead it will only be 331.
42-
# So we use ceil() here (and in resize_block) to round 499.925 up to chunk of 500
43-
block_output_shape = tuple(
44-
np.ceil(np.array(better_chunksize) * factors).astype(int)
45-
)
46-
4756
# Map overlap
4857
def resize_block(image_block: da.Array, block_info: dict) -> da.Array:
4958
# if the input block is smaller than a 'regular' chunk (e.g. edge of image)
@@ -62,6 +71,65 @@ def resize_block(image_block: da.Array, block_info: dict) -> da.Array:
6271
return output.rechunk(image.chunksize).astype(image.dtype)
6372

6473

74+
def local_mean(
75+
image: da.Array, output_shape: Sequence[int], *args, **kwargs
76+
) -> da.Array:
77+
"""
78+
Local mean downscaling.
79+
80+
:type image: :class:`dask.array.Array`
81+
:param image: The dask array to resize
82+
:type output_shape: Sequence[int]
83+
:param output_shape: The shape of the resize array
84+
:return: Resized image.
85+
"""
86+
from skimage.transform import downscale_local_mean
87+
88+
factors = np.array(image.shape).astype(float) / np.array(output_shape)
89+
better_chunksize, block_output_shape = _better_chunksize(image, 1 / factors)
90+
image_prepared = image.rechunk(better_chunksize)
91+
92+
def local_mean_block(image_block: da.Array) -> da.Array:
93+
local_mean = downscale_local_mean(
94+
image_block, tuple(factors.astype(int)), *args, **kwargs
95+
)
96+
return local_mean.astype(image_block.dtype)
97+
98+
output_slices = tuple(slice(0, d) for d in output_shape)
99+
output = da.map_blocks(
100+
local_mean_block, image_prepared, dtype=image.dtype, chunks=block_output_shape
101+
)[output_slices]
102+
return output.rechunk(image.chunksize).astype(image.dtype)
103+
104+
105+
def zoom(image: da.Array, output_shape: Sequence[int], *args, **kwargs) -> da.Array:
106+
"""
107+
Scipy zoom downscaling by integer factors.
108+
109+
:type image: :class:`dask.array.Array`
110+
:param image: The dask array to resize
111+
:type output_shape: Sequence[int]
112+
:param output_shape: The shape of the resize array
113+
:return: Resized image.
114+
"""
115+
from scipy.ndimage import zoom
116+
117+
factors = np.array(image.shape).astype(float) / np.array(output_shape)
118+
better_chunksize, block_output_shape = _better_chunksize(image, 1 / factors)
119+
image_prepared = image.rechunk(better_chunksize)
120+
121+
def zoom_block(image_block: da.Array) -> da.Array:
122+
zoomed = zoom(image_block, 1 / factors, order=1)
123+
return zoomed.astype(image_block.dtype)
124+
125+
output_shape = tuple(d // f for d, f in zip(image.shape, factors))
126+
output_slices = tuple(slice(0, d) for d in output_shape)
127+
output = da.map_blocks(
128+
zoom_block, image_prepared, dtype=image.dtype, chunks=block_output_shape
129+
)[output_slices]
130+
return output.rechunk(image.chunksize).astype(image.dtype)
131+
132+
65133
def downscale_nearest(image: da.Array, factors: tuple[int, ...]) -> da.Array:
66134
"""
67135
Primitive downscaling by integer factors using stepped slicing.

0 commit comments

Comments
 (0)