ngio 0.1.6__py3-none-any.whl → 0.2.0a2__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 (84) hide show
  1. ngio/__init__.py +31 -5
  2. ngio/common/__init__.py +44 -0
  3. ngio/common/_array_pipe.py +160 -0
  4. ngio/common/_axes_transforms.py +63 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +113 -0
  7. ngio/common/_pyramid.py +223 -0
  8. ngio/{core/roi.py → common/_roi.py} +22 -23
  9. ngio/common/_slicer.py +97 -0
  10. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
  11. ngio/hcs/__init__.py +60 -0
  12. ngio/images/__init__.py +23 -0
  13. ngio/images/abstract_image.py +240 -0
  14. ngio/images/create.py +251 -0
  15. ngio/images/image.py +389 -0
  16. ngio/images/label.py +236 -0
  17. ngio/images/omezarr_container.py +535 -0
  18. ngio/ome_zarr_meta/__init__.py +35 -0
  19. ngio/ome_zarr_meta/_generic_handlers.py +320 -0
  20. ngio/ome_zarr_meta/_meta_handlers.py +142 -0
  21. ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
  22. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  23. ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
  27. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
  28. ngio/ome_zarr_meta/v04/__init__.py +11 -0
  29. ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
  30. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
  31. ngio/tables/__init__.py +21 -5
  32. ngio/tables/_validators.py +192 -0
  33. ngio/tables/backends/__init__.py +8 -0
  34. ngio/tables/backends/_abstract_backend.py +71 -0
  35. ngio/tables/backends/_anndata_utils.py +194 -0
  36. ngio/tables/backends/_anndata_v1.py +75 -0
  37. ngio/tables/backends/_json_v1.py +56 -0
  38. ngio/tables/backends/_table_backends.py +102 -0
  39. ngio/tables/tables_container.py +300 -0
  40. ngio/tables/v1/__init__.py +6 -5
  41. ngio/tables/v1/_feature_table.py +161 -0
  42. ngio/tables/v1/_generic_table.py +99 -182
  43. ngio/tables/v1/_masking_roi_table.py +175 -0
  44. ngio/tables/v1/_roi_table.py +226 -0
  45. ngio/utils/__init__.py +23 -10
  46. ngio/utils/_datasets.py +51 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_zarr_utils.py +378 -0
  49. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/METADATA +18 -39
  50. ngio-0.2.0a2.dist-info/RECORD +53 -0
  51. ngio/core/__init__.py +0 -7
  52. ngio/core/dimensions.py +0 -122
  53. ngio/core/image_handler.py +0 -228
  54. ngio/core/image_like_handler.py +0 -549
  55. ngio/core/label_handler.py +0 -410
  56. ngio/core/ngff_image.py +0 -387
  57. ngio/core/utils.py +0 -287
  58. ngio/io/__init__.py +0 -19
  59. ngio/io/_zarr.py +0 -88
  60. ngio/io/_zarr_array_utils.py +0 -0
  61. ngio/io/_zarr_group_utils.py +0 -60
  62. ngio/iterators/__init__.py +0 -1
  63. ngio/ngff_meta/__init__.py +0 -27
  64. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  65. ngio/ngff_meta/meta_handler.py +0 -92
  66. ngio/ngff_meta/utils.py +0 -235
  67. ngio/ngff_meta/v04/__init__.py +0 -6
  68. ngio/ngff_meta/v04/specs.py +0 -158
  69. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  70. ngio/pipes/__init__.py +0 -7
  71. ngio/pipes/_slicer_transforms.py +0 -176
  72. ngio/pipes/_transforms.py +0 -33
  73. ngio/pipes/data_pipe.py +0 -52
  74. ngio/tables/_ad_reader.py +0 -80
  75. ngio/tables/_utils.py +0 -301
  76. ngio/tables/tables_group.py +0 -252
  77. ngio/tables/v1/feature_tables.py +0 -182
  78. ngio/tables/v1/masking_roi_tables.py +0 -243
  79. ngio/tables/v1/roi_tables.py +0 -285
  80. ngio/utils/_common_types.py +0 -5
  81. ngio/utils/_pydantic_utils.py +0 -52
  82. ngio-0.1.6.dist-info/RECORD +0 -44
  83. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/WHEEL +0 -0
  84. {ngio-0.1.6.dist-info → ngio-0.2.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,481 @@
1
+ """Fractal internal module for axes handling."""
2
+
3
+ from collections.abc import Collection
4
+ from enum import Enum
5
+ from logging import Logger
6
+ from typing import TypeVar
7
+
8
+ import numpy as np
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from ngio.utils import NgioValidationError, NgioValueError
12
+
13
+ logger = Logger(__name__)
14
+
15
+ T = TypeVar("T")
16
+
17
+ ################################################################################################
18
+ #
19
+ # Axis Types and Units
20
+ # We define a small set of axis types and units that can be used in the metadata.
21
+ # This axis types are more restrictive than the OME standard.
22
+ # We do that to simplify the data processing.
23
+ #
24
+ #################################################################################################
25
+
26
+
27
+ class AxisType(str, Enum):
28
+ """Allowed axis types."""
29
+
30
+ channel = "channel"
31
+ time = "time"
32
+ space = "space"
33
+
34
+
35
+ class SpaceUnits(str, Enum):
36
+ """Allowed space units."""
37
+
38
+ nanometer = "nanometer"
39
+ nm = "nm"
40
+ micrometer = "micrometer"
41
+ um = "um"
42
+ millimeter = "millimeter"
43
+ mm = "mm"
44
+ centimeter = "centimeter"
45
+ cm = "cm"
46
+
47
+ @classmethod
48
+ def default(cls) -> "SpaceUnits":
49
+ return SpaceUnits.um
50
+
51
+
52
+ class TimeUnits(str, Enum):
53
+ """Allowed time units."""
54
+
55
+ seconds = "seconds"
56
+ s = "s"
57
+
58
+ @classmethod
59
+ def default(cls) -> "TimeUnits":
60
+ return TimeUnits.s
61
+
62
+
63
+ class Axis(BaseModel):
64
+ """Axis infos model."""
65
+
66
+ on_disk_name: str
67
+ unit: SpaceUnits | TimeUnits | None = None
68
+ axis_type: AxisType | None = None
69
+
70
+ model_config = ConfigDict(extra="forbid", frozen=True)
71
+
72
+ def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
73
+ if self.axis_type != cast_type:
74
+ logger.warning(
75
+ f"Axis {self.on_disk_name} has type {self.axis_type}. "
76
+ f"Casting to {cast_type}."
77
+ )
78
+ new_axis = Axis(
79
+ on_disk_name=self.on_disk_name, axis_type=cast_type, unit=self.unit
80
+ )
81
+ if cast_type == AxisType.time and not isinstance(self.unit, TimeUnits):
82
+ logger.warning(
83
+ f"Time axis {self.on_disk_name} has unit {self.unit}. "
84
+ f"Casting to {TimeUnits.default()}."
85
+ )
86
+ new_axis = Axis(
87
+ on_disk_name=self.on_disk_name,
88
+ axis_type=AxisType.time,
89
+ unit=TimeUnits.default(),
90
+ )
91
+ elif cast_type == AxisType.space and not isinstance(self.unit, SpaceUnits):
92
+ logger.warning(
93
+ f"Space axis {self.on_disk_name} has unit {self.unit}. "
94
+ f"Casting to {SpaceUnits.default()}."
95
+ )
96
+ new_axis = Axis(
97
+ on_disk_name=self.on_disk_name,
98
+ axis_type=AxisType.space,
99
+ unit=SpaceUnits.default(),
100
+ )
101
+ elif cast_type == AxisType.channel and self.unit is not None:
102
+ logger.warning(
103
+ f"Channel axis {self.on_disk_name} has unit {self.unit}. Removing unit."
104
+ )
105
+ new_axis = Axis(
106
+ on_disk_name=self.on_disk_name,
107
+ axis_type=AxisType.channel,
108
+ unit=None,
109
+ )
110
+ return new_axis
111
+
112
+ def canonical_axis_cast(self, canonical_name: str) -> "Axis":
113
+ """Cast the implicit axis to the correct type."""
114
+ match canonical_name:
115
+ case "t":
116
+ if self.axis_type != AxisType.time or not isinstance(
117
+ self.unit, TimeUnits
118
+ ):
119
+ return self.implicit_type_cast(AxisType.time)
120
+ case "c":
121
+ if self.axis_type != AxisType.channel or self.unit is not None:
122
+ return self.implicit_type_cast(AxisType.channel)
123
+ case "z" | "y" | "x":
124
+ if self.axis_type != AxisType.space or not isinstance(
125
+ self.unit, SpaceUnits
126
+ ):
127
+ return self.implicit_type_cast(AxisType.space)
128
+ return self
129
+
130
+
131
+ ################################################################################################
132
+ #
133
+ # Axes Handling
134
+ # We define a unique mapping to match the axes on disk to the canonical axes.
135
+ # The canonical axes are the ones that are used consistently in the NGIO internal API.
136
+ # The canonical axes ordered are: t, c, z, y, x.
137
+ #
138
+ #################################################################################################
139
+
140
+
141
+ def canonical_axes_order() -> tuple[str, str, str, str, str]:
142
+ """Get the canonical axes order."""
143
+ return "t", "c", "z", "y", "x"
144
+
145
+
146
+ def canonical_label_axes_order() -> tuple[str, str, str, str]:
147
+ """Get the canonical axes order."""
148
+ return "t", "z", "y", "x"
149
+
150
+
151
+ class AxesSetup(BaseModel):
152
+ """Axes setup model.
153
+
154
+ This model is used to map the on disk axes to the canonical OME-Zarr axes.
155
+ """
156
+
157
+ x: str = "x"
158
+ y: str = "y"
159
+ z: str = "z"
160
+ c: str = "c"
161
+ t: str = "t"
162
+ others: list[str] = Field(default_factory=list)
163
+
164
+ model_config = ConfigDict(extra="forbid", frozen=True)
165
+
166
+
167
+ def _check_unique_names(axes: Collection[Axis]):
168
+ """Check if all axes on disk have unique names."""
169
+ names = [ax.on_disk_name for ax in axes]
170
+ if len(set(names)) != len(names):
171
+ duplicates = {item for item in names if names.count(item) > 1}
172
+ raise NgioValidationError(
173
+ f"All axes must be unique. But found duplicates axes {duplicates}"
174
+ )
175
+
176
+
177
+ def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: bool):
178
+ """Check if all axes are known."""
179
+ if not allow_non_canonical_axes and len(axes_setup.others) > 0:
180
+ raise NgioValidationError(
181
+ f"Unknown axes {axes_setup.others}. Please set "
182
+ "`allow_non_canonical_axes=True` to ignore them"
183
+ )
184
+
185
+
186
+ def _check_axes_validity(axes: Collection[Axis], axes_setup: AxesSetup):
187
+ """Check if all axes are valid."""
188
+ _axes_setup = axes_setup.model_dump(exclude={"others"})
189
+ _all_known_axes = [*_axes_setup.values(), *axes_setup.others]
190
+ for ax in axes:
191
+ if ax.on_disk_name not in _all_known_axes:
192
+ raise NgioValidationError(
193
+ f"Invalid axis name '{ax.on_disk_name}'. "
194
+ f"Please correct map `{ax.on_disk_name}` "
195
+ f"using the AxesSetup model {axes_setup}"
196
+ )
197
+
198
+
199
+ def _check_canonical_order(
200
+ axes: Collection[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
201
+ ):
202
+ """Check if the axes are in the canonical order."""
203
+ if not strict_canonical_order:
204
+ return
205
+ _on_disk_names = [ax.on_disk_name for ax in axes]
206
+ _canonical_order = []
207
+ for name in canonical_axes_order():
208
+ mapped_name = getattr(axes_setup, name)
209
+ if mapped_name in _on_disk_names:
210
+ _canonical_order.append(mapped_name)
211
+
212
+ if _on_disk_names != _canonical_order:
213
+ raise NgioValidationError(
214
+ f"Invalid axes order. The axes must be in the canonical order. "
215
+ f"Expected {_canonical_order}, but found {_on_disk_names}"
216
+ )
217
+
218
+
219
+ def validate_axes(
220
+ axes: Collection[Axis],
221
+ axes_setup: AxesSetup,
222
+ allow_non_canonical_axes: bool = False,
223
+ strict_canonical_order: bool = False,
224
+ ) -> None:
225
+ """Validate the axes."""
226
+ if allow_non_canonical_axes and strict_canonical_order:
227
+ raise NgioValidationError(
228
+ "`allow_non_canonical_axes` and"
229
+ "`strict_canonical_order` cannot be true at the same time."
230
+ "If non canonical axes are allowed, the order cannot be checked."
231
+ )
232
+ _check_unique_names(axes=axes)
233
+ _check_non_canonical_axes(
234
+ axes_setup=axes_setup, allow_non_canonical_axes=allow_non_canonical_axes
235
+ )
236
+ _check_axes_validity(axes=axes, axes_setup=axes_setup)
237
+ _check_canonical_order(
238
+ axes=axes, axes_setup=axes_setup, strict_canonical_order=strict_canonical_order
239
+ )
240
+
241
+
242
+ class AxesTransformation(BaseModel):
243
+ model_config = ConfigDict(extra="forbid", frozen=True, arbitrary_types_allowed=True)
244
+
245
+
246
+ class AxesTranspose(AxesTransformation):
247
+ axes: tuple[int, ...]
248
+
249
+
250
+ class AxesExpand(AxesTransformation):
251
+ axes: tuple[int, ...]
252
+
253
+
254
+ class AxesSqueeze(AxesTransformation):
255
+ axes: tuple[int, ...]
256
+
257
+
258
+ class AxesMapper:
259
+ """Map on disk axes to canonical axes.
260
+
261
+ This class is used to map the on disk axes to the canonical axes.
262
+
263
+ """
264
+
265
+ def __init__(
266
+ self,
267
+ # spec dictated args
268
+ on_disk_axes: Collection[Axis],
269
+ # user defined args
270
+ axes_setup: AxesSetup | None = None,
271
+ allow_non_canonical_axes: bool = False,
272
+ strict_canonical_order: bool = False,
273
+ ):
274
+ """Create a new AxesMapper object.
275
+
276
+ Args:
277
+ on_disk_axes (list[Axis]): The axes on disk.
278
+ axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
279
+ allow_non_canonical_axes (bool, optional): Allow non canonical axes.
280
+ strict_canonical_order (bool, optional): Check if the axes are in the
281
+ canonical order. Defaults to False.
282
+ """
283
+ axes_setup = axes_setup if axes_setup is not None else AxesSetup()
284
+
285
+ validate_axes(
286
+ axes=on_disk_axes,
287
+ axes_setup=axes_setup,
288
+ allow_non_canonical_axes=allow_non_canonical_axes,
289
+ strict_canonical_order=strict_canonical_order,
290
+ )
291
+
292
+ self._allow_non_canonical_axes = allow_non_canonical_axes
293
+ self._strict_canonical_order = strict_canonical_order
294
+
295
+ self._canonical_order = canonical_axes_order()
296
+ self._extended_canonical_order = [*axes_setup.others, *self._canonical_order]
297
+
298
+ self._on_disk_axes = on_disk_axes
299
+ self._axes_setup = axes_setup
300
+
301
+ self._name_mapping = self._compute_name_mapping()
302
+ self._index_mapping = self._compute_index_mapping()
303
+
304
+ # Validate the axes type and cast them if necessary
305
+ # This needs to be done after the name mapping is computed
306
+ self.validate_axex_type()
307
+
308
+ def _compute_name_mapping(self):
309
+ """Compute the name mapping.
310
+
311
+ The name mapping is a dictionary with keys the canonical axes names
312
+ and values the on disk axes names.
313
+ """
314
+ _name_mapping = {}
315
+ axis_setup_dict = self._axes_setup.model_dump(exclude={"others"})
316
+ _on_disk_names = self.on_disk_axes_names
317
+ for canonical_key, on_disk_value in axis_setup_dict.items():
318
+ if on_disk_value in _on_disk_names:
319
+ _name_mapping[canonical_key] = on_disk_value
320
+ else:
321
+ _name_mapping[canonical_key] = None
322
+
323
+ for on_disk_name in _on_disk_names:
324
+ if on_disk_name not in _name_mapping.keys():
325
+ _name_mapping[on_disk_name] = on_disk_name
326
+
327
+ for other in self._axes_setup.others:
328
+ if other not in _name_mapping.keys():
329
+ _name_mapping[other] = None
330
+ return _name_mapping
331
+
332
+ def _compute_index_mapping(self):
333
+ """Compute the index mapping.
334
+
335
+ The index mapping is a dictionary with keys the canonical axes names
336
+ and values the on disk axes index.
337
+ """
338
+ _index_mapping = {}
339
+ for canonical_key, on_disk_value in self._name_mapping.items():
340
+ if on_disk_value is not None:
341
+ _index_mapping[canonical_key] = self.on_disk_axes_names.index(
342
+ on_disk_value
343
+ )
344
+ else:
345
+ _index_mapping[canonical_key] = None
346
+ return _index_mapping
347
+
348
+ @property
349
+ def on_disk_axes(self) -> list[Axis]:
350
+ return list(self._on_disk_axes)
351
+
352
+ @property
353
+ def on_disk_axes_names(self) -> list[str]:
354
+ return [ax.on_disk_name for ax in self._on_disk_axes]
355
+
356
+ def get_index(self, name: str) -> int | None:
357
+ """Get the index of the axis by name."""
358
+ if name not in self._index_mapping.keys():
359
+ raise NgioValueError(
360
+ f"Invalid axis name '{name}'. "
361
+ f"Possible values are {self._index_mapping.keys()}"
362
+ )
363
+ return self._index_mapping[name]
364
+
365
+ def get_axis(self, name: str) -> Axis | None:
366
+ """Get the axis object by name."""
367
+ index = self.get_index(name)
368
+ if index is None:
369
+ return None
370
+ return self.on_disk_axes[index]
371
+
372
+ def validate_axex_type(self):
373
+ """Validate the axes type.
374
+
375
+ If the axes type is not correct, a warning is issued.
376
+ and the axis is implicitly cast to the correct type.
377
+ """
378
+ new_axes = []
379
+ for axes in self.on_disk_axes:
380
+ for name in self._canonical_order:
381
+ if axes == self.get_axis(name):
382
+ new_axes.append(axes.canonical_axis_cast(name))
383
+ break
384
+ else:
385
+ new_axes.append(axes)
386
+ self._on_disk_axes = new_axes
387
+
388
+ def _change_order(
389
+ self, names: Collection[str]
390
+ ) -> tuple[tuple[int, ...], tuple[int, ...]]:
391
+ unique_names = set()
392
+ for name in names:
393
+ if name not in self._index_mapping.keys():
394
+ raise NgioValueError(
395
+ f"Invalid axis name '{name}'. "
396
+ f"Possible values are {self._index_mapping.keys()}"
397
+ )
398
+ _unique_name = self._name_mapping.get(name)
399
+ if _unique_name is None:
400
+ continue
401
+ if _unique_name in unique_names:
402
+ raise NgioValueError(
403
+ f"Duplicate axis name, two or more '{_unique_name}' were found. "
404
+ f"Please provide unique names."
405
+ )
406
+ unique_names.add(_unique_name)
407
+
408
+ if len(self.on_disk_axes_names) > len(unique_names):
409
+ missing_names = set(self.on_disk_axes_names) - unique_names
410
+ raise NgioValueError(
411
+ f"Some axes where not queried. "
412
+ f"Please provide the following missing axes {missing_names}"
413
+ )
414
+ _indices, _insert = [], []
415
+ for i, name in enumerate(names):
416
+ _index = self._index_mapping[name]
417
+ if _index is None:
418
+ _insert.append(i)
419
+ else:
420
+ _indices.append(self._index_mapping[name])
421
+ return tuple(_indices), tuple(_insert)
422
+
423
+ def to_order(self, names: Collection[str]) -> tuple[AxesTransformation, ...]:
424
+ """Get the new order of the axes."""
425
+ _indices, _insert = self._change_order(names)
426
+ return AxesTranspose(axes=_indices), AxesExpand(axes=_insert)
427
+
428
+ def from_order(self, names: Collection[str]) -> tuple[AxesTransformation, ...]:
429
+ """Get the new order of the axes."""
430
+ _indices, _insert = self._change_order(names)
431
+ # Inverse transpose is just the transpose with the inverse indices
432
+ _reverse_indices = tuple(np.argsort(_indices))
433
+ return AxesSqueeze(axes=_insert), AxesTranspose(axes=_reverse_indices)
434
+
435
+ def to_canonical(self) -> tuple[AxesTransformation, ...]:
436
+ """Get the new order of the axes."""
437
+ return self.to_order(self._extended_canonical_order)
438
+
439
+ def from_canonical(self) -> tuple[AxesTransformation, ...]:
440
+ """Get the new order of the axes."""
441
+ return self.from_order(self._extended_canonical_order)
442
+
443
+
444
+ def canonical_axes(
445
+ axes_names: Collection[str],
446
+ space_units: SpaceUnits | None = None,
447
+ time_units: TimeUnits | None = None,
448
+ ) -> list[Axis]:
449
+ """Create a new canonical axes mapper.
450
+
451
+ Args:
452
+ axes_names (Collection[str] | int): The axes names on disk.
453
+ - The axes should be in ['t', 'c', 'z', 'y', 'x']
454
+ - The axes should be in strict canonical order.
455
+ - If an integer is provided, the axes are created from the last axis
456
+ to the first
457
+ e.g. 3 -> ["z", "y", "x"]
458
+ space_units (SpaceUnits, optional): The space units. Defaults to None.
459
+ time_units (TimeUnits, optional): The time units. Defaults to None.
460
+
461
+ """
462
+ axes = []
463
+ for name in axes_names:
464
+ match name:
465
+ case "t":
466
+ axes.append(
467
+ Axis(on_disk_name=name, axis_type=AxisType.time, unit=time_units)
468
+ )
469
+ case "c":
470
+ axes.append(Axis(on_disk_name=name, axis_type=AxisType.channel))
471
+ case "z" | "y" | "x":
472
+ axes.append(
473
+ Axis(on_disk_name=name, axis_type=AxisType.space, unit=space_units)
474
+ )
475
+ case _:
476
+ raise NgioValueError(
477
+ f"Invalid axis name '{name}'. "
478
+ "Only 't', 'c', 'z', 'y', 'x' are allowed."
479
+ )
480
+
481
+ return axes