ngio 0.5.0a1__py3-none-any.whl → 0.5.0a3__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 (48) 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 +218 -78
  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 +114 -123
  9. ngio/images/_abstract_image.py +417 -35
  10. ngio/images/_create_synt_container.py +36 -43
  11. ngio/images/_create_utils.py +423 -0
  12. ngio/images/_image.py +155 -177
  13. ngio/images/_label.py +144 -119
  14. ngio/images/_ome_zarr_container.py +361 -196
  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 +15 -18
  22. ngio/ome_zarr_meta/_meta_handlers.py +334 -713
  23. ngio/ome_zarr_meta/ngio_specs/_axes.py +1 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +13 -22
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +54 -61
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +14 -68
  27. ngio/ome_zarr_meta/v04/__init__.py +1 -1
  28. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +16 -61
  29. ngio/ome_zarr_meta/v05/__init__.py +1 -1
  30. ngio/ome_zarr_meta/v05/{_v05_spec_utils.py → _v05_spec.py} +18 -61
  31. ngio/tables/_tables_container.py +25 -20
  32. ngio/tables/backends/_anndata.py +57 -8
  33. ngio/tables/backends/_anndata_utils.py +1 -6
  34. ngio/tables/backends/_csv.py +3 -19
  35. ngio/tables/backends/_json.py +10 -13
  36. ngio/tables/backends/_parquet.py +3 -31
  37. ngio/tables/backends/_py_arrow_backends.py +222 -0
  38. ngio/tables/v1/_roi_table.py +44 -27
  39. ngio/utils/__init__.py +6 -12
  40. ngio/utils/_cache.py +48 -0
  41. ngio/utils/_zarr_utils.py +285 -245
  42. {ngio-0.5.0a1.dist-info → ngio-0.5.0a3.dist-info}/METADATA +8 -4
  43. {ngio-0.5.0a1.dist-info → ngio-0.5.0a3.dist-info}/RECORD +45 -45
  44. {ngio-0.5.0a1.dist-info → ngio-0.5.0a3.dist-info}/WHEEL +1 -1
  45. ngio/images/_create.py +0 -283
  46. ngio/tables/backends/_non_zarr_backends.py +0 -196
  47. ngio/utils/_logger.py +0 -50
  48. {ngio-0.5.0a1.dist-info → ngio-0.5.0a3.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
 
@@ -41,9 +36,19 @@ def _on_disk_dask_zoom(
41
36
  source_array = da.from_zarr(source)
42
37
  target_array = dask_zoom(source_array, target_shape=target.shape, order=order)
43
38
 
44
- target_array = target_array.rechunk(target.chunks) # type: ignore
45
- target_array.compute_chunk_sizes()
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()
46
49
  target_array.to_zarr(target)
50
+ # Restore previous chunk size
51
+ # dask.config.set({"array.chunk-size": current_chunk_size})
47
52
 
48
53
 
49
54
  def _on_disk_coarsen(
@@ -189,85 +194,220 @@ def consolidate_pyramid(
189
194
  processed.append(target_image)
190
195
 
191
196
 
192
- def _maybe_int(value: float | int) -> float | int:
193
- """Convert a float to an int if it is an integer."""
194
- if isinstance(value, int):
195
- return value
196
- if value.is_integer():
197
- return int(value)
198
- return value
199
-
200
-
201
- def init_empty_pyramid(
202
- store: StoreOrGroup,
203
- paths: list[str],
204
- ref_shape: Sequence[int],
205
- scaling_factors: Sequence[float],
206
- axes: Sequence[str],
207
- chunks: Sequence[int] | Literal["auto"] = "auto",
208
- dtype: str = "uint16",
209
- mode: AccessModeLiteral = "a",
210
- dimension_separator: Literal[".", "/"] = "/",
211
- compressors: CompressorLike = "auto",
212
- zarr_format: Literal[2, 3] = 2,
213
- ) -> None:
214
- # Return the an Image object
215
- if chunks != "auto" and len(chunks) != len(ref_shape):
216
- raise NgioValueError(
217
- "The shape and chunks must have the same number of dimensions."
218
- )
197
+ ################################################
198
+ #
199
+ # Builders for image pyramids
200
+ #
201
+ ################################################
202
+
203
+ ChunksLike = tuple[int, ...] | Literal["auto"]
204
+ ShardsLike = tuple[int, ...] | Literal["auto"]
219
205
 
220
- if chunks != "auto":
221
- chunks = tuple(min(c, s) for c, s in zip(chunks, ref_shape, strict=True))
222
- else:
223
- chunks = "auto"
224
206
 
225
- if len(ref_shape) != len(scaling_factors):
226
- raise NgioValueError(
227
- "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)
228
229
  )
230
+ return shapes
229
231
 
230
- # Ensure scaling factors are int if possible
231
- # To reduce the risk of floating point issues
232
- scaling_factors = [_maybe_int(s) for s in scaling_factors]
233
232
 
234
- 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.")
235
239
 
236
- array_static_kwargs = {
237
- "dtype": dtype,
238
- "overwrite": True,
239
- "compressors": compressors,
240
- }
241
240
 
242
- if zarr_format == 2:
243
- array_static_kwargs["chunk_key_encoding"] = {
244
- "name": "v2",
245
- "separator": dimension_separator,
246
- }
247
- else:
248
- array_static_kwargs["chunk_key_encoding"] = {
249
- "name": "default",
250
- "separator": dimension_separator,
251
- }
252
- 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
253
247
 
254
- for path in paths:
255
- 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):
256
252
  raise NgioValueError(
257
- "Level shape must be at least 1 on all dimensions. "
258
- 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)}"
259
255
  )
260
- new_arr = root_group.create_array(
261
- name=path,
262
- 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,
263
316
  chunks=chunks,
264
- **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,
265
323
  )
266
324
 
267
- ref_shape = [
268
- math.floor(s / sc) for s, sc in zip(ref_shape, scaling_factors, strict=True)
269
- ]
270
- chunks = tuple(
271
- 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,
272
380
  )
273
- 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
+ )