ngio 0.2.1__py3-none-any.whl → 0.2.2__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 +20 -2
- ngio/common/_roi.py +2 -2
- ngio/hcs/__init__.py +16 -2
- ngio/hcs/plate.py +381 -22
- ngio/images/abstract_image.py +10 -0
- ngio/images/create.py +25 -36
- ngio/images/image.py +38 -6
- ngio/images/label.py +23 -2
- ngio/images/ome_zarr_container.py +66 -26
- ngio/ome_zarr_meta/__init__.py +5 -3
- ngio/ome_zarr_meta/ngio_specs/__init__.py +10 -2
- ngio/ome_zarr_meta/ngio_specs/_axes.py +90 -65
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +46 -8
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +242 -70
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +49 -11
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +28 -11
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +2 -2
- {ngio-0.2.1.dist-info → ngio-0.2.2.dist-info}/METADATA +1 -1
- {ngio-0.2.1.dist-info → ngio-0.2.2.dist-info}/RECORD +22 -21
- {ngio-0.2.1.dist-info → ngio-0.2.2.dist-info}/WHEEL +0 -0
- {ngio-0.2.1.dist-info → ngio-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from collections.abc import Collection
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from logging import Logger
|
|
6
|
-
from typing import TypeVar
|
|
6
|
+
from typing import Literal, TypeVar
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, Field
|
|
@@ -32,98 +32,108 @@ class AxisType(str, Enum):
|
|
|
32
32
|
space = "space"
|
|
33
33
|
|
|
34
34
|
|
|
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
|
-
|
|
35
|
+
SpaceUnits = Literal[
|
|
36
|
+
"micrometer",
|
|
37
|
+
"nanometer",
|
|
38
|
+
"angstrom",
|
|
39
|
+
"picometer",
|
|
40
|
+
"millimeter",
|
|
41
|
+
"centimeter",
|
|
42
|
+
"decimeter",
|
|
43
|
+
"meter",
|
|
44
|
+
"inch",
|
|
45
|
+
"foot",
|
|
46
|
+
"yard",
|
|
47
|
+
"mile",
|
|
48
|
+
"kilometer",
|
|
49
|
+
"hectometer",
|
|
50
|
+
"megameter",
|
|
51
|
+
"gigameter",
|
|
52
|
+
"terameter",
|
|
53
|
+
"petameter",
|
|
54
|
+
"exameter",
|
|
55
|
+
"parsec",
|
|
56
|
+
"femtometer",
|
|
57
|
+
"attometer",
|
|
58
|
+
"zeptometer",
|
|
59
|
+
"yoctometer",
|
|
60
|
+
"zettameter",
|
|
61
|
+
"yottameter",
|
|
62
|
+
]
|
|
63
|
+
DefaultSpaceUnit = "micrometer"
|
|
64
|
+
|
|
65
|
+
TimeUnits = Literal[
|
|
66
|
+
"attosecond",
|
|
67
|
+
"centisecond",
|
|
68
|
+
"day",
|
|
69
|
+
"decisecond",
|
|
70
|
+
"exasecond",
|
|
71
|
+
"femtosecond",
|
|
72
|
+
"gigasecond",
|
|
73
|
+
"hectosecond",
|
|
74
|
+
"hour",
|
|
75
|
+
"kilosecond",
|
|
76
|
+
"megasecond",
|
|
77
|
+
"microsecond",
|
|
78
|
+
"millisecond",
|
|
79
|
+
"minute",
|
|
80
|
+
"nanosecond",
|
|
81
|
+
"petasecond",
|
|
82
|
+
"picosecond",
|
|
83
|
+
"second",
|
|
84
|
+
"terasecond",
|
|
85
|
+
"yoctosecond",
|
|
86
|
+
"yottasecond",
|
|
87
|
+
"zeptosecond",
|
|
88
|
+
"zettasecond",
|
|
89
|
+
]
|
|
90
|
+
DefaultTimeUnit = "second"
|
|
61
91
|
|
|
62
92
|
|
|
63
93
|
class Axis(BaseModel):
|
|
64
94
|
"""Axis infos model."""
|
|
65
95
|
|
|
66
96
|
on_disk_name: str
|
|
67
|
-
unit:
|
|
97
|
+
unit: str | None = None
|
|
68
98
|
axis_type: AxisType | None = None
|
|
69
99
|
|
|
70
100
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
71
101
|
|
|
72
102
|
def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
|
|
103
|
+
unit = self.unit
|
|
73
104
|
if self.axis_type != cast_type:
|
|
74
105
|
logger.warning(
|
|
75
106
|
f"Axis {self.on_disk_name} has type {self.axis_type}. "
|
|
76
107
|
f"Casting to {cast_type}."
|
|
77
108
|
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
if cast_type == AxisType.time and not isinstance(self.unit, TimeUnits):
|
|
109
|
+
|
|
110
|
+
if cast_type == AxisType.time and unit is None:
|
|
82
111
|
logger.warning(
|
|
83
112
|
f"Time axis {self.on_disk_name} has unit {self.unit}. "
|
|
84
|
-
f"Casting to {
|
|
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()}."
|
|
113
|
+
f"Casting to {DefaultSpaceUnit}."
|
|
95
114
|
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
unit=SpaceUnits.default(),
|
|
100
|
-
)
|
|
101
|
-
elif cast_type == AxisType.channel and self.unit is not None:
|
|
115
|
+
unit = DefaultTimeUnit
|
|
116
|
+
|
|
117
|
+
if cast_type == AxisType.space and unit is None:
|
|
102
118
|
logger.warning(
|
|
103
|
-
f"
|
|
104
|
-
|
|
105
|
-
new_axis = Axis(
|
|
106
|
-
on_disk_name=self.on_disk_name,
|
|
107
|
-
axis_type=AxisType.channel,
|
|
108
|
-
unit=None,
|
|
119
|
+
f"Space axis {self.on_disk_name} has unit {unit}. "
|
|
120
|
+
f"Casting to {DefaultSpaceUnit}."
|
|
109
121
|
)
|
|
110
|
-
|
|
122
|
+
unit = DefaultSpaceUnit
|
|
123
|
+
|
|
124
|
+
return Axis(on_disk_name=self.on_disk_name, axis_type=cast_type, unit=unit)
|
|
111
125
|
|
|
112
126
|
def canonical_axis_cast(self, canonical_name: str) -> "Axis":
|
|
113
127
|
"""Cast the implicit axis to the correct type."""
|
|
114
128
|
match canonical_name:
|
|
115
129
|
case "t":
|
|
116
|
-
if self.axis_type != AxisType.time or
|
|
117
|
-
self.unit, TimeUnits
|
|
118
|
-
):
|
|
130
|
+
if self.axis_type != AxisType.time or self.unit is None:
|
|
119
131
|
return self.implicit_type_cast(AxisType.time)
|
|
120
132
|
case "c":
|
|
121
|
-
if self.axis_type != AxisType.channel
|
|
133
|
+
if self.axis_type != AxisType.channel:
|
|
122
134
|
return self.implicit_type_cast(AxisType.channel)
|
|
123
135
|
case "z" | "y" | "x":
|
|
124
|
-
if self.axis_type != AxisType.space or
|
|
125
|
-
self.unit, SpaceUnits
|
|
126
|
-
):
|
|
136
|
+
if self.axis_type != AxisType.space or self.unit is None:
|
|
127
137
|
return self.implicit_type_cast(AxisType.space)
|
|
128
138
|
return self
|
|
129
139
|
|
|
@@ -345,6 +355,11 @@ class AxesMapper:
|
|
|
345
355
|
_index_mapping[canonical_key] = None
|
|
346
356
|
return _index_mapping
|
|
347
357
|
|
|
358
|
+
@property
|
|
359
|
+
def axes_setup(self) -> AxesSetup:
|
|
360
|
+
"""Return the axes setup."""
|
|
361
|
+
return self._axes_setup
|
|
362
|
+
|
|
348
363
|
@property
|
|
349
364
|
def on_disk_axes(self) -> list[Axis]:
|
|
350
365
|
return list(self._on_disk_axes)
|
|
@@ -353,6 +368,16 @@ class AxesMapper:
|
|
|
353
368
|
def on_disk_axes_names(self) -> list[str]:
|
|
354
369
|
return [ax.on_disk_name for ax in self._on_disk_axes]
|
|
355
370
|
|
|
371
|
+
@property
|
|
372
|
+
def allow_non_canonical_axes(self) -> bool:
|
|
373
|
+
"""Return if non canonical axes are allowed."""
|
|
374
|
+
return self._allow_non_canonical_axes
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def strict_canonical_order(self) -> bool:
|
|
378
|
+
"""Return if strict canonical order is enforced."""
|
|
379
|
+
return self._strict_canonical_order
|
|
380
|
+
|
|
356
381
|
def get_index(self, name: str) -> int | None:
|
|
357
382
|
"""Get the index of the axis by name."""
|
|
358
383
|
if name not in self._index_mapping.keys():
|
|
@@ -443,8 +468,8 @@ class AxesMapper:
|
|
|
443
468
|
|
|
444
469
|
def canonical_axes(
|
|
445
470
|
axes_names: Collection[str],
|
|
446
|
-
space_units: SpaceUnits | None =
|
|
447
|
-
time_units: TimeUnits | None =
|
|
471
|
+
space_units: SpaceUnits | None = DefaultSpaceUnit,
|
|
472
|
+
time_units: TimeUnits | None = DefaultTimeUnit,
|
|
448
473
|
) -> list[Axis]:
|
|
449
474
|
"""Create a new canonical axes mapper.
|
|
450
475
|
|
|
@@ -6,6 +6,9 @@ from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
|
6
6
|
AxesMapper,
|
|
7
7
|
AxesSetup,
|
|
8
8
|
Axis,
|
|
9
|
+
AxisType,
|
|
10
|
+
DefaultSpaceUnit,
|
|
11
|
+
DefaultTimeUnit,
|
|
9
12
|
SpaceUnits,
|
|
10
13
|
TimeUnits,
|
|
11
14
|
)
|
|
@@ -86,7 +89,7 @@ class Dataset:
|
|
|
86
89
|
return self._path
|
|
87
90
|
|
|
88
91
|
@property
|
|
89
|
-
def space_unit(self) ->
|
|
92
|
+
def space_unit(self) -> str | None:
|
|
90
93
|
"""Return the space unit for a given axis."""
|
|
91
94
|
x_axis = self._axes_mapper.get_axis("x")
|
|
92
95
|
y_axis = self._axes_mapper.get_axis("y")
|
|
@@ -97,8 +100,6 @@ class Dataset:
|
|
|
97
100
|
)
|
|
98
101
|
|
|
99
102
|
if x_axis.unit == y_axis.unit:
|
|
100
|
-
if not isinstance(x_axis.unit, SpaceUnits):
|
|
101
|
-
raise NgioValidationError("The space unit must be of type SpaceUnits.")
|
|
102
103
|
return x_axis.unit
|
|
103
104
|
else:
|
|
104
105
|
raise NgioValidationError(
|
|
@@ -107,13 +108,11 @@ class Dataset:
|
|
|
107
108
|
)
|
|
108
109
|
|
|
109
110
|
@property
|
|
110
|
-
def time_unit(self) ->
|
|
111
|
+
def time_unit(self) -> str | None:
|
|
111
112
|
"""Return the time unit for a given axis."""
|
|
112
113
|
t_axis = self._axes_mapper.get_axis("t")
|
|
113
114
|
if t_axis is None:
|
|
114
115
|
return None
|
|
115
|
-
if not isinstance(t_axis.unit, TimeUnits):
|
|
116
|
-
raise NgioValidationError("The time unit must be of type TimeUnits.")
|
|
117
116
|
return t_axis.unit
|
|
118
117
|
|
|
119
118
|
@property
|
|
@@ -124,11 +123,50 @@ class Dataset:
|
|
|
124
123
|
y=self.get_scale("y"),
|
|
125
124
|
z=self.get_scale("z"),
|
|
126
125
|
t=self.get_scale("t"),
|
|
127
|
-
space_unit=self.space_unit,
|
|
128
|
-
time_unit=self.time_unit,
|
|
126
|
+
space_unit=self.space_unit, # type: ignore
|
|
127
|
+
time_unit=self.time_unit, # type: ignore
|
|
129
128
|
)
|
|
130
129
|
|
|
131
130
|
@property
|
|
132
131
|
def axes_mapper(self) -> AxesMapper:
|
|
133
132
|
"""Return the axes mapper object."""
|
|
134
133
|
return self._axes_mapper
|
|
134
|
+
|
|
135
|
+
def to_units(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
space_unit: SpaceUnits = DefaultSpaceUnit,
|
|
139
|
+
time_unit: TimeUnits = DefaultTimeUnit,
|
|
140
|
+
) -> "Dataset":
|
|
141
|
+
"""Convert the pixel size to the given units.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
space_unit(str): The space unit to convert to.
|
|
145
|
+
time_unit(str): The time unit to convert to.
|
|
146
|
+
"""
|
|
147
|
+
new_axes = []
|
|
148
|
+
for ax in self.axes_mapper.on_disk_axes:
|
|
149
|
+
if ax.axis_type == AxisType.space:
|
|
150
|
+
new_ax = Axis(
|
|
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)
|
|
163
|
+
|
|
164
|
+
return Dataset(
|
|
165
|
+
path=self.path,
|
|
166
|
+
on_disk_axes=new_axes,
|
|
167
|
+
on_disk_scale=self._on_disk_scale,
|
|
168
|
+
on_disk_translation=self._on_disk_translation,
|
|
169
|
+
axes_setup=self.axes_mapper.axes_setup,
|
|
170
|
+
allow_non_canonical_axes=self.axes_mapper.allow_non_canonical_axes,
|
|
171
|
+
strict_canonical_order=self.axes_mapper.strict_canonical_order,
|
|
172
|
+
)
|