ngio 0.2.0b3__py3-none-any.whl → 0.2.2__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.
@@ -6,6 +6,9 @@ from ngio.ome_zarr_meta.ngio_specs._axes import (
6
6
  AxesMapper,
7
7
  AxesSetup,
8
8
  Axis,
9
+ AxisType,
10
+ DefaultSpaceUnit,
11
+ DefaultTimeUnit,
9
12
  SpaceUnits,
10
13
  TimeUnits,
11
14
  )
@@ -86,7 +89,7 @@ class Dataset:
86
89
  return self._path
87
90
 
88
91
  @property
89
- def space_unit(self) -> SpaceUnits:
92
+ def space_unit(self) -> str | None:
90
93
  """Return the space unit for a given axis."""
91
94
  x_axis = self._axes_mapper.get_axis("x")
92
95
  y_axis = self._axes_mapper.get_axis("y")
@@ -97,8 +100,6 @@ class Dataset:
97
100
  )
98
101
 
99
102
  if x_axis.unit == y_axis.unit:
100
- if not isinstance(x_axis.unit, SpaceUnits):
101
- raise NgioValidationError("The space unit must be of type SpaceUnits.")
102
103
  return x_axis.unit
103
104
  else:
104
105
  raise NgioValidationError(
@@ -107,13 +108,11 @@ class Dataset:
107
108
  )
108
109
 
109
110
  @property
110
- def time_unit(self) -> TimeUnits | None:
111
+ def time_unit(self) -> str | None:
111
112
  """Return the time unit for a given axis."""
112
113
  t_axis = self._axes_mapper.get_axis("t")
113
114
  if t_axis is None:
114
115
  return None
115
- if not isinstance(t_axis.unit, TimeUnits):
116
- raise NgioValidationError("The time unit must be of type TimeUnits.")
117
116
  return t_axis.unit
118
117
 
119
118
  @property
@@ -124,11 +123,50 @@ class Dataset:
124
123
  y=self.get_scale("y"),
125
124
  z=self.get_scale("z"),
126
125
  t=self.get_scale("t"),
127
- space_unit=self.space_unit,
128
- time_unit=self.time_unit,
126
+ space_unit=self.space_unit, # type: ignore
127
+ time_unit=self.time_unit, # type: ignore
129
128
  )
130
129
 
131
130
  @property
132
131
  def axes_mapper(self) -> AxesMapper:
133
132
  """Return the axes mapper object."""
134
133
  return self._axes_mapper
134
+
135
+ def to_units(
136
+ self,
137
+ *,
138
+ space_unit: SpaceUnits = DefaultSpaceUnit,
139
+ time_unit: TimeUnits = DefaultTimeUnit,
140
+ ) -> "Dataset":
141
+ """Convert the pixel size to the given units.
142
+
143
+ Args:
144
+ space_unit(str): The space unit to convert to.
145
+ time_unit(str): The time unit to convert to.
146
+ """
147
+ new_axes = []
148
+ for ax in self.axes_mapper.on_disk_axes:
149
+ if ax.axis_type == AxisType.space:
150
+ new_ax = Axis(
151
+ on_disk_name=ax.on_disk_name,
152
+ axis_type=ax.axis_type,
153
+ unit=space_unit,
154
+ )
155
+ new_axes.append(new_ax)
156
+ elif ax.axis_type == AxisType.time:
157
+ new_ax = Axis(
158
+ on_disk_name=ax.on_disk_name, axis_type=ax.axis_type, unit=time_unit
159
+ )
160
+ new_axes.append(new_ax)
161
+ else:
162
+ new_axes.append(ax)
163
+
164
+ return Dataset(
165
+ path=self.path,
166
+ on_disk_axes=new_axes,
167
+ on_disk_scale=self._on_disk_scale,
168
+ on_disk_translation=self._on_disk_translation,
169
+ axes_setup=self.axes_mapper.axes_setup,
170
+ allow_non_canonical_axes=self.axes_mapper.allow_non_canonical_axes,
171
+ strict_canonical_order=self.axes_mapper.strict_canonical_order,
172
+ )
@@ -1,6 +1,6 @@
1
1
  """HCS (High Content Screening) specific metadata classes for NGIO."""
2
2
 
3
- from typing import Literal
3
+ from typing import Annotated
4
4
 
5
5
  from ome_zarr_models.v04.hcs import HCSAttrs
