ngio 0.2.0a2__py3-none-any.whl → 0.5.0b4__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 +40 -12
- ngio/common/__init__.py +16 -32
- ngio/common/_dimensions.py +270 -48
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +267 -73
- ngio/common/_roi.py +290 -66
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +54 -22
- 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 +17 -58
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +30 -9
- ngio/images/_abstract_image.py +968 -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 +417 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1235 -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 +39 -15
- ngio/ome_zarr_meta/_meta_handlers.py +490 -96
- ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
- ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
- ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
- ngio/ome_zarr_meta/v04/__init__.py +21 -5
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
- 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 +20 -4
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +50 -1
- ngio/tables/backends/_abstract_backend.py +200 -31
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +10 -114
- 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 +162 -38
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +19 -4
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +79 -115
- ngio/tables/v1/_generic_table.py +21 -90
- ngio/tables/v1/_roi_table.py +486 -137
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +16 -14
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +121 -13
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +374 -218
- ngio-0.5.0b4.dist-info/METADATA +147 -0
- ngio-0.5.0b4.dist-info/RECORD +88 -0
- {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
- ngio/common/_array_pipe.py +0 -160
- ngio/common/_axes_transforms.py +0 -63
- ngio/common/_common_types.py +0 -5
- ngio/common/_slicer.py +0 -97
- ngio/images/abstract_image.py +0 -240
- ngio/images/create.py +0 -251
- ngio/images/image.py +0 -389
- ngio/images/label.py +0 -236
- ngio/images/omezarr_container.py +0 -535
- ngio/ome_zarr_meta/_generic_handlers.py +0 -320
- ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
- ngio/tables/_validators.py +0 -192
- ngio/tables/backends/_anndata_v1.py +0 -75
- ngio/tables/backends/_json_v1.py +0 -56
- ngio/tables/tables_container.py +0 -300
- ngio/tables/v1/_masking_roi_table.py +0 -175
- ngio/utils/_logger.py +0 -29
- ngio-0.2.0a2.dist-info/METADATA +0 -95
- ngio-0.2.0a2.dist-info/RECORD +0 -53
- {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"""Fractal internal module for axes handling."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import
|
|
3
|
+
from collections.abc import Sequence
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from
|
|
6
|
-
from typing import TypeVar
|
|
5
|
+
from typing import Literal, TypeAlias, TypeVar
|
|
7
6
|
|
|
8
|
-
import numpy as np
|
|
9
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
8
|
|
|
11
9
|
from ngio.utils import NgioValidationError, NgioValueError
|
|
12
10
|
|
|
13
|
-
logger = Logger(__name__)
|
|
14
|
-
|
|
15
11
|
T = TypeVar("T")
|
|
12
|
+
SlicingType: TypeAlias = slice | tuple[int, ...] | int
|
|
16
13
|
|
|
17
14
|
################################################################################################
|
|
18
15
|
#
|
|
@@ -32,98 +29,94 @@ class AxisType(str, Enum):
|
|
|
32
29
|
space = "space"
|
|
33
30
|
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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"
|
|
61
88
|
|
|
62
89
|
|
|
63
90
|
class Axis(BaseModel):
|
|
64
91
|
"""Axis infos model."""
|
|
65
92
|
|
|
66
|
-
|
|
67
|
-
unit:
|
|
93
|
+
name: str
|
|
94
|
+
unit: str | None = None
|
|
68
95
|
axis_type: AxisType | None = None
|
|
69
96
|
|
|
70
97
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
71
98
|
|
|
72
99
|
def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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)
|
|
111
108
|
|
|
112
109
|
def canonical_axis_cast(self, canonical_name: str) -> "Axis":
|
|
113
110
|
"""Cast the implicit axis to the correct type."""
|
|
114
111
|
match canonical_name:
|
|
115
112
|
case "t":
|
|
116
|
-
if self.axis_type != AxisType.time or
|
|
117
|
-
self.unit, TimeUnits
|
|
118
|
-
):
|
|
113
|
+
if self.axis_type != AxisType.time or self.unit is None:
|
|
119
114
|
return self.implicit_type_cast(AxisType.time)
|
|
120
115
|
case "c":
|
|
121
|
-
if self.axis_type != AxisType.channel
|
|
116
|
+
if self.axis_type != AxisType.channel:
|
|
122
117
|
return self.implicit_type_cast(AxisType.channel)
|
|
123
118
|
case "z" | "y" | "x":
|
|
124
|
-
if self.axis_type != AxisType.space or
|
|
125
|
-
self.unit, SpaceUnits
|
|
126
|
-
):
|
|
119
|
+
if self.axis_type != AxisType.space or self.unit is None:
|
|
127
120
|
return self.implicit_type_cast(AxisType.space)
|
|
128
121
|
return self
|
|
129
122
|
|
|
@@ -163,10 +156,40 @@ class AxesSetup(BaseModel):
|
|
|
163
156
|
|
|
164
157
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
165
158
|
|
|
166
|
-
|
|
167
|
-
|
|
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]):
|
|
168
191
|
"""Check if all axes on disk have unique names."""
|
|
169
|
-
names = [ax.
|
|
192
|
+
names = [ax.name for ax in axes]
|
|
170
193
|
if len(set(names)) != len(names):
|
|
171
194
|
duplicates = {item for item in names if names.count(item) > 1}
|
|
172
195
|
raise NgioValidationError(
|
|
@@ -183,41 +206,41 @@ def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: b
|
|
|
183
206
|
)
|
|
184
207
|
|
|
185
208
|
|
|
186
|
-
def _check_axes_validity(axes:
|
|
209
|
+
def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
|
|
187
210
|
"""Check if all axes are valid."""
|
|
188
211
|
_axes_setup = axes_setup.model_dump(exclude={"others"})
|
|
189
212
|
_all_known_axes = [*_axes_setup.values(), *axes_setup.others]
|
|
190
213
|
for ax in axes:
|
|
191
|
-
if ax.
|
|
214
|
+
if ax.name not in _all_known_axes:
|
|
192
215
|
raise NgioValidationError(
|
|
193
|
-
f"Invalid axis name '{ax.
|
|
194
|
-
f"Please correct map `{ax.
|
|
216
|
+
f"Invalid axis name '{ax.name}'. "
|
|
217
|
+
f"Please correct map `{ax.name}` "
|
|
195
218
|
f"using the AxesSetup model {axes_setup}"
|
|
196
219
|
)
|
|
197
220
|
|
|
198
221
|
|
|
199
222
|
def _check_canonical_order(
|
|
200
|
-
axes:
|
|
223
|
+
axes: Sequence[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
|
|
201
224
|
):
|
|
202
225
|
"""Check if the axes are in the canonical order."""
|
|
203
226
|
if not strict_canonical_order:
|
|
204
227
|
return
|
|
205
|
-
|
|
228
|
+
_names = [ax.name for ax in axes]
|
|
206
229
|
_canonical_order = []
|
|
207
230
|
for name in canonical_axes_order():
|
|
208
231
|
mapped_name = getattr(axes_setup, name)
|
|
209
|
-
if mapped_name in
|
|
232
|
+
if mapped_name in _names:
|
|
210
233
|
_canonical_order.append(mapped_name)
|
|
211
234
|
|
|
212
|
-
if
|
|
235
|
+
if _names != _canonical_order:
|
|
213
236
|
raise NgioValidationError(
|
|
214
237
|
f"Invalid axes order. The axes must be in the canonical order. "
|
|
215
|
-
f"Expected {_canonical_order}, but found {
|
|
238
|
+
f"Expected {_canonical_order}, but found {_names}"
|
|
216
239
|
)
|
|
217
240
|
|
|
218
241
|
|
|
219
242
|
def validate_axes(
|
|
220
|
-
axes:
|
|
243
|
+
axes: Sequence[Axis],
|
|
221
244
|
axes_setup: AxesSetup,
|
|
222
245
|
allow_non_canonical_axes: bool = False,
|
|
223
246
|
strict_canonical_order: bool = False,
|
|
@@ -239,33 +262,20 @@ def validate_axes(
|
|
|
239
262
|
)
|
|
240
263
|
|
|
241
264
|
|
|
242
|
-
class
|
|
243
|
-
|
|
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.
|
|
265
|
+
class AxesHandler:
|
|
266
|
+
"""This class is used to handle and operate on OME-Zarr axes.
|
|
262
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.
|
|
263
273
|
"""
|
|
264
274
|
|
|
265
275
|
def __init__(
|
|
266
276
|
self,
|
|
267
277
|
# spec dictated args
|
|
268
|
-
|
|
278
|
+
axes: Sequence[Axis],
|
|
269
279
|
# user defined args
|
|
270
280
|
axes_setup: AxesSetup | None = None,
|
|
271
281
|
allow_non_canonical_axes: bool = False,
|
|
@@ -274,7 +284,7 @@ class AxesMapper:
|
|
|
274
284
|
"""Create a new AxesMapper object.
|
|
275
285
|
|
|
276
286
|
Args:
|
|
277
|
-
|
|
287
|
+
axes (list[Axis]): The axes on disk.
|
|
278
288
|
axes_setup (AxesSetup, optional): The axis setup. Defaults to None.
|
|
279
289
|
allow_non_canonical_axes (bool, optional): Allow non canonical axes.
|
|
280
290
|
strict_canonical_order (bool, optional): Check if the axes are in the
|
|
@@ -283,7 +293,7 @@ class AxesMapper:
|
|
|
283
293
|
axes_setup = axes_setup if axes_setup is not None else AxesSetup()
|
|
284
294
|
|
|
285
295
|
validate_axes(
|
|
286
|
-
axes=
|
|
296
|
+
axes=axes,
|
|
287
297
|
axes_setup=axes_setup,
|
|
288
298
|
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
289
299
|
strict_canonical_order=strict_canonical_order,
|
|
@@ -293,163 +303,181 @@ class AxesMapper:
|
|
|
293
303
|
self._strict_canonical_order = strict_canonical_order
|
|
294
304
|
|
|
295
305
|
self._canonical_order = canonical_axes_order()
|
|
296
|
-
self._extended_canonical_order = [*axes_setup.others, *self._canonical_order]
|
|
297
306
|
|
|
298
|
-
self.
|
|
307
|
+
self._axes = axes
|
|
299
308
|
self._axes_setup = axes_setup
|
|
300
309
|
|
|
301
|
-
self._name_mapping = self._compute_name_mapping()
|
|
302
310
|
self._index_mapping = self._compute_index_mapping()
|
|
303
311
|
|
|
304
312
|
# Validate the axes type and cast them if necessary
|
|
305
313
|
# This needs to be done after the name mapping is computed
|
|
306
|
-
self.
|
|
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
|
|
314
|
+
self.validate_axes_type()
|
|
331
315
|
|
|
332
316
|
def _compute_index_mapping(self):
|
|
333
317
|
"""Compute the index mapping.
|
|
334
318
|
|
|
335
319
|
The index mapping is a dictionary with keys the canonical axes names
|
|
336
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
|
+
}
|
|
337
333
|
"""
|
|
338
334
|
_index_mapping = {}
|
|
339
|
-
for
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
_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]
|
|
346
342
|
return _index_mapping
|
|
347
343
|
|
|
348
344
|
@property
|
|
349
|
-
def
|
|
350
|
-
|
|
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)
|
|
351
357
|
|
|
352
358
|
@property
|
|
353
|
-
def
|
|
354
|
-
|
|
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
|
+
)
|
|
355
428
|
|
|
356
429
|
def get_index(self, name: str) -> int | None:
|
|
357
430
|
"""Get the index of the axis by name."""
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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)
|
|
364
443
|
|
|
365
444
|
def get_axis(self, name: str) -> Axis | None:
|
|
366
445
|
"""Get the axis object by name."""
|
|
367
446
|
index = self.get_index(name)
|
|
368
447
|
if index is None:
|
|
369
448
|
return None
|
|
370
|
-
return self.
|
|
449
|
+
return self.axes[index]
|
|
371
450
|
|
|
372
|
-
def
|
|
451
|
+
def validate_axes_type(self):
|
|
373
452
|
"""Validate the axes type.
|
|
374
453
|
|
|
375
454
|
If the axes type is not correct, a warning is issued.
|
|
376
455
|
and the axis is implicitly cast to the correct type.
|
|
377
456
|
"""
|
|
378
457
|
new_axes = []
|
|
379
|
-
for axes in self.
|
|
458
|
+
for axes in self.axes:
|
|
380
459
|
for name in self._canonical_order:
|
|
381
460
|
if axes == self.get_axis(name):
|
|
382
461
|
new_axes.append(axes.canonical_axis_cast(name))
|
|
383
462
|
break
|
|
384
463
|
else:
|
|
385
464
|
new_axes.append(axes)
|
|
386
|
-
self.
|
|
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)
|
|
465
|
+
self._axes = new_axes
|
|
407
466
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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]:
|
|
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:
|
|
449
477
|
"""Create a new canonical axes mapper.
|
|
450
478
|
|
|
451
479
|
Args:
|
|
452
|
-
axes_names (
|
|
480
|
+
axes_names (Sequence[str] | int): The axes names on disk.
|
|
453
481
|
- The axes should be in ['t', 'c', 'z', 'y', 'x']
|
|
454
482
|
- The axes should be in strict canonical order.
|
|
455
483
|
- If an integer is provided, the axes are created from the last axis
|
|
@@ -457,25 +485,31 @@ def canonical_axes(
|
|
|
457
485
|
e.g. 3 -> ["z", "y", "x"]
|
|
458
486
|
space_units (SpaceUnits, optional): The space units. Defaults to None.
|
|
459
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.
|
|
460
493
|
|
|
461
494
|
"""
|
|
462
495
|
axes = []
|
|
463
496
|
for name in axes_names:
|
|
464
497
|
match name:
|
|
465
498
|
case "t":
|
|
466
|
-
axes.append(
|
|
467
|
-
Axis(on_disk_name=name, axis_type=AxisType.time, unit=time_units)
|
|
468
|
-
)
|
|
499
|
+
axes.append(Axis(name=name, axis_type=AxisType.time, unit=time_units))
|
|
469
500
|
case "c":
|
|
470
|
-
axes.append(Axis(
|
|
501
|
+
axes.append(Axis(name=name, axis_type=AxisType.channel))
|
|
471
502
|
case "z" | "y" | "x":
|
|
472
|
-
axes.append(
|
|
473
|
-
Axis(on_disk_name=name, axis_type=AxisType.space, unit=space_units)
|
|
474
|
-
)
|
|
503
|
+
axes.append(Axis(name=name, axis_type=AxisType.space, unit=space_units))
|
|
475
504
|
case _:
|
|
476
505
|
raise NgioValueError(
|
|
477
506
|
f"Invalid axis name '{name}'. "
|
|
478
507
|
"Only 't', 'c', 'z', 'y', 'x' are allowed."
|
|
479
508
|
)
|
|
480
509
|
|
|
481
|
-
return
|
|
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
|
+
)
|