ngio 0.5.0b6__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 +69 -0
- ngio/common/__init__.py +28 -0
- ngio/common/_dimensions.py +335 -0
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +408 -0
- ngio/common/_roi.py +315 -0
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +188 -0
- 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 +19 -0
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +44 -0
- ngio/images/_abstract_image.py +967 -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 +411 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1237 -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 +65 -0
- ngio/ome_zarr_meta/_meta_handlers.py +536 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
- ngio/ome_zarr_meta/v04/__init__.py +27 -0
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
- 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 +43 -0
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +57 -0
- ngio/tables/backends/_abstract_backend.py +240 -0
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +90 -0
- 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 +226 -0
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +23 -0
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +125 -0
- ngio/tables/v1/_generic_table.py +49 -0
- ngio/tables/v1/_roi_table.py +575 -0
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +45 -0
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +165 -0
- ngio/utils/_errors.py +37 -0
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +534 -0
- ngio-0.5.0b6.dist-info/METADATA +148 -0
- ngio-0.5.0b6.dist-info/RECORD +88 -0
- ngio-0.5.0b6.dist-info/WHEEL +4 -0
- ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Module to handle the channel information in the metadata.
|
|
2
|
+
|
|
3
|
+
Stores the same information as the Omero section of the ngff 0.4 metadata.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from difflib import SequenceMatcher
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
from ngio.utils import NgioValidationError, NgioValueError
|
|
15
|
+
|
|
16
|
+
################################################################################################
|
|
17
|
+
#
|
|
18
|
+
# Omero Section of the Metadata is used to store channel information and visualisation
|
|
19
|
+
# settings.
|
|
20
|
+
# This section is transitory and will be likely changed in the future.
|
|
21
|
+
#
|
|
22
|
+
#################################################################################################
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NgioColors(str, Enum):
|
|
26
|
+
"""Default colors for the channels."""
|
|
27
|
+
|
|
28
|
+
dapi = "0000FF"
|
|
29
|
+
hoechst = "0000FF"
|
|
30
|
+
gfp = "00FF00"
|
|
31
|
+
cy3 = "FFFF00"
|
|
32
|
+
cy5 = "FF0000"
|
|
33
|
+
brightfield = "808080"
|
|
34
|
+
red = "FF0000"
|
|
35
|
+
yellow = "FFFF00"
|
|
36
|
+
magenta = "FF00FF"
|
|
37
|
+
cyan = "00FFFF"
|
|
38
|
+
gray = "808080"
|
|
39
|
+
green = "00FF00"
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def semi_random_pick(channel_name: str | None = None) -> "NgioColors":
|
|
43
|
+
"""Try to fuzzy match the color to the channel name.
|
|
44
|
+
|
|
45
|
+
- If a channel name is given will try to match the channel name to the color.
|
|
46
|
+
- If name has the paatern 'channel_x' cyclic rotate over a list of colors
|
|
47
|
+
[cyan, magenta, yellow, green]
|
|
48
|
+
- If no channel name is given will return a random color.
|
|
49
|
+
"""
|
|
50
|
+
available_colors = NgioColors._member_names_
|
|
51
|
+
|
|
52
|
+
if channel_name is None:
|
|
53
|
+
# Purely random color
|
|
54
|
+
color_str = available_colors[np.random.randint(0, len(available_colors))]
|
|
55
|
+
return NgioColors.__members__[color_str]
|
|
56
|
+
|
|
57
|
+
if channel_name.startswith("channel_"):
|
|
58
|
+
# Rotate over a list of colors
|
|
59
|
+
defaults_colors = [
|
|
60
|
+
NgioColors.cyan,
|
|
61
|
+
NgioColors.magenta,
|
|
62
|
+
NgioColors.yellow,
|
|
63
|
+
NgioColors.green,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
index = int(channel_name.split("_")[-1]) % len(defaults_colors)
|
|
68
|
+
return defaults_colors[index]
|
|
69
|
+
except ValueError:
|
|
70
|
+
# If the name of the channel is something like
|
|
71
|
+
# channel_dapi this will fail an proceed to the
|
|
72
|
+
# standard fuzzy match
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
similarity = {}
|
|
76
|
+
for color in available_colors:
|
|
77
|
+
# try to match the color to the channel name
|
|
78
|
+
similarity[color] = SequenceMatcher(None, channel_name, color).ratio()
|
|
79
|
+
# Get the color with the highest similarity
|
|
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."
|
|
82
|
+
return NgioColors.__members__[color_str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def valid_hex_color(v: str) -> bool:
|
|
86
|
+
"""Validate a hexadecimal color.
|
|
87
|
+
|
|
88
|
+
Check that `color` is made of exactly six elements which are letters
|
|
89
|
+
(a-f or A-F) or digits (0-9).
|
|
90
|
+
|
|
91
|
+
Implementation source:
|
|
92
|
+
https://github.com/fractal-analytics-platform/fractal-tasks-core/fractal_tasks_core/channels.py#L87
|
|
93
|
+
Original authors:
|
|
94
|
+
- Tommaso Comparin <tommaso.comparin@exact-lab.it>
|
|
95
|
+
"""
|
|
96
|
+
if len(v) != 6:
|
|
97
|
+
return False
|
|
98
|
+
allowed_characters = "abcdefABCDEF0123456789"
|
|
99
|
+
for character in v:
|
|
100
|
+
if character not in allowed_characters:
|
|
101
|
+
return False
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def into_valid_hex_color(v: str) -> str:
|
|
106
|
+
"""Convert a string into a valid hexadecimal color.
|
|
107
|
+
|
|
108
|
+
If the string is already a valid hexadecimal color, return it.
|
|
109
|
+
Otherwise, return a hexadecimal color based on the hash of the string.
|
|
110
|
+
"""
|
|
111
|
+
# strip leading '#' if present
|
|
112
|
+
v = v.lstrip("#")
|
|
113
|
+
if valid_hex_color(v):
|
|
114
|
+
return v
|
|
115
|
+
|
|
116
|
+
return NgioColors.semi_random_pick(v.lower()).value
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ChannelVisualisation(BaseModel):
|
|
120
|
+
"""Channel visualisation model.
|
|
121
|
+
|
|
122
|
+
Contains the information about the visualisation of a channel.
|
|
123
|
+
|
|
124
|
+
Attributes:
|
|
125
|
+
color(str): The color of the channel in hexadecimal format or a color name.
|
|
126
|
+
min(int | float): The minimum value of the channel.
|
|
127
|
+
max(int | float): The maximum value of the channel.
|
|
128
|
+
start(int | float): The start value of the channel.
|
|
129
|
+
end(int | float): The end value of the channel.
|
|
130
|
+
active(bool): Whether the channel is active.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
color: str | NgioColors | None = Field(default=None, validate_default=True)
|
|
134
|
+
min: int | float = 0
|
|
135
|
+
max: int | float = 65535
|
|
136
|
+
start: int | float = 0
|
|
137
|
+
end: int | float = 65535
|
|
138
|
+
active: bool = True
|
|
139
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
140
|
+
|
|
141
|
+
@field_validator("color", mode="after")
|
|
142
|
+
def validate_color(cls, value: str | NgioColors) -> str:
|
|
143
|
+
"""Color validator.
|
|
144
|
+
|
|
145
|
+
There are three possible values to set a color:
|
|
146
|
+
- A hexadecimal string.
|
|
147
|
+
- A color name.
|
|
148
|
+
- A NgioColors element.
|
|
149
|
+
"""
|
|
150
|
+
if value is None:
|
|
151
|
+
return NgioColors.semi_random_pick().value
|
|
152
|
+
if isinstance(value, str):
|
|
153
|
+
return into_valid_hex_color(value)
|
|
154
|
+
elif isinstance(value, NgioColors):
|
|
155
|
+
return value.value
|
|
156
|
+
else:
|
|
157
|
+
raise NgioValueError(f"Invalid color {value}.")
|
|
158
|
+
|
|
159
|
+
@model_validator(mode="before")
|
|
160
|
+
def check_start_end(cls, data):
|
|
161
|
+
"""Check that the start and end values are valid.
|
|
162
|
+
|
|
163
|
+
If the start and end values are equal, set the end value to start + 1
|
|
164
|
+
"""
|
|
165
|
+
start = data.get("start", None)
|
|
166
|
+
end = data.get("end", None)
|
|
167
|
+
if start is None or end is None:
|
|
168
|
+
return data
|
|
169
|
+
if abs(end - start) < 1e-6:
|
|
170
|
+
data["end"] = start + 1
|
|
171
|
+
return data
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def default_init(
|
|
175
|
+
cls,
|
|
176
|
+
color: str | NgioColors | None = None,
|
|
177
|
+
start: int | float | None = None,
|
|
178
|
+
end: int | float | None = None,
|
|
179
|
+
active: bool = True,
|
|
180
|
+
data_type: Any = np.uint16,
|
|
181
|
+
) -> "ChannelVisualisation":
|
|
182
|
+
"""Create a ChannelVisualisation object with the default unit.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
color(str): The color of the channel in hexadecimal format or a color name.
|
|
186
|
+
start(int | float | None): The start value of the channel.
|
|
187
|
+
end(int | float | None): The end value of the channel.
|
|
188
|
+
data_type(Any): The data type of the channel.
|
|
189
|
+
active(bool): Whether the channel should be shown by default.
|
|
190
|
+
"""
|
|
191
|
+
for func in [np.iinfo, np.finfo]:
|
|
192
|
+
try:
|
|
193
|
+
min_value = func(data_type).min
|
|
194
|
+
max_value = func(data_type).max
|
|
195
|
+
break
|
|
196
|
+
except ValueError:
|
|
197
|
+
continue
|
|
198
|
+
else:
|
|
199
|
+
raise NgioValueError(f"Invalid data type {data_type}.")
|
|
200
|
+
|
|
201
|
+
start = start if start is not None else min_value
|
|
202
|
+
end = end if end is not None else max_value
|
|
203
|
+
return cls(
|
|
204
|
+
color=color,
|
|
205
|
+
min=min_value,
|
|
206
|
+
max=max_value,
|
|
207
|
+
start=start,
|
|
208
|
+
end=end,
|
|
209
|
+
active=active,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def valid_color(self) -> str:
|
|
214
|
+
"""Return the valid color."""
|
|
215
|
+
if isinstance(self.color, NgioColors):
|
|
216
|
+
return self.color.value
|
|
217
|
+
elif isinstance(self.color, str):
|
|
218
|
+
return self.color
|
|
219
|
+
else:
|
|
220
|
+
raise NgioValueError(f"Invalid color {self.color}.")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def default_channel_name(index: int) -> str:
|
|
224
|
+
"""Return the default channel name."""
|
|
225
|
+
return f"channel_{index}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class Channel(BaseModel):
|
|
229
|
+
"""Information about a channel in the image.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
label(str): The label of the channel.
|
|
233
|
+
wavelength_id(str): The wavelength ID of the channel.
|
|
234
|
+
extra_fields(dict): To reduce the api surface, extra fields are stored in the
|
|
235
|
+
the channel attributes will be stored in the extra_fields attribute.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
label: str
|
|
239
|
+
wavelength_id: str | None = None
|
|
240
|
+
channel_visualisation: ChannelVisualisation
|
|
241
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def default_init(
|
|
245
|
+
cls,
|
|
246
|
+
label: str,
|
|
247
|
+
wavelength_id: str | None = None,
|
|
248
|
+
color: str | NgioColors | None = None,
|
|
249
|
+
start: int | float | None = None,
|
|
250
|
+
end: int | float | None = None,
|
|
251
|
+
active: bool = True,
|
|
252
|
+
data_type: Any = np.uint16,
|
|
253
|
+
) -> "Channel":
|
|
254
|
+
"""Create a Channel object with the default unit.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
label(str): The label of the channel.
|
|
258
|
+
wavelength_id(str | None): The wavelength ID of the channel.
|
|
259
|
+
color(str): The color of the channel in hexadecimal format or a color name.
|
|
260
|
+
If None, the color will be picked based on the label.
|
|
261
|
+
start(int | float | None): The start value of the channel.
|
|
262
|
+
end(int | float | None): The end value of the channel.
|
|
263
|
+
active(bool): Whether the channel should be shown by default.
|
|
264
|
+
data_type(Any): The data type of the channel.
|
|
265
|
+
"""
|
|
266
|
+
if color is None:
|
|
267
|
+
# If no color is provided, try to pick a color based on the label
|
|
268
|
+
# See the NgioColors.semi_random_pick method for more details.
|
|
269
|
+
color = label
|
|
270
|
+
|
|
271
|
+
channel_visualization = ChannelVisualisation.default_init(
|
|
272
|
+
color=color, start=start, end=end, active=active, data_type=data_type
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if wavelength_id is None:
|
|
276
|
+
# TODO Evaluate if a better default value can be used
|
|
277
|
+
wavelength_id = label
|
|
278
|
+
|
|
279
|
+
return cls(
|
|
280
|
+
label=label,
|
|
281
|
+
wavelength_id=wavelength_id,
|
|
282
|
+
channel_visualisation=channel_visualization,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
T = TypeVar("T")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _check_elements(elements: Sequence[T], expected_type: Any) -> Sequence[T]:
|
|
290
|
+
"""Check that the elements are of the same type."""
|
|
291
|
+
if len(elements) == 0:
|
|
292
|
+
raise NgioValidationError("At least one element must be provided.")
|
|
293
|
+
|
|
294
|
+
for element in elements:
|
|
295
|
+
if not isinstance(element, expected_type):
|
|
296
|
+
raise NgioValidationError(
|
|
297
|
+
f"All elements must be of the same type {expected_type}. Got {element}."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return elements
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _check_unique(elements: Sequence[T]) -> Sequence[T]:
|
|
304
|
+
"""Check that the elements are unique."""
|
|
305
|
+
if len(set(elements)) != len(elements):
|
|
306
|
+
raise NgioValidationError("All elements must be unique.")
|
|
307
|
+
return elements
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class ChannelsMeta(BaseModel):
|
|
311
|
+
"""Information about the channels in the image.
|
|
312
|
+
|
|
313
|
+
This model is roughly equivalent to the Omero section of the ngff 0.4 metadata.
|
|
314
|
+
|
|
315
|
+
Attributes:
|
|
316
|
+
channels(list[Channel]): The list of channels in the image.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
channels: list[Channel] = Field(default_factory=list)
|
|
320
|
+
model_config = ConfigDict(extra="allow", frozen=True)
|
|
321
|
+
|
|
322
|
+
@field_validator("channels", mode="after")
|
|
323
|
+
def validate_channels(cls, value: list[Channel]) -> list[Channel]:
|
|
324
|
+
"""Check that the channels are unique."""
|
|
325
|
+
_check_unique([ch.label for ch in value])
|
|
326
|
+
return value
|
|
327
|
+
|
|
328
|
+
@classmethod
|
|
329
|
+
def default_init(
|
|
330
|
+
cls,
|
|
331
|
+
labels: Sequence[str | None] | int,
|
|
332
|
+
wavelength_id: Sequence[str | None] | None = None,
|
|
333
|
+
colors: Sequence[str | NgioColors | None] | None = None,
|
|
334
|
+
start: Sequence[int | float | None] | int | float | None = None,
|
|
335
|
+
end: Sequence[int | float | None] | int | float | None = None,
|
|
336
|
+
active: Sequence[bool | None] | None = None,
|
|
337
|
+
data_type: Any = np.uint16,
|
|
338
|
+
**omero_kwargs: dict,
|
|
339
|
+
) -> "ChannelsMeta":
|
|
340
|
+
"""Create a ChannelsMeta object with the default unit.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
labels(Sequence[str | None] | int): The list of channels names
|
|
344
|
+
in the image. If an integer is provided, the channels will be
|
|
345
|
+
named "channel_i".
|
|
346
|
+
wavelength_id(Sequence[str | None] | None): The wavelength ID of the
|
|
347
|
+
channel. If None, the wavelength ID will be the same as the
|
|
348
|
+
channel name.
|
|
349
|
+
colors(Sequence[str | NgioColors | None] | None): The list of
|
|
350
|
+
colors for the channels. If None, the colors will be random.
|
|
351
|
+
start(Sequence[int | float | None] | int | float | None): The start
|
|
352
|
+
value of the channel. If None, the start value will be the
|
|
353
|
+
minimum value of the data type.
|
|
354
|
+
end(Sequence[int | float | None] | int | float | None): The end
|
|
355
|
+
value of the channel. If None, the end value will be the
|
|
356
|
+
maximum value of the data type.
|
|
357
|
+
data_type(Any): The data type of the channel. Will be used to set the
|
|
358
|
+
min and max values of the channel.
|
|
359
|
+
active (Sequence[bool | None] | None): Whether the channel should
|
|
360
|
+
be shown by default.
|
|
361
|
+
omero_kwargs(dict): Extra fields to store in the omero attributes.
|
|
362
|
+
"""
|
|
363
|
+
if isinstance(labels, int):
|
|
364
|
+
labels = [default_channel_name(i) for i in range(labels)]
|
|
365
|
+
|
|
366
|
+
labels = _check_elements(labels, str)
|
|
367
|
+
labels = _check_unique(labels)
|
|
368
|
+
|
|
369
|
+
_wavelength_id: Sequence[str | None] = [None] * len(labels)
|
|
370
|
+
if wavelength_id is None:
|
|
371
|
+
_wavelength_id: Sequence[str | None] = [None] * len(labels)
|
|
372
|
+
else:
|
|
373
|
+
_wavelength_id = _check_elements(wavelength_id, str)
|
|
374
|
+
_wavelength_id = _check_unique(wavelength_id)
|
|
375
|
+
|
|
376
|
+
if colors is None:
|
|
377
|
+
_colors = [NgioColors.semi_random_pick(label) for label in labels]
|
|
378
|
+
else:
|
|
379
|
+
_colors = _check_elements(colors, str | NgioColors)
|
|
380
|
+
|
|
381
|
+
if start is None:
|
|
382
|
+
_start = [None] * len(labels)
|
|
383
|
+
elif isinstance(start, int | float):
|
|
384
|
+
_start = [start] * len(labels)
|
|
385
|
+
else:
|
|
386
|
+
_start = _check_elements(start, (int, float))
|
|
387
|
+
|
|
388
|
+
if end is None:
|
|
389
|
+
_end = [None] * len(labels)
|
|
390
|
+
elif isinstance(end, int | float):
|
|
391
|
+
_end = [end] * len(labels)
|
|
392
|
+
else:
|
|
393
|
+
_end = _check_elements(end, (int, float))
|
|
394
|
+
|
|
395
|
+
if active is None:
|
|
396
|
+
_active = [True] * len(labels)
|
|
397
|
+
else:
|
|
398
|
+
_active = _check_elements(active, (bool,))
|
|
399
|
+
|
|
400
|
+
all_lengths = [
|
|
401
|
+
len(labels),
|
|
402
|
+
len(_wavelength_id),
|
|
403
|
+
len(_colors),
|
|
404
|
+
len(_start),
|
|
405
|
+
len(_end),
|
|
406
|
+
len(_active),
|
|
407
|
+
]
|
|
408
|
+
if len(set(all_lengths)) != 1:
|
|
409
|
+
raise NgioValueError("Channels information must all have the same length.")
|
|
410
|
+
|
|
411
|
+
channels = []
|
|
412
|
+
for ch_name, w_id, color, s, e, a in zip(
|
|
413
|
+
labels, _wavelength_id, _colors, _start, _end, _active, strict=True
|
|
414
|
+
):
|
|
415
|
+
channels.append(
|
|
416
|
+
Channel.default_init(
|
|
417
|
+
label=ch_name,
|
|
418
|
+
wavelength_id=w_id,
|
|
419
|
+
color=color,
|
|
420
|
+
start=s,
|
|
421
|
+
end=e,
|
|
422
|
+
active=a,
|
|
423
|
+
data_type=data_type,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
return cls(channels=channels, **omero_kwargs)
|
|
427
|
+
|
|
428
|
+
@property
|
|
429
|
+
def channel_labels(self) -> list[str]:
|
|
430
|
+
"""Get the labels of the channels in the image."""
|
|
431
|
+
return [channel.label for channel in self.channels]
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def channel_wavelength_ids(self) -> list[str | None]:
|
|
435
|
+
"""Get the wavelength IDs of the channels in the image."""
|
|
436
|
+
return [channel.wavelength_id for channel in self.channels]
|
|
437
|
+
|
|
438
|
+
def get_channel_idx(
|
|
439
|
+
self, channel_label: str | None = None, wavelength_id: str | None = None
|
|
440
|
+
) -> int:
|
|
441
|
+
"""Get the index of a channel by its label or wavelength ID."""
|
|
442
|
+
# Only one of the arguments must be provided
|
|
443
|
+
if channel_label is not None and wavelength_id is not None:
|
|
444
|
+
raise NgioValueError(
|
|
445
|
+
"get_channel_idx must receive either label or wavelength_id, not both."
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if channel_label is not None:
|
|
449
|
+
if channel_label not in self.channel_labels:
|
|
450
|
+
raise NgioValueError(f"Channel with label {channel_label} not found.")
|
|
451
|
+
return self.channel_labels.index(channel_label)
|
|
452
|
+
|
|
453
|
+
if wavelength_id is not None:
|
|
454
|
+
if wavelength_id not in self.channel_wavelength_ids:
|
|
455
|
+
raise NgioValueError(
|
|
456
|
+
f"Channel with wavelength ID {wavelength_id} not found."
|
|
457
|
+
)
|
|
458
|
+
return self.channel_wavelength_ids.index(wavelength_id)
|
|
459
|
+
|
|
460
|
+
raise NgioValueError(
|
|
461
|
+
"get_channel_idx must receive either label or wavelength_id"
|
|
462
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Fractal internal module for dataset metadata handling."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from ngio.ome_zarr_meta.ngio_specs._axes import (
|
|
6
|
+
AxesHandler,
|
|
7
|
+
)
|
|
8
|
+
from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
|
|
9
|
+
from ngio.utils import NgioValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Dataset:
|
|
13
|
+
"""Model for a dataset in the multiscale."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
*,
|
|
18
|
+
# args coming from ngff specs
|
|
19
|
+
path: str,
|
|
20
|
+
axes_handler: AxesHandler,
|
|
21
|
+
scale: Sequence[float],
|
|
22
|
+
translation: Sequence[float] | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize the Dataset object.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
path (str): The path of the dataset.
|
|
28
|
+
axes_handler (AxesHandler): The axes handler object.
|
|
29
|
+
scale (list[float]): The list of scale transformation.
|
|
30
|
+
The scale transformation must have the same length as the axes.
|
|
31
|
+
translation (list[float] | None): The list of translation.
|
|
32
|
+
The translation must have the same length as the axes.
|
|
33
|
+
"""
|
|
34
|
+
self._path = path
|
|
35
|
+
self._axes_handler = axes_handler
|
|
36
|
+
|
|
37
|
+
if len(scale) != len(axes_handler.axes):
|
|
38
|
+
raise NgioValidationError(
|
|
39
|
+
"The length of the scale transformation must be the same as the axes."
|
|
40
|
+
)
|
|
41
|
+
self._scale = list(scale)
|
|
42
|
+
|
|
43
|
+
translation = translation or [0.0] * len(axes_handler.axes)
|
|
44
|
+
if len(translation) != len(axes_handler.axes):
|
|
45
|
+
raise NgioValidationError(
|
|
46
|
+
"The length of the translation must be the same as the axes."
|
|
47
|
+
)
|
|
48
|
+
self._translation = list(translation)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def path(self) -> str:
|
|
52
|
+
"""Return the path of the dataset."""
|
|
53
|
+
return self._path
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def axes_handler(self) -> AxesHandler:
|
|
57
|
+
"""Return the axes handler object."""
|
|
58
|
+
return self._axes_handler
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def pixel_size(self) -> PixelSize:
|
|
62
|
+
"""Return the pixel size for the dataset."""
|
|
63
|
+
scale = self._scale
|
|
64
|
+
pix_size_dict = {}
|
|
65
|
+
# Mandatory axes: x, y
|
|
66
|
+
for ax in ["x", "y"]:
|
|
67
|
+
index = self.axes_handler.get_index(ax)
|
|
68
|
+
assert index is not None
|
|
69
|
+
pix_size_dict[ax] = scale[index]
|
|
70
|
+
|
|
71
|
+
for ax in ["z", "t"]:
|
|
72
|
+
index = self.axes_handler.get_index(ax)
|
|
73
|
+
pix_size_dict[ax] = scale[index] if index is not None else 1.0
|
|
74
|
+
|
|
75
|
+
return PixelSize(
|
|
76
|
+
**pix_size_dict,
|
|
77
|
+
space_unit=self.axes_handler.space_unit,
|
|
78
|
+
time_unit=self.axes_handler.time_unit,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def scale(self) -> tuple[float, ...]:
|
|
83
|
+
"""Return the scale transformation as a tuple."""
|
|
84
|
+
return tuple(self._scale)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def translation(self) -> tuple[float, ...]:
|
|
88
|
+
"""Return the translation as a tuple."""
|
|
89
|
+
return tuple(self._translation)
|