ngio 0.5.0a2__py3-none-any.whl → 0.5.0b1__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 (49) hide show
  1. ngio/__init__.py +2 -2
  2. ngio/common/__init__.py +11 -6
  3. ngio/common/_masking_roi.py +12 -41
  4. ngio/common/_pyramid.py +206 -76
  5. ngio/common/_roi.py +257 -329
  6. ngio/experimental/iterators/_feature.py +3 -3
  7. ngio/experimental/iterators/_rois_utils.py +10 -11
  8. ngio/hcs/_plate.py +50 -43
  9. ngio/images/_abstract_image.py +418 -35
  10. ngio/images/_create_synt_container.py +35 -42
  11. ngio/images/_create_utils.py +423 -0
  12. ngio/images/_image.py +162 -176
  13. ngio/images/_label.py +182 -137
  14. ngio/images/_ome_zarr_container.py +372 -197
  15. ngio/io_pipes/_io_pipes.py +9 -9
  16. ngio/io_pipes/_io_pipes_masked.py +7 -7
  17. ngio/io_pipes/_io_pipes_roi.py +6 -6
  18. ngio/io_pipes/_io_pipes_types.py +3 -3
  19. ngio/io_pipes/_match_shape.py +5 -4
  20. ngio/io_pipes/_ops_slices_utils.py +8 -5
  21. ngio/ome_zarr_meta/__init__.py +21 -18
  22. ngio/ome_zarr_meta/_meta_handlers.py +409 -701
  23. ngio/ome_zarr_meta/ngio_specs/__init__.py +2 -0
  24. ngio/ome_zarr_meta/ngio_specs/_axes.py +1 -0
  25. ngio/ome_zarr_meta/ngio_specs/_dataset.py +13 -22
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +54 -61
  27. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +21 -68
  28. ngio/ome_zarr_meta/v04/__init__.py +5 -1
  29. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +49 -63
  30. ngio/ome_zarr_meta/v05/__init__.py +5 -1
  31. ngio/ome_zarr_meta/v05/{_v05_spec_utils.py → _v05_spec.py} +57 -64
  32. ngio/tables/_tables_container.py +2 -4
  33. ngio/tables/backends/_anndata.py +58 -8
  34. ngio/tables/backends/_anndata_utils.py +1 -6
  35. ngio/tables/backends/_csv.py +3 -19
  36. ngio/tables/backends/_json.py +10 -13
  37. ngio/tables/backends/_parquet.py +3 -31
  38. ngio/tables/backends/_py_arrow_backends.py +222 -0
  39. ngio/tables/v1/_roi_table.py +41 -24
  40. ngio/utils/__init__.py +4 -12
  41. ngio/utils/_zarr_utils.py +163 -53
  42. {ngio-0.5.0a2.dist-info → ngio-0.5.0b1.dist-info}/METADATA +6 -2
  43. ngio-0.5.0b1.dist-info/RECORD +88 -0
  44. {ngio-0.5.0a2.dist-info → ngio-0.5.0b1.dist-info}/WHEEL +1 -1
  45. ngio/images/_create.py +0 -287
  46. ngio/tables/backends/_non_zarr_backends.py +0 -196
  47. ngio/utils/_logger.py +0 -50
  48. ngio-0.5.0a2.dist-info/RECORD +0 -89
  49. {ngio-0.5.0a2.dist-info → ngio-0.5.0b1.dist-info}/licenses/LICENSE +0 -0
ngio/__init__.py CHANGED
@@ -9,7 +9,7 @@ except PackageNotFoundError: # pragma: no cover
9
9
  __author__ = "Lorenzo Cerrone"
10
10
  __email__ = "lorenzo.cerrone@uzh.ch"
11
11
 
