ngio 0.2.0a2__py3-none-any.whl → 0.5.0b4__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.
Files changed (106) hide show
  1. ngio/__init__.py +40 -12
  2. ngio/common/__init__.py +16 -32
  3. ngio/common/_dimensions.py +270 -48
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +267 -73
  6. ngio/common/_roi.py +290 -66
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +54 -22
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +126 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/__init__.py +17 -58
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +30 -9
  20. ngio/images/_abstract_image.py +968 -0
  21. ngio/images/_create_synt_container.py +132 -0
  22. ngio/images/_create_utils.py +423 -0
  23. ngio/images/_image.py +926 -0
  24. ngio/images/_label.py +417 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1235 -0
  27. ngio/images/_table_ops.py +471 -0
  28. ngio/io_pipes/__init__.py +75 -0
  29. ngio/io_pipes/_io_pipes.py +361 -0
  30. ngio/io_pipes/_io_pipes_masked.py +488 -0
  31. ngio/io_pipes/_io_pipes_roi.py +146 -0
  32. ngio/io_pipes/_io_pipes_types.py +56 -0
  33. ngio/io_pipes/_match_shape.py +377 -0
  34. ngio/io_pipes/_ops_axes.py +344 -0
  35. ngio/io_pipes/_ops_slices.py +411 -0
  36. ngio/io_pipes/_ops_slices_utils.py +199 -0
  37. ngio/io_pipes/_ops_transforms.py +104 -0
  38. ngio/io_pipes/_zoom_transform.py +180 -0
  39. ngio/ome_zarr_meta/__init__.py +39 -15
  40. ngio/ome_zarr_meta/_meta_handlers.py +490 -96
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
  48. ngio/ome_zarr_meta/v04/__init__.py +21 -5
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
  51. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  52. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  53. ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
  54. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  55. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  56. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  57. ngio/resources/__init__.py +55 -0
  58. ngio/resources/resource_model.py +36 -0
  59. ngio/tables/__init__.py +20 -4
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +50 -1
  63. ngio/tables/backends/_abstract_backend.py +200 -31
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +10 -114
  66. ngio/tables/backends/_csv.py +19 -0
  67. ngio/tables/backends/_json.py +92 -0
  68. ngio/tables/backends/_parquet.py +19 -0
  69. ngio/tables/backends/_py_arrow_backends.py +222 -0
  70. ngio/tables/backends/_table_backends.py +162 -38
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +19 -4
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +79 -115
  75. ngio/tables/v1/_generic_table.py +21 -90
  76. ngio/tables/v1/_roi_table.py +486 -137
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +16 -14
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +121 -13
  82. ngio/utils/_fractal_fsspec_store.py +42 -0
  83. ngio/utils/_zarr_utils.py +374 -218
  84. ngio-0.5.0b4.dist-info/METADATA +147 -0
  85. ngio-0.5.0b4.dist-info/RECORD +88 -0
  86. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
  87. ngio/common/_array_pipe.py +0 -160
  88. ngio/common/_axes_transforms.py +0 -63
  89. ngio/common/_common_types.py +0 -5
  90. ngio/common/_slicer.py +0 -97
  91. ngio/images/abstract_image.py +0 -240
  92. ngio/images/create.py +0 -251
  93. ngio/images/image.py +0 -389
  94. ngio/images/label.py +0 -236
  95. ngio/images/omezarr_container.py +0 -535
  96. ngio/ome_zarr_meta/_generic_handlers.py +0 -320
  97. ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
  98. ngio/tables/_validators.py +0 -192
  99. ngio/tables/backends/_anndata_v1.py +0 -75
  100. ngio/tables/backends/_json_v1.py +0 -56
  101. ngio/tables/tables_container.py +0 -300
  102. ngio/tables/v1/_masking_roi_table.py +0 -175
  103. ngio/utils/_logger.py +0 -29
  104. ngio-0.2.0a2.dist-info/METADATA +0 -95
  105. ngio-0.2.0a2.dist-info/RECORD +0 -53
  106. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
ngio/__init__.py CHANGED
@@ -9,33 +9,61 @@ except PackageNotFoundError: # pragma: no cover
9
9
  __author__ = "Lorenzo Cerrone"
10
10
  __email__ = "lorenzo.cerrone@uzh.ch"
11
11
 
