ngio 0.3.5__py3-none-any.whl → 0.4.0a2__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 +6 -0
- ngio/common/__init__.py +50 -48
- ngio/common/_array_io_pipes.py +554 -0
- ngio/common/_array_io_utils.py +508 -0
- ngio/common/_dimensions.py +63 -27
- ngio/common/_masking_roi.py +38 -10
- ngio/common/_pyramid.py +9 -7
- ngio/common/_roi.py +583 -72
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +17 -12
- ngio/common/transforms/__init__.py +5 -0
- ngio/common/transforms/_label.py +12 -0
- ngio/common/transforms/_zoom.py +109 -0
- ngio/experimental/__init__.py +5 -0
- ngio/experimental/iterators/__init__.py +17 -0
- ngio/experimental/iterators/_abstract_iterator.py +170 -0
- ngio/experimental/iterators/_feature.py +151 -0
- ngio/experimental/iterators/_image_processing.py +169 -0
- ngio/experimental/iterators/_rois_utils.py +127 -0
- ngio/experimental/iterators/_segmentation.py +282 -0
- ngio/hcs/_plate.py +41 -36
- ngio/images/__init__.py +22 -1
- ngio/images/_abstract_image.py +247 -117
- ngio/images/_create.py +15 -15
- ngio/images/_create_synt_container.py +128 -0
- ngio/images/_image.py +425 -62
- ngio/images/_label.py +33 -30
- ngio/images/_masked_image.py +396 -122
- ngio/images/_ome_zarr_container.py +203 -66
- ngio/{common → images}/_table_ops.py +41 -41
- ngio/ome_zarr_meta/ngio_specs/__init__.py +2 -8
- ngio/ome_zarr_meta/ngio_specs/_axes.py +151 -128
- ngio/ome_zarr_meta/ngio_specs/_channels.py +55 -18
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +7 -7
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +3 -3
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +11 -68
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +1 -1
- 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 +54 -0
- ngio/resources/resource_model.py +35 -0
- ngio/tables/backends/_abstract_backend.py +5 -6
- ngio/tables/backends/_anndata.py +1 -1
- ngio/tables/backends/_anndata_utils.py +3 -3
- ngio/tables/backends/_non_zarr_backends.py +1 -1
- ngio/tables/backends/_table_backends.py +0 -1
- ngio/tables/backends/_utils.py +3 -3
- ngio/tables/v1/_roi_table.py +156 -69
- ngio/utils/__init__.py +2 -3
- ngio/utils/_logger.py +19 -0
- ngio/utils/_zarr_utils.py +1 -5
- {ngio-0.3.5.dist-info → ngio-0.4.0a2.dist-info}/METADATA +3 -1
- ngio-0.4.0a2.dist-info/RECORD +76 -0
- ngio/common/_array_pipe.py +0 -288
- ngio/common/_axes_transforms.py +0 -64
- ngio/common/_common_types.py +0 -5
- ngio/common/_slicer.py +0 -96
- ngio-0.3.5.dist-info/RECORD +0 -61
- {ngio-0.3.5.dist-info → ngio-0.4.0a2.dist-info}/WHEEL +0 -0
- {ngio-0.3.5.dist-info → ngio-0.4.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
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 typing import Literal, TypeVar
|
|
5
|
+
from typing import Literal, TypeAlias, TypeVar
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
10
|
-
from ngio.utils import NgioValidationError, NgioValueError
|
|
10
|
+
from ngio.utils import NgioValidationError, NgioValueError
|
|
11
11
|
|
|
12
12
|
T = TypeVar("T")
|
|
13
|
+
SlicingType: TypeAlias = slice | tuple[int, ...] | int
|
|
13
14
|
|
|
14
15
|
################################################################################################
|
|
15
16
|
#
|
|
@@ -98,24 +99,10 @@ class Axis(BaseModel):
|
|
|
98
99
|
|
|
99
100
|
def implicit_type_cast(self, cast_type: AxisType) -> "Axis":
|
|
100
101
|
unit = self.unit
|
|
101
|
-
if self.axis_type != cast_type:
|
|
102
|
-
ngio_logger.warning(
|
|
103
|
-
f"Axis {self.on_disk_name} has type {self.axis_type}. "
|
|
104
|
-
f"Casting to {cast_type}."
|
|
105
|
-
)
|
|
106
|
-
|
|
107
102
|
if cast_type == AxisType.time and unit is None:
|
|
108
|
-
ngio_logger.warning(
|
|
109
|
-
f"Time axis {self.on_disk_name} has unit {self.unit}. "
|
|
110
|
-
f"Casting to {DefaultSpaceUnit}."
|
|
111
|
-
)
|
|
112
103
|
unit = DefaultTimeUnit
|
|
113
104
|
|
|
114
105
|
if cast_type == AxisType.space and unit is None:
|
|
115
|
-
ngio_logger.warning(
|
|
116
|
-
f"Space axis {self.on_disk_name} has unit {unit}. "
|
|
117
|
-
f"Casting to {DefaultSpaceUnit}."
|
|
118
|
-
)
|
|
119
106
|
unit = DefaultSpaceUnit
|
|
120
107
|
|
|
121
108
|
return Axis(on_disk_name=self.on_disk_name, axis_type=cast_type, unit=unit)
|
|
@@ -170,8 +157,28 @@ class AxesSetup(BaseModel):
|
|
|
170
157
|
|
|
171
158
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
172
159
|
|
|
173
|
-
|
|
174
|
-
|
|
160
|
+
def canonical_map(self) -> dict[str, str]:
|
|
161
|
+
"""Get the canonical map of axes."""
|
|
162
|
+
return {
|
|
163
|
+
"t": self.t,
|
|
164
|
+
"c": self.c,
|
|
165
|
+
"z": self.z,
|
|
166
|
+
"y": self.y,
|
|
167
|
+
"x": self.x,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def inverse_canonical_map(self) -> dict[str, str]:
|
|
171
|
+
"""Get the on disk map of axes."""
|
|
172
|
+
return {
|
|
173
|
+
self.t: "t",
|
|
174
|
+
self.c: "c",
|
|
175
|
+
self.z: "z",
|
|
176
|
+
self.y: "y",
|
|
177
|
+
self.x: "x",
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _check_unique_names(axes: Sequence[Axis]):
|
|
175
182
|
"""Check if all axes on disk have unique names."""
|
|
176
183
|
names = [ax.on_disk_name for ax in axes]
|
|
177
184
|
if len(set(names)) != len(names):
|
|
@@ -190,7 +197,7 @@ def _check_non_canonical_axes(axes_setup: AxesSetup, allow_non_canonical_axes: b
|
|
|
190
197
|
)
|
|
191
198
|
|
|
192
199
|
|
|
193
|
-
def _check_axes_validity(axes:
|
|
200
|
+
def _check_axes_validity(axes: Sequence[Axis], axes_setup: AxesSetup):
|
|
194
201
|
"""Check if all axes are valid."""
|
|
195
202
|
_axes_setup = axes_setup.model_dump(exclude={"others"})
|
|
196
203
|
_all_known_axes = [*_axes_setup.values(), *axes_setup.others]
|
|
@@ -204,7 +211,7 @@ def _check_axes_validity(axes: Collection[Axis], axes_setup: AxesSetup):
|
|
|
204
211
|
|
|
205
212
|
|
|
206
213
|
def _check_canonical_order(
|
|
207
|
-
axes:
|
|
214
|
+
axes: Sequence[Axis], axes_setup: AxesSetup, strict_canonical_order: bool
|
|
208
215
|
):
|
|
209
216
|
"""Check if the axes are in the canonical order."""
|
|
210
217
|
if not strict_canonical_order:
|
|
@@ -224,7 +231,7 @@ def _check_canonical_order(
|
|
|
224
231
|
|
|
225
232
|
|
|
226
233
|
def validate_axes(
|
|
227
|
-
axes:
|
|
234
|
+
axes: Sequence[Axis],
|
|
228
235
|
axes_setup: AxesSetup,
|
|
229
236
|
allow_non_canonical_axes: bool = False,
|
|
230
237
|
strict_canonical_order: bool = False,
|
|
@@ -246,20 +253,19 @@ def validate_axes(
|
|
|
246
253
|
)
|
|
247
254
|
|
|
248
255
|
|
|
249
|
-
class
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
class AxesExpand(AxesTransformation):
|
|
258
|
-
axes: tuple[int, ...]
|
|
256
|
+
class SlicingOps(BaseModel):
|
|
257
|
+
slice_tuple: tuple[SlicingType, ...] | None = None
|
|
258
|
+
transpose_axes: tuple[int, ...] | None = None
|
|
259
|
+
expand_axes: tuple[int, ...] | None = None
|
|
260
|
+
squeeze_axes: tuple[int, ...] | None = None
|
|
261
|
+
model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)
|
|
259
262
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
@property
|
|
264
|
+
def requires_axes_ops(self) -> bool:
|
|
265
|
+
"""Check if the slicing operations require axes operations."""
|
|
266
|
+
if self.expand_axes or self.transpose_axes or self.squeeze_axes:
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
263
269
|
|
|
264
270
|
|
|
265
271
|
class AxesMapper:
|
|
@@ -272,7 +278,7 @@ class AxesMapper:
|
|
|
272
278
|
def __init__(
|
|
273
279
|
self,
|
|
274
280
|
# spec dictated args
|
|
275
|
-
on_disk_axes:
|
|
281
|
+
on_disk_axes: Sequence[Axis],
|
|
276
282
|
# user defined args
|
|
277
283
|
axes_setup: AxesSetup | None = None,
|
|
278
284
|
allow_non_canonical_axes: bool = False,
|
|
@@ -300,56 +306,42 @@ class AxesMapper:
|
|
|
300
306
|
self._strict_canonical_order = strict_canonical_order
|
|
301
307
|
|
|
302
308
|
self._canonical_order = canonical_axes_order()
|
|
303
|
-
self._extended_canonical_order = [*axes_setup.others, *self._canonical_order]
|
|
304
309
|
|
|
305
310
|
self._on_disk_axes = on_disk_axes
|
|
306
311
|
self._axes_setup = axes_setup
|
|
307
312
|
|
|
308
|
-
self._name_mapping = self._compute_name_mapping()
|
|
309
313
|
self._index_mapping = self._compute_index_mapping()
|
|
310
314
|
|
|
311
315
|
# Validate the axes type and cast them if necessary
|
|
312
316
|
# This needs to be done after the name mapping is computed
|
|
313
|
-
self.
|
|
314
|
-
|
|
315
|
-
def _compute_name_mapping(self):
|
|
316
|
-
"""Compute the name mapping.
|
|
317
|
-
|
|
318
|
-
The name mapping is a dictionary with keys the canonical axes names
|
|
319
|
-
and values the on disk axes names.
|
|
320
|
-
"""
|
|
321
|
-
_name_mapping = {}
|
|
322
|
-
axis_setup_dict = self._axes_setup.model_dump(exclude={"others"})
|
|
323
|
-
_on_disk_names = self.on_disk_axes_names
|
|
324
|
-
for canonical_key, on_disk_value in axis_setup_dict.items():
|
|
325
|
-
if on_disk_value in _on_disk_names:
|
|
326
|
-
_name_mapping[canonical_key] = on_disk_value
|
|
327
|
-
else:
|
|
328
|
-
_name_mapping[canonical_key] = None
|
|
329
|
-
|
|
330
|
-
for on_disk_name in _on_disk_names:
|
|
331
|
-
if on_disk_name not in _name_mapping.keys():
|
|
332
|
-
_name_mapping[on_disk_name] = on_disk_name
|
|
333
|
-
|
|
334
|
-
for other in self._axes_setup.others:
|
|
335
|
-
if other not in _name_mapping.keys():
|
|
336
|
-
_name_mapping[other] = None
|
|
337
|
-
return _name_mapping
|
|
317
|
+
self.validate_axes_type()
|
|
338
318
|
|
|
339
319
|
def _compute_index_mapping(self):
|
|
340
320
|
"""Compute the index mapping.
|
|
341
321
|
|
|
342
322
|
The index mapping is a dictionary with keys the canonical axes names
|
|
343
323
|
and values the on disk axes index.
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
If the on disk axes are ['channel', 't', 'z', 'y', 'x'],
|
|
327
|
+
the index mapping will be:
|
|
328
|
+
{
|
|
329
|
+
'c': 0,
|
|
330
|
+
'channel': 0,
|
|
331
|
+
't': 1,
|
|
332
|
+
'z': 2,
|
|
333
|
+
'y': 3,
|
|
334
|
+
'x': 4,
|
|
335
|
+
}
|
|
344
336
|
"""
|
|
345
337
|
_index_mapping = {}
|
|
346
|
-
for
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
_index_mapping[canonical_key] =
|
|
338
|
+
for i, ax in enumerate(self.axes_names):
|
|
339
|
+
_index_mapping[ax] = i
|
|
340
|
+
# If the axis is not in the canonical order we also set it.
|
|
341
|
+
canonical_map = self._axes_setup.canonical_map()
|
|
342
|
+
for canonical_key, on_disk_value in canonical_map.items():
|
|
343
|
+
if on_disk_value in _index_mapping.keys():
|
|
344
|
+
_index_mapping[canonical_key] = _index_mapping[on_disk_value]
|
|
353
345
|
return _index_mapping
|
|
354
346
|
|
|
355
347
|
@property
|
|
@@ -358,12 +350,12 @@ class AxesMapper:
|
|
|
358
350
|
return self._axes_setup
|
|
359
351
|
|
|
360
352
|
@property
|
|
361
|
-
def
|
|
362
|
-
return
|
|
353
|
+
def axes(self) -> tuple[Axis, ...]:
|
|
354
|
+
return tuple(self._on_disk_axes)
|
|
363
355
|
|
|
364
356
|
@property
|
|
365
|
-
def
|
|
366
|
-
return
|
|
357
|
+
def axes_names(self) -> tuple[str, ...]:
|
|
358
|
+
return tuple(ax.on_disk_name for ax in self._on_disk_axes)
|
|
367
359
|
|
|
368
360
|
@property
|
|
369
361
|
def allow_non_canonical_axes(self) -> bool:
|
|
@@ -377,28 +369,23 @@ class AxesMapper:
|
|
|
377
369
|
|
|
378
370
|
def get_index(self, name: str) -> int | None:
|
|
379
371
|
"""Get the index of the axis by name."""
|
|
380
|
-
|
|
381
|
-
raise NgioValueError(
|
|
382
|
-
f"Invalid axis name '{name}'. "
|
|
383
|
-
f"Possible values are {self._index_mapping.keys()}"
|
|
384
|
-
)
|
|
385
|
-
return self._index_mapping[name]
|
|
372
|
+
return self._index_mapping.get(name, None)
|
|
386
373
|
|
|
387
374
|
def get_axis(self, name: str) -> Axis | None:
|
|
388
375
|
"""Get the axis object by name."""
|
|
389
376
|
index = self.get_index(name)
|
|
390
377
|
if index is None:
|
|
391
378
|
return None
|
|
392
|
-
return self.
|
|
379
|
+
return self.axes[index]
|
|
393
380
|
|
|
394
|
-
def
|
|
381
|
+
def validate_axes_type(self):
|
|
395
382
|
"""Validate the axes type.
|
|
396
383
|
|
|
397
384
|
If the axes type is not correct, a warning is issued.
|
|
398
385
|
and the axis is implicitly cast to the correct type.
|
|
399
386
|
"""
|
|
400
387
|
new_axes = []
|
|
401
|
-
for axes in self.
|
|
388
|
+
for axes in self.axes:
|
|
402
389
|
for name in self._canonical_order:
|
|
403
390
|
if axes == self.get_axis(name):
|
|
404
391
|
new_axes.append(axes.canonical_axis_cast(name))
|
|
@@ -407,71 +394,107 @@ class AxesMapper:
|
|
|
407
394
|
new_axes.append(axes)
|
|
408
395
|
self._on_disk_axes = new_axes
|
|
409
396
|
|
|
410
|
-
def
|
|
411
|
-
self, names:
|
|
412
|
-
) -> tuple[tuple[int, ...], tuple[int, ...]]:
|
|
413
|
-
|
|
397
|
+
def _reorder_axes(
|
|
398
|
+
self, names: Sequence[str]
|
|
399
|
+
) -> tuple[tuple[int, ...] | None, tuple[int, ...] | None, tuple[int, ...] | None]:
|
|
400
|
+
"""Change the order of the axes."""
|
|
401
|
+
# Validate the names
|
|
402
|
+
unique_names = set(names)
|
|
403
|
+
if len(unique_names) != len(names):
|
|
404
|
+
raise NgioValueError(
|
|
405
|
+
"Duplicate axis names found. Please provide unique names for each axis."
|
|
406
|
+
)
|
|
414
407
|
for name in names:
|
|
415
|
-
if
|
|
408
|
+
if not isinstance(name, str):
|
|
416
409
|
raise NgioValueError(
|
|
417
|
-
f"Invalid axis name '{name}'. "
|
|
418
|
-
f"Possible values are {self._index_mapping.keys()}"
|
|
410
|
+
f"Invalid axis name '{name}'. Axis names must be strings."
|
|
419
411
|
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
412
|
+
inv_canonical_map = self.axes_setup.inverse_canonical_map()
|
|
413
|
+
|
|
414
|
+
# Step 1: Check find squeeze axes
|
|
415
|
+
_axes_to_squeeze: list[int] = []
|
|
416
|
+
axes_names_after_squeeze = []
|
|
417
|
+
for i, ax in enumerate(self.axes_names):
|
|
418
|
+
# If the axis is not in the names, it means we need to squeeze it
|
|
419
|
+
ax_canonical = inv_canonical_map.get(ax, None)
|
|
420
|
+
if ax not in names and ax_canonical not in names:
|
|
421
|
+
_axes_to_squeeze.append(i)
|
|
422
|
+
elif ax in names:
|
|
423
|
+
axes_names_after_squeeze.append(ax)
|
|
424
|
+
elif ax_canonical in names:
|
|
425
|
+
# If the axis is in the canonical map, we add it to the names
|
|
426
|
+
axes_names_after_squeeze.append(ax_canonical)
|
|
427
|
+
|
|
428
|
+
axes_to_squeeze = tuple(_axes_to_squeeze) if len(_axes_to_squeeze) > 0 else None
|
|
429
|
+
|
|
430
|
+
# Step 2: Find the transposition order
|
|
431
|
+
_transposition_order: list[int] = []
|
|
432
|
+
axes_names_after_transpose = []
|
|
433
|
+
for ax in names:
|
|
434
|
+
if ax in axes_names_after_squeeze:
|
|
435
|
+
_transposition_order.append(axes_names_after_squeeze.index(ax))
|
|
436
|
+
axes_names_after_transpose.append(ax)
|
|
437
|
+
|
|
438
|
+
if np.allclose(_transposition_order, range(len(_transposition_order))):
|
|
439
|
+
# If the transposition order is the identity, we don't need to transpose
|
|
440
|
+
transposition_order = None
|
|
441
|
+
else:
|
|
442
|
+
transposition_order = tuple(_transposition_order)
|
|
443
|
+
|
|
444
|
+
# Step 3: Find axes to expand
|
|
445
|
+
_axes_to_expand: list[int] = []
|
|
437
446
|
for i, name in enumerate(names):
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
447
|
+
if name not in axes_names_after_transpose:
|
|
448
|
+
# If the axis is not in the mapping, it means we need to expand it
|
|
449
|
+
_axes_to_expand.append(i)
|
|
450
|
+
|
|
451
|
+
axes_to_expand = tuple(_axes_to_expand) if len(_axes_to_expand) > 0 else None
|
|
452
|
+
return axes_to_squeeze, transposition_order, axes_to_expand
|
|
444
453
|
|
|
445
|
-
def to_order(self, names:
|
|
454
|
+
def to_order(self, names: Sequence[str]) -> SlicingOps:
|
|
446
455
|
"""Get the new order of the axes."""
|
|
447
|
-
|
|
448
|
-
return
|
|
456
|
+
axes_to_squeeze, transposition_order, axes_to_expand = self._reorder_axes(names)
|
|
457
|
+
return SlicingOps(
|
|
458
|
+
transpose_axes=transposition_order,
|
|
459
|
+
expand_axes=axes_to_expand,
|
|
460
|
+
squeeze_axes=axes_to_squeeze,
|
|
461
|
+
)
|
|
449
462
|
|
|
450
|
-
def from_order(self, names:
|
|
463
|
+
def from_order(self, names: Sequence[str]) -> SlicingOps:
|
|
451
464
|
"""Get the new order of the axes."""
|
|
452
|
-
|
|
465
|
+
axes_to_squeeze, transposition_order, axes_to_expand = self._reorder_axes(names)
|
|
453
466
|
# Inverse transpose is just the transpose with the inverse indices
|
|
454
|
-
|
|
455
|
-
|
|
467
|
+
if transposition_order is None:
|
|
468
|
+
_reverse_indices = None
|
|
469
|
+
else:
|
|
470
|
+
_reverse_indices = tuple(np.argsort(transposition_order))
|
|
471
|
+
|
|
472
|
+
return SlicingOps(
|
|
473
|
+
transpose_axes=_reverse_indices,
|
|
474
|
+
expand_axes=axes_to_squeeze,
|
|
475
|
+
squeeze_axes=axes_to_expand,
|
|
476
|
+
)
|
|
456
477
|
|
|
457
|
-
def to_canonical(self) ->
|
|
478
|
+
def to_canonical(self) -> SlicingOps:
|
|
458
479
|
"""Get the new order of the axes."""
|
|
459
|
-
|
|
480
|
+
other = self._axes_setup.others
|
|
481
|
+
return self.to_order(other + list(self._canonical_order))
|
|
460
482
|
|
|
461
|
-
def from_canonical(self) ->
|
|
483
|
+
def from_canonical(self) -> SlicingOps:
|
|
462
484
|
"""Get the new order of the axes."""
|
|
463
|
-
|
|
485
|
+
other = self._axes_setup.others
|
|
486
|
+
return self.from_order(other + list(self._canonical_order))
|
|
464
487
|
|
|
465
488
|
|
|
466
489
|
def canonical_axes(
|
|
467
|
-
axes_names:
|
|
468
|
-
space_units: SpaceUnits | None = DefaultSpaceUnit,
|
|
469
|
-
time_units: TimeUnits | None = DefaultTimeUnit,
|
|
490
|
+
axes_names: Sequence[str],
|
|
491
|
+
space_units: SpaceUnits | str | None = DefaultSpaceUnit,
|
|
492
|
+
time_units: TimeUnits | str | None = DefaultTimeUnit,
|
|
470
493
|
) -> list[Axis]:
|
|
471
494
|
"""Create a new canonical axes mapper.
|
|
472
495
|
|
|
473
496
|
Args:
|
|
474
|
-
axes_names (
|
|
497
|
+
axes_names (Sequence[str] | int): The axes names on disk.
|
|
475
498
|
- The axes should be in ['t', 'c', 'z', 'y', 'x']
|
|
476
499
|
- The axes should be in strict canonical order.
|
|
477
500
|
- If an integer is provided, the axes are created from the last axis
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Stores the same information as the Omero section of the ngff 0.4 metadata.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from collections.abc import
|
|
6
|
+
from collections.abc import Sequence
|
|
7
7
|
from difflib import SequenceMatcher
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from typing import Any, TypeVar
|
|
@@ -77,7 +77,8 @@ class NgioColors(str, Enum):
|
|
|
77
77
|
# try to match the color to the channel name
|
|
78
78
|
similarity[color] = SequenceMatcher(None, channel_name, color).ratio()
|
|
79
79
|
# Get the color with the highest similarity
|
|
80
|
-
color_str = max(similarity, key=similarity.get) # type: ignore
|
|
80
|
+
color_str = max(similarity, key=similarity.get) # type: ignore (max type overload fails to infer type)
|
|
81
|
+
assert isinstance(color_str, str), "Color name must be a string."
|
|
81
82
|
return NgioColors.__members__[color_str]
|
|
82
83
|
|
|
83
84
|
|
|
@@ -287,7 +288,7 @@ class Channel(BaseModel):
|
|
|
287
288
|
T = TypeVar("T")
|
|
288
289
|
|
|
289
290
|
|
|
290
|
-
def _check_elements(elements:
|
|
291
|
+
def _check_elements(elements: Sequence[T], expected_type: Any) -> Sequence[T]:
|
|
291
292
|
"""Check that the elements are of the same type."""
|
|
292
293
|
if len(elements) == 0:
|
|
293
294
|
raise NgioValidationError("At least one element must be provided.")
|
|
@@ -301,7 +302,7 @@ def _check_elements(elements: Collection[T], expected_type: Any) -> Collection[T
|
|
|
301
302
|
return elements
|
|
302
303
|
|
|
303
304
|
|
|
304
|
-
def _check_unique(elements:
|
|
305
|
+
def _check_unique(elements: Sequence[T]) -> Sequence[T]:
|
|
305
306
|
"""Check that the elements are unique."""
|
|
306
307
|
if len(set(elements)) != len(elements):
|
|
307
308
|
raise NgioValidationError("All elements must be unique.")
|
|
@@ -329,35 +330,35 @@ class ChannelsMeta(BaseModel):
|
|
|
329
330
|
@classmethod
|
|
330
331
|
def default_init(
|
|
331
332
|
cls,
|
|
332
|
-
labels:
|
|
333
|
-
wavelength_id:
|
|
334
|
-
colors:
|
|
335
|
-
start:
|
|
336
|
-
end:
|
|
337
|
-
active:
|
|
333
|
+
labels: Sequence[str | None] | int,
|
|
334
|
+
wavelength_id: Sequence[str | None] | None = None,
|
|
335
|
+
colors: Sequence[str | NgioColors | None] | None = None,
|
|
336
|
+
start: Sequence[int | float | None] | int | float | None = None,
|
|
337
|
+
end: Sequence[int | float | None] | int | float | None = None,
|
|
338
|
+
active: Sequence[bool | None] | None = None,
|
|
338
339
|
data_type: Any = np.uint16,
|
|
339
340
|
**omero_kwargs: dict,
|
|
340
341
|
) -> "ChannelsMeta":
|
|
341
342
|
"""Create a ChannelsMeta object with the default unit.
|
|
342
343
|
|
|
343
344
|
Args:
|
|
344
|
-
labels(
|
|
345
|
+
labels(Sequence[str | None] | int): The list of channels names
|
|
345
346
|
in the image. If an integer is provided, the channels will be
|
|
346
347
|
named "channel_i".
|
|
347
|
-
wavelength_id(
|
|
348
|
+
wavelength_id(Sequence[str | None] | None): The wavelength ID of the
|
|
348
349
|
channel. If None, the wavelength ID will be the same as the
|
|
349
350
|
channel name.
|
|
350
|
-
colors(
|
|
351
|
+
colors(Sequence[str | NgioColors | None] | None): The list of
|
|
351
352
|
colors for the channels. If None, the colors will be random.
|
|
352
|
-
start(
|
|
353
|
+
start(Sequence[int | float | None] | int | float | None): The start
|
|
353
354
|
value of the channel. If None, the start value will be the
|
|
354
355
|
minimum value of the data type.
|
|
355
|
-
end(
|
|
356
|
+
end(Sequence[int | float | None] | int | float | None): The end
|
|
356
357
|
value of the channel. If None, the end value will be the
|
|
357
358
|
maximum value of the data type.
|
|
358
359
|
data_type(Any): The data type of the channel. Will be used to set the
|
|
359
360
|
min and max values of the channel.
|
|
360
|
-
active (
|
|
361
|
+
active (Sequence[bool | None] | None): Whether the channel should
|
|
361
362
|
be shown by default.
|
|
362
363
|
omero_kwargs(dict): Extra fields to store in the omero attributes.
|
|
363
364
|
"""
|
|
@@ -367,9 +368,9 @@ class ChannelsMeta(BaseModel):
|
|
|
367
368
|
labels = _check_elements(labels, str)
|
|
368
369
|
labels = _check_unique(labels)
|
|
369
370
|
|
|
370
|
-
_wavelength_id:
|
|
371
|
+
_wavelength_id: Sequence[str | None] = [None] * len(labels)
|
|
371
372
|
if wavelength_id is None:
|
|
372
|
-
_wavelength_id:
|
|
373
|
+
_wavelength_id: Sequence[str | None] = [None] * len(labels)
|
|
373
374
|
else:
|
|
374
375
|
_wavelength_id = _check_elements(wavelength_id, str)
|
|
375
376
|
_wavelength_id = _check_unique(wavelength_id)
|
|
@@ -425,3 +426,39 @@ class ChannelsMeta(BaseModel):
|
|
|
425
426
|
)
|
|
426
427
|
)
|
|
427
428
|
return cls(channels=channels, **omero_kwargs)
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def channel_labels(self) -> list[str]:
|
|
432
|
+
"""Get the labels of the channels in the image."""
|
|
433
|
+
return [channel.label for channel in self.channels]
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
def channel_wavelength_ids(self) -> list[str | None]:
|
|
437
|
+
"""Get the wavelength IDs of the channels in the image."""
|
|
438
|
+
return [channel.wavelength_id for channel in self.channels]
|
|
439
|
+
|
|
440
|
+
def get_channel_idx(
|
|
441
|
+
self, channel_label: str | None = None, wavelength_id: str | None = None
|
|
442
|
+
) -> int:
|
|
443
|
+
"""Get the index of a channel by its label or wavelength ID."""
|
|
444
|
+
# Only one of the arguments must be provided
|
|
445
|
+
if channel_label is not None and wavelength_id is not None:
|
|
446
|
+
raise NgioValueError(
|
|
447
|
+
"get_channel_idx must receive either label or wavelength_id, not both."
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if channel_label is not None:
|
|
451
|
+
if channel_label not in self.channel_labels:
|
|
452
|
+
raise NgioValueError(f"Channel with label {channel_label} not found.")
|
|
453
|
+
return self.channel_labels.index(channel_label)
|
|
454
|
+
|
|
455
|
+
if wavelength_id is not None:
|
|
456
|
+
if wavelength_id not in self.channel_wavelength_ids:
|
|
457
|
+
raise NgioValueError(
|
|
458
|
+
f"Channel with wavelength ID {wavelength_id} not found."
|
|
459
|
+
)
|
|
460
|
+
return self.channel_wavelength_ids.index(wavelength_id)
|
|
461
|
+
|
|
462
|
+
raise NgioValueError(
|
|
463
|
+
"get_channel_idx must receive either label or wavelength_id"
|
|
464
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Fractal internal module for dataset metadata handling."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import
|
|
3
|
+
from collections.abc import Sequence
|
|
4
4
|
|
|
5
5
|
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
6
6
|
AxesMapper,
|
|
@@ -28,9 +28,9 @@ class Dataset:
|
|
|
28
28
|
*,
|
|
29
29
|
# args coming from ngff specs
|
|
30
30
|
path: str,
|
|
31
|
-
on_disk_axes:
|
|
32
|
-
on_disk_scale:
|
|
33
|
-
on_disk_translation:
|
|
31
|
+
on_disk_axes: Sequence[Axis],
|
|
32
|
+
on_disk_scale: Sequence[float],
|
|
33
|
+
on_disk_translation: Sequence[float] | None = None,
|
|
34
34
|
# user defined args
|
|
35
35
|
axes_setup: AxesSetup | None = None,
|
|
36
36
|
allow_non_canonical_axes: bool = False,
|
|
@@ -123,8 +123,8 @@ class Dataset:
|
|
|
123
123
|
y=self.get_scale("y"),
|
|
124
124
|
z=self.get_scale("z"),
|
|
125
125
|
t=self.get_scale("t"),
|
|
126
|
-
space_unit=self.space_unit,
|
|
127
|
-
time_unit=self.time_unit,
|
|
126
|
+
space_unit=self.space_unit,
|
|
127
|
+
time_unit=self.time_unit,
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
@property
|
|
@@ -145,7 +145,7 @@ class Dataset:
|
|
|
145
145
|
time_unit(str): The time unit to convert to.
|
|
146
146
|
"""
|
|
147
147
|
new_axes = []
|
|
148
|
-
for ax in self.axes_mapper.
|
|
148
|
+
for ax in self.axes_mapper.axes:
|
|
149
149
|
if ax.axis_type == AxisType.space:
|
|
150
150
|
new_ax = Axis(
|
|
151
151
|
on_disk_name=ax.on_disk_name,
|
|
@@ -51,11 +51,11 @@ class CustomWellImage(WellImage04):
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
class CustomWellMeta(WellMeta04):
|
|
54
|
-
images: list[CustomWellImage] # type: ignore
|
|
54
|
+
images: list[CustomWellImage] # type: ignore (override of WellMeta04.images)
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class CustomWellAttrs(WellAttrs04):
|
|
58
|
-
well: CustomWellMeta # type: ignore
|
|
58
|
+
well: CustomWellMeta # type: ignore (override of WellAttrs04.well)
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
class NgioWellMeta(CustomWellAttrs):
|
|
@@ -531,7 +531,7 @@ class NgioPlateMeta(HCSAttrs):
|
|
|
531
531
|
acquisitions = None
|
|
532
532
|
|
|
533
533
|
if version is None:
|
|
534
|
-
version = self.plate.version # type: ignore
|
|
534
|
+
version = self.plate.version # type: ignore (version is NgffVersions or None)
|
|
535
535
|
|
|
536
536
|
return NgioPlateMeta(
|
|
537
537
|
plate=Plate(
|