ngio 0.5.0b6__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 (88) hide show
  1. ngio/__init__.py +69 -0
  2. ngio/common/__init__.py +28 -0
  3. ngio/common/_dimensions.py +335 -0
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +408 -0
  6. ngio/common/_roi.py +315 -0
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +188 -0
  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 +19 -0
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +44 -0
  20. ngio/images/_abstract_image.py +967 -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 +411 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1237 -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 +65 -0
  40. ngio/ome_zarr_meta/_meta_handlers.py +536 -0
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
  48. ngio/ome_zarr_meta/v04/__init__.py +27 -0
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
  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 +43 -0
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +57 -0
  63. ngio/tables/backends/_abstract_backend.py +240 -0
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +90 -0
  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 +226 -0
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +23 -0
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +125 -0
  75. ngio/tables/v1/_generic_table.py +49 -0
  76. ngio/tables/v1/_roi_table.py +575 -0
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +45 -0
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +165 -0
  82. ngio/utils/_errors.py +37 -0
  83. ngio/utils/_fractal_fsspec_store.py +42 -0
  84. ngio/utils/_zarr_utils.py +534 -0
  85. ngio-0.5.0b6.dist-info/METADATA +148 -0
  86. ngio-0.5.0b6.dist-info/RECORD +88 -0
  87. ngio-0.5.0b6.dist-info/WHEEL +4 -0
  88. ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,408 @@
