ngio 0.1.6__py3-none-any.whl → 0.2.0a1__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 +31 -5
- ngio/common/__init__.py +44 -0
- ngio/common/_array_pipe.py +160 -0
- ngio/common/_axes_transforms.py +63 -0
- ngio/common/_common_types.py +5 -0
- ngio/common/_dimensions.py +113 -0
- ngio/common/_pyramid.py +222 -0
- ngio/{core/roi.py → common/_roi.py} +22 -23
- ngio/common/_slicer.py +97 -0
- ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
- ngio/hcs/__init__.py +60 -0
- ngio/images/__init__.py +23 -0
- ngio/images/abstract_image.py +240 -0
- ngio/images/create.py +251 -0
- ngio/images/image.py +383 -0
- ngio/images/label.py +96 -0
- ngio/images/omezarr_container.py +512 -0
- ngio/ome_zarr_meta/__init__.py +35 -0
- ngio/ome_zarr_meta/_generic_handlers.py +320 -0
- ngio/ome_zarr_meta/_meta_handlers.py +142 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
- ngio/ome_zarr_meta/v04/__init__.py +11 -0
- ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
- ngio/tables/__init__.py +21 -5
- ngio/tables/_validators.py +192 -0
- ngio/tables/backends/__init__.py +8 -0
- ngio/tables/backends/_abstract_backend.py +71 -0
- ngio/tables/backends/_anndata_utils.py +194 -0
- ngio/tables/backends/_anndata_v1.py +75 -0
- ngio/tables/backends/_json_v1.py +56 -0
- ngio/tables/backends/_table_backends.py +102 -0
- ngio/tables/tables_container.py +300 -0
- ngio/tables/v1/__init__.py +6 -5
- ngio/tables/v1/_feature_table.py +161 -0
- ngio/tables/v1/_generic_table.py +99 -182
- ngio/tables/v1/_masking_roi_table.py +175 -0
- ngio/tables/v1/_roi_table.py +226 -0
- ngio/utils/__init__.py +23 -10
- ngio/utils/_datasets.py +51 -0
- ngio/utils/_errors.py +10 -4
- ngio/utils/_zarr_utils.py +378 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/METADATA +18 -39
- ngio-0.2.0a1.dist-info/RECORD +53 -0
- ngio/core/__init__.py +0 -7
- ngio/core/dimensions.py +0 -122
- ngio/core/image_handler.py +0 -228
- ngio/core/image_like_handler.py +0 -549
- ngio/core/label_handler.py +0 -410
- ngio/core/ngff_image.py +0 -387
- ngio/core/utils.py +0 -287
- ngio/io/__init__.py +0 -19
- ngio/io/_zarr.py +0 -88
- ngio/io/_zarr_array_utils.py +0 -0
- ngio/io/_zarr_group_utils.py +0 -60
- ngio/iterators/__init__.py +0 -1
- ngio/ngff_meta/__init__.py +0 -27
- ngio/ngff_meta/fractal_image_meta.py +0 -1267
- ngio/ngff_meta/meta_handler.py +0 -92
- ngio/ngff_meta/utils.py +0 -235
- ngio/ngff_meta/v04/__init__.py +0 -6
- ngio/ngff_meta/v04/specs.py +0 -158
- ngio/ngff_meta/v04/zarr_utils.py +0 -376
- ngio/pipes/__init__.py +0 -7
- ngio/pipes/_slicer_transforms.py +0 -176
- ngio/pipes/_transforms.py +0 -33
- ngio/pipes/data_pipe.py +0 -52
- ngio/tables/_ad_reader.py +0 -80
- ngio/tables/_utils.py +0 -301
- ngio/tables/tables_group.py +0 -252
- ngio/tables/v1/feature_tables.py +0 -182
- ngio/tables/v1/masking_roi_tables.py +0 -243
- ngio/tables/v1/roi_tables.py +0 -285
- ngio/utils/_common_types.py +0 -5
- ngio/utils/_pydantic_utils.py +0 -52
- ngio-0.1.6.dist-info/RECORD +0 -44
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/WHEEL +0 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Image metadata models.
|
|
2
|
+
|
|
3
|
+
This module contains the models for the image metadata.
|
|
4
|
+
These metadata models are not adhering to the OME standard.
|
|
5
|
+
But they can be built from the OME standard metadata, and the
|
|
6
|
+
can be converted to the OME standard.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Collection
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, TypeVar
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
17
|
+
SpaceUnits,
|
|
18
|
+
TimeUnits,
|
|
19
|
+
canonical_axes,
|
|
20
|
+
)
|
|
21
|
+
from ngio.ome_zarr_meta.ngio_specs._channels import Channel, ChannelsMeta
|
|
22
|
+
from ngio.ome_zarr_meta.ngio_specs._dataset import Dataset
|
|
23
|
+
from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
24
|
+
from ngio.utils import NgioValidationError, NgioValueError
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class NgffVersion(str, Enum):
|
|
30
|
+
"""Allowed NGFF versions."""
|
|
31
|
+
|
|
32
|
+
v04 = "0.4"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ImageLabelSource(BaseModel):
|
|
36
|
+
"""Image label source model."""
|
|
37
|
+
|
|
38
|
+
version: NgffVersion
|
|
39
|
+
source: dict[str, str | None]
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def default_init(cls, version: NgffVersion) -> "ImageLabelSource":
|
|
43
|
+
"""Initialize the ImageLabelSource object."""
|
|
44
|
+
return cls(version=version, source={"image": "../../"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AbstractNgioImageMeta:
|
|
48
|
+
"""Base class for ImageMeta and LabelMeta."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, version: str, name: str | None, datasets: list[Dataset]) -> None:
|
|
51
|
+
"""Initialize the ImageMeta object."""
|
|
52
|
+
self._version = NgffVersion(version)
|
|
53
|
+
self._name = name
|
|
54
|
+
|
|
55
|
+
if len(datasets) == 0:
|
|
56
|
+
raise NgioValidationError("At least one dataset must be provided.")
|
|
57
|
+
|
|
58
|
+
self._datasets = datasets
|
|
59
|
+
|
|
60
|
+
def __repr__(self):
|
|
61
|
+
class_name = type(self).__name__
|
|
62
|
+
paths = [dataset.path for dataset in self.datasets]
|
|
63
|
+
on_disk_axes = self.datasets[0].axes_mapper.on_disk_axes_names
|
|
64
|
+
return (
|
|
65
|
+
f"{class_name}(name={self.name}, "
|
|
66
|
+
f"datasets={paths}, "
|
|
67
|
+
f"on_disk_axes={on_disk_axes})"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def version(self) -> NgffVersion:
|
|
72
|
+
"""Version of the OME-NFF metadata used to build the object."""
|
|
73
|
+
return self._version
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def name(self) -> str | None:
|
|
77
|
+
"""Name of the image."""
|
|
78
|
+
return self._name
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def datasets(self) -> list[Dataset]:
|
|
82
|
+
"""List of datasets in the multiscale."""
|
|
83
|
+
return self._datasets
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def levels(self) -> int:
|
|
87
|
+
"""Number of levels in the multiscale."""
|
|
88
|
+
return len(self.datasets)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def paths(self) -> list[str]:
|
|
92
|
+
"""List of paths of the datasets."""
|
|
93
|
+
return [dataset.path for dataset in self.datasets]
|
|
94
|
+
|
|
95
|
+
def _get_dataset_by_path(self, path: str) -> Dataset:
|
|
96
|
+
"""Get a dataset by its path."""
|
|
97
|
+
for dataset in self.datasets:
|
|
98
|
+
if dataset.path == path:
|
|
99
|
+
return dataset
|
|
100
|
+
raise NgioValueError(f"Dataset with path {path} not found.")
|
|
101
|
+
|
|
102
|
+
def _get_dataset_by_index(self, idx: int) -> Dataset:
|
|
103
|
+
"""Get a dataset by its index."""
|
|
104
|
+
if idx < 0 or idx >= len(self.datasets):
|
|
105
|
+
raise NgioValueError(f"Index {idx} out of range.")
|
|
106
|
+
return self.datasets[idx]
|
|
107
|
+
|
|
108
|
+
def _get_dataset_by_pixel_size(
|
|
109
|
+
self, pixel_size: PixelSize, strict: bool = False, tol: float = 1e-6
|
|
110
|
+
) -> Dataset:
|
|
111
|
+
"""Get a dataset with the closest pixel size.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
pixel_size(PixelSize): The pixel size to search for.
|
|
115
|
+
strict(bool): If True, the pixel size must smaller than tol.
|
|
116
|
+
tol(float): Any pixel size with a distance less than tol will be considered.
|
|
117
|
+
"""
|
|
118
|
+
min_dist = np.inf
|
|
119
|
+
|
|
120
|
+
closest_dataset = None
|
|
121
|
+
for dataset in self.datasets:
|
|
122
|
+
dist = dataset.pixel_size.distance(pixel_size)
|
|
123
|
+
if dist < min_dist:
|
|
124
|
+
min_dist = dist
|
|
125
|
+
closest_dataset = dataset
|
|
126
|
+
|
|
127
|
+
if closest_dataset is None:
|
|
128
|
+
raise NgioValueError("No dataset found.")
|
|
129
|
+
|
|
130
|
+
if strict and min_dist > tol:
|
|
131
|
+
raise NgioValueError("No dataset with a pixel size close enough.")
|
|
132
|
+
|
|
133
|
+
return closest_dataset
|
|
134
|
+
|
|
135
|
+
def get_dataset(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
path: str | None = None,
|
|
139
|
+
idx: int | None = None,
|
|
140
|
+
pixel_size: PixelSize | None = None,
|
|
141
|
+
highest_resolution: bool = False,
|
|
142
|
+
strict: bool = False,
|
|
143
|
+
) -> Dataset:
|
|
144
|
+
"""Get a dataset by its path, index or pixel size.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path(str): The path of the dataset.
|
|
148
|
+
idx(int): The index of the dataset.
|
|
149
|
+
pixel_size(PixelSize): The pixel size to search for.
|
|
150
|
+
highest_resolution(bool): If True, the dataset with the highest resolution
|
|
151
|
+
strict(bool): If True, the pixel size must be exactly the same.
|
|
152
|
+
If pixel_size is None, strict is ignored.
|
|
153
|
+
"""
|
|
154
|
+
# Only one of the arguments must be provided
|
|
155
|
+
if (
|
|
156
|
+
sum(
|
|
157
|
+
[
|
|
158
|
+
path is not None,
|
|
159
|
+
idx is not None,
|
|
160
|
+
pixel_size is not None,
|
|
161
|
+
highest_resolution,
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
!= 1
|
|
165
|
+
):
|
|
166
|
+
raise NgioValueError("get_dataset must receive only one argument.")
|
|
167
|
+
|
|
168
|
+
if path is not None:
|
|
169
|
+
return self._get_dataset_by_path(path)
|
|
170
|
+
elif idx is not None:
|
|
171
|
+
return self._get_dataset_by_index(idx)
|
|
172
|
+
elif pixel_size is not None:
|
|
173
|
+
return self._get_dataset_by_pixel_size(pixel_size, strict=strict)
|
|
174
|
+
elif highest_resolution:
|
|
175
|
+
return self.get_highest_resolution_dataset()
|
|
176
|
+
else:
|
|
177
|
+
raise NgioValueError("get_dataset has no valid arguments.")
|
|
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]
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def xy_scaling_factor(self) -> float:
|
|
256
|
+
"""Get the xy scaling factor of the dataset."""
|
|
257
|
+
x_scaling_factors = self.get_scaling_factor("x")
|
|
258
|
+
y_scaling_factors = self.get_scaling_factor("y")
|
|
259
|
+
if not np.isclose(x_scaling_factors, y_scaling_factors):
|
|
260
|
+
raise NgioValidationError(
|
|
261
|
+
"Inconsistent scaling factors are not supported. "
|
|
262
|
+
f"{x_scaling_factors}, {y_scaling_factors}"
|
|
263
|
+
)
|
|
264
|
+
return x_scaling_factors
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def z_scaling_factor(self) -> float:
|
|
268
|
+
"""Get the z scaling factor of the dataset."""
|
|
269
|
+
return self.get_scaling_factor("z")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class NgioLabelMeta(AbstractNgioImageMeta):
|
|
273
|
+
"""Label metadata model."""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
version: str,
|
|
278
|
+
name: str | None,
|
|
279
|
+
datasets: list[Dataset],
|
|
280
|
+
image_label: ImageLabelSource | None = None,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""Initialize the ImageMeta object."""
|
|
283
|
+
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
|
+
image_label = (
|
|
291
|
+
ImageLabelSource.default_init(self.version)
|
|
292
|
+
if image_label is None
|
|
293
|
+
else image_label
|
|
294
|
+
)
|
|
295
|
+
assert image_label is not None
|
|
296
|
+
if image_label.version != version:
|
|
297
|
+
raise NgioValidationError(
|
|
298
|
+
"Label image version must match the metadata version."
|
|
299
|
+
)
|
|
300
|
+
self._image_label = image_label
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def source_image(self) -> str | None:
|
|
304
|
+
source = self._image_label.source
|
|
305
|
+
if "image" not in source:
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
image_path = source["image"]
|
|
309
|
+
return image_path
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def image_label(self) -> ImageLabelSource:
|
|
313
|
+
"""Get the image label metadata."""
|
|
314
|
+
return self._image_label
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class NgioImageMeta(AbstractNgioImageMeta):
|
|
318
|
+
"""Image metadata model."""
|
|
319
|
+
|
|
320
|
+
def __init__(
|
|
321
|
+
self,
|
|
322
|
+
version: str,
|
|
323
|
+
name: str | None,
|
|
324
|
+
datasets: list[Dataset],
|
|
325
|
+
channels: ChannelsMeta | None = None,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Initialize the ImageMeta object."""
|
|
328
|
+
super().__init__(version=version, name=name, datasets=datasets)
|
|
329
|
+
self._channels_meta = channels
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def channels_meta(self) -> ChannelsMeta | None:
|
|
333
|
+
"""Get the channels_meta metadata."""
|
|
334
|
+
return self._channels_meta
|
|
335
|
+
|
|
336
|
+
def set_channels_meta(self, channels_meta: ChannelsMeta) -> None:
|
|
337
|
+
"""Set channels_meta metadata."""
|
|
338
|
+
self._channels_meta = channels_meta
|
|
339
|
+
|
|
340
|
+
def init_channels(
|
|
341
|
+
self,
|
|
342
|
+
labels: list[str] | int,
|
|
343
|
+
wavelength_ids: list[str] | None = None,
|
|
344
|
+
colors: list[str] | None = None,
|
|
345
|
+
active: list[bool] | None = None,
|
|
346
|
+
start: list[int | float] | None = None,
|
|
347
|
+
end: list[int | float] | None = None,
|
|
348
|
+
data_type: Any = np.uint16,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Set the channels_meta metadata for the image.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
labels (list[str]|int): The labels of the channels.
|
|
354
|
+
wavelength_ids (list[str], optional): The wavelengths of the channels.
|
|
355
|
+
colors (list[str], optional): The colors of the channels.
|
|
356
|
+
adjust_window (bool, optional): Whether to adjust the window.
|
|
357
|
+
start_percentile (int, optional): The start percentile.
|
|
358
|
+
end_percentile (int, optional): The end percentile.
|
|
359
|
+
active (list[bool], optional): Whether the channel is active.
|
|
360
|
+
start (list[int | float], optional): The start value of the channel.
|
|
361
|
+
end (list[int | float], optional): The end value of the channel.
|
|
362
|
+
end (int): The end value of the channel.
|
|
363
|
+
data_type (Any): The data type of the channel.
|
|
364
|
+
"""
|
|
365
|
+
channels_meta = ChannelsMeta.default_init(
|
|
366
|
+
labels=labels,
|
|
367
|
+
wavelength_id=wavelength_ids,
|
|
368
|
+
colors=colors,
|
|
369
|
+
active=active,
|
|
370
|
+
start=start,
|
|
371
|
+
end=end,
|
|
372
|
+
data_type=data_type,
|
|
373
|
+
)
|
|
374
|
+
self.set_channels_meta(channels_meta=channels_meta)
|
|
375
|
+
|
|
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
|
+
|
|
434
|
+
NgioImageLabelMeta = NgioImageMeta | NgioLabelMeta
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Fractal internal module for dataset metadata handling."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ngio.ome_zarr_meta.ngio_specs import SpaceUnits, TimeUnits
|
|
6
|
+
|
|
7
|
+
################################################################################################
|
|
8
|
+
#
|
|
9
|
+
# PixelSize model
|
|
10
|
+
# The PixelSize model is used to store the pixel size in 3D space.
|
|
11
|
+
# The model does not store scaling factors and units for other axes.
|
|
12
|
+
#
|
|
13
|
+
#################################################################################################
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_type(value: float, name: str) -> float:
|
|
17
|
+
"""Check the type of the value."""
|
|
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:
|
|
24
|
+
"""PixelSize class to store the pixel size in 3D space."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
x: float,
|
|
29
|
+
y: float,
|
|
30
|
+
z: float,
|
|
31
|
+
t: float = 0,
|
|
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
|
|
48
|
+
|
|
49
|
+
def __repr__(self) -> str:
|
|
50
|
+
"""Return a string representation of the pixel size."""
|
|
51
|
+
return f"PixelSize(x={self.x}, y={self.y}, z={self.z}, t={self.t})"
|
|
52
|
+
|
|
53
|
+
def as_dict(self) -> dict:
|
|
54
|
+
"""Return the pixel size as a dictionary."""
|
|
55
|
+
return {"t": self.t, "z": self.z, "y": self.y, "x": self.x}
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def zyx(self) -> tuple[float, float, float]:
|
|
59
|
+
"""Return the voxel size in z, y, x order."""
|
|
60
|
+
return self.z, self.y, self.x
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def yx(self) -> tuple[float, float]:
|
|
64
|
+
"""Return the xy plane pixel size in y, x order."""
|
|
65
|
+
return self.y, self.x
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def voxel_volume(self) -> float:
|
|
69
|
+
"""Return the volume of a voxel."""
|
|
70
|
+
return self.y * self.x * self.z
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def xy_plane_area(self) -> float:
|
|
74
|
+
"""Return the area of the xy plane."""
|
|
75
|
+
return self.y * self.x
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def time_spacing(self) -> float | None:
|
|
79
|
+
"""Return the time spacing."""
|
|
80
|
+
return self.t
|
|
81
|
+
|
|
82
|
+
def distance(self, other: "PixelSize") -> float:
|
|
83
|
+
"""Return the distance between two pixel sizes in 3D space."""
|
|
84
|
+
return float(np.linalg.norm(np.array(self.zyx) - np.array(other.zyx)))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Concrete implementation of the OME-Zarr metadata handlers for version 0.4."""
|
|
2
|
+
|
|
3
|
+
from ngio.ome_zarr_meta._generic_handlers import (
|
|
4
|
+
BaseImageMetaHandler,
|
|
5
|
+
BaseLabelMetaHandler,
|
|
6
|
+
)
|
|
7
|
+
from ngio.ome_zarr_meta.ngio_specs import AxesSetup
|
|
8
|
+
from ngio.ome_zarr_meta.v04._v04_spec_utils import (
|
|
9
|
+
ngio_to_v04_image_meta,
|
|
10
|
+
ngio_to_v04_label_meta,
|
|
11
|
+
v04_to_ngio_image_meta,
|
|
12
|
+
v04_to_ngio_label_meta,
|
|
13
|
+
)
|
|
14
|
+
from ngio.utils import ZarrGroupHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class V04ImageMetaHandler(BaseImageMetaHandler):
|
|
18
|
+
"""Base class for handling OME-Zarr 0.4 metadata."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
group_handler: ZarrGroupHandler,
|
|
23
|
+
axes_setup: AxesSetup | None = None,
|
|
24
|
+
allow_non_canonical_axes: bool = False,
|
|
25
|
+
strict_canonical_order: bool = True,
|
|
26
|
+
):
|
|
27
|
+
super().__init__(
|
|
28
|
+
meta_importer=v04_to_ngio_image_meta,
|
|
29
|
+
meta_exporter=ngio_to_v04_image_meta,
|
|
30
|
+
group_handler=group_handler,
|
|
31
|
+
axes_setup=axes_setup,
|
|
32
|
+
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
33
|
+
strict_canonical_order=strict_canonical_order,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class V04LabelMetaHandler(BaseLabelMetaHandler):
|
|
38
|
+
"""Base class for handling OME-Zarr 0.4 metadata."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
group_handler: ZarrGroupHandler,
|
|
43
|
+
axes_setup: AxesSetup | None = None,
|
|
44
|
+
allow_non_canonical_axes: bool = False,
|
|
45
|
+
strict_canonical_order: bool = True,
|
|
46
|
+
):
|
|
47
|
+
super().__init__(
|
|
48
|
+
meta_importer=v04_to_ngio_label_meta,
|
|
49
|
+
meta_exporter=ngio_to_v04_label_meta,
|
|
50
|
+
group_handler=group_handler,
|
|
51
|
+
axes_setup=axes_setup,
|
|
52
|
+
allow_non_canonical_axes=allow_non_canonical_axes,
|
|
53
|
+
strict_canonical_order=strict_canonical_order,
|
|
54
|
+
)
|