6
6
  from ome_zarr_models.v04.plate import (
@@ -10,11 +10,34 @@ from ome_zarr_models.v04.plate import (
10
10
  Row,
11
11
  WellInPlate,
12
12
  )
13
- from ome_zarr_models.v04.well import WellAttrs
14
- from ome_zarr_models.v04.well_types import WellImage, WellMeta
15
- from pydantic import BaseModel
16
-
17
- from ngio.utils import NgioValueError
13
+ from ome_zarr_models.v04.well import WellAttrs as WellAttrs04
14
+ from ome_zarr_models.v04.well_types import WellImage as WellImage04
15
+ from ome_zarr_models.v04.well_types import WellMeta as WellMeta04
16
+ from pydantic import BaseModel, SkipValidation, field_serializer
17
+
18
+ from ngio.ome_zarr_meta.ngio_specs._ngio_image import DefaultNgffVersion, NgffVersions
19
+ from ngio.utils import NgioValueError, ngio_logger
20
+
21
+
22
+ def path_in_well_validation(path: str) -> str:
23
+ """Validate the path in the well."""
24
+ if path.find("_") != -1:
25
+ # Remove underscores from the path
26
+ # This is a custom serialization step
27
+ old_value = path
28
+ path = path.replace("_", "")
29
+ ngio_logger.warning(
30
+ f"Underscores in well-paths are not allowed. "
31
+ f"Path '{old_value}' was changed to '{path}'"
32
+ f" to comply with the specification."
33
+ )
34
+ # Check if the value contains only alphanumeric characters
35
+ if not path.isalnum():
36
+ raise NgioValueError(
37
+ f"Path '{path}' contains non-alphanumeric characters. "
38
+ f"Only alphanumeric characters are allowed."
39
+ )
40
+ return path
18
41
 
19
42
 
20
43
  class ImageInWellPath(BaseModel):
@@ -27,23 +50,55 @@ class ImageInWellPath(BaseModel):
27
50
  acquisition_name: str | None = None
28
51
 
29
52
 
30
- class NgioWellMeta(WellAttrs):
53
+ class CustomWellImage(WellImage04):
54
+ path: Annotated[str, SkipValidation]
55
+
56
+ @field_serializer("path")
57
+ def serialize_path(self, value: str) -> str:
58
+ """Custom serialization for the path."""
59
+ return path_in_well_validation(value)
60
+
61
+
62
+ class CustomWellMeta(WellMeta04):
63
+ images: list[CustomWellImage] # type: ignore[valid-type]
64
+
65
+
66
+ class CustomWellAttrs(WellAttrs04):
67
+ well: CustomWellMeta # type: ignore[valid-type]
68
+
69
+
70
+ class NgioWellMeta(CustomWellAttrs):
31
71
  """HCS well metadata."""
32
72
 
33
73
  @classmethod
34
74
  def default_init(
35
75
  cls,
36
- images: list[ImageInWellPath] | None = None,
37
- version: Literal["0.4"] | None = None,
76
+ version: NgffVersions | None = None,
38
77
  ) -> "NgioWellMeta":
39
- well = cls(well=WellMeta(images=[], version=version))
40
- if images is None:
41
- return well
42
-
43
- for image in images:
44
- well = well.add_image(path=image.path, acquisition=image.acquisition_id)
78
+ if version is None:
79
+ version = DefaultNgffVersion
80
+ well = cls(well=CustomWellMeta(images=[], version=version))
45
81
  return well
46
82
 
83
+ @property
84
+ def acquisition_ids(self) -> list[int]:
85
+ """Return the acquisition ids in the well."""
86
+ acquisitions = []
87
+ for images in self.well.images:
88
+ if (
89
+ images.acquisition is not None
90
+ and images.acquisition not in acquisitions
91
+ ):
92
+ acquisitions.append(images.acquisition)
93
+ return acquisitions
94
+
95
+ def get_image_acquisition_id(self, image_path: str) -> int | None:
96
+ """Return the acquisition id for the given image path."""
97
+ for images in self.well.images:
98
+ if images.path == image_path:
99
+ return images.acquisition
100
+ raise NgioValueError(f"Image at path {image_path} not found in the well.")
101
+
47
102
  def paths(self, acquisition: int | None = None) -> list[str]:
48
103
  """Return the images paths in the well.
49
104
 
@@ -61,12 +116,16 @@ class NgioWellMeta(WellAttrs):
61
116
  if images.acquisition == acquisition
62
117
  ]
63
118
 
64
- def add_image(self, path: str, acquisition: int | None = None) -> "NgioWellMeta":
119
+ def add_image(
120
+ self, path: str, acquisition: int | None = None, strict: bool = True
121
+ ) -> "NgioWellMeta":
65
122
  """Add an image to the well.
66
123
 
67
124
  Args:
68
125
  path (str): The path of the image.
69
126
  acquisition (int | None): The acquisition id of the image.
127
+ strict (bool): If True, check if the image already exists in the well.
128
+ If False, do not check if the image already exists in the well.
70
129
  """
71
130
  list_of_images = self.well.images
72
131
  for image in list_of_images:
@@ -75,10 +134,20 @@ class NgioWellMeta(WellAttrs):
75
134
  f"Image at path {path} already exists in the well."
76
135
  )
77
136
 
78
- new_image = WellImage(path=path, acquisition=acquisition)
137
+ if (
138
+ strict
139
+ and (acquisition is not None)
140
+ and (acquisition not in self.acquisition_ids)
141
+ ):
142
+ raise NgioValueError(
143
+ f"Acquisition ID {acquisition} not found in well. "
144
+ "Please add it to the plate metadata first."
145
+ )
146
+
147
+ new_image = CustomWellImage(path=path, acquisition=acquisition)
79
148
  list_of_images.append(new_image)
80
149
  return NgioWellMeta(
81
- well=WellMeta(images=list_of_images, version=self.well.version)
150
+ well=CustomWellMeta(images=list_of_images, version=self.well.version)
82
151
  )
83
152
 
84
153
  def remove_image(self, path: str) -> "NgioWellMeta":
@@ -92,7 +161,9 @@ class NgioWellMeta(WellAttrs):
92
161
  if image.path == path:
93
162
  list_of_images.remove(image)
94
163
  return NgioWellMeta(
95
- well=WellMeta(images=list_of_images, version=self.well.version)
164
+ well=CustomWellMeta(
165
+ images=list_of_images, version=self.well.version
166
+ )
96
167
  )
97
168
  raise NgioValueError(f"Image at path {path} not found in the well.")
98
169
 
@@ -164,7 +235,7 @@ class NgioPlateMeta(HCSAttrs):
164
235
  cls,
165
236
  images: list[ImageInWellPath] | None = None,
166
237
  name: str | None = None,
167
- version: str | None = None,
238
+ version: NgffVersions | None = None,
168
239
  ) -> "NgioPlateMeta":
169
240
  plate = cls(
170
241
  plate=Plate(
@@ -185,9 +256,12 @@ class NgioPlateMeta(HCSAttrs):
185
256
  plate = plate.add_well(
186
257
  row=image.row,
187
258
  column=image.column,
188
- acquisition_id=image.acquisition_id,
189
- acquisition_name=image.acquisition_name,
190
259
  )
260
+ if image.acquisition_id is not None:
261
+ plate = plate.add_acquisition(
262
+ acquisition_id=image.acquisition_id,
263
+ acquisition_name=image.acquisition_name,
264
+ )
191
265
  return plate
192
266
 
193
267
  @property
@@ -208,7 +282,7 @@ class NgioPlateMeta(HCSAttrs):
208
282
  return [acquisitions.name for acquisitions in self.plate.acquisitions]
209
283
 
210
284
  @property
211
- def acquisitions_ids(self) -> list[int]:
285
+ def acquisition_ids(self) -> list[int]:
212
286
  """Return the acquisitions ids in the plate."""
213
287
  if self.plate.acquisitions is None:
214
288
  return []
@@ -255,50 +329,94 @@ class NgioPlateMeta(HCSAttrs):
255
329
  f"Well at row {row} and column {column} not found in the plate."
256
330
  )
257
331
 
258
- def add_well(
259
- self,
260
- row: str,
261
- column: str | int,
262
- acquisition_id: int | None = None,
263
- acquisition_name: str | None = None,
264
- **acquisition_kwargs,
265
- ) -> "NgioPlateMeta":
266
- """Add an image to the well.
332
+ def add_row(self, row: str) -> "tuple[NgioPlateMeta, int]":
333
+ """Add a row to the plate.
267
334
 
268
335
  Args:
269
- row (str): The row of the well.
270
- column (str | int): The column of the well.
271
- acquisition_id (int | None): The acquisition id of the well.
272
- acquisition_name (str | None): The acquisition name of the well.
273
- **acquisition_kwargs: Additional acquisition metadata.
336
+ row (str): The row to add.
274
337
  """
275
338
  relabel_wells = False
276
339
 
277
340
  row_names = self.rows
278
341
  row_idx = _find_row_index(row_names, row)
279
- if row_idx is None:
280
- row_names.append(row)
281
- row_names.sort()
282
- row_idx = row_names.index(row)
283
- relabel_wells = True
342
+ if row_idx is not None:
343
+ # Nothing to do
344
+ return self, row_idx
345
+
346
+ row_names.append(row)
347
+ row_names.sort()
348
+ row_idx = row_names.index(row)
349
+ relabel_wells = True
284
350
 
285
351
  rows = [Row(name=row) for row in row_names]
286
352
 
353
+ if relabel_wells:
354
+ wells = _relabel_wells(self.plate.wells, rows, self.plate.columns)
355
+ else:
356
+ wells = self.plate.wells
357
+
358
+ new_plate = Plate(
359
+ rows=rows,
360
+ columns=self.plate.columns,
361
+ acquisitions=self.plate.acquisitions,
362
+ wells=wells,
363
+ field_count=self.plate.field_count,
364
+ version=self.plate.version,
365
+ )
366
+ return NgioPlateMeta(plate=new_plate), row_idx
367
+
368
+ def add_column(self, column: str | int) -> "tuple[NgioPlateMeta, int]":
369
+ """Add a column to the plate.
370
+
371
+ Args:
372
+ column (str | int): The column to add.
373
+ """
374
+ relabel_wells = False
375
+
287
376
  columns_names = self.columns
288
377
  column_idx = _find_column_index(columns_names, column)
289
- if column_idx is None:
290
- columns_names.append(_stringify_column(column))
291
- # sort as numbers
292
- columns_names.sort(key=lambda x: int(x))
293
- column_idx = columns_names.index(_stringify_column(column))
294
- relabel_wells = True
378
+ if column_idx is not None:
379
+ # Nothing to do
380
+ return self, column_idx
381
+
382
+ columns_names.append(_stringify_column(column))
383
+ # sort as numbers
384
+ columns_names.sort(key=lambda x: int(x))
385
+ column_idx = columns_names.index(_stringify_column(column))
386
+ relabel_wells = True
295
387
 
296
388
  columns = [Column(name=column) for column in columns_names]
297
389
 
298
- wells = self.plate.wells
299
390
  if relabel_wells:
300
- wells = _relabel_wells(wells, rows, columns)
391
+ wells = _relabel_wells(self.plate.wells, self.plate.rows, columns)
392
+ else:
393
+ wells = self.plate.wells
301
394
 
395
+ new_plate = Plate(
396
+ rows=self.plate.rows,
397
+ columns=columns,
398
+ acquisitions=self.plate.acquisitions,
399
+ wells=wells,
400
+ field_count=self.plate.field_count,
401
+ version=self.plate.version,
402
+ )
403
+ return NgioPlateMeta(plate=new_plate), column_idx
404
+
405
+ def add_well(
406
+ self,
407
+ row: str,
408
+ column: str | int,
409
+ ) -> "NgioPlateMeta":
410
+ """Add an image to the well.
411
+
412
+ Args:
413
+ row (str): The row of the well.
414
+ column (str | int): The column of the well.
415
+ """
416
+ plate, row_idx = self.add_row(row=row)
417
+ plate, column_idx = plate.add_column(column=column)
418
+
419
+ wells = plate.plate.wells
302
420
  for well_obj in wells:
303
421
  if well_obj.rowIndex == row_idx and well_obj.columnIndex == column_idx:
304
422
  break
@@ -311,28 +429,49 @@ class NgioPlateMeta(HCSAttrs):
311
429
  )
