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.
Files changed (106) hide show
  1. ngio/__init__.py +40 -12
  2. ngio/common/__init__.py +16 -32
  3. ngio/common/_dimensions.py +270 -48
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +267 -73
  6. ngio/common/_roi.py +290 -66
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +54 -22
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +126 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/__init__.py +17 -58
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +30 -9
  20. ngio/images/_abstract_image.py +968 -0
  21. ngio/images/_create_synt_container.py +132 -0
  22. ngio/images/_create_utils.py +423 -0
  23. ngio/images/_image.py +926 -0
  24. ngio/images/_label.py +417 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1235 -0
  27. ngio/images/_table_ops.py +471 -0
  28. ngio/io_pipes/__init__.py +75 -0
  29. ngio/io_pipes/_io_pipes.py +361 -0
  30. ngio/io_pipes/_io_pipes_masked.py +488 -0
  31. ngio/io_pipes/_io_pipes_roi.py +146 -0
  32. ngio/io_pipes/_io_pipes_types.py +56 -0
  33. ngio/io_pipes/_match_shape.py +377 -0
  34. ngio/io_pipes/_ops_axes.py +344 -0
  35. ngio/io_pipes/_ops_slices.py +411 -0
  36. ngio/io_pipes/_ops_slices_utils.py +199 -0
  37. ngio/io_pipes/_ops_transforms.py +104 -0
  38. ngio/io_pipes/_zoom_transform.py +180 -0
  39. ngio/ome_zarr_meta/__init__.py +39 -15
  40. ngio/ome_zarr_meta/_meta_handlers.py +490 -96
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
  48. ngio/ome_zarr_meta/v04/__init__.py +21 -5
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
  51. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  52. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  53. ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
  54. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  55. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  56. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  57. ngio/resources/__init__.py +55 -0
  58. ngio/resources/resource_model.py +36 -0
  59. ngio/tables/__init__.py +20 -4
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +50 -1
  63. ngio/tables/backends/_abstract_backend.py +200 -31
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +10 -114
  66. ngio/tables/backends/_csv.py +19 -0
  67. ngio/tables/backends/_json.py +92 -0
  68. ngio/tables/backends/_parquet.py +19 -0
  69. ngio/tables/backends/_py_arrow_backends.py +222 -0
  70. ngio/tables/backends/_table_backends.py +162 -38
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +19 -4
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +79 -115
  75. ngio/tables/v1/_generic_table.py +21 -90
  76. ngio/tables/v1/_roi_table.py +486 -137
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +16 -14
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +121 -13
  82. ngio/utils/_fractal_fsspec_store.py +42 -0
  83. ngio/utils/_zarr_utils.py +374 -218
  84. ngio-0.5.0b4.dist-info/METADATA +147 -0
  85. ngio-0.5.0b4.dist-info/RECORD +88 -0
  86. {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
  87. ngio/common/_array_pipe.py +0 -160
  88. ngio/common/_axes_transforms.py +0 -63
  89. ngio/common/_common_types.py +0 -5
  90. ngio/common/_slicer.py +0 -97
  91. ngio/images/abstract_image.py +0 -240
  92. ngio/images/create.py +0 -251
  93. ngio/images/image.py +0 -389
  94. ngio/images/label.py +0 -236
  95. ngio/images/omezarr_container.py +0 -535
  96. ngio/ome_zarr_meta/_generic_handlers.py +0 -320
  97. ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
  98. ngio/tables/_validators.py +0 -192
  99. ngio/tables/backends/_anndata_v1.py +0 -75
  100. ngio/tables/backends/_json_v1.py +0 -56
  101. ngio/tables/tables_container.py +0 -300
  102. ngio/tables/v1/_masking_roi_table.py +0 -175
  103. ngio/utils/_logger.py +0 -29
  104. ngio-0.2.0a2.dist-info/METADATA +0 -95
  105. ngio-0.2.0a2.dist-info/RECORD +0 -53
  106. {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 Collection
10
- from enum import Enum
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 Channel, ChannelsMeta
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: NgffVersion
35
+ version: NgffVersions
39
36
  source: dict[str, str | None]
40
37
 
41
38
  @classmethod
42
- def default_init(cls, version: NgffVersion) -> "ImageLabelSource":
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__(self, version: str, name: str | None, datasets: list[Dataset]) -> None:
54
+ def __init__(
55
+ self, version: NgffVersions, name: str | None, datasets: list[Dataset]
56
+ ) -> None:
51
57
  """Initialize the ImageMeta object."""
52
- self._version = NgffVersion(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
- 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})"
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) -> NgffVersion:
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 _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.
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
- 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.
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
- for dataset in self.datasets:
122
- dist = dataset.pixel_size.distance(pixel_size)
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 = 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 min_dist > tol:
131
- raise NgioValueError("No dataset with a pixel size close enough.")
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
- != 1
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._get_dataset_by_pixel_size(pixel_size, strict=strict)
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
- 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]
315
+ return self.get_highest_resolution_dataset()
253
316
 
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}"
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 x_scaling_factors
326
+ return dataset, lr_dataset
265
327
 
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")
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: str,
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: str,
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 SpaceUnits, TimeUnits
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
- 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:
26
+ @total_ordering
27
+ class PixelSize(BaseModel):
24
28
  """PixelSize class to store the pixel size in 3D space."""
25
29
 
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
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 as_dict(self) -> dict:
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 in 3D space."""
84
- return float(np.linalg.norm(np.array(self.zyx) - np.array(other.zyx)))
121
+ """Return the distance between two pixel sizes."""
122
+ return float(np.linalg.norm(np.array(self.tzyx) - np.array(other.tzyx)))