ngio 0.1.5__py3-none-any.whl → 0.2.0__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 (87) hide show
  1. ngio/__init__.py +33 -5
  2. ngio/common/__init__.py +54 -0
  3. ngio/common/_array_pipe.py +265 -0
  4. ngio/common/_axes_transforms.py +64 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +120 -0
  7. ngio/common/_masking_roi.py +158 -0
  8. ngio/common/_pyramid.py +228 -0
  9. ngio/common/_roi.py +165 -0
  10. ngio/common/_slicer.py +96 -0
  11. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +51 -83
  12. ngio/hcs/__init__.py +5 -0
  13. ngio/hcs/plate.py +448 -0
  14. ngio/images/__init__.py +23 -0
  15. ngio/images/abstract_image.py +349 -0
  16. ngio/images/create.py +270 -0
  17. ngio/images/image.py +453 -0
  18. ngio/images/label.py +285 -0
  19. ngio/images/masked_image.py +273 -0
  20. ngio/images/ome_zarr_container.py +738 -0
  21. ngio/ome_zarr_meta/__init__.py +47 -0
  22. ngio/ome_zarr_meta/_meta_handlers.py +791 -0
  23. ngio/ome_zarr_meta/ngio_specs/__init__.py +71 -0
  24. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  25. ngio/ome_zarr_meta/ngio_specs/_channels.py +389 -0
  26. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  27. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +377 -0
  28. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +489 -0
  29. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +116 -0
  30. ngio/ome_zarr_meta/v04/__init__.py +23 -0
  31. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +485 -0
  32. ngio/tables/__init__.py +24 -6
  33. ngio/tables/_validators.py +190 -0
  34. ngio/tables/backends/__init__.py +8 -0
  35. ngio/tables/backends/_abstract_backend.py +71 -0
  36. ngio/tables/backends/_anndata_utils.py +198 -0
  37. ngio/tables/backends/_anndata_v1.py +76 -0
  38. ngio/tables/backends/_json_v1.py +56 -0
  39. ngio/tables/backends/_table_backends.py +102 -0
  40. ngio/tables/tables_container.py +310 -0
  41. ngio/tables/v1/__init__.py +5 -5
  42. ngio/tables/v1/_feature_table.py +182 -0
  43. ngio/tables/v1/_generic_table.py +160 -179
  44. ngio/tables/v1/_roi_table.py +366 -0
  45. ngio/utils/__init__.py +26 -10
  46. ngio/utils/_datasets.py +53 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_fractal_fsspec_store.py +13 -0
  49. ngio/utils/_logger.py +3 -1
  50. ngio/utils/_zarr_utils.py +401 -0
  51. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/METADATA +31 -43
  52. ngio-0.2.0.dist-info/RECORD +54 -0
  53. ngio/core/__init__.py +0 -7
  54. ngio/core/dimensions.py +0 -122
  55. ngio/core/image_handler.py +0 -228
  56. ngio/core/image_like_handler.py +0 -549
  57. ngio/core/label_handler.py +0 -410
  58. ngio/core/ngff_image.py +0 -387
  59. ngio/core/roi.py +0 -92
  60. ngio/core/utils.py +0 -287
  61. ngio/io/__init__.py +0 -19
  62. ngio/io/_zarr.py +0 -88
  63. ngio/io/_zarr_array_utils.py +0 -0
  64. ngio/io/_zarr_group_utils.py +0 -61
  65. ngio/iterators/__init__.py +0 -1
  66. ngio/ngff_meta/__init__.py +0 -27
  67. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  68. ngio/ngff_meta/meta_handler.py +0 -92
  69. ngio/ngff_meta/utils.py +0 -235
  70. ngio/ngff_meta/v04/__init__.py +0 -6
  71. ngio/ngff_meta/v04/specs.py +0 -158
  72. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  73. ngio/pipes/__init__.py +0 -7
  74. ngio/pipes/_slicer_transforms.py +0 -176
  75. ngio/pipes/_transforms.py +0 -33
  76. ngio/pipes/data_pipe.py +0 -52
  77. ngio/tables/_ad_reader.py +0 -80
  78. ngio/tables/_utils.py +0 -301
  79. ngio/tables/tables_group.py +0 -252
  80. ngio/tables/v1/feature_tables.py +0 -182
  81. ngio/tables/v1/masking_roi_tables.py +0 -243
  82. ngio/tables/v1/roi_tables.py +0 -285
  83. ngio/utils/_common_types.py +0 -5
  84. ngio/utils/_pydantic_utils.py +0 -52
  85. ngio-0.1.5.dist-info/RECORD +0 -44
  86. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/WHEEL +0 -0
  87. {ngio-0.1.5.dist-info → ngio-0.2.0.dist-info}/licenses/LICENSE +0 -0