312
430
  )
313
431
 
432
+ new_plate = Plate(
433
+ rows=plate.plate.rows,
434
+ columns=plate.plate.columns,
435
+ acquisitions=plate.plate.acquisitions,
436
+ wells=wells,
437
+ field_count=plate.plate.field_count,
438
+ version=plate.plate.version,
439
+ )
440
+ return NgioPlateMeta(plate=new_plate)
441
+
442
+ def add_acquisition(
443
+ self,
444
+ acquisition_id: int,
445
+ acquisition_name: str | None = None,
446
+ **acquisition_kwargs,
447
+ ) -> "NgioPlateMeta":
448
+ """Add an acquisition to the plate.
449
+
450
+ Args:
451
+ acquisition_id (int): The acquisition id of the well.
452
+ acquisition_name (str | None): The acquisition name of the well.
453
+ **acquisition_kwargs: Additional acquisition metadata.
454
+ """
314
455
  acquisitions = self.plate.acquisitions
315
- if acquisition_id is not None:
316
- if acquisitions is None and len(wells) > 0:
317
- acquisitions = [Acquisition(id=0, name=acquisition_name)]
318
- elif acquisitions is None:
319
- acquisitions = []
320
-
321
- for acquisition_obj in acquisitions:
322
- if acquisition_obj.id == acquisition_id:
323
- break
324
- else:
325
- acquisitions.append(
326
- Acquisition(
327
- id=acquisition_id, name=acquisition_name, **acquisition_kwargs
328
- )
329
- )
456
+ if acquisitions is None:
457
+ acquisitions = []
458
+
459
+ for acquisition_obj in acquisitions:
460
+ if acquisition_obj.id == acquisition_id:
461
+ # If the acquisition already exists
462
+ # Nothing to do
463
+ # Maybe we should update the acquisition name and kwargs
464
+ return self
465
+
466
+ acquisitions.append(
467
+ Acquisition(id=acquisition_id, name=acquisition_name, **acquisition_kwargs)
468
+ )
330
469
 
