ngio 0.5.0b6__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 +69 -0
- ngio/common/__init__.py +28 -0
- ngio/common/_dimensions.py +335 -0
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +408 -0
- ngio/common/_roi.py +315 -0
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +188 -0
- ngio/experimental/__init__.py +5 -0
- ngio/experimental/iterators/__init__.py +15 -0
- ngio/experimental/iterators/_abstract_iterator.py +390 -0
- ngio/experimental/iterators/_feature.py +189 -0
- ngio/experimental/iterators/_image_processing.py +130 -0
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_rois_utils.py +126 -0
- ngio/experimental/iterators/_segmentation.py +235 -0
- ngio/hcs/__init__.py +19 -0
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +44 -0
- ngio/images/_abstract_image.py +967 -0
- ngio/images/_create_synt_container.py +132 -0
- ngio/images/_create_utils.py +423 -0
- ngio/images/_image.py +926 -0
- ngio/images/_label.py +411 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1237 -0
- ngio/images/_table_ops.py +471 -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 +146 -0
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_match_shape.py +377 -0
- ngio/io_pipes/_ops_axes.py +344 -0
- ngio/io_pipes/_ops_slices.py +411 -0
- ngio/io_pipes/_ops_slices_utils.py +199 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +180 -0
- ngio/ome_zarr_meta/__init__.py +65 -0
- ngio/ome_zarr_meta/_meta_handlers.py +536 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
- ngio/ome_zarr_meta/v04/__init__.py +27 -0
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
- ngio/ome_zarr_meta/v05/__init__.py +27 -0
- ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
- ngio/resources/__init__.py +55 -0
- ngio/resources/resource_model.py +36 -0
- ngio/tables/__init__.py +43 -0
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +57 -0
- ngio/tables/backends/_abstract_backend.py +240 -0
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +90 -0
- ngio/tables/backends/_csv.py +19 -0
- ngio/tables/backends/_json.py +92 -0
- ngio/tables/backends/_parquet.py +19 -0
- ngio/tables/backends/_py_arrow_backends.py +222 -0
- ngio/tables/backends/_table_backends.py +226 -0
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +23 -0
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +125 -0
- ngio/tables/v1/_generic_table.py +49 -0
- ngio/tables/v1/_roi_table.py +575 -0
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +45 -0
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +165 -0
- ngio/utils/_errors.py +37 -0
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +534 -0
- ngio-0.5.0b6.dist-info/METADATA +148 -0
- ngio-0.5.0b6.dist-info/RECORD +88 -0
- ngio-0.5.0b6.dist-info/WHEEL +4 -0
- ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Fractal internal module for axes handling."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Literal, TypeAlias, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from ngio.utils import NgioValidationError, NgioValueError
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
SlicingType: TypeAlias = slice | tuple[int, ...] | int
|
|
13
|
+
|
|
14
|
+
################################################################################################
|
|
15
|
+
#
|
|
16
|
+
# Axis Types and Units
|
|
17
|
+
# We define a small set of axis types and units that can be used in the metadata.
|
|
18
|
+
# This axis types are more restrictive than the OME standard.
|
|
19
|
+
# We do that to simplify the data processing.
|
|
20
|
+
#
|
|
21
|
+
#################################################################################################
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AxisType(str, Enum):
|
|
25
|
+
"""Allowed axis types."""
|
|
26
|
+
|
|
27
|
+
channel = "channel"
|
|
28
|
+
time = "time"
|
|
29
|
+
space = "space"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
SpaceUnits = Literal[
|
|
33
|
+
"micrometer",
|
|
34
|
+
"nanometer",
|
|
35
|
+
"angstrom",
|
|
36
|
+
"picometer",
|
|
37
|
+
"millimeter",
|
|
38
|
+
"centimeter",
|
|
39
|
+
"decimeter",
|
|
40
|
+
"meter",
|
|
41
|
+
"inch",
|
|
42
|
+
"foot",
|
|
43
|
+
"yard",
|
|
44
|
+
"mile",
|
|
45
|
+
"kilometer",
|
|
46
|
+
"hectometer",
|
|
47
|
+
"megameter",
|
|
48
|
+
"gigameter",
|
|
49
|
+
"terameter",
|
|
50
|
+
"petameter",
|
|
51
|
+
"exameter",
|
|
52
|
+
"parsec",
|
|
53
|
+
"femtometer",
|
|
54
|
+
"attometer",
|
|
55
|
+
"zeptometer",
|
|
56
|
+
"yoctometer",
|
|
57
|
+
"zettameter",
|
|
58
|
+
"yottameter",
|
|
59
|
+
]
|
|
60
|
+
DefaultSpaceUnit = "micrometer"
|
|
61
|
+
|
|
62
|
+
TimeUnits = Literal[
|
|
63
|
+
"attosecond",
|
|
64
|
+
"centisecond",
|
|
65
|
+
"day",
|
|
66
|
+
"decisecond",
|
|
67
|
+
"exasecond",
|
|
68
|
+
"femtosecond",
|
|
69
|
+
"gigasecond",
|
|
70
|
+
"hectosecond",
|
|
71
|
+
"hour",
|
|
72
|
+
"kilosecond",
|
|
73
|
+
"megasecond",
|
|
74
|
+
"microsecond",
|
|
75
|
+
"millisecond",
|
|
76
|
+
"minute",
|
|
77
|
+
"nanosecond",
|
|
78
|
+
"petasecond",
|
|
79
|
+
"picosecond",
|
|
80
|
+
"second",
|
|
81
|
+
"terasecond",
|
|
82
|
+
"yoctosecond",
|
|
83
|
+
"yottasecond",
|
|
84
|
+
"zeptosecond",
|
|
85
|
+
"zettasecond",
|
|
86
|
+
]
|
|
87
|
+
DefaultTimeUnit = "second"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Axis(BaseModel):
|
|
91
|
+
"""Axis infos model."""
|
|
92
|
+
|
|
93
|
+
name: str
|
|
94
|
+
unit: str | None = None
|
|
95
|
+
axis_type: AxisType | None = None
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
98
|
+
|
|
99
|
+
def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
|
|
100
|
+
unit = self.unit
|
|
101
|
+
if cast_type == AxisType.time and unit is None:
|
|
102
|
+
unit = DefaultTimeUnit
|
|
103
|
+
|
|
104
|
+
if cast_type == AxisType.space and unit is None:
|
|
105
|
+
unit = DefaultSpaceUnit
|
|
106
|
+
|
|
107
|
+
return Axis(name=self.name, axis_type=cast_type, unit=unit)
|
|
108
|
+
|
|
109
|
+
def canonical_axis_cast(self, canonical_name: str) -> "Axis":
|
|
110
|
+
"""Cast the implicit axis to the correct type."""
|
|
111
|
+
match canonical_name:
|
|
112
|
+
case "t":
|
|
113
|
+
if self.axis_type != AxisType.time or self.unit is None:
|
|
114
|
+
return self.implicit_type_cast(AxisType.time)
|
|
115
|
+
case "c":
|
|
116
|
+
if self.axis_type != AxisType.channel:
|
|
117
|
+
return self.implicit_type_cast(AxisType.channel)
|
|
118
|
+
case "z" | "y" | "x":
|
|
119
|
+
if self.axis_type != AxisType.space or self.unit is None:
|
|
120
|
+
return self.implicit_type_cast(AxisType.space)
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
################################################################################################
|
|
125
|
+
#
|
|
126
|
+
# Axes Handling
|
|
127
|
+
# We define a unique mapping to match the axes on disk to the canonical axes.
|
|
128
|
+
# The canonical axes are the ones that are used consistently in the NGIO internal API.
|
|
129
|
+
# The canonical axes ordered are: t, c, z, y, x.
|
|
130
|
+
#
|
|
131
|
+
#################################################################################################
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def canonical_axes_order() -> tuple[str, str, str, str, str]:
|
|
135
|
+
"""Get the canonical axes order."""
|
|
136
|
+
return "t", "c", "z", "y", "x"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def canonical_label_axes_order() -> tuple[str, str, str, str]:
|
|
140
|
+
"""Get the canonical axes order."""
|
|
141
|
+
return "t", "z", "y", "x"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class AxesSetup(BaseModel):
|
|
145
|
+
"""Axes setup model.
|
|
146
|
+
|
|
147
|
+
This model is used to map the on disk axes to the canonical OME-Zarr axes.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
x: str = "x"
|
|
151
|
+
y: str = "y"
|
|
152
|
+
z: str = "z"
|
|
153
|
+
c: str = "c"
|
|
154
|
+
t: str = "t"
|
|
155
|
+
others: list[str] = Field(default_factory=list)
|
|
156
|
+
|
|
157
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
158
|
+
|
|
159
|
+
def canonical_map(self) -> dict[str, str]:
|
|
160
|
+
"""Get the canonical map of axes."""
|
|
161
|
+
return {
|
|
162
|
+
"t": self.t,
|
|
163
|
+
"c": self.c,
|
|
164
|
+
"z": self.z,
|
|
165
|
+
"y": self.y,
|
|
166
|
+
"x": self.x,
|
|
167
|
+
}
|
|
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
|
+
|
|
174
|
+
def inverse_canonical_map(self) -> dict[str, str]:
|
|
175
|
+
"""Get the on disk map of axes."""
|
|
176
|
+
return {
|
|
177
|
+
self.t: "t",
|
|
178
|
+
self.c: "c",
|
|
179
|
+
self.z: "z",
|
|
180
|
+
self.y: "y",
|
|
181
|
+
self.x: "x",
|
|
182
|
+
}
|
|
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
|
+
|
|
189
|
+
|
|
190
|
+
def _check_unique_names(axes: Sequence[Axis]):
|
|
191
|
+
"""Check if all axes on disk have unique names."""
|
|
192
|
+
names = [ax.name for ax in axes]
|
|
193
|
+
if len(set(names)) != len(names):
|
|
194
|
+
duplicates = {item for item in names if names.count(item) > 1}
|
|
195
|
+
raise NgioValidationError(
|
|
196
|
+
f"All axes must be unique. But found duplicates axes {duplicates}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: bool):
|
|
201
|
+
"""Check if all axes are known."""
|
|
202
|
+
if not allow_non_canonical_axes and len(axes_setup.others) > 0:
|
|
203
|
+
raise NgioValidationError(
|
|
204
|
+
f"Unknown axes {axes_setup.others}. Please set "
|
|
205
|
+
"`allow_non_canonical_axes=True` to ignore them"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
|
|
210
|
+
"""Check if all axes are valid."""
|
|
211
|
+
_axes_setup = axes_setup.model_dump(exclude={"others"})
|
|
212
|
+
_all_known_axes = [*_axes_setup.values(), *axes_setup.others]
|
|
213
|
+
for ax in axes:
|
|
214
|
+
if ax.name not in _all_known_axes:
|
|
215
|
+
raise NgioValidationError(
|
|
216
|
+
f"Invalid axis name '{ax.name}'. "
|
|
217
|
+
f"Please correct map `{ax.name}` "
|
|
218
|
+
f"using the AxesSetup model {axes_setup}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _check_canonical_order(
|
|
223
|
+
axes: Sequence[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
|
|
224
|
+
):
|
|
225
|
+
"""Check if the axes are in the canonical order."""
|
|
226
|
+
if not strict_canonical_order:
|
|
227
|
+
return
|
|
228
|
+
_names = [ax.name for ax in axes]
|
|
229
|
+
_canonical_order = []
|
|
230
|
+
for name in canonical_axes_order():
|
|
231
|
+
mapped_name = getattr(axes_setup, name)
|
|
232
|
+
if mapped_name in _names:
|
|
233
|
+
_canonical_order.append(mapped_name)
|
|
234
|
+
|
|
235
|
+
if _names != _canonical_order:
|
|
236
|
+
raise NgioValidationError(
|
|
237
|
+
f"Invalid axes order. The axes must be in the canonical order. "
|
|
238
|
+
f"Expected {_canonical_order}, but found {_names}"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def validate_axes(
|
|
243
|
+
axes: Sequence[Axis],
|
|
244
|
+
axes_setup: AxesSetup,
|
|
245
|
+
allow_non_canonical_axes: bool = False,
|
|
246
|
+
strict_canonical_order: bool = False,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Validate the axes."""
|
|
249
|
+
if allow_non_canonical_axes and strict_canonical_order:
|
|
250
|
+
raise NgioValidationError(
|
|
251
|
+
"`allow_non_canonical_axes` and"
|
|
252
|
+
"`strict_canonical_order` cannot be true at the same time."
|
|
253
|
+
"If non canonical axes are allowed, the order cannot be checked."
|
|
254
|
+
)
|
|
255
|
+
_check_unique_names(axes=axes)
|
|
256
|
+
_check_non_canonical_axes(
|
|
257
|
+
axes_setup=axes_setup, allow_non_canonical_axes=allow_non_canonical_axes
|
|
258
|
+
)
|
|
259
|
+
_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
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class AxesHandler:
|
|
266
|
+
"""This class is used to handle and operate on OME-Zarr axes.
|
|
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.
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
# spec dictated args
|
|
278
|
+
axes: Sequence[Axis],
|
|
279
|
+
# user defined args
|
|
280
|
+
axes_setup: AxesSetup | None = None,
|
|
281
|
+
allow_non_canonical_axes: bool = False,
|
|
282
|
+
strict_canonical_order: bool = False,
|
|
283
|
+
):
|
|
284
|
+
"""Create a new AxesMapper object.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
axes (list[Axis]): The axes on disk.
|
|
288
|
+
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
|
+
"""
|
|
293
|
+
axes_setup = axes_setup if axes_setup is not None else AxesSetup()
|
|
294
|
+
|
|
295
|
+
validate_axes(
|
|
296
|
+
axes=axes,
|
|
297
|
+
axes_setup=axes_setup,
|
|
298
|
+
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
299
|
+
strict_canonical_order=strict_canonical_order,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self._allow_non_canonical_axes = allow_non_canonical_axes
|
|
303
|
+
self._strict_canonical_order = strict_canonical_order
|
|
304
|
+
|
|
305
|
+
self._canonical_order = canonical_axes_order()
|
|
306
|
+
|
|
307
|
+
self._axes = axes
|
|
308
|
+
self._axes_setup = axes_setup
|
|
309
|
+
|
|
310
|
+
self._index_mapping = self._compute_index_mapping()
|
|
311
|
+
|
|
312
|
+
# Validate the axes type and cast them if necessary
|
|
313
|
+
# This needs to be done after the name mapping is computed
|
|
314
|
+
self.validate_axes_type()
|
|
315
|
+
|
|
316
|
+
def _compute_index_mapping(self):
|
|
317
|
+
"""Compute the index mapping.
|
|
318
|
+
|
|
319
|
+
The index mapping is a dictionary with keys the canonical axes names
|
|
320
|
+
and values the on disk axes index.
|
|
321
|
+
|
|
322
|
+
Example:
|
|
323
|
+
If the on disk axes are ['channel', 't', 'z', 'y', 'x'],
|
|
324
|
+
the index mapping will be:
|
|
325
|
+
{
|
|
326
|
+
'c': 0,
|
|
327
|
+
'channel': 0,
|
|
328
|
+
't': 1,
|
|
329
|
+
'z': 2,
|
|
330
|
+
'y': 3,
|
|
331
|
+
'x': 4,
|
|
332
|
+
}
|
|
333
|
+
"""
|
|
334
|
+
_index_mapping = {}
|
|
335
|
+
for i, ax in enumerate(self.axes_names):
|
|
336
|
+
_index_mapping[ax] = i
|
|
337
|
+
# If the axis is not in the canonical order we also set it.
|
|
338
|
+
canonical_map = self._axes_setup.canonical_map()
|
|
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]
|
|
342
|
+
return _index_mapping
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def axes_setup(self) -> AxesSetup:
|
|
346
|
+
"""Return the axes setup."""
|
|
347
|
+
return self._axes_setup
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def axes(self) -> tuple[Axis, ...]:
|
|
351
|
+
return tuple(self._axes)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def axes_names(self) -> tuple[str, ...]:
|
|
355
|
+
"""On disk axes names."""
|
|
356
|
+
return tuple(ax.name for ax in self._axes)
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def allow_non_canonical_axes(self) -> bool:
|
|
360
|
+
"""Return if non canonical axes are allowed."""
|
|
361
|
+
return self._allow_non_canonical_axes
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def strict_canonical_order(self) -> bool:
|
|
365
|
+
"""Return if strict canonical order is enforced."""
|
|
366
|
+
return self._strict_canonical_order
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def space_unit(self) -> str | None:
|
|
370
|
+
"""Return the space unit for a given axis."""
|
|
371
|
+
x_axis = self.get_axis("x")
|
|
372
|
+
y_axis = self.get_axis("y")
|
|
373
|
+
|
|
374
|
+
if x_axis is None or y_axis is None:
|
|
375
|
+
raise NgioValidationError(
|
|
376
|
+
"The dataset must have x and y axes to determine the space unit."
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if x_axis.unit == y_axis.unit:
|
|
380
|
+
return x_axis.unit
|
|
381
|
+
else:
|
|
382
|
+
raise NgioValidationError(
|
|
383
|
+
"Inconsistent space units. "
|
|
384
|
+
f"x={x_axis.unit} and y={y_axis.unit} should have the same unit."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def time_unit(self) -> str | None:
|
|
389
|
+
"""Return the time unit for a given axis."""
|
|
390
|
+
t_axis = self.get_axis("t")
|
|
391
|
+
if t_axis is None:
|
|
392
|
+
return None
|
|
393
|
+
return t_axis.unit
|
|
394
|
+
|
|
395
|
+
def to_units(
|
|
396
|
+
self,
|
|
397
|
+
*,
|
|
398
|
+
space_unit: SpaceUnits = DefaultSpaceUnit,
|
|
399
|
+
time_unit: TimeUnits = DefaultTimeUnit,
|
|
400
|
+
) -> "AxesHandler":
|
|
401
|
+
"""Convert the pixel size to the given units.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
space_unit(str): The space unit to convert to.
|
|
405
|
+
time_unit(str): The time unit to convert to.
|
|
406
|
+
"""
|
|
407
|
+
new_axes = []
|
|
408
|
+
for ax in self.axes:
|
|
409
|
+
if ax.axis_type == AxisType.space:
|
|
410
|
+
new_ax = Axis(
|
|
411
|
+
name=ax.name,
|
|
412
|
+
axis_type=ax.axis_type,
|
|
413
|
+
unit=space_unit,
|
|
414
|
+
)
|
|
415
|
+
new_axes.append(new_ax)
|
|
416
|
+
elif ax.axis_type == AxisType.time:
|
|
417
|
+
new_ax = Axis(name=ax.name, axis_type=ax.axis_type, unit=time_unit)
|
|
418
|
+
new_axes.append(new_ax)
|
|
419
|
+
else:
|
|
420
|
+
new_axes.append(ax)
|
|
421
|
+
|
|
422
|
+
return AxesHandler(
|
|
423
|
+
axes=new_axes,
|
|
424
|
+
axes_setup=self.axes_setup,
|
|
425
|
+
allow_non_canonical_axes=self.allow_non_canonical_axes,
|
|
426
|
+
strict_canonical_order=self.strict_canonical_order,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def get_index(self, name: str) -> int | None:
|
|
430
|
+
"""Get the index of the axis by name."""
|
|
431
|
+
return self._index_mapping.get(name, None)
|
|
432
|
+
|
|
433
|
+
def has_axis(self, axis_name: str) -> bool:
|
|
434
|
+
"""Return whether the axis exists."""
|
|
435
|
+
index = self.get_index(axis_name)
|
|
436
|
+
if index is None:
|
|
437
|
+
return False
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
def get_canonical_name(self, name: str) -> str | None:
|
|
441
|
+
"""Get the canonical name of the axis by name."""
|
|
442
|
+
return self._axes_setup.get_canonical_name(name)
|
|
443
|
+
|
|
444
|
+
def get_axis(self, name: str) -> Axis | None:
|
|
445
|
+
"""Get the axis object by name."""
|
|
446
|
+
index = self.get_index(name)
|
|
447
|
+
if index is None:
|
|
448
|
+
return None
|
|
449
|
+
return self.axes[index]
|
|
450
|
+
|
|
451
|
+
def validate_axes_type(self):
|
|
452
|
+
"""Validate the axes type.
|
|
453
|
+
|
|
454
|
+
If the axes type is not correct, a warning is issued.
|
|
455
|
+
and the axis is implicitly cast to the correct type.
|
|
456
|
+
"""
|
|
457
|
+
new_axes = []
|
|
458
|
+
for axes in self.axes:
|
|
459
|
+
for name in self._canonical_order:
|
|
460
|
+
if axes == self.get_axis(name):
|
|
461
|
+
new_axes.append(axes.canonical_axis_cast(name))
|
|
462
|
+
break
|
|
463
|
+
else:
|
|
464
|
+
new_axes.append(axes)
|
|
465
|
+
self._axes = new_axes
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def build_canonical_axes_handler(
|
|
469
|
+
axes_names: Sequence[str],
|
|
470
|
+
space_units: SpaceUnits | str | None = DefaultSpaceUnit,
|
|
471
|
+
time_units: TimeUnits | str | None = DefaultTimeUnit,
|
|
472
|
+
# user defined args
|
|
473
|
+
axes_setup: AxesSetup | None = None,
|
|
474
|
+
allow_non_canonical_axes: bool = False,
|
|
475
|
+
strict_canonical_order: bool = False,
|
|
476
|
+
) -> AxesHandler:
|
|
477
|
+
"""Create a new canonical axes mapper.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
axes_names (Sequence[str] | int): The axes names on disk.
|
|
481
|
+
- The axes should be in ['t', 'c', 'z', 'y', 'x']
|
|
482
|
+
- The axes should be in strict canonical order.
|
|
483
|
+
- If an integer is provided, the axes are created from the last axis
|
|
484
|
+
to the first
|
|
485
|
+
e.g. 3 -> ["z", "y", "x"]
|
|
486
|
+
space_units (SpaceUnits, optional): The space units. Defaults to None.
|
|
487
|
+
time_units (TimeUnits, optional): The time units. Defaults to None.
|
|
488
|
+
axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
|
|
489
|
+
allow_non_canonical_axes (bool, optional): Allow non canonical axes.
|
|
490
|
+
Defaults to False.
|
|
491
|
+
strict_canonical_order (bool, optional): Check if the axes are in the
|
|
492
|
+
canonical order. Defaults to False.
|
|
493
|
+
|
|
494
|
+
"""
|
|
495
|
+
axes = []
|
|
496
|
+
for name in axes_names:
|
|
497
|
+
match name:
|
|
498
|
+
case "t":
|
|
499
|
+
axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
|
|
500
|
+
case "c":
|
|
501
|
+
axes.append(Axis(name=name, axis_type=AxisType.channel))
|
|
502
|
+
case "z" | "y" | "x":
|
|
503
|
+
axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
|
|
504
|
+
case _:
|
|
505
|
+
raise NgioValueError(
|
|
506
|
+
f"Invalid axis name '{name}'. "
|
|
507
|
+
"Only 't', 'c', 'z', 'y', 'x' are allowed."
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return AxesHandler(
|
|
511
|
+
axes=axes,
|
|
512
|
+
axes_setup=axes_setup,
|
|
513
|
+
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
514
|
+
strict_canonical_order=strict_canonical_order,
|
|
515
|
+
)
|