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/images/_image.py ADDED
@@ -0,0 +1,926 @@
1
+ """Generic class to handle Image-like data in a OME-NGFF file."""
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any, Literal
5
+
6
+ import dask.array as da
7
+ import numpy as np
8
+ from pydantic import BaseModel, model_validator
9
+ from zarr.core.array import CompressorLike
10
+
11
+ from ngio.common import (
12
+ Dimensions,
13
+ InterpolationOrder,
14
+ Roi,
15
+ )
16
+ from ngio.common._pyramid import ChunksLike, ShardsLike
17
+ from ngio.images._abstract_image import AbstractImage, abstract_derive
18
+ from ngio.io_pipes import (
19
+ SlicingInputType,
20
+ TransformProtocol,
21
+ )
22
+ from ngio.ome_zarr_meta import (
23
+ ImageMetaHandler,
24
+ NgioImageMeta,
25
+ PixelSize,
26
+ )
27
+ from ngio.ome_zarr_meta.ngio_specs import (
28
+ Channel,
29
+ ChannelsMeta,
30
+ ChannelVisualisation,
31
+ DefaultSpaceUnit,
32
+ DefaultTimeUnit,
33
+ NgffVersions,
34
+ SpaceUnits,
35
+ TimeUnits,
36
+ )
37
+ from ngio.utils import (
38
+ NgioValueError,
39
+ StoreOrGroup,
40
+ ZarrGroupHandler,
41
+ )
42
+
43
+
44
+ class ChannelSelectionModel(BaseModel):
45
+ """Model for channel selection.
46
+
47
+ This model is used to select a channel by label, wavelength ID, or index.
48
+
49
+ Args:
50
+ identifier (str): Unique identifier for the channel.
51
+ This can be a channel label, wavelength ID, or index.
52
+ mode (Literal["label", "wavelength_id", "index"]): Specifies how to
53
+ interpret the identifier. Can be "label", "wavelength_id", or
54
+ "index" (must be an integer).
55
+
56
+ """
57
+
58
+ mode: Literal["label", "wavelength_id", "index"] = "label"
59
+ identifier: str
60
+
61
+ @model_validator(mode="after")
62
+ def check_channel_selection(self):
63
+ if self.mode == "index":
64
+ try:
65
+ int(self.identifier)
66
+ except ValueError as e:
67
+ raise ValueError(
68
+ "Identifier must be an integer when mode is 'index'"
69
+ ) from e
70
+ return self
71
+
72
+
73
+ ChannelSlicingInputType = (
74
+ None
75
+ | int
76
+ | str
77
+ | ChannelSelectionModel
78
+ | Sequence[str | ChannelSelectionModel | int]
79
+ )
80
+
81
+
82
+ def _check_channel_meta(meta: NgioImageMeta, dimension: Dimensions) -> ChannelsMeta:
83
+ """Check the channel metadata."""
84
+ c_dim = dimension.get("c", default=1)
85
+
86
+ if meta.channels_meta is None:
87
+ return ChannelsMeta.default_init(labels=c_dim)
88
+
89
+ if len(meta.channels_meta.channels) != c_dim:
90
+ raise NgioValueError(
91
+ "The number of channels does not match the image. "
92
+ f"Expected {len(meta.channels_meta.channels)} channels, got {c_dim}."
93
+ )
94
+
95
+ return meta.channels_meta
96
+
97
+
98
+ class Image(AbstractImage):
99
+ """A class to handle a single image (or level) in an OME-Zarr image.
100
+
101
+ This class is meant to be subclassed by specific image types.
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ group_handler: ZarrGroupHandler,
107
+ path: str,
108
+ meta_handler: ImageMetaHandler | None,
109
+ ) -> None:
110
+ """Initialize the Image at a single level.
111
+
112
+ Args:
113
+ group_handler: The Zarr group handler.
114
+ path: The path to the image in the ome_zarr file.
115
+ meta_handler: The image metadata handler.
116
+
117
+ """
118
+ if meta_handler is None:
119
+ meta_handler = ImageMetaHandler(group_handler)
120
+ super().__init__(
121
+ group_handler=group_handler, path=path, meta_handler=meta_handler
122
+ )
123
+
124
+ @property
125
+ def meta_handler(self) -> ImageMetaHandler:
126
+ """Return the metadata handler."""
127
+ assert isinstance(self._meta_handler, ImageMetaHandler)
128
+ return self._meta_handler
129
+
130
+ @property
131
+ def meta(self) -> NgioImageMeta:
132
+ """Return the metadata."""
133
+ meta = self.meta_handler.get_meta()
134
+ assert isinstance(meta, NgioImageMeta)
135
+ return meta
136
+
137
+ @property
138
+ def channels_meta(self) -> ChannelsMeta:
139
+ """Return the channels metadata."""
140
+ return _check_channel_meta(self.meta, self.dimensions)
141
+
142
+ @property
143
+ def channel_labels(self) -> list[str]:
144
+ """Return the channels of the image."""
145
+ return self.channels_meta.channel_labels
146
+
147
+ @property
148
+ def wavelength_ids(self) -> list[str | None]:
149
+ """Return the list of wavelength of the image."""
150
+ return self.channels_meta.channel_wavelength_ids
151
+
152
+ @property
153
+ def num_channels(self) -> int:
154
+ """Return the number of channels."""
155
+ return len(self.channel_labels)
156
+
157
+ def get_channel_idx(
158
+ self, channel_label: str | None = None, wavelength_id: str | None = None
159
+ ) -> int:
160
+ """Get the index of a channel by its label or wavelength ID."""
161
+ return self.channels_meta.get_channel_idx(
162
+ channel_label=channel_label, wavelength_id=wavelength_id
163
+ )
164
+
165
+ def get_as_numpy(
166
+ self,
167
+ channel_selection: ChannelSlicingInputType = None,
168
+ axes_order: Sequence[str] | None = None,
169
+ transforms: Sequence[TransformProtocol] | None = None,
170
+ **slicing_kwargs: slice | int | Sequence[int] | None,
171
+ ) -> np.ndarray:
172
+ """Get the image as a numpy array.
173
+
174
+ Args:
175
+ channel_selection: Select a specific channel by label.
176
+ If None, all channels are returned.
177
+ Alternatively, you can slice arbitrary channels
178
+ using the slice_kwargs (c=[0, 2]).
179
+ axes_order: The order of the axes to return the array.
180
+ transforms: The transforms to apply to the array.
181
+ **slicing_kwargs: The slices to get the array.
182
+
183
+ Returns:
184
+ The array of the region of interest.
185
+ """
186
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
187
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
188
+ )
189
+ return self._get_as_numpy(
190
+ axes_order=axes_order, transforms=transforms, **_slicing_kwargs
191
+ )
192
+
193
+ def get_roi_as_numpy(
194
+ self,
195
+ roi: Roi,
196
+ channel_selection: ChannelSlicingInputType = None,
197
+ axes_order: Sequence[str] | None = None,
198
+ transforms: Sequence[TransformProtocol] | None = None,
199
+ **slicing_kwargs: SlicingInputType,
200
+ ) -> np.ndarray:
201
+ """Get the image as a numpy array for a region of interest.
202
+
203
+ Args:
204
+ roi: The region of interest to get the array.
205
+ channel_selection: Select a what subset of channels to return.
206
+ If None, all channels are returned.
207
+ axes_order: The order of the axes to return the array.
208
+ transforms: The transforms to apply to the array.
209
+ **slicing_kwargs: The slices to get the array.
210
+
211
+ Returns:
212
+ The array of the region of interest.
213
+ """
214
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
215
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
216
+ )
217
+ return self._get_roi_as_numpy(
218
+ roi=roi, axes_order=axes_order, transforms=transforms, **_slicing_kwargs
219
+ )
220
+
221
+ def get_as_dask(
222
+ self,
223
+ channel_selection: ChannelSlicingInputType = None,
224
+ axes_order: Sequence[str] | None = None,
225
+ transforms: Sequence[TransformProtocol] | None = None,
226
+ **slicing_kwargs: SlicingInputType,
227
+ ) -> da.Array:
228
+ """Get the image as a dask array.
229
+
230
+ Args:
231
+ channel_selection: Select a what subset of channels to return.
232
+ If None, all channels are returned.
233
+ axes_order: The order of the axes to return the array.
234
+ transforms: The transforms to apply to the array.
235
+ **slicing_kwargs: The slices to get the array.
236
+
237
+ Returns:
238
+ The dask array of the region of interest.
239
+ """
240
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
241
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
242
+ )
243
+ return self._get_as_dask(
244
+ axes_order=axes_order, transforms=transforms, **_slicing_kwargs
245
+ )
246
+
247
+ def get_roi_as_dask(
248
+ self,
249
+ roi: Roi,
250
+ channel_selection: ChannelSlicingInputType = None,
251
+ axes_order: Sequence[str] | None = None,
252
+ transforms: Sequence[TransformProtocol] | None = None,
253
+ **slicing_kwargs: SlicingInputType,
254
+ ) -> da.Array:
255
+ """Get the image as a dask array for a region of interest.
256
+
257
+ Args:
258
+ roi: The region of interest to get the array.
259
+ channel_selection: Select a what subset of channels to return.
260
+ If None, all channels are returned.
261
+ axes_order: The order of the axes to return the array.
262
+ transforms: The transforms to apply to the array.
263
+ **slicing_kwargs: The slices to get the array.
264
+
265
+ Returns:
266
+ The dask array of the region of interest.
267
+ """
268
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
269
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
270
+ )
271
+ return self._get_roi_as_dask(
272
+ roi=roi, axes_order=axes_order, transforms=transforms, **_slicing_kwargs
273
+ )
274
+
275
+ def get_array(
276
+ self,
277
+ channel_selection: ChannelSlicingInputType = None,
278
+ axes_order: Sequence[str] | None = None,
279
+ transforms: Sequence[TransformProtocol] | None = None,
280
+ mode: Literal["numpy", "dask"] = "numpy",
281
+ **slicing_kwargs: SlicingInputType,
282
+ ) -> np.ndarray | da.Array:
283
+ """Get the image as a zarr array.
284
+
285
+ Args:
286
+ channel_selection: Select a what subset of channels to return.
287
+ If None, all channels are returned.
288
+ axes_order: The order of the axes to return the array.
289
+ transforms: The transforms to apply to the array.
290
+ mode: The object type to return.
291
+ Can be "dask", "numpy".
292
+ **slicing_kwargs: The slices to get the array.
293
+
294
+ Returns:
295
+ The zarr array of the region of interest.
296
+ """
297
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
298
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
299
+ )
300
+ return self._get_array(
301
+ axes_order=axes_order, mode=mode, transforms=transforms, **_slicing_kwargs
302
+ )
303
+
304
+ def get_roi(
305
+ self,
306
+ roi: Roi,
307
+ channel_selection: ChannelSlicingInputType = None,
308
+ axes_order: Sequence[str] | None = None,
309
+ transforms: Sequence[TransformProtocol] | None = None,
310
+ mode: Literal["numpy", "dask"] = "numpy",
311
+ **slicing_kwargs: SlicingInputType,
312
+ ) -> np.ndarray | da.Array:
313
+ """Get the image as a zarr array for a region of interest.
314
+
315
+ Args:
316
+ roi: The region of interest to get the array.
317
+ channel_selection: Select a what subset of channels to return.
318
+ If None, all channels are returned.
319
+ axes_order: The order of the axes to return the array.
320
+ transforms: The transforms to apply to the array.
321
+ mode: The object type to return.
322
+ Can be "dask", "numpy".
323
+ **slicing_kwargs: The slices to get the array.
324
+
325
+ Returns:
326
+ The zarr array of the region of interest.
327
+ """
328
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
329
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
330
+ )
331
+ return self._get_roi(
332
+ roi=roi,
333
+ axes_order=axes_order,
334
+ mode=mode,
335
+ transforms=transforms,
336
+ **_slicing_kwargs,
337
+ )
338
+
339
+ def set_array(
340
+ self,
341
+ patch: np.ndarray | da.Array,
342
+ channel_selection: ChannelSlicingInputType = None,
343
+ axes_order: Sequence[str] | None = None,
344
+ transforms: Sequence[TransformProtocol] | None = None,
345
+ **slicing_kwargs: SlicingInputType,
346
+ ) -> None:
347
+ """Set the image array.
348
+
349
+ Args:
350
+ patch: The array to set.
351
+ channel_selection: Select a what subset of channels to return.
352
+ If None, all channels are set.
353
+ axes_order: The order of the axes to set the array.
354
+ transforms: The transforms to apply to the array.
355
+ **slicing_kwargs: The slices to set the array.
356
+ """
357
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
358
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
359
+ )
360
+ self._set_array(
361
+ patch=patch, axes_order=axes_order, transforms=transforms, **_slicing_kwargs
362
+ )
363
+
364
+ def set_roi(
365
+ self,
366
+ roi: Roi,
367
+ patch: np.ndarray | da.Array,
368
+ channel_selection: ChannelSlicingInputType = None,
369
+ axes_order: Sequence[str] | None = None,
370
+ transforms: Sequence[TransformProtocol] | None = None,
371
+ **slicing_kwargs: SlicingInputType,
372
+ ) -> None:
373
+ """Set the image array for a region of interest.
374
+
375
+ Args:
376
+ roi: The region of interest to set the array.
377
+ patch: The array to set.
378
+ channel_selection: Select a what subset of channels to return.
379
+ axes_order: The order of the axes to set the array.
380
+ transforms: The transforms to apply to the array.
381
+ **slicing_kwargs: The slices to set the array.
382
+ """
383
+ _slicing_kwargs = add_channel_selection_to_slicing_dict(
384
+ image=self, channel_selection=channel_selection, slicing_dict=slicing_kwargs
385
+ )
386
+ self._set_roi(
387
+ roi=roi,
388
+ patch=patch,
389
+ axes_order=axes_order,
390
+ transforms=transforms,
391
+ **_slicing_kwargs,
392
+ )
393
+
394
+ def consolidate(
395
+ self,
396
+ order: InterpolationOrder = "linear",
397
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
398
+ ) -> None:
399
+ """Consolidate the label on disk."""
400
+ self._consolidate(order=order, mode=mode)
401
+
402
+
403
+ class ImagesContainer:
404
+ """A class to handle the /labels group in an OME-NGFF file."""
405
+
406
+ def __init__(self, group_handler: ZarrGroupHandler) -> None:
407
+ """Initialize the LabelGroupHandler."""
408
+ self._group_handler = group_handler
409
+ self._meta_handler = ImageMetaHandler(group_handler)
410
+
411
+ @property
412
+ def meta(self) -> NgioImageMeta:
413
+ """Return the metadata."""
414
+ return self._meta_handler.get_meta()
415
+
416
+ @property
417
+ def levels(self) -> int:
418
+ """Return the number of levels in the image."""
419
+ return self._meta_handler.get_meta().levels
420
+
421
+ @property
422
+ def levels_paths(self) -> list[str]:
423
+ """Return the paths of the levels in the image."""
424
+ return self._meta_handler.get_meta().paths
425
+
426
+ @property
427
+ def num_channels(self) -> int:
428
+ """Return the number of channels."""
429
+ image = self.get()
430
+ return image.num_channels
431
+
432
+ @property
433
+ def channel_labels(self) -> list[str]:
434
+ """Return the channels of the image."""
435
+ image = self.get()
436
+ return image.channel_labels
437
+
438
+ @property
439
+ def wavelength_ids(self) -> list[str | None]:
440
+ """Return the wavelength of the image."""
441
+ image = self.get()
442
+ return image.wavelength_ids
443
+
444
+ def get_channel_idx(
445
+ self, channel_label: str | None = None, wavelength_id: str | None = None
446
+ ) -> int:
447
+ """Get the index of a channel by label or wavelength ID.
448
+
449
+ Args:
450
+ channel_label (str | None): The label of the channel.
451
+ If None a wavelength ID must be provided.
452
+ wavelength_id (str | None): The wavelength ID of the channel.
453
+ If None a channel label must be provided.
454
+
455
+ Returns:
456
+ int: The index of the channel.
457
+
458
+ """
459
+ image = self.get()
460
+ return image.get_channel_idx(
461
+ channel_label=channel_label, wavelength_id=wavelength_id
462
+ )
463
+
464
+ def _set_channel_meta(
465
+ self,
466
+ channels_meta: ChannelsMeta,
467
+ ) -> None:
468
+ """Set the channels metadata."""
469
+ meta = self.meta
470
+ meta.set_channels_meta(channels_meta)
471
+ self._meta_handler.update_meta(meta)
472
+
473
+ def set_channel_meta(
474
+ self,
475
+ labels: Sequence[str | None] | int | None = None,
476
+ wavelength_id: Sequence[str | None] | None = None,
477
+ start: Sequence[float | None] | None = None,
478
+ end: Sequence[float | None] | None = None,
479
+ percentiles: tuple[float, float] | None = None,
480
+ colors: Sequence[str | None] | None = None,
481
+ active: Sequence[bool | None] | None = None,
482
+ **omero_kwargs: dict,
483
+ ) -> None:
484
+ """Create a ChannelsMeta object with the default unit.
485
+
486
+ Args:
487
+ labels(Sequence[str | None] | int): The list of channels names
488
+ in the image. If an integer is provided, the channels will
489
+ be named "channel_i".
490
+ wavelength_id(Sequence[str | None]): The wavelength ID of the channel.
491
+ If None, the wavelength ID will be the same as the channel name.
492
+ start(Sequence[float | None]): The start value for each channel.
493
+ If None, the start value will be computed from the image.
494
+ end(Sequence[float | None]): The end value for each channel.
495
+ If None, the end value will be computed from the image.
496
+ percentiles(tuple[float, float] | None): The start and end
497
+ percentiles for each channel. If None, the percentiles will
498
+ not be computed.
499
+ colors(Sequence[str | None]): The list of colors for the
500
+ channels. If None, the colors will be random.
501
+ active (Sequence[bool | None]): Whether the channel should
502
+ be shown by default.
503
+ omero_kwargs(dict): Extra fields to store in the omero attributes.
504
+ """
505
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
506
+ ref_image = self.get(path=low_res_dataset.path)
507
+
508
+ if start is not None and end is None:
509
+ raise NgioValueError("If start is provided, end must be provided as well.")
510
+ if end is not None and start is None:
511
+ raise NgioValueError("If end is provided, start must be provided as well.")
512
+
513
+ if start is not None and percentiles is not None:
514
+ raise NgioValueError(
515
+ "If start and end are provided, percentiles must be None."
516
+ )
517
+
518
+ if percentiles is not None:
519
+ start, end = compute_image_percentile(
520
+ ref_image,
521
+ start_percentile=percentiles[0],
522
+ end_percentile=percentiles[1],
523
+ )
524
+ elif start is not None and end is not None:
525
+ if len(start) != len(end):
526
+ raise NgioValueError(
527
+ "The start and end lists must have the same length."
528
+ )
529
+ if len(start) != self.num_channels:
530
+ raise NgioValueError(
531
+ "The start and end lists must have the same length as "
532
+ "the number of channels."
533
+ )
534
+
535
+ start = list(start)
536
+ end = list(end)
537
+
538
+ else:
539
+ start, end = None, None
540
+
541
+ if labels is None:
542
+ labels = ref_image.num_channels
543
+
544
+ channel_meta = ChannelsMeta.default_init(
545
+ labels=labels,
546
+ wavelength_id=wavelength_id,
547
+ colors=colors,
548
+ start=start,
549
+ end=end,
550
+ active=active,
551
+ data_type=ref_image.dtype,
552
+ **omero_kwargs,
553
+ )
554
+ self._set_channel_meta(channel_meta)
555
+
556
+ def set_channel_percentiles(
557
+ self,
558
+ start_percentile: float = 0.1,
559
+ end_percentile: float = 99.9,
560
+ ) -> None:
561
+ """Update the percentiles of the channels."""
562
+ if self.meta._channels_meta is None:
563
+ raise NgioValueError("The channels meta is not initialized.")
564
+
565
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
566
+ ref_image = self.get(path=low_res_dataset.path)
567
+ starts, ends = compute_image_percentile(
568
+ ref_image, start_percentile=start_percentile, end_percentile=end_percentile
569
+ )
570
+
571
+ channels = []
572
+ for c, channel in enumerate(self.meta._channels_meta.channels):
573
+ new_v = ChannelVisualisation(
574
+ start=starts[c],
575
+ end=ends[c],
576
+ **channel.channel_visualisation.model_dump(exclude={"start", "end"}),
577
+ )
578
+ new_c = Channel(
579
+ channel_visualisation=new_v,
580
+ **channel.model_dump(exclude={"channel_visualisation"}),
581
+ )
582
+ channels.append(new_c)
583
+
584
+ new_meta = ChannelsMeta(channels=channels)
585
+
586
+ meta = self.meta
587
+ meta.set_channels_meta(new_meta)
588
+ self._meta_handler.update_meta(meta)
589
+
590
+ def set_axes_unit(
591
+ self,
592
+ space_unit: SpaceUnits = DefaultSpaceUnit,
593
+ time_unit: TimeUnits = DefaultTimeUnit,
594
+ ) -> None:
595
+ """Set the axes unit of the image.
596
+
597
+ Args:
598
+ space_unit (SpaceUnits): The space unit of the image.
599
+ time_unit (TimeUnits): The time unit of the image.
600
+ """
601
+ meta = self.meta
602
+ meta = meta.to_units(space_unit=space_unit, time_unit=time_unit)
603
+ self._meta_handler.update_meta(meta)
604
+
605
+ def derive(
606
+ self,
607
+ store: StoreOrGroup,
608
+ ref_path: str | None = None,
609
+ # Metadata parameters
610
+ shape: Sequence[int] | None = None,
611
+ pixelsize: float | tuple[float, float] | None = None,
612
+ z_spacing: float | None = None,
613
+ time_spacing: float | None = None,
614
+ name: str | None = None,
615
+ channels_meta: Sequence[str | Channel] | None = None,
616
+ channels_policy: Literal["same", "squeeze", "singleton"] | int = "same",
617
+ ngff_version: NgffVersions | None = None,
618
+ # Zarr Array parameters
619
+ chunks: ChunksLike | None = None,
620
+ shards: ShardsLike | None = None,
621
+ dtype: str = "uint16",
622
+ dimension_separator: Literal[".", "/"] = "/",
623
+ compressors: CompressorLike = "auto",
624
+ extra_array_kwargs: Mapping[str, Any] | None = None,
625
+ overwrite: bool = False,
626
+ # Deprecated arguments
627
+ labels: Sequence[str] | None = None,
628
+ pixel_size: PixelSize | None = None,
629
+ ) -> "ImagesContainer":
630
+ """Create an empty OME-Zarr image from an existing image.
631
+
632
+ If a kwarg is not provided, the value from the reference image will be used.
633
+
634
+ Args:
635
+ image_container (ImagesContainer): The image container to derive the new
636
+ image.
637
+ store (StoreOrGroup): The Zarr store or group to create the image in.
638
+ ref_path (str | None): The path to the reference image in the image
639
+ container.
640
+ shape (Sequence[int] | None): The shape of the new image.
641
+ pixelsize (float | tuple[float, float] | None): The pixel size of the new
642
+ image.
643
+ z_spacing (float | None): The z spacing of the new image.
644
+ time_spacing (float | None): The time spacing of the new image.
645
+ name (str | None): The name of the new image.
646
+ channels_meta (Sequence[str | Channel] | None): The channels metadata
647
+ of the new image.
648
+ channels_policy (Literal["same", "squeeze", "singleton"] | int):
649
+ Possible policies:
650
+ - If "squeeze", the channels axis will be removed (no matter its size).
651
+ - If "same", the channels axis will be kept as is (if it exists).
652
+ - If "singleton", the channels axis will be set to size 1.
653
+ - If an integer is provided, the channels axis will be changed to have
654
+ that size.
655
+ ngff_version (NgffVersions | None): The NGFF version to use.
656
+ chunks (ChunksLike | None): The chunk shape of the new image.
657
+ shards (ShardsLike | None): The shard shape of the new image.
658
+ dtype (str | None): The data type of the new image.
659
+ dimension_separator (DIMENSION_SEPARATOR | None): The separator to use for
660
+ dimensions.
661
+ compressors (CompressorLike | None): The compressors to use.
662
+ extra_array_kwargs (Mapping[str, Any] | None): Extra arguments to pass to
663
+ the zarr array creation.
664
+ overwrite (bool): Whether to overwrite an existing image.
665
+ labels (Sequence[str] | None): The labels of the new image.
666
+ This argument is deprecated please use channels_meta instead.
667
+ pixel_size (PixelSize | None): The pixel size of the new image.
668
+ This argument is deprecated please use pixelsize, z_spacing,
669
+ and time_spacing instead.
670
+
671
+ Returns:
672
+ ImagesContainer: The new derived image.
673
+
674
+ """
675
+ return derive_image_container(
676
+ image_container=self,
677
+ store=store,
678
+ ref_path=ref_path,
679
+ shape=shape,
680
+ pixelsize=pixelsize,
681
+ z_spacing=z_spacing,
682
+ time_spacing=time_spacing,
683
+ name=name,
684
+ channels_meta=channels_meta,
685
+ channels_policy=channels_policy,
686
+ ngff_version=ngff_version,
687
+ chunks=chunks,
688
+ shards=shards,
689
+ dtype=dtype,
690
+ dimension_separator=dimension_separator,
691
+ compressors=compressors,
692
+ extra_array_kwargs=extra_array_kwargs,
693
+ overwrite=overwrite,
694
+ labels=labels,
695
+ pixel_size=pixel_size,
696
+ )
697
+
698
+ def get(
699
+ self,
700
+ path: str | None = None,
701
+ pixel_size: PixelSize | None = None,
702
+ strict: bool = False,
703
+ ) -> Image:
704
+ """Get an image at a specific level.
705
+
706
+ Args:
707
+ path (str | None): The path to the image in the ome_zarr file.
708
+ pixel_size (PixelSize | None): The pixel size of the image.
709
+ strict (bool): Only used if the pixel size is provided. If True, the
710
+ pixel size must match the image pixel size exactly. If False, the
711
+ closest pixel size level will be returned.
712
+
713
+ """
714
+ dataset = self._meta_handler.get_meta().get_dataset(
715
+ path=path, pixel_size=pixel_size, strict=strict
716
+ )
717
+ return Image(
718
+ group_handler=self._group_handler,
719
+ path=dataset.path,
720
+ meta_handler=self._meta_handler,
721
+ )
722
+
723
+
724
+ def compute_image_percentile(
725
+ image: Image,
726
+ start_percentile: float = 0.1,
727
+ end_percentile: float = 99.9,
728
+ ) -> tuple[list[float], list[float]]:
729
+ """Compute the start and end percentiles for each channel of an image.
730
+
731
+ Args:
732
+ image: The image to compute the percentiles for.
733
+ start_percentile: The start percentile to compute.
734
+ end_percentile: The end percentile to compute.
735
+
736
+ Returns:
737
+ A tuple containing the start and end percentiles for each channel.
738
+ """
739
+ starts, ends = [], []
740
+ for c in range(image.num_channels):
741
+ if image.num_channels == 1:
742
+ data = image.get_as_dask()
743
+ else:
744
+ data = image.get_as_dask(c=c)
745
+
746
+ data = da.ravel(data)
747
+ # remove all the zeros
748
+ mask = data > 1e-16
749
+ data = data[mask]
750
+ _data = data.compute()
751
+ if _data.size == 0:
752
+ starts.append(0.0)
753
+ ends.append(0.0)
754
+ continue
755
+
756
+ # compute the percentiles
757
+ _s_perc, _e_perc = da.percentile(
758
+ data, [start_percentile, end_percentile], method="nearest"
759
+ ).compute() # type: ignore (return type is a tuple of floats)
760
+
761
+ starts.append(float(_s_perc))
762
+ ends.append(float(_e_perc))
763
+ return starts, ends
764
+
765
+
766
+ def derive_image_container(
767
+ *,
768
+ image_container: ImagesContainer,
769
+ store: StoreOrGroup,
770
+ ref_path: str | None = None,
771
+ # Metadata parameters
772
+ shape: Sequence[int] | None = None,
773
+ pixelsize: float | tuple[float, float] | None = None,
774
+ z_spacing: float | None = None,
775
+ time_spacing: float | None = None,
776
+ name: str | None = None,
777
+ channels_policy: Literal["same", "squeeze", "singleton"] | int = "same",
778
+ channels_meta: Sequence[str | Channel] | None = None,
779
+ ngff_version: NgffVersions | None = None,
780
+ # Zarr Array parameters
781
+ chunks: ChunksLike | None = None,
782
+ shards: ShardsLike | None = None,
783
+ dtype: str | None = None,
784
+ dimension_separator: Literal[".", "/"] | None = None,
785
+ compressors: CompressorLike | None = None,
786
+ extra_array_kwargs: Mapping[str, Any] | None = None,
787
+ overwrite: bool = False,
788
+ # Deprecated arguments
789
+ labels: Sequence[str] | None = None,
790
+ pixel_size: PixelSize | None = None,
791
+ ) -> ImagesContainer:
792
+ """Derive a new OME-Zarr image container from an existing image.
793
+
794
+ If a kwarg is not provided, the value from the reference image will be used.
795
+
796
+ Args:
797
+ image_container (ImagesContainer): The image container to derive the new image
798
+ from.
799
+ store (StoreOrGroup): The Zarr store or group to create the image in.
800
+ ref_path (str | None): The path to the reference image in the image container.
801
+ shape (Sequence[int] | None): The shape of the new image.
802
+ pixelsize (float | tuple[float, float] | None): The pixel size of the new image.
803
+ z_spacing (float | None): The z spacing of the new image.
804
+ time_spacing (float | None): The time spacing of the new image.
805
+ name (str | None): The name of the new image.
806
+ channels_policy (Literal["squeeze", "same", "singleton"] | int): Possible
807
+ policies:
808
+ - If "squeeze", the channels axis will be removed (no matter its size).
809
+ - If "same", the channels axis will be kept as is (if it exists).
810
+ - If "singleton", the channels axis will be set to size 1.
811
+ - If an integer is provided, the channels axis will be changed to have
812
+ that size.
813
+ channels_meta (Sequence[str | Channel] | None): The channels metadata
814
+ of the new image.
815
+ ngff_version (NgffVersions | None): The NGFF version to use.
816
+ chunks (ChunksLike | None): The chunk shape of the new image.
817
+ shards (ShardsLike | None): The shard shape of the new image.
818
+ dtype (str | None): The data type of the new image.
819
+ dimension_separator (Literal[".", "/"] | None): The separator to use for
820
+ dimensions.
821
+ compressors (CompressorLike | None): The compressors to use.
822
+ extra_array_kwargs (Mapping[str, Any] | None): Extra arguments to pass to
823
+ the zarr array creation.
824
+ overwrite (bool): Whether to overwrite an existing image. Defaults to False.
825
+ labels (Sequence[str] | None): Deprecated. This argument is deprecated,
826
+ please use channels_meta instead.
827
+ pixel_size (PixelSize | None): Deprecated. The pixel size of the new image.
828
+ This argument is deprecated, please use pixelsize, z_spacing,
829
+ and time_spacing instead.
830
+
831
+ Returns:
832
+ ImagesContainer: The new derived image container.
833
+
834
+ """
835
+ ref_image = image_container.get(path=ref_path)
836
+ group_handler = abstract_derive(
837
+ ref_image=ref_image,
838
+ meta_type=NgioImageMeta,
839
+ store=store,
840
+ shape=shape,
841
+ pixelsize=pixelsize,
842
+ z_spacing=z_spacing,
843
+ time_spacing=time_spacing,
844
+ name=name,
845
+ channels_meta=channels_meta,
846
+ channels_policy=channels_policy,
847
+ ngff_version=ngff_version,
848
+ chunks=chunks,
849
+ shards=shards,
850
+ dtype=dtype,
851
+ dimension_separator=dimension_separator,
852
+ compressors=compressors,
853
+ extra_array_kwargs=extra_array_kwargs,
854
+ overwrite=overwrite,
855
+ labels=labels,
856
+ pixel_size=pixel_size,
857
+ )
858
+ return ImagesContainer(group_handler=group_handler)
859
+
860
+
861
+ def _parse_str_or_model(
862
+ image: Image, channel_selection: int | str | ChannelSelectionModel
863
+ ) -> int:
864
+ """Parse a string or ChannelSelectionModel to an integer channel index."""
865
+ if isinstance(channel_selection, int):
866
+ if channel_selection < 0:
867
+ raise NgioValueError("Channel index must be a non-negative integer.")
868
+ if channel_selection >= image.num_channels:
869
+ raise NgioValueError(
870
+ "Channel index must be less than the number "
871
+ f"of channels ({image.num_channels})."
872
+ )
873
+ return channel_selection
874
+ elif isinstance(channel_selection, str):
875
+ return image.get_channel_idx(channel_label=channel_selection)
876
+ elif isinstance(channel_selection, ChannelSelectionModel):
877
+ if channel_selection.mode == "label":
878
+ return image.get_channel_idx(
879
+ channel_label=str(channel_selection.identifier)
880
+ )
881
+ elif channel_selection.mode == "wavelength_id":
882
+ return image.get_channel_idx(
883
+ wavelength_id=str(channel_selection.identifier)
884
+ )
885
+ elif channel_selection.mode == "index":
886
+ return int(channel_selection.identifier)
887
+ raise NgioValueError(
888
+ "Invalid channel selection type. "
889
+ f"{channel_selection} is of type {type(channel_selection)} ",
890
+ "supported types are str, ChannelSelectionModel, and int.",
891
+ )
892
+
893
+
894
+ def _parse_channel_selection(
895
+ image: Image, channel_selection: ChannelSlicingInputType
896
+ ) -> dict[str, SlicingInputType]:
897
+ """Parse the channel selection input into a list of channel indices."""
898
+ if channel_selection is None:
899
+ return {}
900
+ if isinstance(channel_selection, int | str | ChannelSelectionModel):
901
+ channel_index = _parse_str_or_model(image, channel_selection)
902
+ return {"c": channel_index}
903
+ elif isinstance(channel_selection, Sequence):
904
+ _sequence = [_parse_str_or_model(image, cs) for cs in channel_selection]
905
+ return {"c": _sequence}
906
+ raise NgioValueError(
907
+ f"Invalid channel selection type {type(channel_selection)}. "
908
+ "Supported types are int, str, ChannelSelectionModel, and Sequence."
909
+ )
910
+
911
+
912
+ def add_channel_selection_to_slicing_dict(
913
+ image: Image,
914
+ channel_selection: ChannelSlicingInputType,
915
+ slicing_dict: dict[str, SlicingInputType],
916
+ ) -> dict[str, SlicingInputType]:
917
+ """Add channel selection information to the slicing dictionary."""
918
+ channel_info = _parse_channel_selection(image, channel_selection)
919
+ if "c" in slicing_dict and channel_info:
920
+ raise NgioValueError(
921
+ "Both channel_selection and 'c' in slicing_kwargs are provided. "
922
+ "Which channel selection should be used is ambiguous. "
923
+ "Please provide only one."
924
+ )
925
+ slicing_dict = slicing_dict | channel_info
926
+ return slicing_dict