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
@@ -0,0 +1,968 @@
1
+ """Generic class to handle Image-like data in a OME-NGFF file."""
2
+
3
+ import warnings
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any, Literal
7
+
8
+ import dask.array as da
9
+ import numpy as np
10
+ import zarr
11
+ from zarr.core.array import CompressorLike
12
+
13
+ from ngio.common import (
14
+ Dimensions,
15
+ InterpolationOrder,
16
+ Roi,
17
+ consolidate_pyramid,
18
+ )
19
+ from ngio.common._pyramid import ChunksLike, ShardsLike, shapes_from_scaling_factors
20
+ from ngio.images._create_utils import (
21
+ _image_or_label_meta,
22
+ init_image_like_from_shapes,
23
+ )
24
+ from ngio.io_pipes import (
25
+ DaskGetter,
26
+ DaskRoiGetter,
27
+ DaskRoiSetter,
28
+ DaskSetter,
29
+ NumpyGetter,
30
+ NumpyRoiGetter,
31
+ NumpyRoiSetter,
32
+ NumpySetter,
33
+ SlicingInputType,
34
+ TransformProtocol,
35
+ )
36
+ from ngio.ome_zarr_meta import (
37
+ AxesHandler,
38
+ Dataset,
39
+ ImageMetaHandler,
40
+ LabelMetaHandler,
41
+ NgioImageMeta,
42
+ PixelSize,
43
+ )
44
+ from ngio.ome_zarr_meta.ngio_specs import (
45
+ Channel,
46
+ NgffVersions,
47
+ NgioLabelMeta,
48
+ )
49
+ from ngio.tables import RoiTable
50
+ from ngio.utils import (
51
+ NgioFileExistsError,
52
+ NgioValueError,
53
+ StoreOrGroup,
54
+ ZarrGroupHandler,
55
+ )
56
+ from ngio.utils._zarr_utils import find_dimension_separator
57
+
58
+
59
+ class AbstractImage(ABC):
60
+ """A class to handle a single image (or level) in an OME-Zarr image.
61
+
62
+ This class is meant to be subclassed by specific image types.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ group_handler: ZarrGroupHandler,
68
+ path: str,
69
+ meta_handler: ImageMetaHandler | LabelMetaHandler,
70
+ ) -> None:
71
+ """Initialize the Image at a single level.
72
+
73
+ Args:
74
+ group_handler: The Zarr group handler.
75
+ path: The path to the image in the ome_zarr file.
76
+ meta_handler: The image metadata handler.
77
+
78
+ """
79
+ self._path = path
80
+ self._group_handler = group_handler
81
+ self._meta_handler = meta_handler
82
+
83
+ try:
84
+ self._zarr_array = self._group_handler.get_array(self._path)
85
+ except NgioFileExistsError as e:
86
+ raise NgioFileExistsError(f"Could not find the dataset at {path}.") from e
87
+
88
+ def __repr__(self) -> str:
89
+ """Return a string representation of the image."""
90
+ return f"Image(path={self.path}, {self.dimensions})"
91
+
92
+ @property
93
+ def path(self) -> str:
94
+ """Return the path of the image."""
95
+ return self._path
96
+
97
+ @property
98
+ @abstractmethod
99
+ def meta_handler(self) -> ImageMetaHandler | LabelMetaHandler:
100
+ """Return the metadata."""
101
+ pass
102
+
103
+ @property
104
+ @abstractmethod
105
+ def meta(self) -> NgioImageMeta | NgioLabelMeta:
106
+ """Return the metadata."""
107
+ pass
108
+
109
+ @property
110
+ def dataset(self) -> Dataset:
111
+ """Return the dataset of the image."""
112
+ return self.meta_handler.get_meta().get_dataset(path=self.path)
113
+
114
+ @property
115
+ def dimensions(self) -> Dimensions:
116
+ """Return the dimensions of the image."""
117
+ return Dimensions(
118
+ shape=self.zarr_array.shape,
119
+ chunks=self.zarr_array.chunks,
120
+ dataset=self.dataset,
121
+ )
122
+
123
+ @property
124
+ def pixel_size(self) -> PixelSize:
125
+ """Return the pixel size of the image."""
126
+ return self.dataset.pixel_size
127
+
128
+ @property
129
+ def axes_handler(self) -> AxesHandler:
130
+ """Return the axes handler of the image."""
131
+ return self.dataset.axes_handler
132
+
133
+ @property
134
+ def axes(self) -> tuple[str, ...]:
135
+ """Return the axes of the image."""
136
+ return self.dimensions.axes
137
+
138
+ @property
139
+ def zarr_array(self) -> zarr.Array:
140
+ """Return the Zarr array."""
141
+ return self._zarr_array
142
+
143
+ @property
144
+ def shape(self) -> tuple[int, ...]:
145
+ """Return the shape of the image."""
146
+ return self.zarr_array.shape
147
+
148
+ @property
149
+ def dtype(self) -> str:
150
+ """Return the dtype of the image."""
151
+ return str(self.zarr_array.dtype)
152
+
153
+ @property
154
+ def chunks(self) -> tuple[int, ...]:
155
+ """Return the chunks of the image."""
156
+ return self.zarr_array.chunks
157
+
158
+ @property
159
+ def is_3d(self) -> bool:
160
+ """Return True if the image is 3D."""
161
+ return self.dimensions.is_3d
162
+
163
+ @property
164
+ def is_2d(self) -> bool:
165
+ """Return True if the image is 2D."""
166
+ return self.dimensions.is_2d
167
+
168
+ @property
169
+ def is_time_series(self) -> bool:
170
+ """Return True if the image is a time series."""
171
+ return self.dimensions.is_time_series
172
+
173
+ @property
174
+ def is_2d_time_series(self) -> bool:
175
+ """Return True if the image is a 2D time series."""
176
+ return self.dimensions.is_2d_time_series
177
+
178
+ @property
179
+ def is_3d_time_series(self) -> bool:
180
+ """Return True if the image is a 3D time series."""
181
+ return self.dimensions.is_3d_time_series
182
+
183
+ @property
184
+ def is_multi_channels(self) -> bool:
185
+ """Return True if the image is multichannel."""
186
+ return self.dimensions.is_multi_channels
187
+
188
+ @property
189
+ def space_unit(self) -> str | None:
190
+ """Return the space unit of the image."""
191
+ return self.axes_handler.space_unit
192
+
193
+ @property
194
+ def time_unit(self) -> str | None:
195
+ """Return the time unit of the image."""
196
+ return self.axes_handler.time_unit
197
+
198
+ def has_axis(self, axis: str) -> bool:
199
+ """Return True if the image has the given axis."""
200
+ return self.axes_handler.has_axis(axis)
201
+
202
+ def _get_as_numpy(
203
+ self,
204
+ axes_order: Sequence[str] | None = None,
205
+ transforms: Sequence[TransformProtocol] | None = None,
206
+ **slicing_kwargs: SlicingInputType,
207
+ ) -> np.ndarray:
208
+ """Get the image as a numpy array.
209
+
210
+ Args:
211
+ axes_order: The order of the axes to return the array.
212
+ transforms: The transforms to apply to the array.
213
+ **slicing_kwargs: The slices to get the array.
214
+
215
+ Returns:
216
+ The array of the region of interest.
217
+ """
218
+ numpy_getter = NumpyGetter(
219
+ zarr_array=self.zarr_array,
220
+ dimensions=self.dimensions,
221
+ axes_order=axes_order,
222
+ transforms=transforms,
223
+ slicing_dict=slicing_kwargs,
224
+ )
225
+ return numpy_getter()
226
+
227
+ def _get_roi_as_numpy(
228
+ self,
229
+ roi: Roi,
230
+ axes_order: Sequence[str] | None = None,
231
+ transforms: Sequence[TransformProtocol] | None = None,
232
+ **slicing_kwargs: SlicingInputType,
233
+ ) -> np.ndarray:
234
+ """Get the image as a numpy array for a region of interest.
235
+
236
+ Args:
237
+ roi: The region of interest to get the array.
238
+ axes_order: The order of the axes to return the array.
239
+ transforms: The transforms to apply to the array.
240
+ **slicing_kwargs: The slices to get the array.
241
+
242
+ Returns:
243
+ The array of the region of interest.
244
+ """
245
+ numpy_roi_getter = NumpyRoiGetter(
246
+ zarr_array=self.zarr_array,
247
+ dimensions=self.dimensions,
248
+ roi=roi,
249
+ axes_order=axes_order,
250
+ transforms=transforms,
251
+ slicing_dict=slicing_kwargs,
252
+ )
253
+ return numpy_roi_getter()
254
+
255
+ def _get_as_dask(
256
+ self,
257
+ axes_order: Sequence[str] | None = None,
258
+ transforms: Sequence[TransformProtocol] | None = None,
259
+ **slicing_kwargs: SlicingInputType,
260
+ ) -> da.Array:
261
+ """Get the image as a dask array.
262
+
263
+ Args:
264
+ axes_order: The order of the axes to return the array.
265
+ transforms: The transforms to apply to the array.
266
+ **slicing_kwargs: The slices to get the array.
267
+ """
268
+ dask_getter = DaskGetter(
269
+ zarr_array=self.zarr_array,
270
+ dimensions=self.dimensions,
271
+ axes_order=axes_order,
272
+ transforms=transforms,
273
+ slicing_dict=slicing_kwargs,
274
+ )
275
+ return dask_getter()
276
+
277
+ def _get_roi_as_dask(
278
+ self,
279
+ roi: Roi,
280
+ axes_order: Sequence[str] | None = None,
281
+ transforms: Sequence[TransformProtocol] | None = None,
282
+ **slicing_kwargs: SlicingInputType,
283
+ ) -> da.Array:
284
+ """Get the image as a dask array for a region of interest.
285
+
286
+ Args:
287
+ roi: The region of interest to get the array.
288
+ axes_order: The order of the axes to return the array.
289
+ transforms: The transforms to apply to the array.
290
+ **slicing_kwargs: The slices to get the array.
291
+ """
292
+ roi_dask_getter = DaskRoiGetter(
293
+ zarr_array=self.zarr_array,
294
+ dimensions=self.dimensions,
295
+ roi=roi,
296
+ axes_order=axes_order,
297
+ transforms=transforms,
298
+ slicing_dict=slicing_kwargs,
299
+ )
300
+ return roi_dask_getter()
301
+
302
+ def _get_array(
303
+ self,
304
+ axes_order: Sequence[str] | None = None,
305
+ transforms: Sequence[TransformProtocol] | None = None,
306
+ mode: Literal["numpy", "dask"] = "numpy",
307
+ **slicing_kwargs: SlicingInputType,
308
+ ) -> np.ndarray | da.Array:
309
+ """Get a slice of the image.
310
+
311
+ Args:
312
+ axes_order: The order of the axes to return the array.
313
+ transforms: The transforms to apply to the array.
314
+ mode: The object type to return.
315
+ Can be "dask", "numpy".
316
+ **slicing_kwargs: The slices to get the array.
317
+
318
+ Returns:
319
+ The array of the region of interest.
320
+ """
321
+ if mode == "numpy":
322
+ return self._get_as_numpy(
323
+ axes_order=axes_order, transforms=transforms, **slicing_kwargs
324
+ )
325
+ elif mode == "dask":
326
+ return self._get_as_dask(
327
+ axes_order=axes_order, transforms=transforms, **slicing_kwargs
328
+ )
329
+ else:
330
+ raise ValueError(
331
+ f"Unsupported mode: {mode}. Supported modes are: numpy, dask."
332
+ )
333
+
334
+ def _get_roi(
335
+ self,
336
+ roi: Roi,
337
+ axes_order: Sequence[str] | None = None,
338
+ transforms: Sequence[TransformProtocol] | None = None,
339
+ mode: Literal["numpy", "dask"] = "numpy",
340
+ **slice_kwargs: SlicingInputType,
341
+ ) -> np.ndarray | da.Array:
342
+ """Get a slice of the image.
343
+
344
+ Args:
345
+ roi: The region of interest to get the array.
346
+ axes_order: The order of the axes to return the array.
347
+ transforms: The transforms to apply to the array.
348
+ mode: The mode to return the array.
349
+ Can be "dask", "numpy".
350
+ **slice_kwargs: The slices to get the array.
351
+
352
+ Returns:
353
+ The array of the region of interest.
354
+ """
355
+ if mode == "numpy":
356
+ return self._get_roi_as_numpy(
357
+ roi=roi, axes_order=axes_order, transforms=transforms, **slice_kwargs
358
+ )
359
+ elif mode == "dask":
360
+ return self._get_roi_as_dask(
361
+ roi=roi, axes_order=axes_order, transforms=transforms, **slice_kwargs
362
+ )
363
+ else:
364
+ raise ValueError(
365
+ f"Unsupported mode: {mode}. Supported modes are: numpy, dask."
366
+ )
367
+
368
+ def _set_array(
369
+ self,
370
+ patch: np.ndarray | da.Array,
371
+ axes_order: Sequence[str] | None = None,
372
+ transforms: Sequence[TransformProtocol] | None = None,
373
+ **slicing_kwargs: SlicingInputType,
374
+ ) -> None:
375
+ """Set a slice of the image.
376
+
377
+ Args:
378
+ patch: The patch to set.
379
+ axes_order: The order of the axes to set the patch.
380
+ transforms: The transforms to apply to the patch.
381
+ **slicing_kwargs: The slices to set the patch.
382
+
383
+ """
384
+ if isinstance(patch, np.ndarray):
385
+ numpy_setter = NumpySetter(
386
+ zarr_array=self.zarr_array,
387
+ dimensions=self.dimensions,
388
+ axes_order=axes_order,
389
+ transforms=transforms,
390
+ slicing_dict=slicing_kwargs,
391
+ )
392
+ numpy_setter(patch)
393
+
394
+ elif isinstance(patch, da.Array):
395
+ dask_setter = DaskSetter(
396
+ zarr_array=self.zarr_array,
397
+ dimensions=self.dimensions,
398
+ axes_order=axes_order,
399
+ transforms=transforms,
400
+ slicing_dict=slicing_kwargs,
401
+ )
402
+ dask_setter(patch)
403
+ else:
404
+ raise TypeError(
405
+ f"Unsupported patch type: {type(patch)}. "
406
+ "Supported types are: "
407
+ "numpy.ndarray, dask.array.Array."
408
+ )
409
+
410
+ def _set_roi(
411
+ self,
412
+ roi: Roi,
413
+ patch: np.ndarray | da.Array,
414
+ axes_order: Sequence[str] | None = None,
415
+ transforms: Sequence[TransformProtocol] | None = None,
416
+ **slicing_kwargs: SlicingInputType,
417
+ ) -> None:
418
+ """Set a slice of the image.
419
+
420
+ Args:
421
+ roi: The region of interest to set the patch.
422
+ patch: The patch to set.
423
+ axes_order: The order of the axes to set the patch.
424
+ transforms: The transforms to apply to the patch.
425
+ **slicing_kwargs: The slices to set the patch.
426
+
427
+ """
428
+ if isinstance(patch, np.ndarray):
429
+ roi_numpy_setter = NumpyRoiSetter(
430
+ zarr_array=self.zarr_array,
431
+ dimensions=self.dimensions,
432
+ roi=roi,
433
+ axes_order=axes_order,
434
+ transforms=transforms,
435
+ slicing_dict=slicing_kwargs,
436
+ )
437
+ roi_numpy_setter(patch)
438
+
439
+ elif isinstance(patch, da.Array):
440
+ roi_dask_setter = DaskRoiSetter(
441
+ zarr_array=self.zarr_array,
442
+ dimensions=self.dimensions,
443
+ roi=roi,
444
+ axes_order=axes_order,
445
+ transforms=transforms,
446
+ slicing_dict=slicing_kwargs,
447
+ )
448
+ roi_dask_setter(patch)
449
+ else:
450
+ raise TypeError(
451
+ f"Unsupported patch type: {type(patch)}. "
452
+ "Supported types are: "
453
+ "numpy.ndarray, dask.array.Array."
454
+ )
455
+
456
+ def _consolidate(
457
+ self,
458
+ order: InterpolationOrder = "linear",
459
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
460
+ ) -> None:
461
+ """Consolidate the image on disk.
462
+
463
+ Args:
464
+ order: The order of the consolidation.
465
+ mode: The mode of the consolidation.
466
+ """
467
+ consolidate_image(image=self, order=order, mode=mode)
468
+
469
+ def roi(self, name: str | None = "image") -> Roi:
470
+ """Return the ROI covering the entire image."""
471
+ slices = {}
472
+ for ax_name in ["t", "z", "y", "x"]:
473
+ axis_size = self.dimensions.get(ax_name, default=None)
474
+ if axis_size is None:
475
+ continue
476
+ slices[ax_name] = slice(0, axis_size)
477
+ roi_px = Roi.from_values(name=name, slices=slices, space="pixel")
478
+ return roi_px.to_world(pixel_size=self.pixel_size)
479
+
480
+ def build_image_roi_table(self, name: str | None = "image") -> RoiTable:
481
+ """Build the ROI table containing the ROI covering the entire image."""
482
+ return RoiTable(rois=[self.roi(name=name)])
483
+
484
+ def require_dimensions_match(
485
+ self,
486
+ other: "AbstractImage",
487
+ allow_singleton: bool = False,
488
+ ) -> None:
489
+ """Assert that two images have matching spatial dimensions.
490
+
491
+ Args:
492
+ other: The other image to compare to.
493
+ allow_singleton: If True, allow singleton dimensions to be
494
+ compatible with non-singleton dimensions.
495
+
496
+ Raises:
497
+ NgioValueError: If the images do not have compatible dimensions.
498
+ """
499
+ self.dimensions.require_dimensions_match(
500
+ other.dimensions, allow_singleton=allow_singleton
501
+ )
502
+
503
+ def check_if_dimensions_match(
504
+ self,
505
+ other: "AbstractImage",
506
+ allow_singleton: bool = False,
507
+ ) -> bool:
508
+ """Check if two images have matching spatial dimensions.
509
+
510
+ Args:
511
+ other: The other image to compare to.
512
+ allow_singleton: If True, allow singleton dimensions to be
513
+ compatible with non-singleton dimensions.
514
+
515
+ Returns:
516
+ bool: True if the images have matching dimensions, False otherwise.
517
+ """
518
+ return self.dimensions.check_if_dimensions_match(
519
+ other.dimensions, allow_singleton=allow_singleton
520
+ )
521
+
522
+ def require_axes_match(
523
+ self,
524
+ other: "AbstractImage",
525
+ ) -> None:
526
+ """Assert that two images have compatible axes.
527
+
528
+ Args:
529
+ other: The other image to compare to.
530
+
531
+ Raises:
532
+ NgioValueError: If the images do not have compatible axes.
533
+ """
534
+ self.dimensions.require_axes_match(other.dimensions)
535
+
536
+ def check_if_axes_match(
537
+ self,
538
+ other: "AbstractImage",
539
+ ) -> bool:
540
+ """Check if two images have compatible axes.
541
+
542
+ Args:
543
+ other: The other image to compare to.
544
+
545
+ Returns:
546
+ bool: True if the images have compatible axes, False otherwise.
547
+
548
+ """
549
+ return self.dimensions.check_if_axes_match(other.dimensions)
550
+
551
+ def require_rescalable(
552
+ self,
553
+ other: "AbstractImage",
554
+ ) -> None:
555
+ """Assert that two images can be rescaled to each other.
556
+
557
+ For this to be true, the images must have the same axes, and
558
+ the pixel sizes must be compatible (i.e. one can be scaled to the other).
559
+
560
+ Args:
561
+ other: The other image to compare to.
562
+
563
+ Raises:
564
+ NgioValueError: If the images cannot be scaled to each other.
565
+ """
566
+ self.dimensions.require_rescalable(other.dimensions)
567
+
568
+ def check_if_rescalable(
569
+ self,
570
+ other: "AbstractImage",
571
+ ) -> bool:
572
+ """Check if two images can be rescaled to each other.
573
+
574
+ For this to be true, the images must have the same axes, and
575
+ the pixel sizes must be compatible (i.e. one can be scaled to the other).
576
+
577
+ Args:
578
+ other: The other image to compare to.
579
+
580
+ Returns:
581
+ bool: True if the images can be rescaled to each other, False otherwise.
582
+ """
583
+ return self.dimensions.check_if_rescalable(other.dimensions)
584
+
585
+
586
+ def consolidate_image(
587
+ image: AbstractImage,
588
+ order: InterpolationOrder = "linear",
589
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
590
+ ) -> None:
591
+ """Consolidate the image on disk."""
592
+ target_paths = image.meta_handler.get_meta().paths
593
+ targets = [
594
+ image._group_handler.get_array(path)
595
+ for path in target_paths
596
+ if path != image.path
597
+ ]
598
+ consolidate_pyramid(
599
+ source=image.zarr_array, targets=targets, order=order, mode=mode
600
+ )
601
+
602
+
603
+ def _shapes_from_ref_image(
604
+ ref_image: AbstractImage,
605
+ ) -> list[tuple[int, ...]]:
606
+ """Rebuild base shape based on a new shape."""
607
+ paths = ref_image.meta.paths
608
+ index_path = paths.index(ref_image.path)
609
+ sub_paths = paths[index_path:]
610
+ group_handler = ref_image._group_handler
611
+ shapes = []
612
+ for path in sub_paths:
613
+ zarr_array = group_handler.get_array(path)
614
+ shapes.append(zarr_array.shape)
615
+ if len(shapes) == len(paths):
616
+ return shapes
617
+ missing_levels = len(paths) - len(shapes)
618
+ extended_shapes = shapes_from_scaling_factors(
619
+ base_shape=shapes[-1],
620
+ scaling_factors=ref_image.meta.scaling_factor(),
621
+ num_levels=missing_levels + 1,
622
+ )
623
+ shapes.extend(extended_shapes[1:])
624
+ return shapes
625
+
626
+
627
+ def _shapes_from_new_shape(
628
+ ref_image: AbstractImage,
629
+ shape: Sequence[int],
630
+ ) -> list[tuple[int, ...]]:
631
+ """Rebuild pyramid shapes based on a new base shape."""
632
+ if len(shape) != len(ref_image.shape):
633
+ raise NgioValueError(
634
+ "The shape of the new image does not match the reference image."
635
+ f"Got shape {shape} for reference shape {ref_image.shape}."
636
+ )
637
+ base_shape = tuple(shape)
638
+ scaling_factors = ref_image.meta.scaling_factor()
639
+ num_levels = len(ref_image.meta.paths)
640
+ return shapes_from_scaling_factors(
641
+ base_shape=base_shape,
642
+ scaling_factors=scaling_factors,
643
+ num_levels=num_levels,
644
+ )
645
+
646
+
647
+ def _compute_pyramid_shapes(
648
+ ref_image: AbstractImage,
649
+ shape: Sequence[int] | None,
650
+ ) -> list[tuple[int, ...]]:
651
+ """Rebuild pyramid shapes based on a new base shape."""
652
+ if shape is None:
653
+ return _shapes_from_ref_image(ref_image=ref_image)
654
+ return _shapes_from_new_shape(ref_image=ref_image, shape=shape)
655
+
656
+
657
+ def _check_chunks_and_shards_compatibility(
658
+ ref_shape: tuple[int, ...],
659
+ chunks: ChunksLike,
660
+ shards: ShardsLike | None,
661
+ ) -> None:
662
+ """Check if the chunks and shards are compatible with the reference shape.
663
+
664
+ Args:
665
+ ref_shape: The reference shape.
666
+ chunks: The chunks to check.
667
+ shards: The shards to check.
668
+ """
669
+ if chunks != "auto":
670
+ if len(chunks) != len(ref_shape):
671
+ raise NgioValueError(
672
+ "The length of the chunks must be the same as the number of dimensions."
673
+ )
674
+ if shards is not None and shards != "auto":
675
+ if len(shards) != len(ref_shape):
676
+ raise NgioValueError(
677
+ "The length of the shards must be the same as the number of dimensions."
678
+ )
679
+
680
+
681
+ def _apply_channel_policy(
682
+ ref_image: AbstractImage,
683
+ channels_policy: Literal["squeeze", "same", "singleton"] | int,
684
+ shapes: list[tuple[int, ...]],
685
+ axes: tuple[str, ...],
686
+ chunks: ChunksLike,
687
+ shards: ShardsLike | None,
688
+ ) -> tuple[list[tuple[int, ...]], tuple[str, ...], ChunksLike, ShardsLike | None]:
689
+ """Apply the channel policy to the shapes and axes.
690
+
691
+ Args:
692
+ ref_image: The reference image.
693
+ channels_policy: The channels policy to apply.
694
+ shapes: The shapes of the pyramid levels.
695
+ axes: The axes of the image.
696
+ chunks: The chunks of the image.
697
+ shards: The shards of the image.
698
+
699
+ Returns:
700
+ The new shapes and axes after applying the channel policy.
701
+ """
702
+ if channels_policy == "same":
703
+ return shapes, axes, chunks, shards
704
+
705
+ if channels_policy == "singleton":
706
+ # Treat 'singleton' as setting channel size to 1
707
+ channels_policy = 1
708
+
709
+ channel_index = ref_image.axes_handler.get_index("c")
710
+ if channel_index is None:
711
+ if channels_policy == "squeeze":
712
+ return shapes, axes, chunks, shards
713
+ raise NgioValueError(
714
+ f"Cannot apply channel policy {channels_policy=} to an image "
715
+ "without channels axis."
716
+ )
717
+ if channels_policy == "squeeze":
718
+ new_shapes = []
719
+ for shape in shapes:
720
+ new_shape = shape[:channel_index] + shape[channel_index + 1 :]
721
+ new_shapes.append(new_shape)
722
+ new_axes = axes[:channel_index] + axes[channel_index + 1 :]
723
+ if chunks == "auto":
724
+ new_chunks: ChunksLike = "auto"
725
+ else:
726
+ new_chunks = chunks[:channel_index] + chunks[channel_index + 1 :]
727
+ if shards == "auto" or shards is None:
728
+ new_shards: ShardsLike | None = shards
729
+ else:
730
+ new_shards = shards[:channel_index] + shards[channel_index + 1 :]
731
+ return new_shapes, new_axes, new_chunks, new_shards
732
+ elif isinstance(channels_policy, int):
733
+ new_shapes = []
734
+ for shape in shapes:
735
+ new_shape = (
736
+ *shape[:channel_index],
737
+ channels_policy,
738
+ *shape[channel_index + 1 :],
739
+ )
740
+ new_shapes.append(new_shape)
741
+ return new_shapes, axes, chunks, shards
742
+ else:
743
+ raise NgioValueError(
744
+ f"Invalid channels policy: {channels_policy}. "
745
+ "Must be 'squeeze', 'same', or an integer."
746
+ )
747
+
748
+
749
+ def _check_channels_meta_compatibility(
750
+ meta_type: type[_image_or_label_meta],
751
+ ref_image: AbstractImage,
752
+ channels_meta: Sequence[str | Channel] | None,
753
+ ) -> Sequence[str | Channel] | None:
754
+ """Check if the channels metadata is compatible with the reference image.
755
+
756
+ Args:
757
+ meta_type: The metadata type.
758
+ ref_image: The reference image.
759
+ channels_meta: The channels metadata to check.
760
+
761
+ Returns:
762
+ The channels metadata if compatible, None otherwise.
763
+ """
764
+ if issubclass(meta_type, NgioLabelMeta):
765
+ if channels_meta is not None:
766
+ raise NgioValueError("Cannot set channels_meta for a label image.")
767
+ return None
768
+ if channels_meta is not None:
769
+ return channels_meta
770
+ assert isinstance(ref_image.meta, NgioImageMeta)
771
+ ref_meta = ref_image.meta
772
+ index_c = ref_meta.axes_handler.get_index("c")
773
+ if index_c is None:
774
+ return None
775
+
776
+ # If the channels number does not match, return None
777
+ # Else return the channels metadata from the reference image
778
+ ref_shape = ref_image.shape
779
+ ref_num_channels = ref_shape[index_c] if index_c is not None else 1
780
+ channels_ = ref_meta.channels_meta.channels if ref_meta.channels_meta else []
781
+ # Reset to None if number of channels do not match
782
+ channels_meta_ = channels_ if ref_num_channels == len(channels_) else None
783
+ return channels_meta_
784
+
785
+
786
+ def abstract_derive(
787
+ *,
788
+ ref_image: AbstractImage,
789
+ meta_type: type[_image_or_label_meta],
790
+ store: StoreOrGroup,
791
+ overwrite: bool = False,
792
+ # Metadata parameters
793
+ shape: Sequence[int] | None = None,
794
+ pixelsize: float | tuple[float, float] | None = None,
795
+ z_spacing: float | None = None,
796
+ time_spacing: float | None = None,
797
+ name: str | None = None,
798
+ channels_policy: Literal["squeeze", "same", "singleton"] | int = "same",
799
+ channels_meta: Sequence[str | Channel] | None = None,
800
+ ngff_version: NgffVersions | None = None,
801
+ # Zarr Array parameters
802
+ chunks: ChunksLike | None = None,
803
+ shards: ShardsLike | None = None,
804
+ dtype: str | None = None,
805
+ dimension_separator: Literal[".", "/"] | None = None,
806
+ compressors: CompressorLike | None = None,
807
+ extra_array_kwargs: Mapping[str, Any] | None = None,
808
+ # Deprecated arguments
809
+ labels: Sequence[str] | None = None,
810
+ pixel_size: PixelSize | None = None,
811
+ ) -> ZarrGroupHandler:
812
+ """Create an empty OME-Zarr image from an existing image.
813
+
814
+ If a kwarg is not provided, the value from the reference image will be used.
815
+
816
+ Args:
817
+ ref_image (AbstractImage): The reference image to derive from.
818
+ meta_type (type[_image_or_label_meta]): The metadata type to use.
819
+ store (StoreOrGroup): The Zarr store or group to create the image in.
820
+ overwrite (bool): Whether to overwrite an existing image.
821
+ shape (Sequence[int] | None): The shape of the new image.
822
+ pixelsize (float | tuple[float, float] | None): The pixel size of the new image.
823
+ z_spacing (float | None): The z spacing of the new image.
824
+ time_spacing (float | None): The time spacing of the new image.
825
+ axes_names (Sequence[str] | None): The axes names of the new image.
826
+ name (str | None): The name of the new image.
827
+ channels_policy (Literal["squeeze", "same", "singleton"] | int):
828
+ Possible policies:
829
+ - If "squeeze", the channels axis will be removed (no matter its size).
830
+ - If "same", the channels axis will be kept as is (if it exists).
831
+ - If "singleton", the channels axis will be set to size 1.
832
+ - If an integer is provided, the channels axis will be changed to have that
833
+ size.
834
+ channels_meta (Sequence[str | Channel] | None): The channels metadata
835
+ of the new image.
836
+ ngff_version (NgffVersions | None): The NGFF version to use.
837
+ chunks (ChunksLike | None): The chunk shape of the new image.
838
+ shards (ShardsLike | None): The shard shape of the new image.
839
+ dtype (str | None): The data type of the new image.
840
+ dimension_separator (DIMENSION_SEPARATOR | None): The separator to use for
841
+ dimensions.
842
+ compressors (CompressorLike | None): The compressors to use.
843
+ extra_array_kwargs (Mapping[str, Any] | None): Extra arguments to pass to
844
+ the zarr array creation.
845
+ labels (Sequence[str] | None): The labels of the new image.
846
+ This argument is DEPRECATED please use channels_meta instead.
847
+ pixel_size (PixelSize | None): The pixel size of the new image.
848
+ This argument is DEPRECATED please use pixelsize, z_spacing,
849
+ and time_spacing instead.
850
+
851
+ Returns:
852
+ ImagesContainer: The new derived image.
853
+
854
+ """
855
+ # TODO: remove in ngio 0.6
856
+ if labels is not None:
857
+ warnings.warn(
858
+ "The 'labels' argument is deprecated and will be removed in "
859
+ "a future release.",
860
+ DeprecationWarning,
861
+ stacklevel=2,
862
+ )
863
+ channels_meta = list(labels)
864
+ if pixel_size is not None:
865
+ warnings.warn(
866
+ "The 'pixel_size' argument is deprecated and will be removed in "
867
+ "a future release.",
868
+ DeprecationWarning,
869
+ stacklevel=2,
870
+ )
871
+ pixelsize_ = (pixel_size.y, pixel_size.x)
872
+ z_spacing_ = pixel_size.z
873
+ time_spacing_ = pixel_size.t
874
+ else:
875
+ if pixelsize is None:
876
+ pixelsize_ = (ref_image.pixel_size.y, ref_image.pixel_size.x)
877
+ else:
878
+ pixelsize_ = pixelsize
879
+
880
+ if z_spacing is None:
881
+ z_spacing_ = ref_image.pixel_size.z
882
+ else:
883
+ z_spacing_ = z_spacing
884
+
885
+ if time_spacing is None:
886
+ time_spacing_ = ref_image.pixel_size.t
887
+ else:
888
+ time_spacing_ = time_spacing
889
+ ref_meta = ref_image.meta
890
+
891
+ shapes = _compute_pyramid_shapes(
892
+ shape=shape,
893
+ ref_image=ref_image,
894
+ )
895
+ ref_shape = next(iter(shapes))
896
+
897
+ if pixelsize is None:
898
+ pixelsize = (ref_image.pixel_size.y, ref_image.pixel_size.x)
899
+
900
+ if z_spacing is None:
901
+ z_spacing = ref_image.pixel_size.z
902
+
903
+ if time_spacing is None:
904
+ time_spacing = ref_image.pixel_size.t
905
+
906
+ if name is None:
907
+ name = ref_meta.name
908
+
909
+ if dtype is None:
910
+ dtype = ref_image.dtype
911
+
912
+ if dimension_separator is None:
913
+ dimension_separator = find_dimension_separator(ref_image.zarr_array)
914
+
915
+ if compressors is None:
916
+ compressors = ref_image.zarr_array.compressors # type: ignore
917
+
918
+ if chunks is None:
919
+ chunks = ref_image.zarr_array.chunks
920
+ if shards is None:
921
+ shards = ref_image.zarr_array.shards
922
+
923
+ _check_chunks_and_shards_compatibility(
924
+ ref_shape=ref_shape,
925
+ chunks=chunks,
926
+ shards=shards,
927
+ )
928
+
929
+ if ngff_version is None:
930
+ ngff_version = ref_meta.version
931
+
932
+ shapes, axes, chunks, shards = _apply_channel_policy(
933
+ ref_image=ref_image,
934
+ channels_policy=channels_policy,
935
+ shapes=shapes,
936
+ axes=ref_image.axes,
937
+ chunks=chunks,
938
+ shards=shards,
939
+ )
940
+ channels_meta_ = _check_channels_meta_compatibility(
941
+ meta_type=meta_type,
942
+ ref_image=ref_image,
943
+ channels_meta=channels_meta,
944
+ )
945
+
946
+ handler = init_image_like_from_shapes(
947
+ store=store,
948
+ meta_type=meta_type,
949
+ shapes=shapes,
950
+ pixelsize=pixelsize_,
951
+ z_spacing=z_spacing_,
952
+ time_spacing=time_spacing_,
953
+ levels=ref_meta.paths,
954
+ time_unit=ref_image.time_unit,
955
+ space_unit=ref_image.space_unit,
956
+ axes_names=axes,
957
+ name=name,
958
+ channels_meta=channels_meta_,
959
+ chunks=chunks,
960
+ shards=shards,
961
+ dtype=dtype,
962
+ dimension_separator=dimension_separator,
963
+ compressors=compressors,
964
+ overwrite=overwrite,
965
+ ngff_version=ngff_version,
966
+ extra_array_kwargs=extra_array_kwargs,
967
+ )
968
+ return handler