12
- from ngio.common import Dimensions, Roi, RoiPixels
12
+ from ngio.common import Dimensions, Roi, RoiSlice
13
13
  from ngio.hcs import (
14
14
  OmeZarrPlate,
15
15
  OmeZarrWell,
@@ -52,7 +52,7 @@ __all__ = [
52
52
  "OmeZarrWell",
53
53
  "PixelSize",
54
54
  "Roi",
55
- "RoiPixels",
55
+ "RoiSlice",
56
56
  "create_empty_ome_zarr",
57
57
  "create_empty_plate",
58
58
  "create_empty_well",
ngio/common/__init__.py CHANGED
@@ -2,22 +2,27 @@
2
2
 
3
3
  from ngio.common._dimensions import Dimensions
4
4
  from ngio.common._masking_roi import compute_masking_roi
5
- from ngio.common._pyramid import consolidate_pyramid, init_empty_pyramid, on_disk_zoom
6
- from ngio.common._roi import (
7
- Roi,
8
- RoiPixels,
5
+ from ngio.common._pyramid import (
6
+ ChunksLike,
7
+ ImagePyramidBuilder,
8
+ ShardsLike,
9
+ consolidate_pyramid,
10
+ on_disk_zoom,
9
11
  )
12
+ from ngio.common._roi import Roi, RoiSlice
10
13
  from ngio.common._zoom import InterpolationOrder, dask_zoom, numpy_zoom
11
14
 
12
15
  __all__ = [
16
+ "ChunksLike",
13
17
  "Dimensions",
18
+ "ImagePyramidBuilder",
14
19
  "InterpolationOrder",
15
20
  "Roi",
16
- "RoiPixels",
21
+ "RoiSlice",
22
+ "ShardsLike",
17
23
  "compute_masking_roi",
18
24
  "consolidate_pyramid",
19
25
  "dask_zoom",
20
- "init_empty_pyramid",
21
26
  "numpy_zoom",
22
27
  "on_disk_zoom",
23
28
  ]
@@ -7,7 +7,7 @@ import numpy as np
7
7
  import scipy.ndimage as ndi
8
8
  from dask.delayed import delayed
9
9
 
10
- from ngio.common._roi import Roi, RoiPixels
10
+ from ngio.common._roi import Roi
11
11
  from ngio.ome_zarr_meta import PixelSize
12
12
  from ngio.utils import NgioValueError
13
13
 
@@ -135,52 +135,23 @@ def compute_masking_roi(
135
135
  rois = []
136
136
  for label, slice_ in slices.items():
137
137
  if len(slice_) == 2:
138
- min_t, max_t = None, None
139
- min_z, max_z = None, None
140
- min_y, min_x = slice_[0].start, slice_[1].start
141
- max_y, max_x = slice_[0].stop, slice_[1].stop
138
+ slices = {"y": slice_[0], "x": slice_[1]}
142
139
  elif len(slice_) == 3:
143
- min_t, max_t = None, None
144
- min_z, min_y, min_x = slice_[0].start, slice_[1].start, slice_[2].start
145
- max_z, max_y, max_x = slice_[0].stop, slice_[1].stop, slice_[2].stop
140
+ slices = {"z": slice_[0], "y": slice_[1], "x": slice_[2]}
146
141
  elif len(slice_) == 4:
147
- min_t, min_z, min_y, min_x = (
148
- slice_[0].start,
149
- slice_[1].start,
150
- slice_[2].start,
151
- slice_[3].start,
152
- )
153
- max_t, max_z, max_y, max_x = (
154
- slice_[0].stop,
155
- slice_[1].stop,
156
- slice_[2].stop,
157
- slice_[3].stop,
158
- )
142
+ slices = {
143
+ "t": slice_[0],
144
+ "z": slice_[1],
145
+ "y": slice_[2],
146
+ "x": slice_[3],
147
+ }
159
148
  else:
160
149
  raise ValueError("Invalid slice length.")
161
150
 
162
- if max_t is None:
163
- t_length = None
164
- else:
165
- t_length = max_t - min_t
166
-
167
- if max_z is None:
168
- z_length = None
169
- else:
170
- z_length = max_z - min_z
171
-
172
- roi = RoiPixels(
173
- name=str(label),
174
- x_length=max_x - min_x,
175
- y_length=max_y - min_y,
176
- z_length=z_length,
177
- t_length=t_length,
178
- x=min_x,
179
- y=min_y,
180
- z=min_z,
181
- label=label,
151
+ roi = Roi.from_values(
152
+ name=str(label), slices=slices, label=label, space="pixel"
182
153
  )
183
154
 
184
- roi = roi.to_roi(pixel_size)
155
+ roi = roi.to_world(pixel_size=pixel_size)
185
156
  rois.append(roi)
186
157
  return rois
ngio/common/_pyramid.py CHANGED
@@ -1,13 +1,11 @@
1
- import math
2
- from collections.abc import Callable, Sequence
3
- from typing import Literal
1
+ from collections.abc import Callable, Mapping, Sequence
2
+ from typing import Any, Literal
4
3
 
5
4
  import dask.array as da
6
5
  import numpy as np
7
6
  import zarr
8
- from zarr.core.array import CompressorLike
7
+ from pydantic import BaseModel, ConfigDict, model_validator
9
8
 
10
- # from zarr.types import DIMENSION_SEPARATOR
11
9
  from ngio.common._zoom import (
12
10
  InterpolationOrder,
13
11
  _zoom_inputs_check,
@@ -15,10 +13,7 @@ from ngio.common._zoom import (
15
13
  numpy_zoom,
16
14
  )
17
15
  from ngio.utils import (
18
- AccessModeLiteral,
19
16
  NgioValueError,
20
- StoreOrGroup,
21
- open_group_wrapper,
22
17
  )
23
18
 
24
19
 
@@ -199,85 +194,220 @@ def consolidate_pyramid(
199
194
  processed.append(target_image)
200
195
 
201
196
 
202
- def _maybe_int(value: float | int) -> float | int:
203
- """Convert a float to an int if it is an integer."""
204
- if isinstance(value, int):
205
- return value
206
- if value.is_integer():
207
- return int(value)
208
- return value
209
-
210
-
211
- def init_empty_pyramid(
212
- store: StoreOrGroup,
213
- paths: list[str],
214
- ref_shape: Sequence[int],
215
- scaling_factors: Sequence[float],
216
- axes: Sequence[str],
217
- chunks: Sequence[int] | Literal["auto"] = "auto",
218
- dtype: str = "uint16",
219
- mode: AccessModeLiteral = "a",
220
- dimension_separator: Literal[".", "/"] = "/",
221
- compressors: CompressorLike = "auto",
222
- zarr_format: Literal[2, 3] = 2,
223
- ) -> None:
224
- # Return the an Image object
225
- if chunks != "auto" and len(chunks) != len(ref_shape):
226
- raise NgioValueError(
227
- "The shape and chunks must have the same number of dimensions."
228
- )
197
+ ################################################
198
+ #
199
+ # Builders for image pyramids
200
+ #
201
+ ################################################
202
+
203
+ ChunksLike = tuple[int, ...] | Literal["auto"]
204
+ ShardsLike = tuple[int, ...] | Literal["auto"]
229
205
 
230
- if chunks != "auto":
231
- chunks = tuple(min(c, s) for c, s in zip(chunks, ref_shape, strict=True))
232
- else:
233
- chunks = "auto"
234
206
 
235
- if len(ref_shape) != len(scaling_factors):
236
- raise NgioValueError(
237
- "The shape and scaling factor must have the same number of dimensions."
207
+ def shapes_from_scaling_factors(
208
+ base_shape: tuple[int, ...],
209
+ scaling_factors: tuple[float, ...],
210
+ num_levels: int,
211
+ ) -> list[tuple[int, ...]]:
212
+ """Compute the shapes of each level in the pyramid from scaling factors.
213
+
214
+ Args:
215
+ base_shape (tuple[int, ...]): The shape of the base level.
216
+ scaling_factors (tuple[float, ...]): The scaling factors between levels.
217
+ num_levels (int): The number of levels in the pyramid.
218
+
219
+ Returns:
220
+ list[tuple[int, ...]]: The shapes of each level in the pyramid.
221
+ """
222
+ shapes = []
223
+ current_shape = base_shape
224
+ for _ in range(num_levels):
225
+ shapes.append(current_shape)
226
+ current_shape = tuple(
227
+ max(1, int(s / f))
228
+ for s, f in zip(current_shape, scaling_factors, strict=True)
238
229
  )
230
+ return shapes
239
231
 
240
- # Ensure scaling factors are int if possible
241
- # To reduce the risk of floating point issues
242
- scaling_factors = [_maybe_int(s) for s in scaling_factors]
243
232
 
244
- root_group = open_group_wrapper(store, mode=mode, zarr_format=zarr_format)
233
+ def _check_order(shapes: Sequence[tuple[int, ...]]):
234
+ """Check if the shapes are in decreasing order."""
235
+ num_pixels = [np.prod(shape) for shape in shapes]
236
+ for i in range(1, len(num_pixels)):
237
+ if num_pixels[i] >= num_pixels[i - 1]:
238
+ raise NgioValueError("Shapes are not in decreasing order.")
245
239
 
246
- array_static_kwargs = {
247
- "dtype": dtype,
248
- "overwrite": True,
249
- "compressors": compressors,
250
- }
251
240
 
252
- if zarr_format == 2:
253
- array_static_kwargs["chunk_key_encoding"] = {
254
- "name": "v2",
255
- "separator": dimension_separator,
256
- }
257
- else:
258
- array_static_kwargs["chunk_key_encoding"] = {
259
- "name": "default",
260
- "separator": dimension_separator,
261
- }
262
- array_static_kwargs["dimension_names"] = axes
241
+ class PyramidLevel(BaseModel):
242
+ path: str
243
+ shape: tuple[int, ...]
244
+ scale: tuple[float, ...]
245
+ chunks: ChunksLike = "auto"
246
+ shards: ShardsLike | None = None
263
247
 
264
- for path in paths:
265
- if any(s < 1 for s in ref_shape):
248
+ @model_validator(mode="after")
249
+ def _model_validation(self) -> "PyramidLevel":
250
+ # Same length as shape
251
+ if len(self.scale) != len(self.shape):
266
252
  raise NgioValueError(
267
- "Level shape must be at least 1 on all dimensions. "
268
- f"Calculated shape: {ref_shape} at level {path}."
253
+ "Scale must have the same length as shape "
254
+ f"({len(self.shape)}), got {len(self.scale)}"
269
255
  )
270
- new_arr = root_group.create_array(
271
- name=path,
272
- shape=tuple(ref_shape),
256
+ if any(isinstance(s, float) and s < 0 for s in self.scale):
257
+ raise NgioValueError("Scale values must be positive.")
258
+
259
+ if isinstance(self.chunks, tuple):
260
+ if len(self.chunks) != len(self.shape):
261
+ raise NgioValueError(
262
+ "Chunks must have the same length as shape "
263
+ f"({len(self.shape)}), got {len(self.chunks)}"
264
+ )
265
+ normalized_chunks = []
266
+ for dim_size, chunk_size in zip(self.shape, self.chunks, strict=True):
267
+ normalized_chunks.append(min(dim_size, chunk_size))
268
+ self.chunks = tuple(normalized_chunks)
269
+
270
+ if isinstance(self.shards, tuple):
271
+ if len(self.shards) != len(self.shape):
272
+ raise NgioValueError(
273
+ "Shards must have the same length as shape "
274
+ f"({len(self.shape)}), got {len(self.shards)}"
275
+ )
276
+ return self
277
+
278
+
279
+ class ImagePyramidBuilder(BaseModel):
280
+ levels: list[PyramidLevel]
281
+ axes: tuple[str, ...]
282
+ data_type: str = "uint16"
283
+ dimension_separator: Literal[".", "/"] = "/"
284
+ compressors: Any = "auto"
285
+ zarr_format: Literal[2, 3] = 2
286
+ other_array_kwargs: Mapping[str, Any] = {}
287
+
288
+ model_config = ConfigDict(arbitrary_types_allowed=True)
289
+
290
+ @classmethod
291
+ def from_scaling_factors(
292
+ cls,
293
+ levels_paths: tuple[str, ...],
294
+ scaling_factors: tuple[float, ...],
295
+ base_shape: tuple[int, ...],
296
+ base_scale: tuple[float, ...],
297
+ axes: tuple[str, ...],
298
+ chunks: ChunksLike = "auto",
299
+ shards: ShardsLike | None = None,
300
+ data_type: str = "uint16",
301
+ dimension_separator: Literal[".", "/"] = "/",
302
+ compressors: Any = "auto",
303
+ zarr_format: Literal[2, 3] = 2,
304
+ other_array_kwargs: Mapping[str, Any] | None = None,
305
+ ) -> "ImagePyramidBuilder":
306
+ shapes = shapes_from_scaling_factors(
307
+ base_shape=base_shape,
308
+ scaling_factors=scaling_factors,
309
+ num_levels=len(levels_paths),
310
+ )
311
+ return cls.from_shapes(
312
+ shapes=shapes,
313
+ base_scale=base_scale,
314
+ axes=axes,
315
+ levels_paths=levels_paths,
273
316
  chunks=chunks,
274
- **array_static_kwargs,
317
+ shards=shards,
318
+ data_type=data_type,
319
+ dimension_separator=dimension_separator,
320
+ compressors=compressors,
321
+ zarr_format=zarr_format,
322
+ other_array_kwargs=other_array_kwargs,
275
323
  )
276
324
 
277
- ref_shape = [
278
- math.floor(s / sc) for s, sc in zip(ref_shape, scaling_factors, strict=True)
279
- ]
280
- chunks = tuple(
281
- min(c, s) for c, s in zip(new_arr.chunks, ref_shape, strict=True)
325
+ @classmethod
326
+ def from_shapes(
327
+ cls,
328
+ shapes: Sequence[tuple[int, ...]],
329
+ base_scale: tuple[float, ...],
330
+ axes: tuple[str, ...],
331
+ levels_paths: Sequence[str] | None = None,
332
+ chunks: ChunksLike = "auto",
333
+ shards: ShardsLike | None = None,
334
+ data_type: str = "uint16",
335
+ dimension_separator: Literal[".", "/"] = "/",
336
+ compressors: Any = "auto",
337
+ zarr_format: Literal[2, 3] = 2,
338
+ other_array_kwargs: Mapping[str, Any] | None = None,
339
+ ) -> "ImagePyramidBuilder":
340
+ levels = []
341
+ if levels_paths is None:
342
+ levels_paths = tuple(str(i) for i in range(len(shapes)))
343
+ _check_order(shapes)
344
+ scale_ = base_scale
345
+ for i, (path, shape) in enumerate(zip(levels_paths, shapes, strict=True)):
346
+ levels.append(
347
+ PyramidLevel(
348
+ path=path,
349
+ shape=shape,
350
+ scale=scale_,
351
+ chunks=chunks,
352
+ shards=shards,
353
+ )
354
+ )
355
+ if i + 1 < len(shapes):
356
+ # This only works for downsampling pyramids
357
+ # The _check_order function ensures that
358
+ # shapes are decreasing
359
+ next_shape = shapes[i + 1]
360
+ scaling_factor = tuple(
361
+ s1 / s2
362
+ for s1, s2 in zip(
363
+ shape,
364
+ next_shape,
365
+ strict=True,
366
+ )
367
+ )
368
+ scale_ = tuple(
369
+ s * f for s, f in zip(scale_, scaling_factor, strict=True)
370
+ )
371
+ other_array_kwargs = other_array_kwargs or {}
372
+ return cls(
373
+ levels=levels,
374
+ axes=axes,
375
+ data_type=data_type,
376
+ dimension_separator=dimension_separator,
377
+ compressors=compressors,
378
+ zarr_format=zarr_format,
379
+ other_array_kwargs=other_array_kwargs,
282
380
  )
283
- return None
381
+
382
+ def to_zarr(self, group: zarr.Group) -> None:
383
+ """Save the pyramid specification to a Zarr group.
384
+
385
+ Args:
386
+ group (zarr.Group): The Zarr group to save the pyramid specification to.
387
+ """
388
+ array_static_kwargs = {
389
+ "dtype": self.data_type,
390
+ "overwrite": True,
391
+ "compressors": self.compressors,
392
+ **self.other_array_kwargs,
393
+ }
394
+
395
+ if self.zarr_format == 2:
396
+ array_static_kwargs["chunk_key_encoding"] = {
397
+ "name": "v2",
398
+ "separator": self.dimension_separator,
399
+ }
400
+ else:
401
+ array_static_kwargs["chunk_key_encoding"] = {
402
+ "name": "default",
403
+ "separator": self.dimension_separator,
404
+ }
405
+ array_static_kwargs["dimension_names"] = self.axes
406
+ for p_level in self.levels:
407
+ group.create_array(
408
+ name=p_level.path,
409
+ shape=tuple(p_level.shape),
410
+ chunks=p_level.chunks,
411
+ shards=p_level.shards,
412
+ **array_static_kwargs,
413
+ )