ngio/images/image.py ADDED
@@ -0,0 +1,453 @@
1
+ """Generic class to handle Image-like data in a OME-NGFF file."""
2
+
3
+ from collections.abc import Collection
4
+ from typing import Literal
5
+
6
+ from dask import array as da
7
+
8
+ from ngio.common import Dimensions
9
+ from ngio.images.abstract_image import AbstractImage, consolidate_image
10
+ from ngio.images.create import _create_empty_image
11
+ from ngio.ome_zarr_meta import (
12
+ ImageMetaHandler,
13
+ NgioImageMeta,
14
+ PixelSize,
15
+ find_image_meta_handler,
16
+ )
17
+ from ngio.ome_zarr_meta.ngio_specs import Channel, ChannelsMeta, ChannelVisualisation
18
+ from ngio.utils import (
19
+ NgioValidationError,
20
+ StoreOrGroup,
21
+ ZarrGroupHandler,
22
+ )
23
+
24
+
25
+ def _check_channel_meta(meta: NgioImageMeta, dimension: Dimensions) -> ChannelsMeta:
26
+ """Check the channel metadata."""
27
+ c_dim = dimension.get("c", strict=False)
28
+ c_dim = 1 if c_dim is None else c_dim
29
+
30
+ if meta.channels_meta is None:
31
+ return ChannelsMeta.default_init(labels=c_dim)
32
+
33
+ if len(meta.channels) != c_dim:
34
+ raise NgioValidationError(
35
+ "The number of channels does not match the image. "
36
+ f"Expected {len(meta.channels)} channels, got {c_dim}."
37
+ )
38
+
39
+ return meta.channels_meta
40
+
41
+
42
+ class Image(AbstractImage[ImageMetaHandler]):
43
+ """A class to handle a single image (or level) in an OME-Zarr image.
44
+
45
+ This class is meant to be subclassed by specific image types.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ group_handler: ZarrGroupHandler,
51
+ path: str,
52
+ meta_handler: ImageMetaHandler | None,
53
+ ) -> None:
54
+ """Initialize the Image at a single level.
55
+
56
+ Args:
57
+ group_handler: The Zarr group handler.
58
+ path: The path to the image in the ome_zarr file.
59
+ meta_handler: The image metadata handler.
60
+
61
+ """
62
+ if meta_handler is None:
63
+ meta_handler = find_image_meta_handler(group_handler)
64
+ super().__init__(
65
+ group_handler=group_handler, path=path, meta_handler=meta_handler
66
+ )
67
+ self._channels_meta = _check_channel_meta(self.meta, self.dimensions)
68
+
69
+ @property
70
+ def meta(self) -> NgioImageMeta:
71
+ """Return the metadata."""
72
+ return self._meta_handler.meta
73
+
74
+ @property
75
+ def channel_labels(self) -> list[str]:
76
+ """Return the channels of the image."""
77
+ channel_labels = []
78
+ for c in self._channels_meta.channels:
79
+ channel_labels.append(c.label)
80
+ return channel_labels
81
+
82
+ @property
83
+ def wavelength_ids(self) -> list[str | None]:
84
+ """Return the list of wavelength of the image."""
85
+ wavelength_ids = []
86
+ for c in self._channels_meta.channels:
87
+ wavelength_ids.append(c.wavelength_id)
88
+ return wavelength_ids
89
+
90
+ @property
91
+ def num_channels(self) -> int:
92
+ """Return the number of channels."""
93
+ return len(self._channels_meta.channels)
94
+
95
+ def consolidate(
96
+ self,
97
+ order: Literal[0, 1, 2] = 1,
98
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
99
+ ) -> None:
100
+ """Consolidate the label on disk."""
101
+ consolidate_image(self, order=order, mode=mode)
102
+
103
+
104
+ class ImagesContainer:
105
+ """A class to handle the /labels group in an OME-NGFF file."""
106
+
107
+ def __init__(self, group_handler: ZarrGroupHandler) -> None:
108
+ """Initialize the LabelGroupHandler."""
109
+ self._group_handler = group_handler
110
+ self._meta_handler = find_image_meta_handler(group_handler)
111
+
112
+ @property
113
+ def meta(self) -> NgioImageMeta:
114
+ """Return the metadata."""
115
+ return self._meta_handler.meta
116
+
117
+ @property
118
+ def levels(self) -> int:
119
+ """Return the number of levels in the image."""
120
+ return self._meta_handler.meta.levels
121
+
122
+ @property
123
+ def levels_paths(self) -> list[str]:
124
+ """Return the paths of the levels in the image."""
125
+ return self._meta_handler.meta.paths
126
+
127
+ @property
128
+ def num_channels(self) -> int:
129
+ """Return the number of channels."""
130
+ image = self.get()
131
+ return image.num_channels
132
+
133
+ @property
134
+ def channel_labels(self) -> list[str]:
135
+ """Return the channels of the image."""
136
+ image = self.get()
137
+ return image.channel_labels
138
+
139
+ @property
140
+ def wavelength_ids(self) -> list[str | None]:
141
+ """Return the wavelength of the image."""
142
+ image = self.get()
143
+ return image.wavelength_ids
144
+
145
+ def initialize_channel_meta(
146
+ self,
147
+ labels: Collection[str] | int | None = None,
148
+ wavelength_id: Collection[str] | None = None,
149
+ percentiles: tuple[float, float] | None = None,
150
+ colors: Collection[str] | None = None,
151
+ active: Collection[bool] | None = None,
152
+ **omero_kwargs: dict,
153
+ ) -> None:
154
+ """Create a ChannelsMeta object with the default unit.
155
+
156
+ Args:
157
+ labels(Collection[str] | int): The list of channels names in the image.
158
+ If an integer is provided, the channels will be named "channel_i".
159
+ wavelength_id(Collection[str] | None): The wavelength ID of the channel.
160
+ If None, the wavelength ID will be the same as the channel name.
161
+ percentiles(tuple[float, float] | None): The start and end percentiles
162
+ for each channel. If None, the percentiles will not be computed.
163
+ colors(Collection[str, NgioColors] | None): The list of colors for the
164
+ channels. If None, the colors will be random.
165
+ active (Collection[bool] | None):active(bool): Whether the channel should
166
+ be shown by default.
167
+ omero_kwargs(dict): Extra fields to store in the omero attributes.
168
+ """
169
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
170
+ ref_image = self.get(path=low_res_dataset.path)
171
+
172
+ if percentiles is not None:
173
+ start, end = compute_image_percentile(
174
+ ref_image,
175
+ start_percentile=percentiles[0],
176
+ end_percentile=percentiles[1],
177
+ )
178
+ else:
179
+ start, end = None, None
180
+
181
+ if labels is None:
182
+ labels = ref_image.num_channels
183
+
184
+ channel_meta = ChannelsMeta.default_init(
185
+ labels=labels,
186
+ wavelength_id=wavelength_id,
187
+ colors=colors,
188
+ start=start,
189
+ end=end,
190
+ active=active,
191
+ data_type=ref_image.dtype,
192
+ **omero_kwargs,
193
+ )
194
+
195
+ meta = self.meta
196
+ meta.set_channels_meta(channel_meta)
197
+ self._meta_handler.write_meta(meta)
198
+
199
+ def update_percentiles(
200
+ self,
201
+ start_percentile: float = 0.1,
202
+ end_percentile: float = 99.9,
203
+ ) -> None:
204
+ """Update the percentiles of the channels."""
205
+ if self.meta._channels_meta is None:
206
+ raise NgioValidationError("The channels meta is not initialized.")
207
+
208
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
209
+ ref_image = self.get(path=low_res_dataset.path)
210
+ starts, ends = compute_image_percentile(
211
+ ref_image, start_percentile=start_percentile, end_percentile=end_percentile
212
+ )
213
+
214
+ channels = []
215
+ for c, channel in enumerate(self.meta._channels_meta.channels):
216
+ new_v = ChannelVisualisation(
217
+ start=starts[c],
218
+ end=ends[c],
219
+ **channel.channel_visualisation.model_dump(exclude={"start", "end"}),
220
+ )
221
+ new_c = Channel(
222
+ channel_visualisation=new_v,
223
+ **channel.model_dump(exclude={"channel_visualisation"}),
224
+ )
225
+ channels.append(new_c)
226
+
227
+ new_meta = ChannelsMeta(channels=channels)
228
+
229
+ meta = self.meta
230
+ meta.set_channels_meta(new_meta)
231
+ self._meta_handler.write_meta(meta)
232
+
233
+ def derive(
234
+ self,
235
+ store: StoreOrGroup,
236
+ ref_path: str | None = None,
237
+ shape: Collection[int] | None = None,
238
+ labels: Collection[str] | None = None,
239
+ pixel_size: PixelSize | None = None,
240
+ axes_names: Collection[str] | None = None,
241
+ chunks: Collection[int] | None = None,
242
+ dtype: str | None = None,
243
+ overwrite: bool = False,
244
+ ) -> "ImagesContainer":
245
+ """Create an empty OME-Zarr image from an existing image.
246
+
247
+ Args:
248
+ store (StoreOrGroup): The Zarr store or group to create the image in.
249
+ ref_path (str | None): The path to the reference image in
250
+ the image container.
251
+ shape (Collection[int] | None): The shape of the new image.
252
+ labels (Collection[str] | None): The labels of the new image.
253
+ pixel_size (PixelSize | None): The pixel size of the new image.
254
+ axes_names (Collection[str] | None): The axes names of the new image.
255
+ chunks (Collection[int] | None): The chunk shape of the new image.
256
+ dtype (str | None): The data type of the new image.
257
+ overwrite (bool): Whether to overwrite an existing image.
258
+
259
+ Returns:
260
+ ImagesContainer: The new image
261
+ """
262
+ return derive_image_container(
263
+ image_container=self,
264
+ store=store,
265
+ ref_path=ref_path,
266
+ shape=shape,
267
+ labels=labels,
268
+ pixel_size=pixel_size,
269
+ axes_names=axes_names,
270
+ chunks=chunks,
271
+ dtype=dtype,
272
+ overwrite=overwrite,
273
+ )
274
+
275
+ def get(
276
+ self,
277
+ path: str | None = None,
278
+ pixel_size: PixelSize | None = None,
279
+ strict: bool = False,
280
+ ) -> Image:
281
+ """Get an image at a specific level.
282
+
283
+ Args:
284
+ path (str | None): The path to the image in the ome_zarr file.
285
+ pixel_size (PixelSize | None): The pixel size of the image.
286
+ strict (bool): Only used if the pixel size is provided. If True, the
287
+ pixel size must match the image pixel size exactly. If False, the
288
+ closest pixel size level will be returned.
289
+
290
+ """
291
+ dataset = self._meta_handler.meta.get_dataset(
292
+ path=path, pixel_size=pixel_size, strict=strict
293
+ )
294
+ return Image(
295
+ group_handler=self._group_handler,
296
+ path=dataset.path,
297
+ meta_handler=self._meta_handler,
298
+ )
299
+
300
+
301
+ def compute_image_percentile(
302
+ image: Image,
303
+ start_percentile: float = 0.1,
304
+ end_percentile: float = 99.9,
305
+ ) -> tuple[list[float], list[float]]:
306
+ """Compute the start and end percentiles for each channel of an image.
307
+
308
+ Args:
309
+ image: The image to compute the percentiles for.
310
+ start_percentile: The start percentile to compute.
311
+ end_percentile: The end percentile to compute.
312
+
313
+ Returns:
314
+ A tuple containing the start and end percentiles for each channel.
315
+ """
316
+ starts, ends = [], []
317
+ for c in range(image.num_channels):
318
+ if image.num_channels == 1:
319
+ data = image.get_array(mode="dask").ravel()
320
+ else:
321
+ data = image.get_array(c=c, mode="dask").ravel()
322
+ # remove all the zeros
323
+ mask = data > 1e-16
324
+ data = data[mask]
325
+ _data = data.compute()
326
+ if _data.size == 0:
327
+ starts.append(0.0)
328
+ ends.append(0.0)
329
+ continue
330
+
331
+ # compute the percentiles
332
+ _s_perc, _e_perc = da.percentile(
333
+ data, [start_percentile, end_percentile], method="nearest"
334
+ ).compute()
335
+
336
+ starts.append(float(_s_perc))
337
+ ends.append(float(_e_perc))
338
+ return starts, ends
339
+
340
+
341
+ def derive_image_container(
342
+ image_container: ImagesContainer,
343
+ store: StoreOrGroup,
344
+ ref_path: str | None = None,
345
+ shape: Collection[int] | None = None,
346
+ labels: Collection[str] | None = None,
347
+ pixel_size: PixelSize | None = None,
348
+ axes_names: Collection[str] | None = None,
349
+ chunks: Collection[int] | None = None,
350
+ dtype: str | None = None,
351
+ overwrite: bool = False,
352
+ ) -> ImagesContainer:
353
+ """Create an empty OME-Zarr image from an existing image.
354
+
355
+ Args:
356
+ image_container (ImagesContainer): The image container to derive the new image.
357
+ store (StoreOrGroup): The Zarr store or group to create the image in.
358
+ ref_path (str | None): The path to the reference image in the image container.
359
+ shape (Collection[int] | None): The shape of the new image.
360
+ labels (Collection[str] | None): The labels of the new image.
361
+ pixel_size (PixelSize | None): The pixel size of the new image.
362
+ axes_names (Collection[str] | None): The axes names of the new image.
363
+ chunks (Collection[int] | None): The chunk shape of the new image.
364
+ dtype (str | None): The data type of the new image.
365
+ overwrite (bool): Whether to overwrite an existing image.
366
+
367
+ Returns:
368
+ ImagesContainer: The new image
369
+
370
+ """
371
+ if ref_path is None:
372
+ ref_image = image_container.get()
373
+ else:
374
+ ref_image = image_container.get(path=ref_path)
375
+
376
+ ref_meta = ref_image.meta
377
+
378
+ if shape is None:
379
+ shape = ref_image.shape
380
+
381
+ if pixel_size is None:
382
+ pixel_size = ref_image.pixel_size
383
+
384
+ if axes_names is None:
385
+ axes_names = ref_meta.axes_mapper.on_disk_axes_names
386
+
387
+ if len(axes_names) != len(shape):
388
+ raise NgioValidationError(
389
+ "The axes names of the new image does not match the reference image."
390
+ f"Got {axes_names} for shape {shape}."
391
+ )
392
+
393
+ if chunks is None:
394
+ chunks = ref_image.chunks
395
+
396
+ if len(chunks) != len(shape):
397
+ raise NgioValidationError(
398
+ "The chunks of the new image does not match the reference image."
399
+ f"Got {chunks} for shape {shape}."
400
+ )
401
+
402
+ if dtype is None:
403
+ dtype = ref_image.dtype
404
+ handler = _create_empty_image(
405
+ store=store,
406
+ shape=shape,
407
+ pixelsize=pixel_size.x,
408
+ z_spacing=pixel_size.z,
409
+ time_spacing=pixel_size.t,
410
+ levels=ref_meta.levels,
411
+ yx_scaling_factor=ref_meta.yx_scaling(),
412
+ z_scaling_factor=ref_meta.z_scaling(),
413
+ time_unit=pixel_size.time_unit,
414
+ space_unit=pixel_size.space_unit,
415
+ axes_names=axes_names,
416
+ chunks=chunks,
417
+ dtype=dtype,
418
+ overwrite=overwrite,
419
+ version=ref_meta.version,
420
+ )
421
+ image_container = ImagesContainer(handler)
422
+
423
+ if ref_image.num_channels == image_container.num_channels:
424
+ _labels = ref_image.channel_labels
425
+ wavelength_id = ref_image.wavelength_ids
426
+
427
+ colors = [
428
+ c.channel_visualisation.color for c in ref_image._channels_meta.channels
429
+ ]
430
+ active = [
431
+ c.channel_visualisation.active for c in ref_image._channels_meta.channels
432
+ ]
433
+ else:
434
+ _labels = None
435
+ wavelength_id = None
436
+ colors = None
437
+ active = None
438
+
439
+ if labels is not None:
440
+ if len(labels) != image_container.num_channels:
441
+ raise NgioValidationError(
442
+ "The number of labels does not match the number of channels."
443
+ )
444
+ _labels = labels
445
+
446
+ image_container.initialize_channel_meta(
447
+ labels=_labels,
448
+ wavelength_id=wavelength_id,
449
+ percentiles=None,
450
+ colors=colors,
451
+ active=active,
452
+ )
453
+ return image_container