1
+ from collections.abc import Callable, Mapping, Sequence
2
+ from typing import Any, Literal
3
+
4
+ import dask.array as da
5
+ import numpy as np
6
+ import zarr
7
+ from pydantic import BaseModel, ConfigDict, model_validator
8
+
9
+ from ngio.common._zoom import (
10
+ InterpolationOrder,
11
+ _zoom_inputs_check,
12
+ dask_zoom,
13
+ numpy_zoom,
14
+ )
15
+ from ngio.utils import (
16
+ NgioValueError,
17
+ )
18
+
19
+
20
+ def _on_disk_numpy_zoom(
21
+ source: zarr.Array,
22
+ target: zarr.Array,
23
+ order: InterpolationOrder,
24
+ ) -> None:
25
+ source_array = source[...]
26
+ if not isinstance(source_array, np.ndarray):
27
+ raise NgioValueError("source zarr array could not be read as a numpy array")
28
+ target[...] = numpy_zoom(source_array, target_shape=target.shape, order=order)
29
+
30
+
31
+ def _on_disk_dask_zoom(
32
+ source: zarr.Array,
33
+ target: zarr.Array,
34
+ order: InterpolationOrder,
35
+ ) -> None:
36
+ source_array = da.from_zarr(source)
37
+ target_array = dask_zoom(source_array, target_shape=target.shape, order=order)
38
+
39
+ # This is a potential fix for Dask 2025.11
40
+ # import dask.config
41
+ # chunk_size_bytes = np.prod(target.chunks) * target_array.dtype.itemsize
42
+ # current_chunk_size = dask.config.get("array.chunk-size")
43
+ # Increase the chunk size to avoid dask potentially creating
44
+ # corrupted chunks when writing chunks that are not multiple of the
45
+ # target chunk size
46
+ # dask.config.set({"array.chunk-size": f"{chunk_size_bytes}B"})
47
+ target_array = target_array.rechunk(target.chunks)
48
+ target_array = target_array.compute_chunk_sizes()
49
+ target_array.to_zarr(target)
50
+ # Restore previous chunk size
51
+ # dask.config.set({"array.chunk-size": current_chunk_size})
52
+
53
+
54
+ def _on_disk_coarsen(
55
+ source: zarr.Array,
56
+ target: zarr.Array,
57
+ order: InterpolationOrder = "linear",
58
+ aggregation_function: Callable | None = None,
59
+ ) -> None:
60
+ """Apply a coarsening operation from a source zarr array to a target zarr array.
61
+
62
+ Args:
63
+ source (zarr.Array): The source array to coarsen.
64
+ target (zarr.Array): The target array to save the coarsened result to.
65
+ order (InterpolationOrder): The order of interpolation is not really implemented
66
+ for coarsening, but it is kept for compatibility with the zoom function.
67
+ order="linear" -> linear interpolation ~ np.mean
68
+ order="nearest" -> nearest interpolation ~ np.max
69
+ aggregation_function (np.ufunc): The aggregation function to use.
70
+ """
71
+ source_array = da.from_zarr(source)
72
+
73
+ _scale, _target_shape = _zoom_inputs_check(
74
+ source_array=source_array, scale=None, target_shape=target.shape
75
+ )
76
+
77
+ assert _target_shape == target.shape, (
78
+ "Target shape must match the target array shape"
79
+ )
80
+
81
+ if aggregation_function is None:
82
+ if order == "linear":
83
+ aggregation_function = np.mean
84
+ elif order == "nearest":
85
+ aggregation_function = np.max
86
+ elif order == "cubic":
87
+ raise NgioValueError("Cubic interpolation is not supported for coarsening.")
88
+ else:
89
+ raise NgioValueError(
90
+ f"Aggregation function must be provided for order {order}"
91
+ )
92
+
93
+ coarsening_setup = {}
94
+ for i, s in enumerate(_scale):
95
+ coarsening_setup[i] = int(np.round(1 / s))
96
+
97
+ out_target = da.coarsen(
98
+ aggregation_function, source_array, coarsening_setup, trim_excess=True
99
+ )
100
+ out_target = out_target.rechunk(target.chunks)
101
+ out_target.to_zarr(target)
102
+
103
+
104
+ def on_disk_zoom(
105
+ source: zarr.Array,
106
+ target: zarr.Array,
107
+ order: InterpolationOrder = "linear",
108
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
109
+ ) -> None:
110
+ """Apply a zoom operation from a source zarr array to a target zarr array.
111
+
112
+ Args:
113
+ source (zarr.Array): The source array to zoom.
114
+ target (zarr.Array): The target array to save the zoomed result to.
115
+ order (InterpolationOrder): The order of interpolation. Defaults to "linear".
116
+ mode (Literal["dask", "numpy", "coarsen"]): The mode to use. Defaults to "dask".
117
+ """
118
+ if not isinstance(source, zarr.Array):
119
+ raise NgioValueError("source must be a zarr array")
120
+
121
+ if not isinstance(target, zarr.Array):
122
+ raise NgioValueError("target must be a zarr array")
123
+
124
+ if source.dtype != target.dtype:
125
+ raise NgioValueError("source and target must have the same dtype")
126
+
127
+ match mode:
128
+ case "numpy":
129
+ return _on_disk_numpy_zoom(source, target, order)
130
+ case "dask":
131
+ return _on_disk_dask_zoom(source, target, order)
132
+ case "coarsen":
133
+ return _on_disk_coarsen(
134
+ source,
135
+ target,
136
+ )
137
+ case _:
138
+ raise NgioValueError("mode must be either 'dask', 'numpy' or 'coarsen'")
139
+
140
+
141
+ def _find_closest_arrays(
142
+ processed: list[zarr.Array], to_be_processed: list[zarr.Array]
143
+ ) -> tuple[np.intp, np.intp]:
144
+ dist_matrix = np.zeros((len(processed), len(to_be_processed)))
145
+ for i, arr_to_proc in enumerate(to_be_processed):
146
+ for j, proc_arr in enumerate(processed):
147
+ dist_matrix[j, i] = np.sqrt(
148
+ np.sum(
149
+ [
150
+ (s1 - s2) ** 2
151
+ for s1, s2 in zip(
152
+ arr_to_proc.shape, proc_arr.shape, strict=False
153
+ )
154
+ ]
155
+ )
156
+ )
157
+
158
+ indices = np.unravel_index(dist_matrix.argmin(), dist_matrix.shape)
159
+ assert len(indices) == 2, "Indices must be of length 2"
160
+ return indices
161
+
162
+
163
+ def consolidate_pyramid(
164
+ source: zarr.Array,
165
+ targets: list[zarr.Array],
166
+ order: InterpolationOrder = "linear",
167
+ mode: Literal["dask", "numpy", "coarsen"] = "dask",
168
+ ) -> None:
169
+ """Consolidate the Zarr array."""
170
+ processed = [source]
171
+ to_be_processed = targets
172
+
173
+ while to_be_processed:
174
+ source_id, target_id = _find_closest_arrays(processed, to_be_processed)
175
+
176
+ source_image = processed[source_id]
177
+ target_image = to_be_processed.pop(target_id)
178
+
179
+ on_disk_zoom(
180
+ source=source_image,
181
+ target=target_image,
182
+ mode=mode,
183
+ order=order,
184
+ )
185
+ processed.append(target_image)
186
+
187
+
188
+ ################################################
189
+ #
190
+ # Builders for image pyramids
191
+ #
192
+ ################################################
193
+
194
+ ChunksLike = tuple[int, ...] | Literal["auto"]
195
+ ShardsLike = tuple[int, ...] | Literal["auto"]
196
+
197
+
198
+ def shapes_from_scaling_factors(
199
+ base_shape: tuple[int, ...],
200
+ scaling_factors: tuple[float, ...],
201
+ num_levels: int,
202
+ ) -> list[tuple[int, ...]]:
203
+ """Compute the shapes of each level in the pyramid from scaling factors.
204
+
205
+ Args:
206
+ base_shape (tuple[int, ...]): The shape of the base level.
207
+ scaling_factors (tuple[float, ...]): The scaling factors between levels.
208
+ num_levels (int): The number of levels in the pyramid.
209
+
210
+ Returns:
211
+ list[tuple[int, ...]]: The shapes of each level in the pyramid.
212
+ """
213
+ shapes = []
214
+ current_shape = base_shape
215
+ for _ in range(num_levels):
216
+ shapes.append(current_shape)
217
+ current_shape = tuple(
218
+ max(1, int(s / f))
219
+ for s, f in zip(current_shape, scaling_factors, strict=True)
220
+ )
221
+ return shapes
222
+
223
+
224
+ def _check_order(shapes: Sequence[tuple[int, ...]]):
225
+ """Check if the shapes are in decreasing order."""
226
+ num_pixels = [np.prod(shape) for shape in shapes]
227
+ for i in range(1, len(num_pixels)):
228
+ if num_pixels[i] >= num_pixels[i - 1]:
229
+ raise NgioValueError("Shapes are not in decreasing order.")
230
+
231
+
232
+ class PyramidLevel(BaseModel):
233
+ path: str
234
+ shape: tuple[int, ...]
235
+ scale: tuple[float, ...]
236
+ chunks: ChunksLike = "auto"
237
+ shards: ShardsLike | None = None
238
+
239
+ @model_validator(mode="after")
240
+ def _model_validation(self) -> "PyramidLevel":
241
+ # Same length as shape
242
+ if len(self.scale) != len(self.shape):
243
+ raise NgioValueError(
244
+ "Scale must have the same length as shape "
245
+ f"({len(self.shape)}), got {len(self.scale)}"
246
+ )
247
+ if any(isinstance(s, float) and s < 0 for s in self.scale):
248
+ raise NgioValueError("Scale values must be positive.")
249
+
250
+ if isinstance(self.chunks, tuple):
251
+ if len(self.chunks) != len(self.shape):
252
+ raise NgioValueError(
253
+ "Chunks must have the same length as shape "
254
+ f"({len(self.shape)}), got {len(self.chunks)}"
255
+ )
256
+ normalized_chunks = []
257
+ for dim_size, chunk_size in zip(self.shape, self.chunks, strict=True):
258
+ normalized_chunks.append(min(dim_size, chunk_size))
259
+ self.chunks = tuple(normalized_chunks)
260
+
261
+ if isinstance(self.shards, tuple):
262
+ if len(self.shards) != len(self.shape):
263
+ raise NgioValueError(
264
+ "Shards must have the same length as shape "
265
+ f"({len(self.shape)}), got {len(self.shards)}"
266
+ )
267
+ normalized_shards = []
268
+ for dim_size, shard_size in zip(self.shape, self.shards, strict=True):
269
+ normalized_shards.append(min(dim_size, shard_size))
270
+ self.shards = tuple(normalized_shards)
271
+ return self
272
+
273
+
274
+ class ImagePyramidBuilder(BaseModel):
275
+ levels: list[PyramidLevel]
276
+ axes: tuple[str, ...]
277
+ data_type: str = "uint16"
278
+ dimension_separator: Literal[".", "/"] = "/"
279
+ compressors: Any = "auto"
280
+ zarr_format: Literal[2, 3] = 2
281
+ other_array_kwargs: Mapping[str, Any] = {}
282
+
283
+ model_config = ConfigDict(arbitrary_types_allowed=True)
284
+
285
+ @classmethod
286
+ def from_scaling_factors(
287
+ cls,
288
+ levels_paths: tuple[str, ...],
289
+ scaling_factors: tuple[float, ...],
290
+ base_shape: tuple[int, ...],
291
+ base_scale: tuple[float, ...],
292
+ axes: tuple[str, ...],
293
+ chunks: ChunksLike = "auto",
294
+ shards: ShardsLike | None = None,
295
+ data_type: str = "uint16",
296
+ dimension_separator: Literal[".", "/"] = "/",
297
+ compressors: Any = "auto",
298
+ zarr_format: Literal[2, 3] = 2,
299
+ other_array_kwargs: Mapping[str, Any] | None = None,
300
+ ) -> "ImagePyramidBuilder":
301
+ shapes = shapes_from_scaling_factors(
302
+ base_shape=base_shape,
303
+ scaling_factors=scaling_factors,
304
+ num_levels=len(levels_paths),
305
+ )
306
+ return cls.from_shapes(
307
+ shapes=shapes,
308
+ base_scale=base_scale,
309
+ axes=axes,
310
+ levels_paths=levels_paths,
311
+ chunks=chunks,
312
+ shards=shards,
313
+ data_type=data_type,
314
+ dimension_separator=dimension_separator,
315
+ compressors=compressors,
316
+ zarr_format=zarr_format,
317
+ other_array_kwargs=other_array_kwargs,
318
+ )
319
+
320
+ @classmethod
321
+ def from_shapes(
322
+ cls,
323
+ shapes: Sequence[tuple[int, ...]],
324
+ base_scale: tuple[float, ...],
325
+ axes: tuple[str, ...],
326
+ levels_paths: Sequence[str] | None = None,
327
+ chunks: ChunksLike = "auto",
328
+ shards: ShardsLike | None = None,
329
+ data_type: str = "uint16",
330
+ dimension_separator: Literal[".", "/"] = "/",
331
+ compressors: Any = "auto",
332
+ zarr_format: Literal[2, 3] = 2,
333
+ other_array_kwargs: Mapping[str, Any] | None = None,
334
+ ) -> "ImagePyramidBuilder":
335
+ levels = []
336
+ if levels_paths is None:
337
+ levels_paths = tuple(str(i) for i in range(len(shapes)))
338
+ _check_order(shapes)
339
+ scale_ = base_scale
340
+ for i, (path, shape) in enumerate(zip(levels_paths, shapes, strict=True)):
341
+ levels.append(
342
+ PyramidLevel(
343
+ path=path,
344
+ shape=shape,
345
+ scale=scale_,
346
+ chunks=chunks,
347
+ shards=shards,
348
+ )
349
+ )
350
+ if i + 1 < len(shapes):
351
+ # This only works for downsampling pyramids
352
+ # The _check_order function ensures that
353
+ # shapes are decreasing
354
+ next_shape = shapes[i + 1]
355
+ scaling_factor = tuple(
356
+ s1 / s2
357
+ for s1, s2 in zip(
358
+ shape,
359
+ next_shape,
360
+ strict=True,
361
+ )
362
+ )
363
+ scale_ = tuple(
364
+ s * f for s, f in zip(scale_, scaling_factor, strict=True)
365
+ )
366
+ other_array_kwargs = other_array_kwargs or {}
367
+ return cls(
368
+ levels=levels,
369
+ axes=axes,
370
+ data_type=data_type,
371
+ dimension_separator=dimension_separator,
372
+ compressors=compressors,
373
+ zarr_format=zarr_format,
374
+ other_array_kwargs=other_array_kwargs,
375
+ )
376
+
377
+ def to_zarr(self, group: zarr.Group) -> None:
378
+ """Save the pyramid specification to a Zarr group.
379
+
380
+ Args:
381
+ group (zarr.Group): The Zarr group to save the pyramid specification to.
382
+ """
383
+ array_static_kwargs = {
384
+ "dtype": self.data_type,
385
+ "overwrite": True,
386
+ "compressors": self.compressors,
387
+ **self.other_array_kwargs,
388
+ }
389
+
390
+ if self.zarr_format == 2:
391
+ array_static_kwargs["chunk_key_encoding"] = {
392
+ "name": "v2",
393
+ "separator": self.dimension_separator,
394
+ }
395
+ else:
396
+ array_static_kwargs["chunk_key_encoding"] = {
397
+ "name": "default",
398
+ "separator": self.dimension_separator,
399
+ }
400
+ array_static_kwargs["dimension_names"] = self.axes
401
+ for p_level in self.levels:
402
+ group.create_array(
403
+ name=p_level.path,
404
+ shape=tuple(p_level.shape),
405
+ chunks=p_level.chunks,
406
+ shards=p_level.shards,
407
+ **array_static_kwargs,
408
+ )