ngio 0.4.8__py3-none-any.whl → 0.5.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 (56) hide show
  1. ngio/__init__.py +5 -2
  2. ngio/common/__init__.py +11 -6
  3. ngio/common/_masking_roi.py +34 -54
  4. ngio/common/_pyramid.py +322 -75
  5. ngio/common/_roi.py +258 -330
  6. ngio/experimental/iterators/_feature.py +3 -3
  7. ngio/experimental/iterators/_rois_utils.py +10 -11
  8. ngio/hcs/_plate.py +192 -136
  9. ngio/images/_abstract_image.py +539 -35
  10. ngio/images/_create_synt_container.py +45 -47
  11. ngio/images/_create_utils.py +406 -0
  12. ngio/images/_image.py +524 -248
  13. ngio/images/_label.py +257 -180
  14. ngio/images/_masked_image.py +2 -2
  15. ngio/images/_ome_zarr_container.py +658 -255
  16. ngio/io_pipes/_io_pipes.py +9 -9
  17. ngio/io_pipes/_io_pipes_masked.py +7 -7
  18. ngio/io_pipes/_io_pipes_roi.py +6 -6
  19. ngio/io_pipes/_io_pipes_types.py +3 -3
  20. ngio/io_pipes/_match_shape.py +6 -8
  21. ngio/io_pipes/_ops_slices_utils.py +8 -5
  22. ngio/ome_zarr_meta/__init__.py +29 -18
  23. ngio/ome_zarr_meta/_meta_handlers.py +402 -689
  24. ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -0
  25. ngio/ome_zarr_meta/ngio_specs/_axes.py +152 -51
  26. ngio/ome_zarr_meta/ngio_specs/_dataset.py +13 -22
  27. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +129 -91
  28. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +69 -69
  29. ngio/ome_zarr_meta/v04/__init__.py +5 -1
  30. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +55 -86
  31. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  32. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  33. ngio/ome_zarr_meta/v05/_v05_spec.py +495 -0
  34. ngio/resources/__init__.py +1 -1
  35. ngio/resources/resource_model.py +1 -1
  36. ngio/tables/_tables_container.py +82 -24
  37. ngio/tables/backends/_abstract_backend.py +7 -0
  38. ngio/tables/backends/_anndata.py +60 -7
  39. ngio/tables/backends/_anndata_utils.py +2 -4
  40. ngio/tables/backends/_csv.py +3 -19
  41. ngio/tables/backends/_json.py +10 -13
  42. ngio/tables/backends/_parquet.py +3 -31
  43. ngio/tables/backends/_py_arrow_backends.py +222 -0
  44. ngio/tables/backends/_utils.py +1 -1
  45. ngio/tables/v1/_roi_table.py +41 -24
  46. ngio/utils/__init__.py +8 -12
  47. ngio/utils/_cache.py +48 -0
  48. ngio/utils/_zarr_utils.py +354 -236
  49. {ngio-0.4.8.dist-info → ngio-0.5.0.dist-info}/METADATA +12 -5
  50. ngio-0.5.0.dist-info/RECORD +88 -0
  51. ngio/images/_create.py +0 -276
  52. ngio/tables/backends/_non_zarr_backends.py +0 -196
  53. ngio/utils/_logger.py +0 -50
  54. ngio-0.4.8.dist-info/RECORD +0 -85
  55. {ngio-0.4.8.dist-info → ngio-0.5.0.dist-info}/WHEEL +0 -0
  56. {ngio-0.4.8.dist-info → ngio-0.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -15,6 +15,7 @@ from ngio.ome_zarr_meta.ngio_specs._axes import (
15
15
  DefaultTimeUnit,
16
16
  SpaceUnits,
17
17
  TimeUnits,
18
+ build_axes_handler,
18
19
  build_canonical_axes_handler,
19
20
  canonical_axes_order,
20
21
  canonical_label_axes_order,
@@ -40,6 +41,7 @@ from ngio.ome_zarr_meta.ngio_specs._ngio_image import (
40
41
  NgioImageLabelMeta,
41
42
  NgioImageMeta,
42
43
  NgioLabelMeta,
44
+ NgioLabelsGroupMeta,
43
45
  )
44
46
  from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
45
47
 
@@ -62,11 +64,13 @@ __all__ = [
62
64
  "NgioImageLabelMeta",
63
65
  "NgioImageMeta",
64
66
  "NgioLabelMeta",
67
+ "NgioLabelsGroupMeta",
65
68
  "NgioPlateMeta",
66
69
  "NgioWellMeta",
67
70
  "PixelSize",
68
71
  "SpaceUnits",
69
72
  "TimeUnits",
73
+ "build_axes_handler",
70
74
  "build_canonical_axes_handler",
71
75
  "canonical_axes_order",
72
76
  "canonical_label_axes_order",
@@ -4,7 +4,7 @@ from collections.abc import Sequence
4
4
  from enum import Enum
5
5
  from typing import Literal, TypeAlias, TypeVar
6
6
 
7
- from pydantic import BaseModel, ConfigDict, Field
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
8
 
9
9
  from ngio.utils import NgioValidationError, NgioValueError
10
10
 
@@ -153,9 +153,51 @@ class AxesSetup(BaseModel):
153
153
  c: str = "c"
154
154
  t: str = "t"
155
155
  others: list[str] = Field(default_factory=list)
156
+ allow_non_canonical_axes: bool = False
157
+ strict_canonical_order: bool = False
156
158
 
157
159
  model_config = ConfigDict(extra="forbid", frozen=True)
158
160
 
161
+ @model_validator(mode="after")
162
+ def _validate_axes_values(self) -> "AxesSetup":
163
+ """Validate the axes values."""
164
+ canonical = {"x", "y", "z", "c", "t"}
165
+ axes = {"x": self.x, "y": self.y, "z": self.z, "c": self.c, "t": self.t}
166
+
167
+ for axis_name, axis_value in axes.items():
168
+ reserved = canonical - {axis_name}
169
+ if axis_value in reserved:
170
+ raise NgioValueError(
171
+ f"The {axis_name} axis cannot be called: '{axis_value}'. "
172
+ f"{axis_value} is reserved. If you want to set a non canonical "
173
+ "axis order, please set the 'strict_canonical_order'to False."
174
+ )
175
+ return self
176
+
177
+ @classmethod
178
+ def from_ordered_list(
179
+ cls, axes_names: Sequence[str], canonical_order: Sequence[str]
180
+ ):
181
+ """Create an AxesSetup from an ordered list of axes names."""
182
+ # Make sure to only keep as many default axes as provided in axes_names
183
+ if len(axes_names) > len(canonical_order):
184
+ raise NgioValueError(
185
+ f"Cannot create AxesSetup from axes names {axes_names} "
186
+ f"and canonical order {canonical_order}. "
187
+ "The number of axes names cannot be greater than the "
188
+ "number of canonical axes."
189
+ )
190
+ canonical_order = list(canonical_order)
191
+ chanonical_axes = canonical_axes_order()
192
+ axes_mapping = {}
193
+ for ax in reversed(axes_names):
194
+ c_ax = canonical_order.pop()
195
+ if ax in chanonical_axes:
196
+ axes_mapping[ax] = ax
197
+ else:
198
+ axes_mapping[c_ax] = ax
199
+ return cls(**axes_mapping)
200
+
159
201
  def canonical_map(self) -> dict[str, str]:
160
202
  """Get the canonical map of axes."""
161
203
  return {
@@ -197,9 +239,9 @@ def _check_unique_names(axes: Sequence[Axis]):
197
239
  )
198
240
 
199
241
 
200
- def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: bool):
242
+ def _check_non_canonical_axes(axes_setup: AxesSetup):
201
243
  """Check if all axes are known."""
202
- if not allow_non_canonical_axes and len(axes_setup.others) > 0:
244
+ if not axes_setup.allow_non_canonical_axes and len(axes_setup.others) > 0:
203
245
  raise NgioValidationError(
204
246
  f"Unknown axes {axes_setup.others}. Please set "
205
247
  "`allow_non_canonical_axes=True` to ignore them"
@@ -219,11 +261,9 @@ def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
219
261
  )
220
262
 
221
263
 
222
- def _check_canonical_order(
223
- axes: Sequence[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
224
- ):
264
+ def _check_canonical_order(axes: Sequence[Axis], axes_setup: AxesSetup):
225
265
  """Check if the axes are in the canonical order."""
226
- if not strict_canonical_order:
266
+ if not axes_setup.strict_canonical_order:
227
267
  return
228
268
  _names = [ax.name for ax in axes]
229
269
  _canonical_order = []
@@ -242,24 +282,18 @@ def _check_canonical_order(
242
282
  def validate_axes(
243
283
  axes: Sequence[Axis],
244
284
  axes_setup: AxesSetup,
245
- allow_non_canonical_axes: bool = False,
246
- strict_canonical_order: bool = False,
247
285
  ) -> None:
248
286
  """Validate the axes."""
249
- if allow_non_canonical_axes and strict_canonical_order:
250
- raise NgioValidationError(
287
+ if axes_setup.allow_non_canonical_axes and axes_setup.strict_canonical_order:
288
+ raise NgioValueError(
251
289
  "`allow_non_canonical_axes` and"
252
290
  "`strict_canonical_order` cannot be true at the same time."
253
291
  "If non canonical axes are allowed, the order cannot be checked."
254
292
  )
255
293
  _check_unique_names(axes=axes)
256
- _check_non_canonical_axes(
257
- axes_setup=axes_setup, allow_non_canonical_axes=allow_non_canonical_axes
258
- )
294
+ _check_non_canonical_axes(axes_setup=axes_setup)
259
295
  _check_axes_validity(axes=axes, axes_setup=axes_setup)
260
- _check_canonical_order(
261
- axes=axes, axes_setup=axes_setup, strict_canonical_order=strict_canonical_order
262
- )
296
+ _check_canonical_order(axes=axes, axes_setup=axes_setup)
263
297
 
264
298
 
265
299
  class AxesHandler:
@@ -278,29 +312,22 @@ class AxesHandler:
278
312
  axes: Sequence[Axis],
279
313
  # user defined args
280
314
  axes_setup: AxesSetup | None = None,
281
- allow_non_canonical_axes: bool = False,
282
- strict_canonical_order: bool = False,
283
315
  ):
284
316
  """Create a new AxesMapper object.
285
317
 
286
318
  Args:
287
319
  axes (list[Axis]): The axes on disk.
288
320
  axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
289
- allow_non_canonical_axes (bool, optional): Allow non canonical axes.
290
- strict_canonical_order (bool, optional): Check if the axes are in the
291
- canonical order. Defaults to False.
292
321
  """
293
322
  axes_setup = axes_setup if axes_setup is not None else AxesSetup()
294
323
 
295
324
  validate_axes(
296
325
  axes=axes,
297
326
  axes_setup=axes_setup,
298
- allow_non_canonical_axes=allow_non_canonical_axes,
299
- strict_canonical_order=strict_canonical_order,
300
327
  )
301
328
 
302
- self._allow_non_canonical_axes = allow_non_canonical_axes
303
- self._strict_canonical_order = strict_canonical_order
329
+ self._allow_non_canonical_axes = axes_setup.allow_non_canonical_axes
330
+ self._strict_canonical_order = axes_setup.strict_canonical_order
304
331
 
305
332
  self._canonical_order = canonical_axes_order()
306
333
 
@@ -352,6 +379,7 @@ class AxesHandler:
352
379
 
353
380
  @property
354
381
  def axes_names(self) -> tuple[str, ...]:
382
+ """On disk axes names."""
355
383
  return tuple(ax.name for ax in self._axes)
356
384
 
357
385
  @property
@@ -421,8 +449,48 @@ class AxesHandler:
421
449
  return AxesHandler(
422
450
  axes=new_axes,
423
451
  axes_setup=self.axes_setup,
424
- allow_non_canonical_axes=self.allow_non_canonical_axes,
425
- strict_canonical_order=self.strict_canonical_order,
452
+ )
453
+
454
+ def rename_axes(self, axes_names: Sequence[str]) -> "AxesHandler":
455
+ """Rename the axes.
456
+
457
+ Args:
458
+ axes_names (Sequence[str]): The new axes names.
459
+ """
460
+ if len(axes_names) != len(self.axes):
461
+ raise NgioValueError(
462
+ f"Cannot rename axes. "
463
+ f"Expected {len(self.axes)} axes, but got {len(axes_names)}."
464
+ )
465
+ new_axes = []
466
+ axes_setup = self.axes_setup
467
+ for ax, new_name in zip(self.axes, axes_names, strict=True):
468
+ if ax.name == new_name:
469
+ new_axes.append(ax)
470
+ continue
471
+ new_ax = Axis(name=new_name, axis_type=ax.axis_type, unit=ax.unit)
472
+ match ax.name:
473
+ case axes_setup.x:
474
+ axes_setup = axes_setup.model_copy(update={"x": new_name})
475
+ case axes_setup.y:
476
+ axes_setup = axes_setup.model_copy(update={"y": new_name})
477
+ case axes_setup.z:
478
+ axes_setup = axes_setup.model_copy(update={"z": new_name})
479
+ case axes_setup.c:
480
+ axes_setup = axes_setup.model_copy(update={"c": new_name})
481
+ case axes_setup.t:
482
+ axes_setup = axes_setup.model_copy(update={"t": new_name})
483
+ case _:
484
+ if ax.name in axes_setup.others:
485
+ others = axes_setup.others.copy()
486
+ others.remove(ax.name)
487
+ others.append(new_name)
488
+ axes_setup = axes_setup.model_copy(update={"others": others})
489
+ new_axes.append(new_ax)
490
+ # Update the axes setup
491
+ return AxesHandler(
492
+ axes=new_axes,
493
+ axes_setup=axes_setup,
426
494
  )
427
495
 
428
496
  def get_index(self, name: str) -> int | None:
@@ -464,14 +532,33 @@ class AxesHandler:
464
532
  self._axes = new_axes
465
533
 
466
534
 
535
+ def _build_axes_list_from_names(
536
+ axes_names: Sequence[str],
537
+ axes_setup: AxesSetup,
538
+ space_units: SpaceUnits | str | None = DefaultSpaceUnit,
539
+ time_units: TimeUnits | str | None = DefaultTimeUnit,
540
+ ) -> list[Axis]:
541
+ """Build a list of Axis objects from a list of axis names."""
542
+ axes = []
543
+ for name in axes_names:
544
+ c_name = axes_setup.get_canonical_name(name)
545
+ match c_name:
546
+ case "t":
547
+ axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
548
+ case "c":
549
+ axes.append(Axis(name=name, axis_type=AxisType.channel))
550
+ case "z" | "y" | "x":
551
+ axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
552
+ case _:
553
+ axes.append(Axis(name=name, axis_type=AxisType.space))
554
+ return axes
555
+
556
+
467
557
  def build_canonical_axes_handler(
468
558
  axes_names: Sequence[str],
559
+ canonical_channel_order: Sequence[str] | None = None,
469
560
  space_units: SpaceUnits | str | None = DefaultSpaceUnit,
470
561
  time_units: TimeUnits | str | None = DefaultTimeUnit,
471
- # user defined args
472
- axes_setup: AxesSetup | None = None,
473
- allow_non_canonical_axes: bool = False,
474
- strict_canonical_order: bool = False,
475
562
  ) -> AxesHandler:
476
563
  """Create a new canonical axes mapper.
477
564
 
@@ -482,33 +569,47 @@ def build_canonical_axes_handler(
482
569
  - If an integer is provided, the axes are created from the last axis
483
570
  to the first
484
571
  e.g. 3 -> ["z", "y", "x"]
572
+ canonical_channel_order (Sequence[str], optional): The canonical channel
573
+ order. Defaults to None, which uses the default order.
485
574
  space_units (SpaceUnits, optional): The space units. Defaults to None.
486
575
  time_units (TimeUnits, optional): The time units. Defaults to None.
576
+ """
577
+ if canonical_channel_order is None:
578
+ canonical_channel_order = canonical_axes_order()
579
+ axes_setup = AxesSetup.from_ordered_list(
580
+ axes_names=axes_names, canonical_order=canonical_channel_order
581
+ )
582
+ axes = _build_axes_list_from_names(
583
+ axes_names=axes_names,
584
+ axes_setup=axes_setup,
585
+ space_units=space_units,
586
+ time_units=time_units,
587
+ )
588
+ return AxesHandler(
589
+ axes=axes,
590
+ axes_setup=axes_setup,
591
+ )
592
+
593
+
594
+ def build_axes_handler(
595
+ axes_names: Sequence[str],
596
+ axes_setup: AxesSetup | None = None,
597
+ ) -> AxesHandler:
598
+ """Create a new axes mapper.
599
+
600
+ Args:
601
+ axes_names (Sequence[str]): The axes names on disk.
487
602
  axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
488
603
  allow_non_canonical_axes (bool, optional): Allow non canonical axes.
489
- Defaults to False.
490
604
  strict_canonical_order (bool, optional): Check if the axes are in the
491
605
  canonical order. Defaults to False.
492
-
493
606
  """
494
- axes = []
495
- for name in axes_names:
496
- match name:
497
- case "t":
498
- axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
499
- case "c":
500
- axes.append(Axis(name=name, axis_type=AxisType.channel))
501
- case "z" | "y" | "x":
502
- axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
503
- case _:
504
- raise NgioValueError(
505
- f"Invalid axis name '{name}'. "
506
- "Only 't', 'c', 'z', 'y', 'x' are allowed."
507
- )
508
-
607
+ axes_setup = axes_setup if axes_setup is not None else AxesSetup()
608
+ axes = _build_axes_list_from_names(
609
+ axes_names=axes_names,
610
+ axes_setup=axes_setup,
611
+ )
509
612
  return AxesHandler(
510
613
  axes=axes,
511
614
  axes_setup=axes_setup,
512
- allow_non_canonical_axes=allow_non_canonical_axes,
513
- strict_canonical_order=strict_canonical_order,
514
615
  )
@@ -60,11 +60,20 @@ class Dataset:
60
60
  @property
61
61
  def pixel_size(self) -> PixelSize:
62
62
  """Return the pixel size for the dataset."""
63
+ scale = self._scale
64
+ pix_size_dict = {}
65
+ # Mandatory axes: x, y
66
+ for ax in ["x", "y"]:
67
+ index = self.axes_handler.get_index(ax)
68
+ assert index is not None
69
+ pix_size_dict[ax] = scale[index]
70
+
71
+ for ax in ["z", "t"]:
72
+ index = self.axes_handler.get_index(ax)
73
+ pix_size_dict[ax] = scale[index] if index is not None else 1.0
74
+
63
75
  return PixelSize(
64
- x=self.get_scale("x", default=1.0),
65
- y=self.get_scale("y", default=1.0),
66
- z=self.get_scale("z", default=1.0),
67
- t=self.get_scale("t", default=1.0),
76
+ **pix_size_dict,
68
77
  space_unit=self.axes_handler.space_unit,
69
78
  time_unit=self.axes_handler.time_unit,
70
79
  )
@@ -78,21 +87,3 @@ class Dataset:
78
87
  def translation(self) -> tuple[float, ...]:
79
88
  """Return the translation as a tuple."""
80
89
  return tuple(self._translation)
81
-
82
- def get_scale(self, axis_name: str, default: float | None = None) -> float:
83
- """Return the scale for a given axis."""
84
- idx = self.axes_handler.get_index(axis_name)
85
- if idx is None:
86
- if default is not None:
87
- return default
88
- raise ValueError(f"Axis {axis_name} not found in axes {self.axes_handler}.")
89
- return self._scale[idx]
90
-
91
- def get_translation(self, axis_name: str, default: float | None = None) -> float:
92
- """Return the translation for a given axis."""
93
- idx = self.axes_handler.get_index(axis_name)
94
- if idx is None:
95
- if default is not None:
96
- return default
97
- raise ValueError(f"Axis {axis_name} not found in axes {self.axes_handler}.")
98
- return self._translation[idx]