331
470
  new_plate = Plate(
332
- rows=rows,
333
- columns=columns,
471
+ rows=self.plate.rows,
472
+ columns=self.plate.columns,
334
473
  acquisitions=acquisitions,
335
- wells=wells,
474
+ wells=self.plate.wells,
336
475
  field_count=self.plate.field_count,
337
476
  version=self.plate.version,
338
477
  )
@@ -373,5 +512,39 @@ class NgioPlateMeta(HCSAttrs):
373
512
  )
374
513
  return NgioPlateMeta(plate=new_plate)
375
514
 
515
+ def derive(
516
+ self,
517
+ name: str | None = None,
518
+ version: NgffVersions | None = None,
519
+ keep_acquisitions: bool = False,
520
+ ) -> "NgioPlateMeta":
521
+ """Derive the plate metadata.
522
+
523
+ Args:
524
+ name (str): The name of the derived plate.
525
+ version (NgffVersion | None): The version of the derived plate.
526
+ If None, use the version of the original plate.
527
+ keep_acquisitions (bool): If True, keep the acquisitions in the plate.
528
+ """
529
+ columns = self.plate.columns
530
+ rows = self.plate.rows
531
+
532
+ if keep_acquisitions:
533
+ acquisitions = self.plate.acquisitions
534
+ else:
535
+ acquisitions = None
376
536
 
