ngio 0.4.0a3__py3-none-any.whl → 0.4.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.
- ngio/__init__.py +1 -2
- ngio/common/__init__.py +2 -51
- ngio/common/_dimensions.py +253 -74
- ngio/common/_pyramid.py +42 -23
- ngio/common/_roi.py +49 -413
- ngio/common/_zoom.py +32 -7
- ngio/experimental/iterators/__init__.py +0 -2
- ngio/experimental/iterators/_abstract_iterator.py +246 -26
- ngio/experimental/iterators/_feature.py +90 -52
- ngio/experimental/iterators/_image_processing.py +24 -63
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_rois_utils.py +4 -4
- ngio/experimental/iterators/_segmentation.py +38 -85
- ngio/images/_abstract_image.py +192 -95
- ngio/images/_create.py +16 -0
- ngio/images/_create_synt_container.py +10 -0
- ngio/images/_image.py +35 -9
- ngio/images/_label.py +26 -3
- ngio/images/_masked_image.py +45 -61
- ngio/images/_ome_zarr_container.py +33 -0
- ngio/io_pipes/__init__.py +75 -0
- ngio/io_pipes/_io_pipes.py +361 -0
- ngio/io_pipes/_io_pipes_masked.py +488 -0
- ngio/io_pipes/_io_pipes_roi.py +152 -0
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_match_shape.py +376 -0
- ngio/io_pipes/_ops_axes.py +344 -0
- ngio/io_pipes/_ops_slices.py +446 -0
- ngio/io_pipes/_ops_slices_utils.py +196 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +175 -0
- ngio/ome_zarr_meta/__init__.py +4 -2
- ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -4
- ngio/ome_zarr_meta/ngio_specs/_axes.py +129 -141
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +47 -121
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +30 -22
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +17 -1
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +33 -30
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/__init__.py +1 -0
- ngio/resources/resource_model.py +1 -0
- ngio/{common/transforms → transforms}/__init__.py +1 -1
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/_datasets.py +5 -0
- ngio/utils/_zarr_utils.py +5 -1
- {ngio-0.4.0a3.dist-info → ngio-0.4.0b1.dist-info}/METADATA +1 -1
- ngio-0.4.0b1.dist-info/RECORD +85 -0
- ngio/common/_array_io_pipes.py +0 -554
- ngio/common/_array_io_utils.py +0 -508
- ngio/common/transforms/_label.py +0 -12
- ngio/common/transforms/_zoom.py +0 -109
- ngio-0.4.0a3.dist-info/RECORD +0 -76
- {ngio-0.4.0a3.dist-info → ngio-0.4.0b1.dist-info}/WHEEL +0 -0
- {ngio-0.4.0a3.dist-info → ngio-0.4.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,7 +4,6 @@ from collections.abc import Sequence
|
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from typing import Literal, TypeAlias, TypeVar
|
|
6
6
|
|
|
7
|
-
import numpy as np
|
|
8
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
8
|
|
|
10
9
|
from ngio.utils import NgioValidationError, NgioValueError
|
|
@@ -91,7 +90,7 @@ DefaultTimeUnit = "second"
|
|
|
91
90
|
class Axis(BaseModel):
|
|
92
91
|
"""Axis infos model."""
|
|
93
92
|
|
|
94
|
-
|
|
93
|
+
name: str
|
|
95
94
|
unit: str | None = None
|
|
96
95
|
axis_type: AxisType | None = None
|
|
97
96
|
|
|
@@ -105,7 +104,7 @@ class Axis(BaseModel):
|
|
|
105
104
|
if cast_type == AxisType.space and unit is None:
|
|
106
105
|
unit = DefaultSpaceUnit
|
|
107
106
|
|
|
108
|
-
return Axis(
|
|
107
|
+
return Axis(name=self.name, axis_type=cast_type, unit=unit)
|
|
109
108
|
|
|
110
109
|
def canonical_axis_cast(self, canonical_name: str) -> "Axis":
|
|
111
110
|
"""Cast the implicit axis to the correct type."""
|
|
@@ -167,6 +166,11 @@ class AxesSetup(BaseModel):
|
|
|
167
166
|
"x": self.x,
|
|
168
167
|
}
|
|
169
168
|
|
|
169
|
+
def get_on_disk_name(self, canonical_name: str) -> str | None:
|
|
170
|
+
"""Get the on disk name of the axis by its canonical name."""
|
|
171
|
+
canonical_map = self.canonical_map()
|
|
172
|
+
return canonical_map.get(canonical_name, None)
|
|
173
|
+
|
|
170
174
|
def inverse_canonical_map(self) -> dict[str, str]:
|
|
171
175
|
"""Get the on disk map of axes."""
|
|
172
176
|
return {
|
|
@@ -177,10 +181,15 @@ class AxesSetup(BaseModel):
|
|
|
177
181
|
self.x: "x",
|
|
178
182
|
}
|
|
179
183
|
|
|
184
|
+
def get_canonical_name(self, on_disk_name: str) -> str | None:
|
|
185
|
+
"""Get the canonical name of the axis by its on disk name."""
|
|
186
|
+
inv_map = self.inverse_canonical_map()
|
|
187
|
+
return inv_map.get(on_disk_name, None)
|
|
188
|
+
|
|
180
189
|
|
|
181
190
|
def _check_unique_names(axes: Sequence[Axis]):
|
|
182
191
|
"""Check if all axes on disk have unique names."""
|
|
183
|
-
names = [ax.
|
|
192
|
+
names = [ax.name for ax in axes]
|
|
184
193
|
if len(set(names)) != len(names):
|
|
185
194
|
duplicates = {item for item in names if names.count(item) > 1}
|
|
186
195
|
raise NgioValidationError(
|
|
@@ -202,10 +211,10 @@ def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
|
|
|
202
211
|
_axes_setup = axes_setup.model_dump(exclude={"others"})
|
|
203
212
|
_all_known_axes = [*_axes_setup.values(), *axes_setup.others]
|
|
204
213
|
for ax in axes:
|
|
205
|
-
if ax.
|
|
214
|
+
if ax.name not in _all_known_axes:
|
|
206
215
|
raise NgioValidationError(
|
|
207
|
-
f"Invalid axis name '{ax.
|
|
208
|
-
f"Please correct map `{ax.
|
|
216
|
+
f"Invalid axis name '{ax.name}'. "
|
|
217
|
+
f"Please correct map `{ax.name}` "
|
|
209
218
|
f"using the AxesSetup model {axes_setup}"
|
|
210
219
|
)
|
|
211
220
|
|
|
@@ -216,17 +225,17 @@ def _check_canonical_order(
|
|
|
216
225
|
"""Check if the axes are in the canonical order."""
|
|
217
226
|
if not strict_canonical_order:
|
|
218
227
|
return
|
|
219
|
-
|
|
228
|
+
_names = [ax.name for ax in axes]
|
|
220
229
|
_canonical_order = []
|
|
221
230
|
for name in canonical_axes_order():
|
|
222
231
|
mapped_name = getattr(axes_setup, name)
|
|
223
|
-
if mapped_name in
|
|
232
|
+
if mapped_name in _names:
|
|
224
233
|
_canonical_order.append(mapped_name)
|
|
225
234
|
|
|
226
|
-
if
|
|
235
|
+
if _names != _canonical_order:
|
|
227
236
|
raise NgioValidationError(
|
|
228
237
|
f"Invalid axes order. The axes must be in the canonical order. "
|
|
229
|
-
f"Expected {_canonical_order}, but found {
|
|
238
|
+
f"Expected {_canonical_order}, but found {_names}"
|
|
230
239
|
)
|
|
231
240
|
|
|
232
241
|
|
|
@@ -253,32 +262,20 @@ def validate_axes(
|
|
|
253
262
|
)
|
|
254
263
|
|
|
255
264
|
|
|
256
|
-
class
|
|
257
|
-
|
|
258
|
-
transpose_axes: tuple[int, ...] | None = None
|
|
259
|
-
expand_axes: tuple[int, ...] | None = None
|
|
260
|
-
squeeze_axes: tuple[int, ...] | None = None
|
|
261
|
-
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
262
|
-
|
|
263
|
-
@property
|
|
264
|
-
def requires_axes_ops(self) -> bool:
|
|
265
|
-
"""Check if the slicing operations require axes operations."""
|
|
266
|
-
if self.expand_axes or self.transpose_axes or self.squeeze_axes:
|
|
267
|
-
return True
|
|
268
|
-
return False
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class AxesMapper:
|
|
272
|
-
"""Map on disk axes to canonical axes.
|
|
273
|
-
|
|
274
|
-
This class is used to map the on disk axes to the canonical axes.
|
|
265
|
+
class AxesHandler:
|
|
266
|
+
"""This class is used to handle and operate on OME-Zarr axes.
|
|
275
267
|
|
|
268
|
+
The class also provides:
|
|
269
|
+
- methods to reorder, squeeze and expand axes.
|
|
270
|
+
- methods to validate the axes.
|
|
271
|
+
- methods to get axis by name or index.
|
|
272
|
+
- methods to operate on the axes.
|
|
276
273
|
"""
|
|
277
274
|
|
|
278
275
|
def __init__(
|
|
279
276
|
self,
|
|
280
277
|
# spec dictated args
|
|
281
|
-
|
|
278
|
+
axes: Sequence[Axis],
|
|
282
279
|
# user defined args
|
|
283
280
|
axes_setup: AxesSetup | None = None,
|
|
284
281
|
allow_non_canonical_axes: bool = False,
|
|
@@ -287,7 +284,7 @@ class AxesMapper:
|
|
|
287
284
|
"""Create a new AxesMapper object.
|
|
288
285
|
|
|
289
286
|
Args:
|
|
290
|
-
|
|
287
|
+
axes (list[Axis]): The axes on disk.
|
|
291
288
|
axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
|
|
292
289
|
allow_non_canonical_axes (bool, optional): Allow non canonical axes.
|
|
293
290
|
strict_canonical_order (bool, optional): Check if the axes are in the
|
|
@@ -296,7 +293,7 @@ class AxesMapper:
|
|
|
296
293
|
axes_setup = axes_setup if axes_setup is not None else AxesSetup()
|
|
297
294
|
|
|
298
295
|
validate_axes(
|
|
299
|
-
axes=
|
|
296
|
+
axes=axes,
|
|
300
297
|
axes_setup=axes_setup,
|
|
301
298
|
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
302
299
|
strict_canonical_order=strict_canonical_order,
|
|
@@ -307,7 +304,7 @@ class AxesMapper:
|
|
|
307
304
|
|
|
308
305
|
self._canonical_order = canonical_axes_order()
|
|
309
306
|
|
|
310
|
-
self.
|
|
307
|
+
self._axes = axes
|
|
311
308
|
self._axes_setup = axes_setup
|
|
312
309
|
|
|
313
310
|
self._index_mapping = self._compute_index_mapping()
|
|
@@ -339,9 +336,9 @@ class AxesMapper:
|
|
|
339
336
|
_index_mapping[ax] = i
|
|
340
337
|
# If the axis is not in the canonical order we also set it.
|
|
341
338
|
canonical_map = self._axes_setup.canonical_map()
|
|
342
|
-
for
|
|
343
|
-
if
|
|
344
|
-
_index_mapping[
|
|
339
|
+
for canonical_name, on_disk_name in canonical_map.items():
|
|
340
|
+
if on_disk_name in _index_mapping.keys():
|
|
341
|
+
_index_mapping[canonical_name] = _index_mapping[on_disk_name]
|
|
345
342
|
return _index_mapping
|
|
346
343
|
|
|
347
344
|
@property
|
|
@@ -351,11 +348,11 @@ class AxesMapper:
|
|
|
351
348
|
|
|
352
349
|
@property
|
|
353
350
|
def axes(self) -> tuple[Axis, ...]:
|
|
354
|
-
return tuple(self.
|
|
351
|
+
return tuple(self._axes)
|
|
355
352
|
|
|
356
353
|
@property
|
|
357
354
|
def axes_names(self) -> tuple[str, ...]:
|
|
358
|
-
return tuple(ax.
|
|
355
|
+
return tuple(ax.name for ax in self._axes)
|
|
359
356
|
|
|
360
357
|
@property
|
|
361
358
|
def allow_non_canonical_axes(self) -> bool:
|
|
@@ -367,10 +364,82 @@ class AxesMapper:
|
|
|
367
364
|
"""Return if strict canonical order is enforced."""
|
|
368
365
|
return self._strict_canonical_order
|
|
369
366
|
|
|
367
|
+
@property
|
|
368
|
+
def space_unit(self) -> str | None:
|
|
369
|
+
"""Return the space unit for a given axis."""
|
|
370
|
+
x_axis = self.get_axis("x")
|
|
371
|
+
y_axis = self.get_axis("y")
|
|
372
|
+
|
|
373
|
+
if x_axis is None or y_axis is None:
|
|
374
|
+
raise NgioValidationError(
|
|
375
|
+
"The dataset must have x and y axes to determine the space unit."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
if x_axis.unit == y_axis.unit:
|
|
379
|
+
return x_axis.unit
|
|
380
|
+
else:
|
|
381
|
+
raise NgioValidationError(
|
|
382
|
+
"Inconsistent space units. "
|
|
383
|
+
f"x={x_axis.unit} and y={y_axis.unit} should have the same unit."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def time_unit(self) -> str | None:
|
|
388
|
+
"""Return the time unit for a given axis."""
|
|
389
|
+
t_axis = self.get_axis("t")
|
|
390
|
+
if t_axis is None:
|
|
391
|
+
return None
|
|
392
|
+
return t_axis.unit
|
|
393
|
+
|
|
394
|
+
def to_units(
|
|
395
|
+
self,
|
|
396
|
+
*,
|
|
397
|
+
space_unit: SpaceUnits = DefaultSpaceUnit,
|
|
398
|
+
time_unit: TimeUnits = DefaultTimeUnit,
|
|
399
|
+
) -> "AxesHandler":
|
|
400
|
+
"""Convert the pixel size to the given units.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
space_unit(str): The space unit to convert to.
|
|
404
|
+
time_unit(str): The time unit to convert to.
|
|
405
|
+
"""
|
|
406
|
+
new_axes = []
|
|
407
|
+
for ax in self.axes:
|
|
408
|
+
if ax.axis_type == AxisType.space:
|
|
409
|
+
new_ax = Axis(
|
|
410
|
+
name=ax.name,
|
|
411
|
+
axis_type=ax.axis_type,
|
|
412
|
+
unit=space_unit,
|
|
413
|
+
)
|
|
414
|
+
new_axes.append(new_ax)
|
|
415
|
+
elif ax.axis_type == AxisType.time:
|
|
416
|
+
new_ax = Axis(name=ax.name, axis_type=ax.axis_type, unit=time_unit)
|
|
417
|
+
new_axes.append(new_ax)
|
|
418
|
+
else:
|
|
419
|
+
new_axes.append(ax)
|
|
420
|
+
|
|
421
|
+
return AxesHandler(
|
|
422
|
+
axes=new_axes,
|
|
423
|
+
axes_setup=self.axes_setup,
|
|
424
|
+
allow_non_canonical_axes=self.allow_non_canonical_axes,
|
|
425
|
+
strict_canonical_order=self.strict_canonical_order,
|
|
426
|
+
)
|
|
427
|
+
|
|
370
428
|
def get_index(self, name: str) -> int | None:
|
|
371
429
|
"""Get the index of the axis by name."""
|
|
372
430
|
return self._index_mapping.get(name, None)
|
|
373
431
|
|
|
432
|
+
def has_axis(self, axis_name: str) -> bool:
|
|
433
|
+
"""Return whether the axis exists."""
|
|
434
|
+
index = self.get_index(axis_name)
|
|
435
|
+
if index is None:
|
|
436
|
+
return False
|
|
437
|
+
return True
|
|
438
|
+
|
|
439
|
+
def get_canonical_name(self, name: str) -> str | None:
|
|
440
|
+
"""Get the canonical name of the axis by name."""
|
|
441
|
+
return self._axes_setup.get_canonical_name(name)
|
|
442
|
+
|
|
374
443
|
def get_axis(self, name: str) -> Axis | None:
|
|
375
444
|
"""Get the axis object by name."""
|
|
376
445
|
index = self.get_index(name)
|
|
@@ -392,105 +461,18 @@ class AxesMapper:
|
|
|
392
461
|
break
|
|
393
462
|
else:
|
|
394
463
|
new_axes.append(axes)
|
|
395
|
-
self.
|
|
396
|
-
|
|
397
|
-
def _reorder_axes(
|
|
398
|
-
self, names: Sequence[str]
|
|
399
|
-
) -> tuple[tuple[int, ...] | None, tuple[int, ...] | None, tuple[int, ...] | None]:
|
|
400
|
-
"""Change the order of the axes."""
|
|
401
|
-
# Validate the names
|
|
402
|
-
unique_names = set(names)
|
|
403
|
-
if len(unique_names) != len(names):
|
|
404
|
-
raise NgioValueError(
|
|
405
|
-
"Duplicate axis names found. Please provide unique names for each axis."
|
|
406
|
-
)
|
|
407
|
-
for name in names:
|
|
408
|
-
if not isinstance(name, str):
|
|
409
|
-
raise NgioValueError(
|
|
410
|
-
f"Invalid axis name '{name}'. Axis names must be strings."
|
|
411
|
-
)
|
|
412
|
-
inv_canonical_map = self.axes_setup.inverse_canonical_map()
|
|
464
|
+
self._axes = new_axes
|
|
413
465
|
|
|
414
|
-
# Step 1: Check find squeeze axes
|
|
415
|
-
_axes_to_squeeze: list[int] = []
|
|
416
|
-
axes_names_after_squeeze = []
|
|
417
|
-
for i, ax in enumerate(self.axes_names):
|
|
418
|
-
# If the axis is not in the names, it means we need to squeeze it
|
|
419
|
-
ax_canonical = inv_canonical_map.get(ax, None)
|
|
420
|
-
if ax not in names and ax_canonical not in names:
|
|
421
|
-
_axes_to_squeeze.append(i)
|
|
422
|
-
elif ax in names:
|
|
423
|
-
axes_names_after_squeeze.append(ax)
|
|
424
|
-
elif ax_canonical in names:
|
|
425
|
-
# If the axis is in the canonical map, we add it to the names
|
|
426
|
-
axes_names_after_squeeze.append(ax_canonical)
|
|
427
|
-
|
|
428
|
-
axes_to_squeeze = tuple(_axes_to_squeeze) if len(_axes_to_squeeze) > 0 else None
|
|
429
|
-
|
|
430
|
-
# Step 2: Find the transposition order
|
|
431
|
-
_transposition_order: list[int] = []
|
|
432
|
-
axes_names_after_transpose = []
|
|
433
|
-
for ax in names:
|
|
434
|
-
if ax in axes_names_after_squeeze:
|
|
435
|
-
_transposition_order.append(axes_names_after_squeeze.index(ax))
|
|
436
|
-
axes_names_after_transpose.append(ax)
|
|
437
|
-
|
|
438
|
-
if np.allclose(_transposition_order, range(len(_transposition_order))):
|
|
439
|
-
# If the transposition order is the identity, we don't need to transpose
|
|
440
|
-
transposition_order = None
|
|
441
|
-
else:
|
|
442
|
-
transposition_order = tuple(_transposition_order)
|
|
443
|
-
|
|
444
|
-
# Step 3: Find axes to expand
|
|
445
|
-
_axes_to_expand: list[int] = []
|
|
446
|
-
for i, name in enumerate(names):
|
|
447
|
-
if name not in axes_names_after_transpose:
|
|
448
|
-
# If the axis is not in the mapping, it means we need to expand it
|
|
449
|
-
_axes_to_expand.append(i)
|
|
450
|
-
|
|
451
|
-
axes_to_expand = tuple(_axes_to_expand) if len(_axes_to_expand) > 0 else None
|
|
452
|
-
return axes_to_squeeze, transposition_order, axes_to_expand
|
|
453
|
-
|
|
454
|
-
def to_order(self, names: Sequence[str]) -> SlicingOps:
|
|
455
|
-
"""Get the new order of the axes."""
|
|
456
|
-
axes_to_squeeze, transposition_order, axes_to_expand = self._reorder_axes(names)
|
|
457
|
-
return SlicingOps(
|
|
458
|
-
transpose_axes=transposition_order,
|
|
459
|
-
expand_axes=axes_to_expand,
|
|
460
|
-
squeeze_axes=axes_to_squeeze,
|
|
461
|
-
)
|
|
462
466
|
|
|
463
|
-
|
|
464
|
-
"""Get the new order of the axes."""
|
|
465
|
-
axes_to_squeeze, transposition_order, axes_to_expand = self._reorder_axes(names)
|
|
466
|
-
# Inverse transpose is just the transpose with the inverse indices
|
|
467
|
-
if transposition_order is None:
|
|
468
|
-
_reverse_indices = None
|
|
469
|
-
else:
|
|
470
|
-
_reverse_indices = tuple(np.argsort(transposition_order))
|
|
471
|
-
|
|
472
|
-
return SlicingOps(
|
|
473
|
-
transpose_axes=_reverse_indices,
|
|
474
|
-
expand_axes=axes_to_squeeze,
|
|
475
|
-
squeeze_axes=axes_to_expand,
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
def to_canonical(self) -> SlicingOps:
|
|
479
|
-
"""Get the new order of the axes."""
|
|
480
|
-
other = self._axes_setup.others
|
|
481
|
-
return self.to_order(other + list(self._canonical_order))
|
|
482
|
-
|
|
483
|
-
def from_canonical(self) -> SlicingOps:
|
|
484
|
-
"""Get the new order of the axes."""
|
|
485
|
-
other = self._axes_setup.others
|
|
486
|
-
return self.from_order(other + list(self._canonical_order))
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def canonical_axes(
|
|
467
|
+
def build_canonical_axes_handler(
|
|
490
468
|
axes_names: Sequence[str],
|
|
491
469
|
space_units: SpaceUnits | str | None = DefaultSpaceUnit,
|
|
492
470
|
time_units: TimeUnits | str | None = DefaultTimeUnit,
|
|
493
|
-
|
|
471
|
+
# user defined args
|
|
472
|
+
axes_setup: AxesSetup | None = None,
|
|
473
|
+
allow_non_canonical_axes: bool = False,
|
|
474
|
+
strict_canonical_order: bool = False,
|
|
475
|
+
) -> AxesHandler:
|
|
494
476
|
"""Create a new canonical axes mapper.
|
|
495
477
|
|
|
496
478
|
Args:
|
|
@@ -502,25 +484,31 @@ def canonical_axes(
|
|
|
502
484
|
e.g. 3 -> ["z", "y", "x"]
|
|
503
485
|
space_units (SpaceUnits, optional): The space units. Defaults to None.
|
|
504
486
|
time_units (TimeUnits, optional): The time units. Defaults to None.
|
|
487
|
+
axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
|
|
488
|
+
allow_non_canonical_axes (bool, optional): Allow non canonical axes.
|
|
489
|
+
Defaults to False.
|
|
490
|
+
strict_canonical_order (bool, optional): Check if the axes are in the
|
|
491
|
+
canonical order. Defaults to False.
|
|
505
492
|
|
|
506
493
|
"""
|
|
507
494
|
axes = []
|
|
508
495
|
for name in axes_names:
|
|
509
496
|
match name:
|
|
510
497
|
case "t":
|
|
511
|
-
axes.append(
|
|
512
|
-
Axis(on_disk_name=name, axis_type=AxisType.time, unit=time_units)
|
|
513
|
-
)
|
|
498
|
+
axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
|
|
514
499
|
case "c":
|
|
515
|
-
axes.append(Axis(
|
|
500
|
+
axes.append(Axis(name=name, axis_type=AxisType.channel))
|
|
516
501
|
case "z" | "y" | "x":
|
|
517
|
-
axes.append(
|
|
518
|
-
Axis(on_disk_name=name, axis_type=AxisType.space, unit=space_units)
|
|
519
|
-
)
|
|
502
|
+
axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
|
|
520
503
|
case _:
|
|
521
504
|
raise NgioValueError(
|
|
522
505
|
f"Invalid axis name '{name}'. "
|
|
523
506
|
"Only 't', 'c', 'z', 'y', 'x' are allowed."
|
|
524
507
|
)
|
|
525
508
|
|
|
526
|
-
return
|
|
509
|
+
return AxesHandler(
|
|
510
|
+
axes=axes,
|
|
511
|
+
axes_setup=axes_setup,
|
|
512
|
+
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
513
|
+
strict_canonical_order=strict_canonical_order,
|
|
514
|
+
)
|
|
@@ -3,85 +3,49 @@
|
|
|
3
3
|
from collections.abc import Sequence
|
|
4
4
|
|
|
5
5
|
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
6
|
-
|
|
7
|
-
AxesSetup,
|
|
8
|
-
Axis,
|
|
9
|
-
AxisType,
|
|
10
|
-
DefaultSpaceUnit,
|
|
11
|
-
DefaultTimeUnit,
|
|
12
|
-
SpaceUnits,
|
|
13
|
-
TimeUnits,
|
|
6
|
+
AxesHandler,
|
|
14
7
|
)
|
|
15
8
|
from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
16
9
|
from ngio.utils import NgioValidationError
|
|
17
10
|
|
|
18
11
|
|
|
19
12
|
class Dataset:
|
|
20
|
-
"""Model for a dataset in the multiscale.
|
|
21
|
-
|
|
22
|
-
To initialize the Dataset object, the path, the axes, scale, and translation list
|
|
23
|
-
can be provided with on_disk order.
|
|
24
|
-
"""
|
|
13
|
+
"""Model for a dataset in the multiscale."""
|
|
25
14
|
|
|
26
15
|
def __init__(
|
|
27
16
|
self,
|
|
28
17
|
*,
|
|
29
18
|
# args coming from ngff specs
|
|
30
19
|
path: str,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# user defined args
|
|
35
|
-
axes_setup: AxesSetup | None = None,
|
|
36
|
-
allow_non_canonical_axes: bool = False,
|
|
37
|
-
strict_canonical_order: bool = False,
|
|
20
|
+
axes_handler: AxesHandler,
|
|
21
|
+
scale: Sequence[float],
|
|
22
|
+
translation: Sequence[float] | None = None,
|
|
38
23
|
):
|
|
39
24
|
"""Initialize the Dataset object.
|
|
40
25
|
|
|
41
26
|
Args:
|
|
42
27
|
path (str): The path of the dataset.
|
|
43
|
-
|
|
44
|
-
|
|
28
|
+
axes_handler (AxesHandler): The axes handler object.
|
|
29
|
+
scale (list[float]): The list of scale transformation.
|
|
45
30
|
The scale transformation must have the same length as the axes.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
allow_non_canonical_axes (bool): Allow non-canonical axes.
|
|
49
|
-
strict_canonical_order (bool): Strict canonical order.
|
|
31
|
+
translation (list[float] | None): The list of translation.
|
|
32
|
+
The translation must have the same length as the axes.
|
|
50
33
|
"""
|
|
51
34
|
self._path = path
|
|
52
|
-
self.
|
|
53
|
-
on_disk_axes=on_disk_axes,
|
|
54
|
-
axes_setup=axes_setup,
|
|
55
|
-
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
56
|
-
strict_canonical_order=strict_canonical_order,
|
|
57
|
-
)
|
|
35
|
+
self._axes_handler = axes_handler
|
|
58
36
|
|
|
59
|
-
if len(
|
|
37
|
+
if len(scale) != len(axes_handler.axes):
|
|
60
38
|
raise NgioValidationError(
|
|
61
39
|
"The length of the scale transformation must be the same as the axes."
|
|
62
40
|
)
|
|
63
|
-
self.
|
|
41
|
+
self._scale = list(scale)
|
|
64
42
|
|
|
65
|
-
|
|
66
|
-
if len(
|
|
43
|
+
translation = translation or [0.0] * len(axes_handler.axes)
|
|
44
|
+
if len(translation) != len(axes_handler.axes):
|
|
67
45
|
raise NgioValidationError(
|
|
68
46
|
"The length of the translation must be the same as the axes."
|
|
69
47
|
)
|
|
70
|
-
self.
|
|
71
|
-
|
|
72
|
-
def get_scale(self, axis_name: str) -> float:
|
|
73
|
-
"""Return the scale for a given axis."""
|
|
74
|
-
idx = self._axes_mapper.get_index(axis_name)
|
|
75
|
-
if idx is None:
|
|
76
|
-
return 1.0
|
|
77
|
-
return self._on_disk_scale[idx]
|
|
78
|
-
|
|
79
|
-
def get_translation(self, axis_name: str) -> float:
|
|
80
|
-
"""Return the translation for a given axis."""
|
|
81
|
-
idx = self._axes_mapper.get_index(axis_name)
|
|
82
|
-
if idx is None:
|
|
83
|
-
return 0.0
|
|
84
|
-
return self._on_disk_translation[idx]
|
|
48
|
+
self._translation = list(translation)
|
|
85
49
|
|
|
86
50
|
@property
|
|
87
51
|
def path(self) -> str:
|
|
@@ -89,84 +53,46 @@ class Dataset:
|
|
|
89
53
|
return self._path
|
|
90
54
|
|
|
91
55
|
@property
|
|
92
|
-
def
|
|
93
|
-
"""Return the
|
|
94
|
-
|
|
95
|
-
y_axis = self._axes_mapper.get_axis("y")
|
|
96
|
-
|
|
97
|
-
if x_axis is None or y_axis is None:
|
|
98
|
-
raise NgioValidationError(
|
|
99
|
-
"The dataset must have x and y axes to determine the space unit."
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
if x_axis.unit == y_axis.unit:
|
|
103
|
-
return x_axis.unit
|
|
104
|
-
else:
|
|
105
|
-
raise NgioValidationError(
|
|
106
|
-
"Inconsistent space units. "
|
|
107
|
-
f"x={x_axis.unit} and y={y_axis.unit} should have the same unit."
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
@property
|
|
111
|
-
def time_unit(self) -> str | None:
|
|
112
|
-
"""Return the time unit for a given axis."""
|
|
113
|
-
t_axis = self._axes_mapper.get_axis("t")
|
|
114
|
-
if t_axis is None:
|
|
115
|
-
return None
|
|
116
|
-
return t_axis.unit
|
|
56
|
+
def axes_handler(self) -> AxesHandler:
|
|
57
|
+
"""Return the axes handler object."""
|
|
58
|
+
return self._axes_handler
|
|
117
59
|
|
|
118
60
|
@property
|
|
119
61
|
def pixel_size(self) -> PixelSize:
|
|
120
62
|
"""Return the pixel size for the dataset."""
|
|
121
63
|
return PixelSize(
|
|
122
|
-
x=self.get_scale("x"),
|
|
123
|
-
y=self.get_scale("y"),
|
|
124
|
-
z=self.get_scale("z"),
|
|
125
|
-
t=self.get_scale("t"),
|
|
126
|
-
space_unit=self.space_unit,
|
|
127
|
-
time_unit=self.time_unit,
|
|
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),
|
|
68
|
+
space_unit=self.axes_handler.space_unit,
|
|
69
|
+
time_unit=self.axes_handler.time_unit,
|
|
128
70
|
)
|
|
129
71
|
|
|
130
72
|
@property
|
|
131
|
-
def
|
|
132
|
-
"""Return the
|
|
133
|
-
return self.
|
|
73
|
+
def scale(self) -> tuple[float, ...]:
|
|
74
|
+
"""Return the scale transformation as a tuple."""
|
|
75
|
+
return tuple(self._scale)
|
|
134
76
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
time_unit: TimeUnits = DefaultTimeUnit,
|
|
140
|
-
) -> "Dataset":
|
|
141
|
-
"""Convert the pixel size to the given units.
|
|
77
|
+
@property
|
|
78
|
+
def translation(self) -> tuple[float, ...]:
|
|
79
|
+
"""Return the translation as a tuple."""
|
|
80
|
+
return tuple(self._translation)
|
|
142
81
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
on_disk_name=ax.on_disk_name,
|
|
152
|
-
axis_type=ax.axis_type,
|
|
153
|
-
unit=space_unit,
|
|
154
|
-
)
|
|
155
|
-
new_axes.append(new_ax)
|
|
156
|
-
elif ax.axis_type == AxisType.time:
|
|
157
|
-
new_ax = Axis(
|
|
158
|
-
on_disk_name=ax.on_disk_name, axis_type=ax.axis_type, unit=time_unit
|
|
159
|
-
)
|
|
160
|
-
new_axes.append(new_ax)
|
|
161
|
-
else:
|
|
162
|
-
new_axes.append(ax)
|
|
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]
|
|
163
90
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
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]
|