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
|
@@ -6,71 +6,144 @@ But they can be built from the OME standard metadata, and the
|
|
|
6
6
|
can be converted to the OME standard.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from collections.abc import
|
|
10
|
-
from
|
|
11
|
-
from typing import Any, TypeVar
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from typing import Any, Literal, TypeVar
|
|
12
11
|
|
|
13
12
|
import numpy as np
|
|
14
13
|
from pydantic import BaseModel
|
|
15
14
|
|
|
16
15
|
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
16
|
+
AxesHandler,
|
|
17
|
+
DefaultSpaceUnit,
|
|
18
|
+
DefaultTimeUnit,
|
|
17
19
|
SpaceUnits,
|
|
18
20
|
TimeUnits,
|
|
19
|
-
canonical_axes,
|
|
20
21
|
)
|
|
21
|
-
from ngio.ome_zarr_meta.ngio_specs._channels import
|
|
22
|
+
from ngio.ome_zarr_meta.ngio_specs._channels import ChannelsMeta
|
|
22
23
|
from ngio.ome_zarr_meta.ngio_specs._dataset import Dataset
|
|
23
24
|
from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
24
25
|
from ngio.utils import NgioValidationError, NgioValueError
|
|
25
26
|
|
|
26
27
|
T = TypeVar("T")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class NgffVersion(str, Enum):
|
|
30
|
-
"""Allowed NGFF versions."""
|
|
31
|
-
|
|
32
|
-
v04 = "0.4"
|
|
28
|
+
NgffVersions = Literal["0.4", "0.5"]
|
|
29
|
+
DefaultNgffVersion: Literal["0.4"] = "0.4"
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
class ImageLabelSource(BaseModel):
|
|
36
33
|
"""Image label source model."""
|
|
37
34
|
|
|
38
|
-
version:
|
|
35
|
+
version: NgffVersions
|
|
39
36
|
source: dict[str, str | None]
|
|
40
37
|
|
|
41
38
|
@classmethod
|
|
42
|
-
def default_init(cls, version:
|
|
39
|
+
def default_init(cls, version: NgffVersions) -> "ImageLabelSource":
|
|
43
40
|
"""Initialize the ImageLabelSource object."""
|
|
44
41
|
return cls(version=version, source={"image": "../../"})
|
|
45
42
|
|
|
46
43
|
|
|
44
|
+
class NgioLabelsGroupMeta(BaseModel):
|
|
45
|
+
"""Metadata model for the /labels group in OME-NGFF."""
|
|
46
|
+
|
|
47
|
+
version: NgffVersions
|
|
48
|
+
labels: list[str]
|
|
49
|
+
|
|
50
|
+
|
|
47
51
|
class AbstractNgioImageMeta:
|
|
48
52
|
"""Base class for ImageMeta and LabelMeta."""
|
|
49
53
|
|
|
50
|
-
def __init__(
|
|
54
|
+
def __init__(
|
|
55
|
+
self, version: NgffVersions, name: str | None, datasets: list[Dataset]
|
|
56
|
+
) -> None:
|
|
51
57
|
"""Initialize the ImageMeta object."""
|
|
52
|
-
self._version =
|
|
58
|
+
self._version = version
|
|
53
59
|
self._name = name
|
|
54
60
|
|
|
55
61
|
if len(datasets) == 0:
|
|
56
62
|
raise NgioValidationError("At least one dataset must be provided.")
|
|
57
63
|
|
|
58
64
|
self._datasets = datasets
|
|
65
|
+
self._axes_handler = datasets[0].axes_handler
|
|
59
66
|
|
|
60
67
|
def __repr__(self):
|
|
61
68
|
class_name = type(self).__name__
|
|
62
69
|
paths = [dataset.path for dataset in self.datasets]
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
axes = self.axes_handler.axes_names
|
|
71
|
+
return f"{class_name}(name={self.name}, datasets={paths}, axes={axes})"
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def default_init(
|
|
75
|
+
cls,
|
|
76
|
+
levels: Sequence[str],
|
|
77
|
+
axes_handler: AxesHandler,
|
|
78
|
+
scales: Sequence[tuple[float, ...]],
|
|
79
|
+
translations: Sequence[tuple[float, ...] | None],
|
|
80
|
+
name: str | None = None,
|
|
81
|
+
version: NgffVersions = DefaultNgffVersion,
|
|
82
|
+
):
|
|
83
|
+
"""Initialize the ImageMeta object."""
|
|
84
|
+
datasets = []
|
|
85
|
+
for level, scale, translation in zip(levels, scales, translations, strict=True):
|
|
86
|
+
dataset = Dataset(
|
|
87
|
+
path=level,
|
|
88
|
+
axes_handler=axes_handler,
|
|
89
|
+
scale=scale,
|
|
90
|
+
translation=translation,
|
|
91
|
+
)
|
|
92
|
+
datasets.append(dataset)
|
|
93
|
+
|
|
94
|
+
return cls(
|
|
95
|
+
version=version,
|
|
96
|
+
name=name,
|
|
97
|
+
datasets=datasets,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_units(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
space_unit: SpaceUnits = DefaultSpaceUnit,
|
|
104
|
+
time_unit: TimeUnits = DefaultTimeUnit,
|
|
105
|
+
):
|
|
106
|
+
"""Convert the pixel size to the given units.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
space_unit(str): The space unit to convert to.
|
|
110
|
+
time_unit(str): The time unit to convert to.
|
|
111
|
+
"""
|
|
112
|
+
new_axes_handler = self.axes_handler.to_units(
|
|
113
|
+
space_unit=space_unit,
|
|
114
|
+
time_unit=time_unit,
|
|
115
|
+
)
|
|
116
|
+
new_datasets = []
|
|
117
|
+
for dataset in self.datasets:
|
|
118
|
+
new_dataset = Dataset(
|
|
119
|
+
path=dataset.path,
|
|
120
|
+
axes_handler=new_axes_handler,
|
|
121
|
+
scale=dataset.scale,
|
|
122
|
+
translation=dataset.translation,
|
|
123
|
+
)
|
|
124
|
+
new_datasets.append(new_dataset)
|
|
125
|
+
|
|
126
|
+
return type(self)(
|
|
127
|
+
version=self.version,
|
|
128
|
+
name=self.name,
|
|
129
|
+
datasets=new_datasets,
|
|
68
130
|
)
|
|
69
131
|
|
|
70
132
|
@property
|
|
71
|
-
def version(self) ->
|
|
133
|
+
def version(self) -> NgffVersions:
|
|
72
134
|
"""Version of the OME-NFF metadata used to build the object."""
|
|
73
|
-
return self._version
|
|
135
|
+
return self._version # type: ignore (version is a Literal type)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def zarr_format(self) -> Literal[2, 3]:
|
|
139
|
+
"""Zarr version used to store the data."""
|
|
140
|
+
match self.version:
|
|
141
|
+
case "0.4":
|
|
142
|
+
return 2
|
|
143
|
+
case "0.5":
|
|
144
|
+
return 3
|
|
145
|
+
case _:
|
|
146
|
+
raise NgioValueError(f"Unsupported NGFF version: {self.version}")
|
|
74
147
|
|
|
75
148
|
@property
|
|
76
149
|
def name(self) -> str | None:
|
|
@@ -82,6 +155,11 @@ class AbstractNgioImageMeta:
|
|
|
82
155
|
"""List of datasets in the multiscale."""
|
|
83
156
|
return self._datasets
|
|
84
157
|
|
|
158
|
+
@property
|
|
159
|
+
def axes_handler(self):
|
|
160
|
+
"""Return the axes mapper."""
|
|
161
|
+
return self._axes_handler
|
|
162
|
+
|
|
85
163
|
@property
|
|
86
164
|
def levels(self) -> int:
|
|
87
165
|
"""Number of levels in the multiscale."""
|
|
@@ -92,6 +170,16 @@ class AbstractNgioImageMeta:
|
|
|
92
170
|
"""List of paths of the datasets."""
|
|
93
171
|
return [dataset.path for dataset in self.datasets]
|
|
94
172
|
|
|
173
|
+
@property
|
|
174
|
+
def space_unit(self) -> str | None:
|
|
175
|
+
"""Get the space unit of the pixel size."""
|
|
176
|
+
return self.axes_handler.space_unit
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def time_unit(self) -> str | None:
|
|
180
|
+
"""Get the time unit of the pixel size."""
|
|
181
|
+
return self.axes_handler.time_unit
|
|
182
|
+
|
|
95
183
|
def _get_dataset_by_path(self, path: str) -> Dataset:
|
|
96
184
|
"""Get a dataset by its path."""
|
|
97
185
|
for dataset in self.datasets:
|
|
@@ -105,49 +193,102 @@ class AbstractNgioImageMeta:
|
|
|
105
193
|
raise NgioValueError(f"Index {idx} out of range.")
|
|
106
194
|
return self.datasets[idx]
|
|
107
195
|
|
|
108
|
-
def
|
|
109
|
-
self, pixel_size: PixelSize,
|
|
110
|
-
) -> Dataset:
|
|
111
|
-
"""
|
|
196
|
+
def _find_closest_dataset(
|
|
197
|
+
self, pixel_size: PixelSize, mode: str = "any"
|
|
198
|
+
) -> Dataset | None:
|
|
199
|
+
"""Find the closest dataset to the given pixel size.
|
|
112
200
|
|
|
113
201
|
Args:
|
|
114
202
|
pixel_size(PixelSize): The pixel size to search for.
|
|
115
|
-
|
|
116
|
-
|
|
203
|
+
mode(str): The mode to find the closest dataset.
|
|
204
|
+
"any": Will find the closest dataset.
|
|
205
|
+
"lr": Will find closest "lower" resolution dataset.
|
|
206
|
+
"hr": Will find closest "higher" resolution
|
|
117
207
|
"""
|
|
118
208
|
min_dist = np.inf
|
|
119
|
-
|
|
120
209
|
closest_dataset = None
|
|
121
|
-
|
|
122
|
-
|
|
210
|
+
|
|
211
|
+
if mode == "any":
|
|
212
|
+
datasets = self.datasets
|
|
213
|
+
elif mode == "lr":
|
|
214
|
+
# Lower resolution means that the pixel size is larger.
|
|
215
|
+
datasets = [d for d in self.datasets if d.pixel_size > pixel_size]
|
|
216
|
+
elif mode == "hr":
|
|
217
|
+
# Higher resolution means that the pixel size is smaller.
|
|
218
|
+
datasets = [d for d in self.datasets if d.pixel_size < pixel_size]
|
|
219
|
+
else:
|
|
220
|
+
raise NgioValueError(f"Mode {mode} not recognized.")
|
|
221
|
+
|
|
222
|
+
for d in datasets:
|
|
223
|
+
dist = d.pixel_size.distance(pixel_size)
|
|
123
224
|
if dist < min_dist:
|
|
124
225
|
min_dist = dist
|
|
125
|
-
closest_dataset =
|
|
226
|
+
closest_dataset = d
|
|
227
|
+
|
|
228
|
+
return closest_dataset
|
|
229
|
+
|
|
230
|
+
def _get_closest_dataset(
|
|
231
|
+
self, pixel_size: PixelSize, strict: bool = False
|
|
232
|
+
) -> Dataset:
|
|
233
|
+
"""Get a dataset with the closest pixel size.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
pixel_size(PixelSize): The pixel size to search for.
|
|
237
|
+
strict(bool): If True, the pixel size must be exactly the same.
|
|
238
|
+
"""
|
|
239
|
+
closest_dataset = self._find_closest_dataset(pixel_size, mode="any")
|
|
126
240
|
|
|
127
241
|
if closest_dataset is None:
|
|
128
242
|
raise NgioValueError("No dataset found.")
|
|
129
243
|
|
|
130
|
-
if strict and
|
|
131
|
-
raise NgioValueError(
|
|
132
|
-
|
|
244
|
+
if strict and closest_dataset.pixel_size != pixel_size:
|
|
245
|
+
raise NgioValueError(
|
|
246
|
+
"No dataset with a pixel size close enough. "
|
|
247
|
+
"Best match is "
|
|
248
|
+
f"{closest_dataset.path}:{closest_dataset.pixel_size}"
|
|
249
|
+
)
|
|
133
250
|
return closest_dataset
|
|
134
251
|
|
|
252
|
+
def get_lowest_resolution_dataset(self) -> Dataset:
|
|
253
|
+
"""Get the dataset with the lowest resolution."""
|
|
254
|
+
dataset = self.datasets[-1]
|
|
255
|
+
while True:
|
|
256
|
+
lower_res_dataset = self._find_closest_dataset(
|
|
257
|
+
dataset.pixel_size, mode="lr"
|
|
258
|
+
)
|
|
259
|
+
if lower_res_dataset is None:
|
|
260
|
+
break
|
|
261
|
+
dataset = lower_res_dataset
|
|
262
|
+
return dataset
|
|
263
|
+
|
|
264
|
+
def get_highest_resolution_dataset(self) -> Dataset:
|
|
265
|
+
"""Get the dataset with the highest resolution."""
|
|
266
|
+
dataset = self.datasets[0]
|
|
267
|
+
while True:
|
|
268
|
+
higher_res_dataset = self._find_closest_dataset(
|
|
269
|
+
dataset.pixel_size, mode="hr"
|
|
270
|
+
)
|
|
271
|
+
if higher_res_dataset is None:
|
|
272
|
+
break
|
|
273
|
+
dataset = higher_res_dataset
|
|
274
|
+
return dataset
|
|
275
|
+
|
|
135
276
|
def get_dataset(
|
|
136
277
|
self,
|
|
137
278
|
*,
|
|
138
279
|
path: str | None = None,
|
|
139
280
|
idx: int | None = None,
|
|
140
281
|
pixel_size: PixelSize | None = None,
|
|
141
|
-
highest_resolution: bool = False,
|
|
142
282
|
strict: bool = False,
|
|
143
283
|
) -> Dataset:
|
|
144
284
|
"""Get a dataset by its path, index or pixel size.
|
|
145
285
|
|
|
286
|
+
If all arguments are None, the dataset with the highest resolution is returned.
|
|
287
|
+
|
|
146
288
|
Args:
|
|
147
289
|
path(str): The path of the dataset.
|
|
148
290
|
idx(int): The index of the dataset.
|
|
149
291
|
pixel_size(PixelSize): The pixel size to search for.
|
|
150
|
-
highest_resolution(bool): If True, the dataset with the highest resolution
|
|
151
292
|
strict(bool): If True, the pixel size must be exactly the same.
|
|
152
293
|
If pixel_size is None, strict is ignored.
|
|
153
294
|
"""
|
|
@@ -158,115 +299,41 @@ class AbstractNgioImageMeta:
|
|
|
158
299
|
path is not None,
|
|
159
300
|
idx is not None,
|
|
160
301
|
pixel_size is not None,
|
|
161
|
-
highest_resolution,
|
|
162
302
|
]
|
|
163
303
|
)
|
|
164
|
-
|
|
304
|
+
> 1
|
|
165
305
|
):
|
|
166
|
-
raise NgioValueError("get_dataset must receive only one argument.")
|
|
306
|
+
raise NgioValueError("get_dataset must receive only one argument or None.")
|
|
167
307
|
|
|
168
308
|
if path is not None:
|
|
169
309
|
return self._get_dataset_by_path(path)
|
|
170
310
|
elif idx is not None:
|
|
171
311
|
return self._get_dataset_by_index(idx)
|
|
172
312
|
elif pixel_size is not None:
|
|
173
|
-
return self.
|
|
174
|
-
elif highest_resolution:
|
|
175
|
-
return self.get_highest_resolution_dataset()
|
|
313
|
+
return self._get_closest_dataset(pixel_size, strict=strict)
|
|
176
314
|
else:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
@classmethod
|
|
180
|
-
def default_init(
|
|
181
|
-
cls,
|
|
182
|
-
levels: int | Collection[str],
|
|
183
|
-
axes_names: Collection[str],
|
|
184
|
-
pixel_size: PixelSize,
|
|
185
|
-
scaling_factors: Collection[float] | None = None,
|
|
186
|
-
name: str | None = None,
|
|
187
|
-
version: str = "0.4",
|
|
188
|
-
):
|
|
189
|
-
"""Initialize the ImageMeta object."""
|
|
190
|
-
axes = canonical_axes(
|
|
191
|
-
axes_names,
|
|
192
|
-
space_units=pixel_size.space_unit,
|
|
193
|
-
time_units=pixel_size.time_unit,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
px_size_dict = pixel_size.as_dict()
|
|
197
|
-
scale = [px_size_dict.get(ax.on_disk_name, 1.0) for ax in axes]
|
|
198
|
-
translation = [0.0] * len(scale)
|
|
199
|
-
|
|
200
|
-
if scaling_factors is None:
|
|
201
|
-
_default = {"x": 2.0, "y": 2.0}
|
|
202
|
-
scaling_factors = [_default.get(ax.on_disk_name, 1.0) for ax in axes]
|
|
203
|
-
|
|
204
|
-
if isinstance(levels, int):
|
|
205
|
-
levels = [str(i) for i in range(levels)]
|
|
206
|
-
|
|
207
|
-
datasets = []
|
|
208
|
-
for level in levels:
|
|
209
|
-
dataset = Dataset(
|
|
210
|
-
path=level,
|
|
211
|
-
on_disk_axes=axes,
|
|
212
|
-
on_disk_scale=scale,
|
|
213
|
-
on_disk_translation=translation,
|
|
214
|
-
allow_non_canonical_axes=False,
|
|
215
|
-
strict_canonical_order=True,
|
|
216
|
-
)
|
|
217
|
-
datasets.append(dataset)
|
|
218
|
-
scale = [s * f for s, f in zip(scale, scaling_factors, strict=True)]
|
|
219
|
-
|
|
220
|
-
return cls(
|
|
221
|
-
version=version,
|
|
222
|
-
name=name,
|
|
223
|
-
datasets=datasets,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
def get_highest_resolution_dataset(self) -> Dataset:
|
|
227
|
-
"""Get the dataset with the highest resolution."""
|
|
228
|
-
return self._get_dataset_by_pixel_size(
|
|
229
|
-
pixel_size=PixelSize(
|
|
230
|
-
x=0.0,
|
|
231
|
-
y=0.0,
|
|
232
|
-
z=0.0,
|
|
233
|
-
t=0.0,
|
|
234
|
-
space_unit=SpaceUnits.micrometer,
|
|
235
|
-
time_unit=TimeUnits.s,
|
|
236
|
-
),
|
|
237
|
-
strict=False,
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
def get_scaling_factor(self, axis_name: str) -> float:
|
|
241
|
-
"""Get the scaling factors of the dataset."""
|
|
242
|
-
scaling_factors = []
|
|
243
|
-
for d1, d2 in zip(self.datasets[1:], self.datasets[:-1], strict=True):
|
|
244
|
-
scale_d1 = d1.get_scale(axis_name)
|
|
245
|
-
scale_d2 = d2.get_scale(axis_name)
|
|
246
|
-
scaling_factors.append(scale_d1 / scale_d2)
|
|
247
|
-
|
|
248
|
-
if not np.allclose(scaling_factors, scaling_factors[0]):
|
|
249
|
-
raise NgioValidationError(
|
|
250
|
-
f"Inconsistent scaling factors are not supported. {scaling_factors}"
|
|
251
|
-
)
|
|
252
|
-
return scaling_factors[0]
|
|
315
|
+
return self.get_highest_resolution_dataset()
|
|
253
316
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
f"{x_scaling_factors}, {y_scaling_factors}"
|
|
317
|
+
def _get_closest_datasets(self, path: str | None = None) -> tuple[Dataset, Dataset]:
|
|
318
|
+
"""Get the closest datasets to a dataset."""
|
|
319
|
+
dataset = self.get_dataset(path=path)
|
|
320
|
+
lr_dataset = self._find_closest_dataset(dataset.pixel_size, mode="lr")
|
|
321
|
+
if lr_dataset is None:
|
|
322
|
+
raise NgioValueError(
|
|
323
|
+
"No lower resolution dataset found. "
|
|
324
|
+
"This is the lowest resolution dataset."
|
|
263
325
|
)
|
|
264
|
-
return
|
|
326
|
+
return dataset, lr_dataset
|
|
265
327
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
328
|
+
def scaling_factor(self, path: str | None = None) -> tuple[float, ...]:
|
|
329
|
+
"""Get the scaling factors to downscale to the next lower resolution dataset."""
|
|
330
|
+
if self.levels == 1:
|
|
331
|
+
return (1.0,) * len(self.axes_handler.axes_names)
|
|
332
|
+
dataset, lr_dataset = self._get_closest_datasets(path=path)
|
|
333
|
+
scale = dataset.scale
|
|
334
|
+
lr_scale = lr_dataset.scale
|
|
335
|
+
scaling_factors = [s / s_lr for s_lr, s in zip(scale, lr_scale, strict=True)]
|
|
336
|
+
return tuple(scaling_factors)
|
|
270
337
|
|
|
271
338
|
|
|
272
339
|
class NgioLabelMeta(AbstractNgioImageMeta):
|
|
@@ -274,19 +341,13 @@ class NgioLabelMeta(AbstractNgioImageMeta):
|
|
|
274
341
|
|
|
275
342
|
def __init__(
|
|
276
343
|
self,
|
|
277
|
-
version:
|
|
344
|
+
version: NgffVersions,
|
|
278
345
|
name: str | None,
|
|
279
346
|
datasets: list[Dataset],
|
|
280
347
|
image_label: ImageLabelSource | None = None,
|
|
281
348
|
) -> None:
|
|
282
349
|
"""Initialize the ImageMeta object."""
|
|
283
350
|
super().__init__(version, name, datasets)
|
|
284
|
-
|
|
285
|
-
# Make sure that there are no channel axes
|
|
286
|
-
channel_axis = self.datasets[0].axes_mapper.get_axis("c")
|
|
287
|
-
if channel_axis is not None:
|
|
288
|
-
raise NgioValidationError("Label metadata must not have channel axes.")
|
|
289
|
-
|
|
290
351
|
image_label = (
|
|
291
352
|
ImageLabelSource.default_init(self.version)
|
|
292
353
|
if image_label is None
|
|
@@ -319,7 +380,7 @@ class NgioImageMeta(AbstractNgioImageMeta):
|
|
|
319
380
|
|
|
320
381
|
def __init__(
|
|
321
382
|
self,
|
|
322
|
-
version:
|
|
383
|
+
version: NgffVersions,
|
|
323
384
|
name: str | None,
|
|
324
385
|
datasets: list[Dataset],
|
|
325
386
|
channels: ChannelsMeta | None = None,
|
|
@@ -373,62 +434,5 @@ class NgioImageMeta(AbstractNgioImageMeta):
|
|
|
373
434
|
)
|
|
374
435
|
self.set_channels_meta(channels_meta=channels_meta)
|
|
375
436
|
|
|
376
|
-
@property
|
|
377
|
-
def channels(self) -> list[Channel]:
|
|
378
|
-
"""Get the channels in the image."""
|
|
379
|
-
if self._channels_meta is None:
|
|
380
|
-
return []
|
|
381
|
-
assert self.channels_meta is not None
|
|
382
|
-
return self.channels_meta.channels
|
|
383
|
-
|
|
384
|
-
@property
|
|
385
|
-
def channel_labels(self) -> list[str]:
|
|
386
|
-
"""Get the labels of the channels in the image."""
|
|
387
|
-
return [channel.label for channel in self.channels]
|
|
388
|
-
|
|
389
|
-
@property
|
|
390
|
-
def channel_wavelength_ids(self) -> list[str | None]:
|
|
391
|
-
"""Get the wavelength IDs of the channels in the image."""
|
|
392
|
-
return [channel.wavelength_id for channel in self.channels]
|
|
393
|
-
|
|
394
|
-
def _get_channel_idx_by_label(self, label: str) -> int | None:
|
|
395
|
-
"""Get the index of a channel by its label."""
|
|
396
|
-
if self._channels_meta is None:
|
|
397
|
-
return None
|
|
398
|
-
|
|
399
|
-
if label not in self.channel_labels:
|
|
400
|
-
raise NgioValueError(f"Channel with label {label} not found.")
|
|
401
|
-
|
|
402
|
-
return self.channel_labels.index(label)
|
|
403
|
-
|
|
404
|
-
def _get_channel_idx_by_wavelength_id(self, wavelength_id: str) -> int | None:
|
|
405
|
-
"""Get the index of a channel by its wavelength ID."""
|
|
406
|
-
if self._channels_meta is None:
|
|
407
|
-
return None
|
|
408
|
-
|
|
409
|
-
if wavelength_id not in self.channel_wavelength_ids:
|
|
410
|
-
raise NgioValueError(
|
|
411
|
-
f"Channel with wavelength ID {wavelength_id} not found."
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
return self.channel_wavelength_ids.index(wavelength_id)
|
|
415
|
-
|
|
416
|
-
def get_channel_idx(
|
|
417
|
-
self, label: str | None = None, wavelength_id: str | None = None
|
|
418
|
-
) -> int | None:
|
|
419
|
-
"""Get the index of a channel by its label or wavelength ID."""
|
|
420
|
-
# Only one of the arguments must be provided
|
|
421
|
-
if sum([label is not None, wavelength_id is not None]) != 1:
|
|
422
|
-
raise NgioValueError("get_channel_idx must receive only one argument.")
|
|
423
|
-
|
|
424
|
-
if label is not None:
|
|
425
|
-
return self._get_channel_idx_by_label(label)
|
|
426
|
-
elif wavelength_id is not None:
|
|
427
|
-
return self._get_channel_idx_by_wavelength_id(wavelength_id)
|
|
428
|
-
else:
|
|
429
|
-
raise NgioValueError(
|
|
430
|
-
"get_channel_idx must receive either label or wavelength_id."
|
|
431
|
-
)
|
|
432
|
-
|
|
433
437
|
|
|
434
438
|
NgioImageLabelMeta = NgioImageMeta | NgioLabelMeta
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
"""Fractal internal module for dataset metadata handling."""
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
from functools import total_ordering
|
|
5
|
+
from typing import overload
|
|
6
|
+
|
|
3
7
|
import numpy as np
|
|
8
|
+
from pydantic import BaseModel
|
|
4
9
|
|
|
5
|
-
from ngio.ome_zarr_meta.ngio_specs import
|
|
10
|
+
from ngio.ome_zarr_meta.ngio_specs import (
|
|
11
|
+
DefaultSpaceUnit,
|
|
12
|
+
DefaultTimeUnit,
|
|
13
|
+
SpaceUnits,
|
|
14
|
+
TimeUnits,
|
|
15
|
+
)
|
|
6
16
|
|
|
7
17
|
################################################################################################
|
|
8
18
|
#
|
|
@@ -13,47 +23,75 @@ from ngio.ome_zarr_meta.ngio_specs import SpaceUnits, TimeUnits
|
|
|
13
23
|
#################################################################################################
|
|
14
24
|
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if not isinstance(value, int | float):
|
|
19
|
-
raise TypeError(f"{name} must be a number.")
|
|
20
|
-
return float(value)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class PixelSize:
|
|
26
|
+
@total_ordering
|
|
27
|
+
class PixelSize(BaseModel):
|
|
24
28
|
"""PixelSize class to store the pixel size in 3D space."""
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
space_unit: SpaceUnits = SpaceUnits.micrometer,
|
|
33
|
-
time_unit: TimeUnits | None = TimeUnits.s,
|
|
34
|
-
):
|
|
35
|
-
"""Initialize the pixel size."""
|
|
36
|
-
self.x = _validate_type(x, "x")
|
|
37
|
-
self.y = _validate_type(y, "y")
|
|
38
|
-
self.z = _validate_type(z, "z")
|
|
39
|
-
self.t = _validate_type(t, "t")
|
|
40
|
-
|
|
41
|
-
if not isinstance(space_unit, SpaceUnits):
|
|
42
|
-
raise TypeError("space_unit must be of type SpaceUnits.")
|
|
43
|
-
self.space_unit = space_unit
|
|
44
|
-
|
|
45
|
-
if time_unit is not None and not isinstance(time_unit, TimeUnits):
|
|
46
|
-
raise TypeError("time_unit must be of type TimeUnits.")
|
|
47
|
-
self.time_unit = time_unit
|
|
30
|
+
x: float
|
|
31
|
+
y: float
|
|
32
|
+
z: float
|
|
33
|
+
t: float = 1
|
|
34
|
+
space_unit: SpaceUnits | str | None = DefaultSpaceUnit
|
|
35
|
+
time_unit: TimeUnits | str | None = DefaultTimeUnit
|
|
48
36
|
|
|
49
37
|
def __repr__(self) -> str:
|
|
50
38
|
"""Return a string representation of the pixel size."""
|
|
51
39
|
return f"PixelSize(x={self.x}, y={self.y}, z={self.z}, t={self.t})"
|
|
52
40
|
|
|
53
|
-
def
|
|
41
|
+
def __eq__(self, other) -> bool:
|
|
42
|
+
"""Check if two pixel sizes are equal."""
|
|
43
|
+
if not isinstance(other, PixelSize):
|
|
44
|
+
raise TypeError("Can only compare PixelSize with PixelSize.")
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
self.time_unit is not None
|
|
48
|
+
and other.time_unit is None
|
|
49
|
+
and self.time_unit != other.time_unit
|
|
50
|
+
):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
if self.space_unit != other.space_unit:
|
|
54
|
+
return False
|
|
55
|
+
return math.isclose(self.distance(other), 0)
|
|
56
|
+
|
|
57
|
+
def __lt__(self, other: "PixelSize") -> bool:
|
|
58
|
+
"""Check if one pixel size is less than the other."""
|
|
59
|
+
if not isinstance(other, PixelSize):
|
|
60
|
+
raise TypeError("Can only compare PixelSize with PixelSize.")
|
|
61
|
+
ref = PixelSize(
|
|
62
|
+
x=0,
|
|
63
|
+
y=0,
|
|
64
|
+
z=0,
|
|
65
|
+
t=0,
|
|
66
|
+
space_unit=self.space_unit,
|
|
67
|
+
time_unit=self.time_unit, # type: ignore
|
|
68
|
+
)
|
|
69
|
+
return self.distance(ref) < other.distance(ref)
|
|
70
|
+
|
|
71
|
+
def as_dict(self) -> dict[str, float]:
|
|
54
72
|
"""Return the pixel size as a dictionary."""
|
|
55
73
|
return {"t": self.t, "z": self.z, "y": self.y, "x": self.x}
|
|
56
74
|
|
|
75
|
+
@overload
|
|
76
|
+
def get(self, axis: str, default: float) -> float: ...
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
def get(self, axis: str, default: None = None) -> float | None: ...
|
|
80
|
+
|
|
81
|
+
def get(self, axis: str, default: float | None = None) -> float | None:
|
|
82
|
+
"""Get the pixel size for a given axis (in canonical name)."""
|
|
83
|
+
px_size = self.as_dict().get(axis, default)
|
|
84
|
+
if px_size is None:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Invalid axis name: {axis}, must be one of 'x', 'y', 'z', 't'."
|
|
87
|
+
)
|
|
88
|
+
return px_size
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def tzyx(self) -> tuple[float, float, float, float]:
|
|
92
|
+
"""Return the voxel size in t, z, y, x order."""
|
|
93
|
+
return self.t, self.z, self.y, self.x
|
|
94
|
+
|
|
57
95
|
@property
|
|
58
96
|
def zyx(self) -> tuple[float, float, float]:
|
|
59
97
|
"""Return the voxel size in z, y, x order."""
|
|
@@ -80,5 +118,5 @@ class PixelSize:
|
|
|
80
118
|
return self.t
|
|
81
119
|
|
|
82
120
|
def distance(self, other: "PixelSize") -> float:
|
|
83
|
-
"""Return the distance between two pixel sizes
|
|
84
|
-
return float(np.linalg.norm(np.array(self.
|
|
121
|
+
"""Return the distance between two pixel sizes."""
|
|
122
|
+
return float(np.linalg.norm(np.array(self.tzyx) - np.array(other.tzyx)))
|