377
- # %%
537
+ if version is None:
538
+ version = self.plate.version # type: ignore[assignment]
539
+
540
+ return NgioPlateMeta(
541
+ plate=Plate(
542
+ rows=rows,
543
+ columns=columns,
544
+ acquisitions=acquisitions,
545
+ wells=[],
546
+ field_count=self.plate.field_count,
547
+ version=version,
548
+ name=name,
549
+ )
550
+ )
@@ -7,13 +7,16 @@ can be converted to the OME standard.
7
7
  """
8
8
 
9
9
  from collections.abc import Collection
10
- from enum import Enum
11
- from typing import Any, TypeVar
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
+ DefaultSpaceUnit,
17
+ DefaultTimeUnit,
18
+ SpaceUnits,
19
+ TimeUnits,
17
20
  canonical_axes,
18
21
  )
19
22
  from ngio.ome_zarr_meta.ngio_specs._channels import Channel, ChannelsMeta
@@ -22,22 +25,18 @@ from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize
22
25
  from ngio.utils import NgioValidationError, NgioValueError
23
26
 
24
27
  T = TypeVar("T")
25
-
26
-
27
- class NgffVersion(str, Enum):
28
- """Allowed NGFF versions."""
29
-
30
- v04 = "0.4"
28
+ NgffVersions = Literal["0.4"]
29
+ DefaultNgffVersion: Literal["0.4"] = "0.4"
31
30
 
32
31
 
33
32
  class ImageLabelSource(BaseModel):
34
33
  """Image label source model."""
35
34
 
36
- version: NgffVersion
35
+ version: NgffVersions
37
36
  source: dict[str, str | None]
38
37
 
39
38
  @classmethod
40
- def default_init(cls, version: NgffVersion) -> "ImageLabelSource":
39
+ def default_init(cls, version: NgffVersions) -> "ImageLabelSource":
41
40
  """Initialize the ImageLabelSource object."""
42
41
  return cls(version=version, source={"image": "../../"})
43
42
 
@@ -45,9 +44,11 @@ class ImageLabelSource(BaseModel):
45
44
  class AbstractNgioImageMeta:
46
45
  """Base class for ImageMeta and LabelMeta."""
47
46
 
48
- def __init__(self, version: str, name: str | None, datasets: list[Dataset]) -> None:
47
+ def __init__(
48
+ self, version: NgffVersions, name: str | None, datasets: list[Dataset]
49
+ ) -> None:
49
50
  """Initialize the ImageMeta object."""
50
- self._version = NgffVersion(version)
51
+ self._version = version
51
52
  self._name = name
52
53
 
53
54
  if len(datasets) == 0:
@@ -70,13 +71,13 @@ class AbstractNgioImageMeta:
70
71
  pixel_size: PixelSize,
71
72
  scaling_factors: Collection[float] | None = None,
72
73
  name: str | None = None,
73
- version: str = "0.4",
74
+ version: NgffVersions = DefaultNgffVersion,
74
75
  ):
75
76
  """Initialize the ImageMeta object."""
76
77
  axes = canonical_axes(
77
78
  axes_names,
78
- space_units=pixel_size.space_unit,
79
- time_units=pixel_size.time_unit,
79
+ space_units=pixel_size.space_unit, # type: ignore[arg-type]
80
+ time_units=pixel_size.time_unit, # type: ignore[arg-type]
80
81
  )
81
82
 
82
83
  px_size_dict = pixel_size.as_dict()
@@ -109,10 +110,33 @@ class AbstractNgioImageMeta:
109
110
  datasets=datasets,
110
111
  )
111
112
 
113
+ def to_units(
114
+ self,
115
+ *,
116
+ space_unit: SpaceUnits = DefaultSpaceUnit,
117
+ time_unit: TimeUnits = DefaultTimeUnit,
118
+ ):
119
+ """Convert the pixel size to the given units.
120
+
121
+ Args:
122
+ space_unit(str): The space unit to convert to.
123
+ time_unit(str): The time unit to convert to.
124
+ """
125
+ new_datasets = []
126
+ for dataset in self.datasets:
127
+ new_dataset = dataset.to_units(space_unit=space_unit, time_unit=time_unit)
128
+ new_datasets.append(new_dataset)
129
+
130
+ return type(self)(
131
+ version=self.version,
132
+ name=self.name,
133
+ datasets=new_datasets,
134
+ )
135
+
112
136
  @property
113
- def version(self) -> NgffVersion:
137
+ def version(self) -> NgffVersions:
114
138
  """Version of the OME-NFF metadata used to build the object."""
115
- return self._version
139
+ return self._version # type: ignore[return-value]
116
140
 
117
141
  @property
118
142
  def name(self) -> str | None:
@@ -139,6 +163,16 @@ class AbstractNgioImageMeta:
139
163
  """List of paths of the datasets."""
140
164
  return [dataset.path for dataset in self.datasets]
141
165
 
166
+ @property
167
+ def space_unit(self) -> str | None:
168
+ """Get the space unit of the pixel size."""
169
+ return self.datasets[0].pixel_size.space_unit
170
+
171
+ @property
172
+ def time_unit(self) -> str | None:
173
+ """Get the time unit of the pixel size."""
174
+ return self.datasets[0].pixel_size.time_unit
175
+
142
176
  def _get_dataset_by_path(self, path: str) -> Dataset:
143
177
  """Get a dataset by its path."""
144
178
  for dataset in self.datasets:
@@ -335,7 +369,7 @@ class NgioLabelMeta(AbstractNgioImageMeta):
335
369
 
336
370
  def __init__(
337
371
  self,
338
- version: str,
372
+ version: NgffVersions,
339
373
  name: str | None,
340
374
  datasets: list[Dataset],
341
375
  image_label: ImageLabelSource | None = None,
@@ -374,7 +408,7 @@ class NgioImageMeta(AbstractNgioImageMeta):
374
408
 
375
409
  def __init__(
376
410
  self,
377
- version: str,
411
+ version: NgffVersions,
378
412
  name: str | None,
379
413
  datasets: list[Dataset],
380
414
  channels: ChannelsMeta | None = None,