12
- from ngio.common import ArrayLike, Dimensions
13
- from ngio.hcs import OmeZarrPlate, OmeZarrWell, open_omezarr_plate, open_omezarr_well
12
+ from ngio.common import Dimensions, Roi, RoiSlice
13
+ from ngio.hcs import (
14
+ OmeZarrPlate,
15
+ OmeZarrWell,
16
+ create_empty_plate,
17
+ create_empty_well,
18
+ open_ome_zarr_plate,
19
+ open_ome_zarr_well,
20
+ )
14
21
  from ngio.images import (
22
+ ChannelSelectionModel,
15
23
  Image,
16
24
  Label,
17
25
  OmeZarrContainer,
18
- create_empty_omezarr,
19
- create_omezarr_from_array,
26
+ create_empty_ome_zarr,
27
+ create_ome_zarr_from_array,
28
+ create_synthetic_ome_zarr,
20
29
  open_image,
21
- open_omezarr_container,
30
+ open_label,
31
+ open_ome_zarr_container,
32
+ )
33
+ from ngio.ome_zarr_meta.ngio_specs import (
34
+ AxesSetup,
35
+ DefaultNgffVersion,
36
+ ImageInWellPath,
37
+ NgffVersions,
38
+ PixelSize,
22
39
  )
23
- from ngio.ome_zarr_meta.ngio_specs import AxesSetup, PixelSize
40
+ from ngio.utils import NgioSupportedStore, StoreOrGroup
24
41
 
25
42
  __all__ = [
26
- "ArrayLike",
27
43
  "AxesSetup",
44
+ "ChannelSelectionModel",
45
+ "DefaultNgffVersion",
28
46
  "Dimensions",
29
47
  "Image",
48
+ "ImageInWellPath",
30
49
  "Label",
50
+ "NgffVersions",
51
+ "NgioSupportedStore",
31
52
  "OmeZarrContainer",
32
53
  "OmeZarrPlate",
33
54
  "OmeZarrWell",
34
55
  "PixelSize",
35
- "create_empty_omezarr",
36
- "create_omezarr_from_array",
56
+ "Roi",
57
+ "RoiSlice",
58
+ "StoreOrGroup",
59
+ "create_empty_ome_zarr",
60
+ "create_empty_plate",
61
+ "create_empty_well",
62
+ "create_ome_zarr_from_array",
63
+ "create_synthetic_ome_zarr",
37
64
  "open_image",
38
- "open_omezarr_container",
39
- "open_omezarr_plate",
40
- "open_omezarr_well",
65
+ "open_label",
66
+ "open_ome_zarr_container",
67
+ "open_ome_zarr_plate",
68
+ "open_ome_zarr_well",
41
69
  ]
ngio/common/__init__.py CHANGED
@@ -1,44 +1,28 @@
1
1
  """Common classes and functions that are used across the package."""
2
2
 
3
- from ngio.common._array_pipe import get_pipe, set_pipe
4
- from ngio.common._axes_transforms import (
5
- transform_dask_array,
6
- transform_list,
7
- transform_numpy_array,
8
- )
9
- from ngio.common._common_types import ArrayLike
10
3
  from ngio.common._dimensions import Dimensions
