ngio 0.4.0a2__py3-none-any.whl → 0.4.0a4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ngio/__init__.py +1 -2
- ngio/common/__init__.py +2 -51
- ngio/common/_dimensions.py +223 -64
- ngio/common/_pyramid.py +42 -23
- ngio/common/_roi.py +94 -418
- ngio/common/_zoom.py +32 -7
- ngio/experimental/iterators/_abstract_iterator.py +2 -2
- ngio/experimental/iterators/_feature.py +10 -15
- ngio/experimental/iterators/_image_processing.py +18 -28
- ngio/experimental/iterators/_rois_utils.py +6 -6
- ngio/experimental/iterators/_segmentation.py +38 -54
- ngio/images/_abstract_image.py +136 -94
- ngio/images/_create.py +16 -0
- ngio/images/_create_synt_container.py +10 -0
- ngio/images/_image.py +33 -9
- ngio/images/_label.py +24 -3
- ngio/images/_masked_image.py +60 -81
- ngio/images/_ome_zarr_container.py +34 -1
- ngio/io_pipes/__init__.py +49 -0
- ngio/io_pipes/_io_pipes.py +286 -0
- ngio/io_pipes/_io_pipes_masked.py +481 -0
- ngio/io_pipes/_io_pipes_roi.py +143 -0
- ngio/io_pipes/_io_pipes_utils.py +299 -0
- ngio/io_pipes/_match_shape.py +376 -0
- ngio/io_pipes/_ops_axes.py +146 -0
- ngio/io_pipes/_ops_slices.py +218 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +175 -0
- ngio/ome_zarr_meta/__init__.py +6 -2
- ngio/ome_zarr_meta/ngio_specs/__init__.py +6 -4
- ngio/ome_zarr_meta/ngio_specs/_axes.py +182 -70
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +47 -121
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +30 -22
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +17 -1
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +33 -30
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/__init__.py +1 -0
- ngio/resources/resource_model.py +1 -0
- ngio/tables/v1/_roi_table.py +11 -3
- ngio/{common/transforms → transforms}/__init__.py +1 -1
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/_zarr_utils.py +5 -1
- {ngio-0.4.0a2.dist-info → ngio-0.4.0a4.dist-info}/METADATA +1 -1
- ngio-0.4.0a4.dist-info/RECORD +83 -0
- ngio/common/_array_io_pipes.py +0 -554
- ngio/common/_array_io_utils.py +0 -508
- ngio/common/transforms/_label.py +0 -12
- ngio/common/transforms/_zoom.py +0 -109
- ngio-0.4.0a2.dist-info/RECORD +0 -76
- {ngio-0.4.0a2.dist-info → ngio-0.4.0a4.dist-info}/WHEEL +0 -0
- {ngio-0.4.0a2.dist-info → ngio-0.4.0a4.dist-info}/licenses/LICENSE +0 -0
ngio/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ except PackageNotFoundError: # pragma: no cover
|
|
|
9
9
|
__author__ = "Lorenzo Cerrone"
|
|
10
10
|
__email__ = "lorenzo.cerrone@uzh.ch"
|
|
11
11
|
|
|
12
|
-
from ngio.common import
|
|
12
|
+
from ngio.common import Dimensions, Roi, RoiPixels
|
|
13
13
|
from ngio.hcs import (
|
|
14
14
|
OmeZarrPlate,
|
|
15
15
|
OmeZarrWell,
|
|
@@ -39,7 +39,6 @@ from ngio.ome_zarr_meta.ngio_specs import (
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
__all__ = [
|
|
42
|
-
"ArrayLike",
|
|
43
42
|
"AxesSetup",
|
|
44
43
|
"ChannelSelectionModel",
|
|
45
44
|
"DefaultNgffVersion",
|
ngio/common/__init__.py
CHANGED
|
@@ -1,72 +1,23 @@
|
|
|
1
1
|
"""Common classes and functions that are used across the package."""
|
|
2
2
|
|
|
3
|
-
from ngio.common._array_io_pipes import (
|
|
4
|
-
build_dask_getter,
|
|
5
|
-
build_dask_setter,
|
|
6
|
-
build_masked_dask_getter,
|
|
7
|
-
build_masked_dask_setter,
|
|
8
|
-
build_masked_numpy_getter,
|
|
9
|
-
build_masked_numpy_setter,
|
|
10
|
-
build_numpy_getter,
|
|
11
|
-
build_numpy_setter,
|
|
12
|
-
)
|
|
13
|
-
from ngio.common._array_io_utils import (
|
|
14
|
-
ArrayLike,
|
|
15
|
-
SlicingInputType,
|
|
16
|
-
TransformProtocol,
|
|
17
|
-
apply_dask_axes_ops,
|
|
18
|
-
apply_numpy_axes_ops,
|
|
19
|
-
apply_sequence_axes_ops,
|
|
20
|
-
)
|
|
21
3
|
from ngio.common._dimensions import Dimensions
|
|
22
4
|
from ngio.common._masking_roi import compute_masking_roi
|
|
23
5
|
from ngio.common._pyramid import consolidate_pyramid, init_empty_pyramid, on_disk_zoom
|
|
24
6
|
from ngio.common._roi import (
|
|
25
7
|
Roi,
|
|
26
8
|
RoiPixels,
|
|
27
|
-
build_roi_dask_getter,
|
|
28
|
-
build_roi_dask_setter,
|
|
29
|
-
build_roi_masked_dask_getter,
|
|
30
|
-
build_roi_masked_dask_setter,
|
|
31
|
-
build_roi_masked_numpy_getter,
|
|
32
|
-
build_roi_masked_numpy_setter,
|
|
33
|
-
build_roi_numpy_getter,
|
|
34
|
-
build_roi_numpy_setter,
|
|
35
|
-
roi_to_slicing_dict,
|
|
36
9
|
)
|
|
37
|
-
from ngio.common._zoom import dask_zoom, numpy_zoom
|
|
10
|
+
from ngio.common._zoom import InterpolationOrder, dask_zoom, numpy_zoom
|
|
38
11
|
|
|
39
12
|
__all__ = [
|
|
40
|
-
"ArrayLike",
|
|
41
13
|
"Dimensions",
|
|
14
|
+
"InterpolationOrder",
|
|
42
15
|
"Roi",
|
|
43
16
|
"RoiPixels",
|
|
44
|
-
"SlicingInputType",
|
|
45
|
-
"TransformProtocol",
|
|
46
|
-
"apply_dask_axes_ops",
|
|
47
|
-
"apply_numpy_axes_ops",
|
|
48
|
-
"apply_sequence_axes_ops",
|
|
49
|
-
"build_dask_getter",
|
|
50
|
-
"build_dask_setter",
|
|
51
|
-
"build_masked_dask_getter",
|
|
52
|
-
"build_masked_dask_setter",
|
|
53
|
-
"build_masked_numpy_getter",
|
|
54
|
-
"build_masked_numpy_setter",
|
|
55
|
-
"build_numpy_getter",
|
|
56
|
-
"build_numpy_setter",
|
|
57
|
-
"build_roi_dask_getter",
|
|
58
|
-
"build_roi_dask_setter",
|
|
59
|
-
"build_roi_masked_dask_getter",
|
|
60
|
-
"build_roi_masked_dask_setter",
|
|
61
|
-
"build_roi_masked_numpy_getter",
|
|
62
|
-
"build_roi_masked_numpy_setter",
|
|
63
|
-
"build_roi_numpy_getter",
|
|
64
|
-
"build_roi_numpy_setter",
|
|
65
17
|
"compute_masking_roi",
|
|
66
18
|
"consolidate_pyramid",
|
|
67
19
|
"dask_zoom",
|
|
68
20
|
"init_empty_pyramid",
|
|
69
21
|
"numpy_zoom",
|
|
70
22
|
"on_disk_zoom",
|
|
71
|
-
"roi_to_slicing_dict",
|
|
72
23
|
]
|
ngio/common/_dimensions.py
CHANGED
|
@@ -4,88 +4,68 @@ This is not related to the NGFF metadata,
|
|
|
4
4
|
but it is based on the actual metadata of the image data.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import math
|
|
7
8
|
from typing import overload
|
|
8
9
|
|
|
9
|
-
from ngio.ome_zarr_meta import
|
|
10
|
-
|
|
10
|
+
from ngio.ome_zarr_meta import (
|
|
11
|
+
AxesHandler,
|
|
12
|
+
)
|
|
13
|
+
from ngio.ome_zarr_meta.ngio_specs._dataset import Dataset
|
|
14
|
+
from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
11
15
|
from ngio.utils import NgioValueError
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class Dimensions:
|
|
15
|
-
"""Dimension metadata.
|
|
19
|
+
"""Dimension metadata Handling Class.
|
|
20
|
+
|
|
21
|
+
This class is used to handle and manipulate dimension metadata.
|
|
22
|
+
It provides methods to access and validate dimension information,
|
|
23
|
+
such as shape, axes, and properties like is_2d, is_3d, is_time_series, etc.
|
|
24
|
+
"""
|
|
16
25
|
|
|
17
26
|
def __init__(
|
|
18
27
|
self,
|
|
19
28
|
shape: tuple[int, ...],
|
|
20
|
-
|
|
29
|
+
dataset: Dataset,
|
|
21
30
|
) -> None:
|
|
22
31
|
"""Create a Dimension object from a Zarr array.
|
|
23
32
|
|
|
24
33
|
Args:
|
|
25
34
|
shape: The shape of the Zarr array.
|
|
26
|
-
|
|
35
|
+
dataset: The dataset object.
|
|
27
36
|
"""
|
|
28
37
|
self._shape = shape
|
|
29
|
-
self.
|
|
38
|
+
self._axes_handler = dataset.axes_handler
|
|
39
|
+
self._pixel_size = dataset.pixel_size
|
|
30
40
|
|
|
31
|
-
if len(self._shape) != len(self.
|
|
41
|
+
if len(self._shape) != len(self._axes_handler.axes):
|
|
32
42
|
raise NgioValueError(
|
|
33
43
|
"The number of dimensions must match the number of axes. "
|
|
34
|
-
f"Expected Axis {self.
|
|
44
|
+
f"Expected Axis {self._axes_handler.axes_names} but got shape "
|
|
35
45
|
f"{self._shape}."
|
|
36
46
|
)
|
|
37
47
|
|
|
38
48
|
def __str__(self) -> str:
|
|
39
49
|
"""Return the string representation of the object."""
|
|
40
50
|
dims = ", ".join(
|
|
41
|
-
f"{ax.
|
|
42
|
-
for ax, s in zip(self.
|
|
51
|
+
f"{ax.name}: {s}"
|
|
52
|
+
for ax, s in zip(self._axes_handler.axes, self._shape, strict=True)
|
|
43
53
|
)
|
|
44
54
|
return f"Dimensions({dims})"
|
|
45
55
|
|
|
46
|
-
@overload
|
|
47
|
-
def get(self, axis_name: str, default: None = None) -> int | None:
|
|
48
|
-
pass
|
|
49
|
-
|
|
50
|
-
@overload
|
|
51
|
-
def get(self, axis_name: str, default: int) -> int:
|
|
52
|
-
pass
|
|
53
|
-
|
|
54
|
-
def get(self, axis_name: str, default: int | None = None) -> int | None:
|
|
55
|
-
"""Return the dimension of the given axis name.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
axis_name: The name of the axis (either canonical or non-canonical).
|
|
59
|
-
default: The default value to return if the axis does not exist.
|
|
60
|
-
"""
|
|
61
|
-
index = self._axes_mapper.get_index(axis_name)
|
|
62
|
-
if index is None:
|
|
63
|
-
return default
|
|
64
|
-
return self._shape[index]
|
|
65
|
-
|
|
66
|
-
def get_index(self, axis_name: str) -> int | None:
|
|
67
|
-
"""Return the index of the given axis name.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
axis_name: The name of the axis (either canonical or non-canonical).
|
|
71
|
-
"""
|
|
72
|
-
return self._axes_mapper.get_index(axis_name)
|
|
73
|
-
|
|
74
|
-
def has_axis(self, axis_name: str) -> bool:
|
|
75
|
-
"""Return whether the axis exists."""
|
|
76
|
-
index = self._axes_mapper.get_axis(axis_name)
|
|
77
|
-
if index is None:
|
|
78
|
-
return False
|
|
79
|
-
return True
|
|
80
|
-
|
|
81
56
|
def __repr__(self) -> str:
|
|
82
57
|
"""Return the string representation of the object."""
|
|
83
58
|
return str(self)
|
|
84
59
|
|
|
85
60
|
@property
|
|
86
|
-
def
|
|
87
|
-
"""Return the axes
|
|
88
|
-
return self.
|
|
61
|
+
def axes_handler(self) -> AxesHandler:
|
|
62
|
+
"""Return the axes handler object."""
|
|
63
|
+
return self._axes_handler
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def pixel_size(self) -> PixelSize:
|
|
67
|
+
"""Return the pixel size object."""
|
|
68
|
+
return self._pixel_size
|
|
89
69
|
|
|
90
70
|
@property
|
|
91
71
|
def shape(self) -> tuple[int, ...]:
|
|
@@ -95,7 +75,7 @@ class Dimensions:
|
|
|
95
75
|
@property
|
|
96
76
|
def axes(self) -> tuple[str, ...]:
|
|
97
77
|
"""Return the axes as a tuple of strings."""
|
|
98
|
-
return self.
|
|
78
|
+
return self.axes_handler.axes_names
|
|
99
79
|
|
|
100
80
|
@property
|
|
101
81
|
def is_time_series(self) -> bool:
|
|
@@ -133,24 +113,203 @@ class Dimensions:
|
|
|
133
113
|
return False
|
|
134
114
|
return True
|
|
135
115
|
|
|
136
|
-
|
|
137
|
-
|
|
116
|
+
@overload
|
|
117
|
+
def get(self, axis_name: str, default: None = None) -> int | None:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def get(self, axis_name: str, default: int) -> int:
|
|
122
|
+
pass
|
|
138
123
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
124
|
+
def get(self, axis_name: str, default: int | None = None) -> int | None:
|
|
125
|
+
"""Return the dimension of the given axis name.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
axis_name: The name of the axis (either canonical or non-canonical).
|
|
129
|
+
default: The default value to return if the axis does not exist.
|
|
142
130
|
"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
index = self.axes_handler.get_index(axis_name)
|
|
132
|
+
if index is None:
|
|
133
|
+
return default
|
|
134
|
+
return self._shape[index]
|
|
135
|
+
|
|
136
|
+
def get_index(self, axis_name: str) -> int | None:
|
|
137
|
+
"""Return the index of the given axis name.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
axis_name: The name of the axis (either canonical or non-canonical).
|
|
141
|
+
"""
|
|
142
|
+
return self.axes_handler.get_index(axis_name)
|
|
143
|
+
|
|
144
|
+
def has_axis(self, axis_name: str) -> bool:
|
|
145
|
+
"""Return whether the axis exists."""
|
|
146
|
+
index = self.axes_handler.get_index(axis_name)
|
|
147
|
+
if index is None:
|
|
146
148
|
return False
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def require_axes_match(self, other: "Dimensions") -> None:
|
|
152
|
+
"""Check if two Dimensions objects have the same axes.
|
|
153
|
+
|
|
154
|
+
Besides the channel axis (which is a special case), all axes must be
|
|
155
|
+
present in both Dimensions objects.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
other (Dimensions): The other dimensions object to compare against.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
NgioValueError: If the axes do not match.
|
|
162
|
+
"""
|
|
163
|
+
require_axes_match(self, other)
|
|
164
|
+
|
|
165
|
+
def require_dimensions_match(
|
|
166
|
+
self, other: "Dimensions", allow_singleton: bool = False
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Check if two Dimensions objects have the same axes and dimensions.
|
|
169
|
+
|
|
170
|
+
Besides the channel axis, all axes must have the same dimension in
|
|
171
|
+
both images.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
other (Dimensions): The other dimensions object to compare against.
|
|
175
|
+
allow_singleton (bool): Whether to allow singleton dimensions to be
|
|
176
|
+
different. For example, if the input image has shape
|
|
177
|
+
(5, 100, 100) and the label has shape (1, 100, 100).
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
NgioValueError: If the dimensions do not match.
|
|
181
|
+
"""
|
|
182
|
+
require_dimensions_match(self, other, allow_singleton)
|
|
183
|
+
|
|
184
|
+
def require_can_be_rescaled(self, other: "Dimensions") -> None:
|
|
185
|
+
"""Assert that two images can be rescaled.
|
|
186
|
+
|
|
187
|
+
For this to be true, the images must have the same axes, and
|
|
188
|
+
the pixel sizes must be compatible (i.e. one can be scaled to the other).
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
other (Dimensions): The other dimensions object to compare against.
|
|
192
|
+
|
|
193
|
+
"""
|
|
194
|
+
require_rescalable(self, other)
|
|
147
195
|
|
|
148
|
-
|
|
149
|
-
|
|
196
|
+
|
|
197
|
+
def _are_compatible(shape1: int, shape2: int, scaling: float) -> bool:
|
|
198
|
+
"""Check if shape2 is consistent with shape1 given pixel sizes.
|
|
199
|
+
|
|
200
|
+
Since we only deal with shape discrepancies due to rounding, we
|
|
201
|
+
shape1, needs to be larger than shape2.
|
|
202
|
+
"""
|
|
203
|
+
if shape1 < shape2:
|
|
204
|
+
return _are_compatible(shape2, shape1, 1 / scaling)
|
|
205
|
+
expected_shape2 = shape1 * scaling
|
|
206
|
+
expected_shape2_floor = math.floor(expected_shape2)
|
|
207
|
+
expected_shape2_ceil = math.ceil(expected_shape2)
|
|
208
|
+
return shape2 in {expected_shape2_floor, expected_shape2_ceil}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def require_axes_match(dimensions1: Dimensions, dimensions2: Dimensions) -> None:
|
|
212
|
+
"""Check if two Dimensions objects have the same axes.
|
|
213
|
+
|
|
214
|
+
Besides the channel axis (which is a special case), all axes must be
|
|
215
|
+
present in both Dimensions objects.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
dimensions1 (Dimensions): The first dimensions object to compare against.
|
|
219
|
+
dimensions2 (Dimensions): The second dimensions object to compare against.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
NgioValueError: If the axes do not match.
|
|
223
|
+
"""
|
|
224
|
+
for s_axis in dimensions1.axes_handler.axes:
|
|
225
|
+
if s_axis.axis_type == "channel":
|
|
226
|
+
continue
|
|
227
|
+
o_axis = dimensions2.axes_handler.get_axis(s_axis.name)
|
|
228
|
+
if o_axis is None:
|
|
229
|
+
raise NgioValueError(
|
|
230
|
+
f"Axes do not match. The axis {s_axis.name} "
|
|
231
|
+
f"is not present in either dimensions."
|
|
232
|
+
)
|
|
233
|
+
# Check for axes present in the other dimensions but not in this one
|
|
234
|
+
for o_axis in dimensions2.axes_handler.axes:
|
|
235
|
+
if o_axis.axis_type == "channel":
|
|
236
|
+
continue
|
|
237
|
+
s_axis = dimensions1.axes_handler.get_axis(o_axis.name)
|
|
238
|
+
if s_axis is None:
|
|
239
|
+
raise NgioValueError(
|
|
240
|
+
f"Axes do not match. The axis {o_axis.name} "
|
|
241
|
+
f"is not present in either dimensions."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def require_dimensions_match(
|
|
246
|
+
dimensions1: Dimensions, dimensions2: Dimensions, allow_singleton: bool = False
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Check if two Dimensions objects have the same axes and dimensions.
|
|
249
|
+
|
|
250
|
+
Besides the channel axis, all axes must have the same dimension in
|
|
251
|
+
both images.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
dimensions1 (Dimensions): The first dimensions object to compare against.
|
|
255
|
+
dimensions2 (Dimensions): The second dimensions object to compare against.
|
|
256
|
+
allow_singleton (bool): Whether to allow singleton dimensions to be
|
|
257
|
+
different. For example, if the input image has shape
|
|
258
|
+
(5, 100, 100) and the label has shape (1, 100, 100).
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
NgioValueError: If the dimensions do not match.
|
|
262
|
+
"""
|
|
263
|
+
require_axes_match(dimensions1, dimensions2)
|
|
264
|
+
for s_axis in dimensions1.axes_handler.axes:
|
|
265
|
+
if s_axis.axis_type == "channel":
|
|
266
|
+
continue
|
|
267
|
+
o_axis = dimensions2.axes_handler.get_axis(s_axis.name)
|
|
268
|
+
assert o_axis is not None # already checked in assert_axes_match
|
|
269
|
+
|
|
270
|
+
i_dim = dimensions1.get(s_axis.name, default=1)
|
|
271
|
+
o_dim = dimensions2.get(o_axis.name, default=1)
|
|
272
|
+
|
|
273
|
+
if i_dim != o_dim:
|
|
274
|
+
if allow_singleton and (i_dim == 1 or o_dim == 1):
|
|
150
275
|
continue
|
|
276
|
+
raise NgioValueError(
|
|
277
|
+
f"Dimensions do not match for axis "
|
|
278
|
+
f"{s_axis.name}. Got {i_dim} and {o_dim}."
|
|
279
|
+
)
|
|
151
280
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
281
|
+
|
|
282
|
+
def require_rescalable(dimensions1: Dimensions, dimensions2: Dimensions) -> None:
|
|
283
|
+
"""Assert that two images can be rescaled.
|
|
284
|
+
|
|
285
|
+
For this to be true, the images must have the same axes, and
|
|
286
|
+
the pixel sizes must be compatible (i.e. one can be scaled to the other).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
dimensions1 (Dimensions): The first dimensions object to compare against.
|
|
290
|
+
dimensions2 (Dimensions): The second dimensions object to compare against.
|
|
291
|
+
|
|
292
|
+
"""
|
|
293
|
+
require_axes_match(dimensions1, dimensions2)
|
|
294
|
+
for ax1 in dimensions1.axes_handler.axes:
|
|
295
|
+
if ax1.axis_type == "channel":
|
|
296
|
+
continue
|
|
297
|
+
ax2 = dimensions2.axes_handler.get_axis(ax1.name)
|
|
298
|
+
assert ax2 is not None, "Axes do not match."
|
|
299
|
+
px1 = dimensions1.pixel_size.get(ax1.name, default=1.0)
|
|
300
|
+
px2 = dimensions2.pixel_size.get(ax2.name, default=1.0)
|
|
301
|
+
shape1 = dimensions1.get(ax1.name, default=1)
|
|
302
|
+
shape2 = dimensions2.get(ax2.name, default=1)
|
|
303
|
+
scale = px1 / px2
|
|
304
|
+
if not _are_compatible(
|
|
305
|
+
shape1=shape1,
|
|
306
|
+
shape2=shape2,
|
|
307
|
+
scaling=scale,
|
|
308
|
+
):
|
|
309
|
+
raise NgioValueError(
|
|
310
|
+
f"Image1 with shape {dimensions1.shape}, "
|
|
311
|
+
f"and pixel size {dimensions1.pixel_size}, "
|
|
312
|
+
f"cannot be rescaled to "
|
|
313
|
+
f"Image2 with shape {dimensions2.shape}, "
|
|
314
|
+
f"and pixel size {dimensions2.pixel_size}. "
|
|
315
|
+
)
|
ngio/common/_pyramid.py
CHANGED
|
@@ -5,8 +5,14 @@ from typing import Literal
|
|
|
5
5
|
import dask.array as da
|
|
6
6
|
import numpy as np
|
|
7
7
|
import zarr
|
|
8
|
+
from zarr.types import DIMENSION_SEPARATOR
|
|
8
9
|
|
|
9
|
-
from ngio.common._zoom import
|
|
10
|
+
from ngio.common._zoom import (
|
|
11
|
+
InterpolationOrder,
|
|
12
|
+
_zoom_inputs_check,
|
|
13
|
+
dask_zoom,
|
|
14
|
+
numpy_zoom,
|
|
15
|
+
)
|
|
10
16
|
from ngio.utils import (
|
|
11
17
|
AccessModeLiteral,
|
|
12
18
|
NgioValueError,
|
|
@@ -18,7 +24,7 @@ from ngio.utils import (
|
|
|
18
24
|
def _on_disk_numpy_zoom(
|
|
19
25
|
source: zarr.Array,
|
|
20
26
|
target: zarr.Array,
|
|
21
|
-
order:
|
|
27
|
+
order: InterpolationOrder,
|
|
22
28
|
) -> None:
|
|
23
29
|
target[...] = numpy_zoom(source[...], target_shape=target.shape, order=order)
|
|
24
30
|
|
|
@@ -26,7 +32,7 @@ def _on_disk_numpy_zoom(
|
|
|
26
32
|
def _on_disk_dask_zoom(
|
|
27
33
|
source: zarr.Array,
|
|
28
34
|
target: zarr.Array,
|
|
29
|
-
order:
|
|
35
|
+
order: InterpolationOrder,
|
|
30
36
|
) -> None:
|
|
31
37
|
source_array = da.from_zarr(source)
|
|
32
38
|
target_array = dask_zoom(source_array, target_shape=target.shape, order=order)
|
|
@@ -39,7 +45,7 @@ def _on_disk_dask_zoom(
|
|
|
39
45
|
def _on_disk_coarsen(
|
|
40
46
|
source: zarr.Array,
|
|
41
47
|
target: zarr.Array,
|
|
42
|
-
|
|
48
|
+
order: InterpolationOrder = "linear",
|
|
43
49
|
aggregation_function: Callable | None = None,
|
|
44
50
|
) -> None:
|
|
45
51
|
"""Apply a coarsening operation from a source zarr array to a target zarr array.
|
|
@@ -47,10 +53,10 @@ def _on_disk_coarsen(
|
|
|
47
53
|
Args:
|
|
48
54
|
source (zarr.Array): The source array to coarsen.
|
|
49
55
|
target (zarr.Array): The target array to save the coarsened result to.
|
|
50
|
-
|
|
56
|
+
order (InterpolationOrder): The order of interpolation is not really implemented
|
|
51
57
|
for coarsening, but it is kept for compatibility with the zoom function.
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
order="linear" -> linear interpolation ~ np.mean
|
|
59
|
+
order="nearest" -> nearest interpolation ~ np.max
|
|
54
60
|
aggregation_function (np.ufunc): The aggregation function to use.
|
|
55
61
|
"""
|
|
56
62
|
source_array = da.from_zarr(source)
|
|
@@ -64,13 +70,15 @@ def _on_disk_coarsen(
|
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
if aggregation_function is None:
|
|
67
|
-
if
|
|
73
|
+
if order == "linear":
|
|
68
74
|
aggregation_function = np.mean
|
|
69
|
-
elif
|
|
75
|
+
elif order == "nearest":
|
|
70
76
|
aggregation_function = np.max
|
|
77
|
+
elif order == "cubic":
|
|
78
|
+
raise NgioValueError("Cubic interpolation is not supported for coarsening.")
|
|
71
79
|
else:
|
|
72
80
|
raise NgioValueError(
|
|
73
|
-
f"Aggregation function must be provided for order {
|
|
81
|
+
f"Aggregation function must be provided for order {order}"
|
|
74
82
|
)
|
|
75
83
|
|
|
76
84
|
coarsening_setup = {}
|
|
@@ -96,7 +104,7 @@ def _on_disk_coarsen(
|
|
|
96
104
|
def on_disk_zoom(
|
|
97
105
|
source: zarr.Array,
|
|
98
106
|
target: zarr.Array,
|
|
99
|
-
order:
|
|
107
|
+
order: InterpolationOrder = "linear",
|
|
100
108
|
mode: Literal["dask", "numpy", "coarsen"] = "dask",
|
|
101
109
|
) -> None:
|
|
102
110
|
"""Apply a zoom operation from a source zarr array to a target zarr array.
|
|
@@ -104,7 +112,7 @@ def on_disk_zoom(
|
|
|
104
112
|
Args:
|
|
105
113
|
source (zarr.Array): The source array to zoom.
|
|
106
114
|
target (zarr.Array): The target array to save the zoomed result to.
|
|
107
|
-
order (
|
|
115
|
+
order (InterpolationOrder): The order of interpolation. Defaults to "linear".
|
|
108
116
|
mode (Literal["dask", "numpy", "coarsen"]): The mode to use. Defaults to "dask".
|
|
109
117
|
"""
|
|
110
118
|
if not isinstance(source, zarr.Array):
|
|
@@ -155,7 +163,7 @@ def _find_closest_arrays(
|
|
|
155
163
|
def consolidate_pyramid(
|
|
156
164
|
source: zarr.Array,
|
|
157
165
|
targets: list[zarr.Array],
|
|
158
|
-
order:
|
|
166
|
+
order: InterpolationOrder = "linear",
|
|
159
167
|
mode: Literal["dask", "numpy", "coarsen"] = "dask",
|
|
160
168
|
) -> None:
|
|
161
169
|
"""Consolidate the Zarr array."""
|
|
@@ -177,6 +185,15 @@ def consolidate_pyramid(
|
|
|
177
185
|
processed.append(target_image)
|
|
178
186
|
|
|
179
187
|
|
|
188
|
+
def _maybe_int(value: float | int) -> float | int:
|
|
189
|
+
"""Convert a float to an int if it is an integer."""
|
|
190
|
+
if isinstance(value, int):
|
|
191
|
+
return value
|
|
192
|
+
if value.is_integer():
|
|
193
|
+
return int(value)
|
|
194
|
+
return value
|
|
195
|
+
|
|
196
|
+
|
|
180
197
|
def init_empty_pyramid(
|
|
181
198
|
store: StoreOrGroup,
|
|
182
199
|
paths: list[str],
|
|
@@ -185,6 +202,8 @@ def init_empty_pyramid(
|
|
|
185
202
|
chunks: Sequence[int] | None = None,
|
|
186
203
|
dtype: str = "uint16",
|
|
187
204
|
mode: AccessModeLiteral = "a",
|
|
205
|
+
dimension_separator: DIMENSION_SEPARATOR = "/",
|
|
206
|
+
compressor="default",
|
|
188
207
|
) -> None:
|
|
189
208
|
# Return the an Image object
|
|
190
209
|
if chunks is not None and len(chunks) != len(ref_shape):
|
|
@@ -200,6 +219,10 @@ def init_empty_pyramid(
|
|
|
200
219
|
"The shape and scaling factor must have the same number of dimensions."
|
|
201
220
|
)
|
|
202
221
|
|
|
222
|
+
# Ensure scaling factors are int if possible
|
|
223
|
+
# To reduce the risk of floating point issues
|
|
224
|
+
scaling_factors = [_maybe_int(s) for s in scaling_factors]
|
|
225
|
+
|
|
203
226
|
root_group = open_group_wrapper(store, mode=mode)
|
|
204
227
|
|
|
205
228
|
for path in paths:
|
|
@@ -213,22 +236,18 @@ def init_empty_pyramid(
|
|
|
213
236
|
shape=ref_shape,
|
|
214
237
|
dtype=dtype,
|
|
215
238
|
chunks=chunks,
|
|
216
|
-
dimension_separator=
|
|
239
|
+
dimension_separator=dimension_separator,
|
|
217
240
|
overwrite=True,
|
|
241
|
+
compressor=compressor,
|
|
218
242
|
)
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if math.floor(s / sc) % 2 == 0:
|
|
224
|
-
_shape.append(math.floor(s / sc))
|
|
225
|
-
else:
|
|
226
|
-
_shape.append(math.ceil(s / sc))
|
|
244
|
+
_shape = [
|
|
245
|
+
math.floor(s / sc) for s, sc in zip(ref_shape, scaling_factors, strict=True)
|
|
246
|
+
]
|
|
227
247
|
ref_shape = _shape
|
|
228
248
|
|
|
229
249
|
if chunks is None:
|
|
230
250
|
chunks = new_arr.chunks
|
|
231
|
-
|
|
232
|
-
raise NgioValueError("Something went wrong with the chunks")
|
|
251
|
+
assert chunks is not None
|
|
233
252
|
chunks = [min(c, s) for c, s in zip(chunks, ref_shape, strict=True)]
|
|
234
253
|
return None
|