11
- from ngio.common._pyramid import consolidate_pyramid, init_empty_pyramid, on_disk_zoom
12
- from ngio.common._roi import RasterCooROI, WorldCooROI
13
- from ngio.common._slicer import (
14
- SliceTransform,
15
- compute_and_slices,
16
- dask_get_slice,
17
- dask_set_slice,
18
- numpy_get_slice,
19
- numpy_set_slice,
4
+ from ngio.common._masking_roi import compute_masking_roi
5
+ from ngio.common._pyramid import (
6
+ ChunksLike,
7
+ ImagePyramidBuilder,
8
+ ShardsLike,
9
+ consolidate_pyramid,
10
+ on_disk_zoom,
20
11
  )
21
- from ngio.common._zoom import dask_zoom, numpy_zoom
12
+ from ngio.common._roi import Roi, RoiSlice
13
+ from ngio.common._zoom import InterpolationOrder, dask_zoom, numpy_zoom
22
14
 
23
15
  __all__ = [
24
- "ArrayLike",
16
+ "ChunksLike",
25
17
  "Dimensions",
26
- "RasterCooROI",
27
- "SliceTransform",
28
- "WorldCooROI",
29
- "compute_and_slices",
18
+ "ImagePyramidBuilder",
19
+ "InterpolationOrder",
20
+ "Roi",
21
+ "RoiSlice",
22
+ "ShardsLike",
23
+ "compute_masking_roi",
30
24
  "consolidate_pyramid",
31
- "dask_get_slice",
32
- "dask_set_slice",
33
25
  "dask_zoom",
34
- "get_pipe",
35
- "init_empty_pyramid",
36
- "numpy_get_slice",
37
- "numpy_set_slice",
38
26
  "numpy_zoom",
39
27
  "on_disk_zoom",
40
- "set_pipe",
41
- "transform_dask_array",
42
- "transform_list",
43
- "transform_numpy_array",
44
28
  ]
@@ -4,110 +4,332 @@ 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
- from collections.abc import Collection
7
+ import math
8
+ from typing import overload
8
9
 
9
- from ngio.common._axes_transforms import transform_list
10
- from ngio.ome_zarr_meta import AxesMapper
11
- from ngio.utils import NgioValidationError, NgioValueError
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
15
+ from ngio.utils import NgioValueError
16
+
17
+
18
+ def _are_compatible(shape1: int, shape2: int, scaling: float) -> bool:
19
+ """Check if shape2 is consistent with shape1 given pixel sizes.
20
+
21
+ Since we only deal with shape discrepancies due to rounding, we
22
+ shape1, needs to be larger than shape2.
23
+ """
24
+ if shape1 < shape2:
25
+ return _are_compatible(shape2, shape1, 1 / scaling)
26
+ expected_shape2 = shape1 * scaling
27
+ expected_shape2_floor = math.floor(expected_shape2)
28
+ expected_shape2_ceil = math.ceil(expected_shape2)
29
+ return shape2 in {expected_shape2_floor, expected_shape2_ceil}
30
+
31
+
32
+ def require_axes_match(reference: "Dimensions", other: "Dimensions") -> None:
33
+ """Check if two Dimensions objects have the same axes.
34
+
35
+ Besides the channel axis (which is a special case), all axes must be
36
+ present in both Dimensions objects.
37
+
38
+ Args:
39
+ reference (Dimensions): The reference dimensions object to compare against.
40
+ other (Dimensions): The other dimensions object to compare against.
41
+
42
+ Raises:
43
+ NgioValueError: If the axes do not match.
44
+ """
45
+ for s_axis in reference.axes_handler.axes:
46
+ if s_axis.axis_type == "channel":
47
+ continue
48
+ o_axis = other.axes_handler.get_axis(s_axis.name)
49
+ if o_axis is None:
50
+ raise NgioValueError(
51
+ f"Axes do not match. The axis {s_axis.name} "
52
+ f"is not present in either dimensions."
53
+ )
54
+ # Check for axes present in the other dimensions but not in this one
55
+ for o_axis in other.axes_handler.axes:
56
+ if o_axis.axis_type == "channel":
57
+ continue
58
+ s_axis = reference.axes_handler.get_axis(o_axis.name)
59
+ if s_axis is None:
60
+ raise NgioValueError(
61
+ f"Axes do not match. The axis {o_axis.name} "
62
+ f"is not present in either dimensions."
63
+ )
64
+
65
+
66
+ def check_if_axes_match(reference: "Dimensions", other: "Dimensions") -> bool:
67
+ """Check if two Dimensions objects have the same axes.
68
+
69
+ Besides the channel axis (which is a special case), all axes must be
70
+ present in both Dimensions objects.
71
+
72
+ Args:
73
+ reference (Dimensions): The reference dimensions object to compare against.
74
+ other (Dimensions): The other dimensions object to compare against.
75
+
76
+ Returns:
77
+ bool: True if the axes match, False otherwise.
78
+ """
79
+ try:
80
+ require_axes_match(reference, other)
81
+ return True
82
+ except NgioValueError:
83
+ return False
84
+
85
+
86
+ def require_dimensions_match(
87
+ reference: "Dimensions", other: "Dimensions", allow_singleton: bool = False
88
+ ) -> None:
89
+ """Check if two Dimensions objects have the same axes and dimensions.
90
+
91
+ Besides the channel axis, all axes must have the same dimension in
92
+ both images.
93
+
94
+ Args:
95
+ reference (Dimensions): The reference dimensions object to compare against.
96
+ other (Dimensions): The other dimensions object to compare against.
97
+ allow_singleton (bool): Whether to allow singleton dimensions to be
98
+ different. For example, if the input image has shape
99
+ (5, 100, 100) and the label has shape (1, 100, 100).
100
+
101
+ Raises:
102
+ NgioValueError: If the dimensions do not match.
103
+ """
104
+ require_axes_match(reference, other)
105
+ for r_axis in reference.axes_handler.axes:
106
+ if r_axis.axis_type == "channel":
107
+ continue
108
+ o_axis = other.axes_handler.get_axis(r_axis.name)
109
+ assert o_axis is not None # already checked in assert_axes_match
110
+
111
+ r_dim = reference.get(r_axis.name, default=1)
112
+ o_dim = other.get(o_axis.name, default=1)
113
+
114
+ if r_dim != o_dim:
115
+ if allow_singleton and (r_dim == 1 or o_dim == 1):
116
+ continue
117
+ raise NgioValueError(
118
+ f"Dimensions do not match for axis "
119
+ f"{r_axis.name}. Got {r_dim} and {o_dim}."
120
+ )
121
+
122
+
123
+ def check_if_dimensions_match(
124
+ reference: "Dimensions", other: "Dimensions", allow_singleton: bool = False
125
+ ) -> bool:
126
+ """Check if two Dimensions objects have the same axes and dimensions.
127
+
128
+ Besides the channel axis, all axes must have the same dimension in
129
+ both images.
130
+
131
+ Args:
132
+ reference (Dimensions): The reference dimensions object to compare against.
133
+ other (Dimensions): The other dimensions object to compare against.
134
+ allow_singleton (bool): Whether to allow singleton dimensions to be
135
+ different. For example, if the input image has shape
136
+ (5, 100, 100) and the label has shape (1, 100, 100).
137
+
138
+ Returns:
139
+ bool: True if the dimensions match, False otherwise.
140
+ """
141
+ try:
142
+ require_dimensions_match(reference, other, allow_singleton)
143
+ return True
144
+ except NgioValueError:
145
+ return False
146
+
147
+
148
+ def require_rescalable(reference: "Dimensions", other: "Dimensions") -> None:
149
+ """Assert that two images can be rescaled.
150
+
151
+ For this to be true, the images must have the same axes, and
152
+ the pixel sizes must be compatible (i.e. one can be scaled to the other).
153
+
154
+ Args:
155
+ reference (Dimensions): The reference dimensions object to compare against.
156
+ other (Dimensions): The other dimensions object to compare against.
157
+
158
+ """
159
+ require_axes_match(reference, other)
160
+ for ax_r in reference.axes_handler.axes:
161
+ if ax_r.axis_type == "channel":
162
+ continue
163
+ ax_o = other.axes_handler.get_axis(ax_r.name)
164
+ assert ax_o is not None, "Axes do not match."
165
+ px_r = reference.pixel_size.get(ax_r.name, default=1.0)
166
+ px_o = other.pixel_size.get(ax_o.name, default=1.0)
167
+ shape_r = reference.get(ax_r.name, default=1)
168
+ shape_o = other.get(ax_o.name, default=1)
169
+ scale = px_r / px_o
170
+ if not _are_compatible(
171
+ shape1=shape_r,
172
+ shape2=shape_o,
173
+ scaling=scale,
174
+ ):
175
+ raise NgioValueError(
176
+ f"Reference image with shape {reference.shape}, "
177
+ f"and pixel size {reference.pixel_size}, "
178
+ f"cannot be rescaled to "
179
+ f"image with shape {other.shape} "
180
+ f"and pixel size {other.pixel_size}. "
181
+ )
182
+
183
+
184
+ def check_if_rescalable(reference: "Dimensions", other: "Dimensions") -> bool:
185
+ """Check if 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
+ reference (Dimensions): The reference dimensions object to compare against.
192
+ other (Dimensions): The other dimensions object to compare against.
193
+
194
+ Returns:
195
+ bool: True if the images can be rescaled, False otherwise.
196
+ """
197
+ try:
198
+ require_rescalable(reference, other)
199
+ return True
200
+ except NgioValueError:
201
+ return False
12
202
 
13
203
 
14
204
  class Dimensions:
15
- """Dimension metadata."""
205
+ """Dimension metadata Handling Class.
206
+
207
+ This class is used to handle and manipulate dimension metadata.
208
+ It provides methods to access and validate dimension information,
209
+ such as shape, axes, and properties like is_2d, is_3d, is_time_series, etc.
210
+ """
211
+
212
+ require_axes_match = require_axes_match
213
+ check_if_axes_match = check_if_axes_match
214
+ require_dimensions_match = require_dimensions_match
215
+ check_if_dimensions_match = check_if_dimensions_match
216
+ require_rescalable = require_rescalable
217
+ check_if_rescalable = check_if_rescalable
16
218
 
17
219
  def __init__(
18
220
  self,
19
221
  shape: tuple[int, ...],
20
- axes_mapper: AxesMapper,
222
+ chunks: tuple[int, ...],
223
+ dataset: Dataset,
21
224
  ) -> None:
22
225
  """Create a Dimension object from a Zarr array.
23
226
 
24
227
  Args:
25
228
  shape: The shape of the Zarr array.
26
- axes_mapper: The axes mapper object.
229
+ chunks: The chunks of the Zarr array.
230
+ dataset: The dataset object.
27
231
  """
28
232
  self._shape = shape
29
- self._axes_mapper = axes_mapper
233
+ self._chunks = chunks
234
+ self._axes_handler = dataset.axes_handler
235
+ self._pixel_size = dataset.pixel_size
30
236
 
31
- if len(self._shape) != len(self._axes_mapper.on_disk_axes):
32
- raise NgioValidationError(
237
+ if len(self._shape) != len(self._axes_handler.axes):
238
+ raise NgioValueError(
33
239
  "The number of dimensions must match the number of axes. "
34
- f"Expected Axis {self._axes_mapper.on_disk_axes_names} but got shape "
240
+ f"Expected Axis {self._axes_handler.axes_names} but got shape "
35
241
  f"{self._shape}."
36
242
  )
37
243
 
38
244
  def __str__(self) -> str:
39
245
  """Return the string representation of the object."""
40
246
  dims = ", ".join(
41
- f"{ax.on_disk_name}: {s}"
42
- for ax, s in zip(self._axes_mapper.on_disk_axes, self._shape, strict=True)
247
+ f"{ax.name}: {s}"
248
+ for ax, s in zip(self._axes_handler.axes, self._shape, strict=True)
43
249
  )
44
250
  return f"Dimensions({dims})"
45
251
 
46
- def get(self, axis_name: str, strict: bool = True) -> int:
47
- """Return the dimension of the given axis name.
48
-
49
- Args:
50
- axis_name: The name of the axis (either canonical or non-canonical).
51
- strict: If True, raise an error if the axis does not exist.
52
- """
53
- index = self._axes_mapper.get_index(axis_name)
54
- if index is None and strict:
55
- raise NgioValueError(f"Axis {axis_name} does not exist.")
56
- elif index is None:
57
- return 1
58
- return self._shape[index]
59
-
60
- def get_shape(self, axes_order: Collection[str]) -> tuple[int, ...]:
61
- """Return the shape in the given axes order."""
62
- transforms = self._axes_mapper.to_order(axes_order)
63
- return tuple(transform_list(list(self._shape), 1, transforms))
64
-
65
- def get_canonical_shape(self) -> tuple[int, ...]:
66
- """Return the shape in the canonical order."""
67
- transforms = self._axes_mapper.to_canonical()
68
- return tuple(transform_list(list(self._shape), 1, transforms))
69
-
70
252
  def __repr__(self) -> str:
71
253
  """Return the string representation of the object."""
72
254
  return str(self)
73
255
 
74
256
  @property
75
- def on_disk_shape(self) -> tuple[int, ...]:
257
+ def axes_handler(self) -> AxesHandler:
258
+ """Return the axes handler object."""
259
+ return self._axes_handler
260
+
261
+ @property
262
+ def pixel_size(self) -> PixelSize:
263
+ """Return the pixel size object."""
264
+ return self._pixel_size
265
+
266
+ @property
267
+ def shape(self) -> tuple[int, ...]:
76
268
  """Return the shape as a tuple."""
77
- return tuple(self._shape)
269
+ return self._shape
270
+
271
+ @property
272
+ def chunks(self) -> tuple[int, ...]:
273
+ """Return the chunks as a tuple."""
274
+ return self._chunks
275
+
276
+ @property
277
+ def axes(self) -> tuple[str, ...]:
278
+ """Return the axes as a tuple of strings."""
279
+ return self.axes_handler.axes_names
78
280
 
79
281
  @property
80
282
  def is_time_series(self) -> bool:
81
- """Return whether the data is a time series."""
82
- if self.get("t", strict=False) == 1:
283
+ """Return whether the image is a time series."""
284
+ if self.get("t", default=1) == 1:
83
285
  return False
84
286
  return True
85
287
 
86
288
  @property
87
289
  def is_2d(self) -> bool:
88
- """Return whether the data is 2D."""
89
- if self.get("z", strict=False) != 1:
290
+ """Return whether the image is 2D."""
291
+ if self.get("z", default=1) != 1:
90
292
  return False
91
293
  return True
92
294
 
93
295
  @property
94
296
  def is_2d_time_series(self) -> bool:
95
- """Return whether the data is a 2D time series."""
297
+ """Return whether the image is a 2D time series."""
96
298
  return self.is_2d and self.is_time_series
97
299
 
98
300
  @property
99
301
  def is_3d(self) -> bool:
100
- """Return whether the data is 3D."""
302
+ """Return whether the image is 3D."""
101
303
  return not self.is_2d
102
304
 
103
305
  @property
104
306
  def is_3d_time_series(self) -> bool:
105
- """Return whether the data is a 3D time series."""
307
+ """Return whether the image is a 3D time series."""
106
308
  return self.is_3d and self.is_time_series
107
309
 
108
310
  @property
109
311
  def is_multi_channels(self) -> bool:
110
- """Return whether the data has multiple channels."""
111
- if self.get("c", strict=False) == 1:
312
+ """Return whether the image has multiple channels."""
313
+ if self.get("c", default=1) == 1:
112
314
  return False
113
315
  return True
316
+
317
+ @overload
318
+ def get(self, axis_name: str, default: None = None) -> int | None:
319
+ pass
320
+
321
+ @overload
322
+ def get(self, axis_name: str, default: int) -> int:
323
+ pass
324
+
325
+ def get(self, axis_name: str, default: int | None = None) -> int | None:
326
+ """Return the dimension/shape of the given axis name.
327
+
328
+ Args:
329
+ axis_name: The name of the axis (either canonical or non-canonical).
330
+ default: The default value to return if the axis does not exist.
331
+ """
332
+ index = self.axes_handler.get_index(axis_name)
333
+ if index is None:
334
+ return default
335
+ return self._shape[index]
@@ -0,0 +1,153 @@
1
+ """Utilities to build masking regions of interest (ROIs)."""
2
+
3
+ import itertools
4
+ from collections.abc import Sequence
5
+
6
+ import dask.array as da
7
+ import numpy as np
8
+ import scipy.ndimage as ndi
9
+ from dask.delayed import delayed
10
+
11
+ from ngio.common._roi import Roi
12
+ from ngio.ome_zarr_meta import PixelSize
13
+ from ngio.utils import NgioValueError
14
+
15
+
16
+ def _compute_offsets(chunks):
17
+ """Given a chunks tuple, compute cumulative offsets for each axis.
18
+
19
+ Returns a list where each element is a list of offsets for that dimension.
20
+ """
21
+ offsets = []
22
+ for dim_chunks in chunks:
23
+ dim_offsets = [0]
24
+ for size in dim_chunks:
25
+ dim_offsets.append(dim_offsets[-1] + size)
26
+ offsets.append(dim_offsets)
27
+ return offsets
28
+
29
+
30
+ def _adjust_slices(slices, offset):
31
+ """Adjust slices to global coordinates using the provided offset."""
32
+ adjusted_slices = {}
33
+ for label, s in slices.items():
34
+ adjusted = tuple(
35
+ slice(s_dim.start + off, s_dim.stop + off)
36
+ for s_dim, off in zip(s, offset, strict=True)
37
+ )
38
+ adjusted_slices[label] = adjusted
39
+ return adjusted_slices
40
+
41
+
42
+ @delayed
43
+ def _process_chunk(chunk, offset):
44
+ """Process a single chunk.
45
+
46
+ run ndi.find_objects and adjust the slices
47
+ to global coordinates using the provided offset.
48
+ """
49
+ local_slices = compute_slices(chunk)
50
+ local_slices = _adjust_slices(local_slices, offset)
51
+ return local_slices
52
+
53
+
54
+ def _merge_slices(
55
+ slice1: tuple[slice, ...], slice2: tuple[slice, ...]
56
+ ) -> tuple[slice, ...]:
57
+ """Merge two slices."""
58
+ merged = []
59
+ for s1, s2 in zip(slice1, slice2, strict=True):
60
+ start = min(s1.start, s2.start)
61
+ stop = max(s1.stop, s2.stop)
62
+ merged.append(slice(start, stop))
63
+ return tuple(merged)
64
+
65
+
66
+ @delayed
67
+ def _collect_slices(
68
+ local_slices: list[dict[int, tuple[slice, ...]]],
69
+ ) -> dict[int, tuple[slice]]:
70
+ """Collect the slices from the delayed results."""
71
+ global_slices = {}
72
+ for result in local_slices:
73
+ for label, s in result.items():
74
+ if label in global_slices:
75
+ global_slices[label] = _merge_slices(global_slices[label], s)
76
+ else:
77
+ global_slices[label] = s
78
+ return global_slices
79
+
80
+
81
+ def compute_slices(segmentation: np.ndarray) -> dict[int, tuple[slice, ...]]:
82
+ """Compute slices for each label in a segmentation.
83
+
84
+ Args:
85
+ segmentation (ndarray): The segmentation array.
86
+
87
+ Returns:
88
+ dict[int, tuple[slice]]: A dictionary with the label as key
89
+ and the slice as value.
90
+ """
91
+ slices = ndi.find_objects(segmentation)
92
+ slices_dict = {}
93
+ for label, s in enumerate(slices, start=1):
94
+ if s is None:
95
+ continue
96
+ else:
97
+ slices_dict[label] = s
98
+ return slices_dict
99
+
100
+
101
+ def lazy_compute_slices(segmentation: da.Array) -> dict[int, tuple[slice, ...]]:
102
+ """Compute slices for each label in a segmentation."""
103
+ global_offsets = _compute_offsets(segmentation.chunks)
104
+ delayed_chunks = segmentation.to_delayed() # type: ignore
105
+
106
+ grid_shape = tuple(len(c) for c in segmentation.chunks)
107
+
108
+ grid_indices = list(itertools.product(*[range(n) for n in grid_shape]))
109
+ delayed_results = []
110
+ for idx, chunk in zip(grid_indices, np.ravel(delayed_chunks), strict=True):
111
+ offset = tuple(global_offsets[dim][idx[dim]] for dim in range(len(idx)))
112
+ delayed_result = _process_chunk(chunk, offset)
113
+ delayed_results.append(delayed_result)
114
+
115
+ return _collect_slices(delayed_results).compute()
116
+
117
+
118
+ def compute_masking_roi(
119
+ segmentation: np.ndarray | da.Array,
120
+ pixel_size: PixelSize,
121
+ axes_order: Sequence[str],
122
+ ) -> list[Roi]:
123
+ """Compute a ROIs for each label in a segmentation.
124
+
125
+ This function expects a 2D or 3D segmentation array.
126
+ And this function expects the axes order to be 'zyx' or 'yx'.
127
+ Other axes orders are not supported.
128
+
129
+ """
130
+ if segmentation.ndim not in [2, 3, 4]:
131
+ raise NgioValueError("Only 2D, 3D, and 4D segmentations are supported.")
132
+
133
+ if len(axes_order) != segmentation.ndim:
134
+ raise NgioValueError(
135
+ "The length of axes_order must match the number of dimensions "
136
+ "of the segmentation."
137
+ )
138
+
139
+ if isinstance(segmentation, da.Array):
140
+ slices = lazy_compute_slices(segmentation)
141
+ else:
142
+ slices = compute_slices(segmentation)
143
+
144
+ rois = []
145
+ for label, slice_ in slices.items():
146
+ assert len(slice_) == len(axes_order)
147
+ slices = dict(zip(axes_order, slice_, strict=True))
148
+ roi = Roi.from_values(
149
+ name=str(label), slices=slices, label=label, space="pixel"
150
+ )
151
+ roi = roi.to_world(pixel_size=pixel_size)
152
+ rois.append(roi)
153
+ return rois