ngio 0.2.9__py3-none-any.whl → 0.3.0__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 (38) hide show
  1. ngio/common/__init__.py +16 -0
  2. ngio/common/_array_pipe.py +50 -27
  3. ngio/common/_table_ops.py +471 -0
  4. ngio/hcs/__init__.py +1 -1
  5. ngio/hcs/{plate.py → _plate.py} +451 -78
  6. ngio/images/__init__.py +3 -3
  7. ngio/images/{image.py → _image.py} +26 -21
  8. ngio/images/{label.py → _label.py} +6 -4
  9. ngio/images/{masked_image.py → _masked_image.py} +2 -2
  10. ngio/images/{ome_zarr_container.py → _ome_zarr_container.py} +152 -86
  11. ngio/ome_zarr_meta/_meta_handlers.py +16 -8
  12. ngio/ome_zarr_meta/ngio_specs/_channels.py +41 -29
  13. ngio/tables/__init__.py +14 -2
  14. ngio/tables/_abstract_table.py +269 -0
  15. ngio/tables/{tables_container.py → _tables_container.py} +186 -100
  16. ngio/tables/backends/__init__.py +20 -0
  17. ngio/tables/backends/_abstract_backend.py +58 -80
  18. ngio/tables/backends/{_anndata_v1.py → _anndata.py} +5 -1
  19. ngio/tables/backends/_csv.py +35 -0
  20. ngio/tables/backends/{_json_v1.py → _json.py} +4 -1
  21. ngio/tables/backends/{_csv_v1.py → _non_zarr_backends.py} +61 -27
  22. ngio/tables/backends/_parquet.py +47 -0
  23. ngio/tables/backends/_table_backends.py +39 -18
  24. ngio/tables/backends/_utils.py +147 -1
  25. ngio/tables/v1/__init__.py +19 -3
  26. ngio/tables/v1/_condition_table.py +71 -0
  27. ngio/tables/v1/_feature_table.py +63 -129
  28. ngio/tables/v1/_generic_table.py +21 -159
  29. ngio/tables/v1/_roi_table.py +285 -201
  30. ngio/utils/_fractal_fsspec_store.py +29 -0
  31. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/METADATA +4 -3
  32. ngio-0.3.0.dist-info/RECORD +61 -0
  33. ngio/tables/_validators.py +0 -108
  34. ngio-0.2.9.dist-info/RECORD +0 -57
  35. /ngio/images/{abstract_image.py → _abstract_image.py} +0 -0
  36. /ngio/images/{create.py → _create.py} +0 -0
  37. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/WHEEL +0 -0
  38. {ngio-0.2.9.dist-info → ngio-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,11 +3,11 @@
3
3
  from collections.abc import Collection
4
4
  from typing import Literal
5
5
 
6
- from dask import array as da
6
+ import dask.array as da
7
7
 
8
8
  from ngio.common import Dimensions
9
- from ngio.images.abstract_image import AbstractImage, consolidate_image
10
- from ngio.images.create import create_empty_image_container
9
+ from ngio.images._abstract_image import AbstractImage, consolidate_image
10
+ from ngio.images._create import create_empty_image_container
11
11
  from ngio.ome_zarr_meta import (
12
12
  ImageMetaHandler,
13
13
  NgioImageMeta,
@@ -152,31 +152,33 @@ class ImagesContainer:
152
152
 
153
153
  def set_channel_meta(
154
154
  self,
155
- labels: Collection[str] | int | None = None,
156
- wavelength_id: Collection[str] | None = None,
157
- start: Collection[float] | None = None,
158
- end: Collection[float] | None = None,
155
+ labels: Collection[str | None] | int | None = None,
156
+ wavelength_id: Collection[str | None] | None = None,
157
+ start: Collection[float | None] | None = None,
158
+ end: Collection[float | None] | None = None,
159
159
  percentiles: tuple[float, float] | None = None,
160
- colors: Collection[str] | None = None,
161
- active: Collection[bool] | None = None,
160
+ colors: Collection[str | None] | None = None,
161
+ active: Collection[bool | None] | None = None,
162
162
  **omero_kwargs: dict,
163
163
  ) -> None:
164
164
  """Create a ChannelsMeta object with the default unit.
165
165
 
166
166
  Args:
167
- labels(Collection[str] | int): The list of channels names in the image.
168
- If an integer is provided, the channels will be named "channel_i".
169
- wavelength_id(Collection[str] | None): The wavelength ID of the channel.
167
+ labels(Collection[str | None] | int): The list of channels names
168
+ in the image. If an integer is provided, the channels will
169
+ be named "channel_i".
170
+ wavelength_id(Collection[str | None]): The wavelength ID of the channel.
170
171
  If None, the wavelength ID will be the same as the channel name.
171
- start(Collection[float] | None): The start value for each channel.
172
+ start(Collection[float | None]): The start value for each channel.
172
173
  If None, the start value will be computed from the image.
173
- end(Collection[float] | None): The end value for each channel.
174
+ end(Collection[float | None]): The end value for each channel.
174
175
  If None, the end value will be computed from the image.
175
- percentiles(tuple[float, float] | None): The start and end percentiles
176
- for each channel. If None, the percentiles will not be computed.
177
- colors(Collection[str, NgioColors] | None): The list of colors for the
176
+ percentiles(tuple[float, float] | None): The start and end
177
+ percentiles for each channel. If None, the percentiles will
178
+ not be computed.
179
+ colors(Collection[str | None]): The list of colors for the
178
180
  channels. If None, the colors will be random.
179
- active (Collection[bool] | None):active(bool): Whether the channel should
181
+ active (Collection[bool | None]): Whether the channel should
180
182
  be shown by default.
181
183
  omero_kwargs(dict): Extra fields to store in the omero attributes.
182
184
  """
@@ -376,9 +378,12 @@ def compute_image_percentile(
376
378
  starts, ends = [], []
377
379
  for c in range(image.num_channels):
378
380
  if image.num_channels == 1:
379
- data = image.get_array(mode="dask").ravel()
381
+ data = image.get_array(mode="dask")
380
382
  else:
381
- data = image.get_array(c=c, mode="dask").ravel()
383
+ data = image.get_array(c=c, mode="dask")
384
+
385
+ assert isinstance(data, da.Array), "Data must be a Dask array."
386
+ data = da.ravel(data)
382
387
  # remove all the zeros
383
388
  mask = data > 1e-16
384
389
  data = data[mask]
@@ -391,7 +396,7 @@ def compute_image_percentile(
391
396
  # compute the percentiles
392
397
  _s_perc, _e_perc = da.percentile(
393
398
  data, [start_percentile, end_percentile], method="nearest"
394
- ).compute()
399
+ ).compute() # type: ignore
395
400
 
396
401
  starts.append(float(_s_perc))
397
402
  ends.append(float(_e_perc))
@@ -3,10 +3,12 @@
3
3
  from collections.abc import Collection
4
4
  from typing import Literal
5
5
 
6
+ import dask.array as da
7
+
6
8
  from ngio.common import compute_masking_roi
7
- from ngio.images.abstract_image import AbstractImage, consolidate_image
8
- from ngio.images.create import create_empty_label_container
9
- from ngio.images.image import Image
9
+ from ngio.images._abstract_image import AbstractImage, consolidate_image
10
+ from ngio.images._create import create_empty_label_container
11
+ from ngio.images._image import Image
10
12
  from ngio.ome_zarr_meta import (
11
13
  LabelMetaHandler,
12
14
  NgioLabelMeta,
@@ -309,6 +311,6 @@ def build_masking_roi_table(label: Label) -> MaskingRoiTable:
309
311
  raise NgioValueError("Time series labels are not supported.")
310
312
 
311
313
  array = label.get_array(axes_order=["z", "y", "x"], mode="dask")
312
-
314
+ assert isinstance(array, da.Array), "Array must be a Dask array."
313
315
  rois = compute_masking_roi(array, label.pixel_size)
314
316
  return MaskingRoiTable(rois, reference_label=label.meta.name)
@@ -4,8 +4,8 @@ from collections.abc import Collection, Iterable
4
4
  from typing import Literal
5
5
 
6
6
  from ngio.common import ArrayLike, get_masked_pipe, roi_to_slice_kwargs, set_masked_pipe
7
- from ngio.images.image import Image
8
- from ngio.images.label import Label
7
+ from ngio.images._image import Image
8
+ from ngio.images._label import Label
9
9
  from ngio.ome_zarr_meta import ImageMetaHandler, LabelMetaHandler
10
10
  from ngio.tables import MaskingRoiTable
11
11
  from ngio.utils import (
@@ -1,14 +1,14 @@
1
1
  """Abstract class for handling OME-NGFF images."""
2
2
 
3
+ import warnings
3
4
  from collections.abc import Collection
4
- from typing import Literal, overload
5
5
 
6
6
  import numpy as np
7
7
 
8
- from ngio.images.create import create_empty_image_container
9
- from ngio.images.image import Image, ImagesContainer
10
- from ngio.images.label import Label, LabelsContainer
11
- from ngio.images.masked_image import MaskedImage, MaskedLabel
8
+ from ngio.images._create import create_empty_image_container
9
+ from ngio.images._image import Image, ImagesContainer
10
+ from ngio.images._label import Label, LabelsContainer
11
+ from ngio.images._masked_image import MaskedImage, MaskedLabel
12
12
  from ngio.ome_zarr_meta import (
13
13
  NgioImageMeta,
14
14
  PixelSize,
@@ -22,12 +22,15 @@ from ngio.ome_zarr_meta.ngio_specs import (
22
22
  TimeUnits,
23
23
  )
24
24
  from ngio.tables import (
25
+ ConditionTable,
25
26
  FeatureTable,
26
27
  GenericRoiTable,
27
28
  MaskingRoiTable,
28
29
  RoiTable,
29
30
  Table,
31
+ TableBackend,
30
32
  TablesContainer,
33
+ TableType,
31
34
  TypedTable,
32
35
  )
33
36
  from ngio.utils import (
@@ -65,7 +68,7 @@ class OmeZarrContainer:
65
68
  group_handler: ZarrGroupHandler,
66
69
  table_container: TablesContainer | None = None,
67
70
  label_container: LabelsContainer | None = None,
68
- validate_arrays: bool = True,
71
+ validate_paths: bool = False,
69
72
  ) -> None:
70
73
  """Initialize the OmeZarrContainer."""
71
74
  self._group_handler = group_handler
@@ -74,6 +77,10 @@ class OmeZarrContainer:
74
77
  self._labels_container = label_container
75
78
  self._tables_container = table_container
76
79
 
80
+ if validate_paths:
81
+ for level_path in self._images_container.levels_paths:
82
+ self.get_image(path=level_path)
83
+
77
84
  def __repr__(self) -> str:
78
85
  """Return a string representation of the image."""
79
86
  num_labels = len(self.list_labels())
@@ -96,24 +103,40 @@ class OmeZarrContainer:
96
103
  """Return the image container."""
97
104
  return self._images_container
98
105
 
99
- @property
100
- def labels_container(self) -> LabelsContainer:
106
+ def _get_labels_container(self) -> LabelsContainer | None:
101
107
  """Return the labels container."""
102
108
  if self._labels_container is None:
103
- self._labels_container = _default_label_container(self._group_handler)
104
- if self._labels_container is None:
105
- raise NgioValidationError("No labels found in the image.")
109
+ _labels_container = _default_label_container(self._group_handler)
110
+ if _labels_container is None:
111
+ return None
112
+ self._labels_container = _labels_container
106
113
  return self._labels_container
107
114
 
108
115
  @property
109
- def tables_container(self) -> TablesContainer:
116
+ def labels_container(self) -> LabelsContainer:
117
+ """Return the labels container."""
118
+ _labels_container = self._get_labels_container()
119
+ if _labels_container is None:
120
+ raise NgioValidationError("No labels found in the image.")
121
+ return _labels_container
122
+
123
+ def _get_tables_container(self) -> TablesContainer | None:
110
124
  """Return the tables container."""
111
125
  if self._tables_container is None:
112
- self._tables_container = _default_table_container(self._group_handler)
113
- if self._tables_container is None:
114
- raise NgioValidationError("No tables found in the image.")
126
+ _tables_container = _default_table_container(self._group_handler)
127
+ if _tables_container is None:
128
+ return None
129
+ self._tables_container = _tables_container
115
130
  return self._tables_container
116
131
 
132
+ @property
133
+ def tables_container(self) -> TablesContainer:
134
+ """Return the tables container."""
135
+ _tables_container = self._get_tables_container()
136
+ if _tables_container is None:
137
+ raise NgioValidationError("No tables found in the image.")
138
+ return _tables_container
139
+
117
140
  @property
118
141
  def image_meta(self) -> NgioImageMeta:
119
142
  """Return the image metadata."""
@@ -264,9 +287,7 @@ class OmeZarrContainer:
264
287
  if masking_table_name is None:
265
288
  masking_table = masking_label.build_masking_roi_table()
266
289
  else:
267
- masking_table = self.get_table(
268
- masking_table_name, check_type="masking_roi_table"
269
- )
290
+ masking_table = self.get_masking_roi_table(name=masking_table_name)
270
291
 
271
292
  return MaskedImage(
272
293
  group_handler=image._group_handler,
@@ -331,7 +352,7 @@ class OmeZarrContainer:
331
352
 
332
353
  new_ome_zarr = OmeZarrContainer(
333
354
  group_handler=handler,
334
- validate_arrays=False,
355
+ validate_paths=False,
335
356
  )
336
357
 
337
358
  if copy_labels:
@@ -345,84 +366,128 @@ class OmeZarrContainer:
345
366
  )
346
367
  return new_ome_zarr
347
368
 
348
- def list_tables(self) -> list[str]:
369
+ def list_tables(self, filter_types: TypedTable | str | None = None) -> list[str]:
349
370
  """List all tables in the image."""
350
- return self.tables_container.list()
371
+ table_container = self._get_tables_container()
372
+ if table_container is None:
373
+ return []
374
+
375
+ return table_container.list(
376
+ filter_types=filter_types,
377
+ )
351
378
 
352
379
  def list_roi_tables(self) -> list[str]:
353
380
  """List all ROI tables in the image."""
354
- return self.tables_container.list_roi_tables()
381
+ masking_roi = self.tables_container.list(
382
+ filter_types="masking_roi_table",
383
+ )
384
+ roi = self.tables_container.list(
385
+ filter_types="roi_table",
386
+ )
387
+ return masking_roi + roi
355
388
 
356
- @overload
357
- def get_table(self, name: str) -> Table: ...
389
+ def get_roi_table(self, name: str) -> RoiTable:
390
+ """Get a ROI table from the image.
358
391
 
359
- @overload
360
- def get_table(self, name: str, check_type: None) -> Table: ...
392
+ Args:
393
+ name (str): The name of the table.
394
+ """
395
+ table = self.tables_container.get(name=name, strict=True)
396
+ if not isinstance(table, RoiTable):
397
+ raise NgioValueError(f"Table {name} is not a ROI table. Got {type(table)}")
398
+ return table
361
399
 
362
- @overload
363
- def get_table(self, name: str, check_type: Literal["roi_table"]) -> RoiTable: ...
400
+ def get_masking_roi_table(self, name: str) -> MaskingRoiTable:
401
+ """Get a masking ROI table from the image.
364
402
 
365
- @overload
366
- def get_table(
367
- self, name: str, check_type: Literal["masking_roi_table"]
368
- ) -> MaskingRoiTable: ...
403
+ Args:
404
+ name (str): The name of the table.
405
+ """
406
+ table = self.tables_container.get(name=name, strict=True)
407
+ if not isinstance(table, MaskingRoiTable):
408
+ raise NgioValueError(
409
+ f"Table {name} is not a masking ROI table. Got {type(table)}"
410
+ )
411
+ return table
369
412
 
370
- @overload
371
- def get_table(
372
- self, name: str, check_type: Literal["feature_table"]
373
- ) -> FeatureTable: ...
413
+ def get_feature_table(self, name: str) -> FeatureTable:
414
+ """Get a feature table from the image.
374
415
 
375
- @overload
376
- def get_table(
377
- self, name: str, check_type: Literal["generic_roi_table"]
378
- ) -> GenericRoiTable: ...
416
+ Args:
417
+ name (str): The name of the table.
418
+ """
419
+ table = self.tables_container.get(name=name, strict=True)
420
+ if not isinstance(table, FeatureTable):
421
+ raise NgioValueError(
422
+ f"Table {name} is not a feature table. Got {type(table)}"
423
+ )
424
+ return table
425
+
426
+ def get_generic_roi_table(self, name: str) -> GenericRoiTable:
427
+ """Get a generic ROI table from the image.
428
+
429
+ Args:
430
+ name (str): The name of the table.
431
+ """
432
+ table = self.tables_container.get(name=name, strict=True)
433
+ if not isinstance(table, GenericRoiTable):
434
+ raise NgioValueError(
435
+ f"Table {name} is not a generic ROI table. Got {type(table)}"
436
+ )
437
+ return table
438
+
439
+ def get_condition_table(self, name: str) -> ConditionTable:
440
+ """Get a condition table from the image.
441
+
442
+ Args:
443
+ name (str): The name of the table.
444
+ """
445
+ table = self.tables_container.get(name=name, strict=True)
446
+ if not isinstance(table, ConditionTable):
447
+ raise NgioValueError(
448
+ f"Table {name} is not a condition table. Got {type(table)}"
449
+ )
450
+ return table
379
451
 
380
452
  def get_table(self, name: str, check_type: TypedTable | None = None) -> Table:
381
453
  """Get a table from the image.
382
454
 
383
455
  Args:
384
456
  name (str): The name of the table.
385
- check_type (TypedTable | None): The type of the table. If None, the
386
- type is not checked. If a type is provided, the table must be of that
387
- type.
457
+ check_type (TypedTable | None): Deprecated. Please use
458
+ 'get_table_as' instead, or one of the type specific
459
+ get_*table() methods.
460
+
461
+ """
462
+ if check_type is not None:
463
+ warnings.warn(
464
+ "The 'check_type' argument is deprecated, and will be removed in "
465
+ "ngio=0.3. Use 'get_table_as' instead or one of the "
466
+ "type specific get_*table() methods.",
467
+ DeprecationWarning,
468
+ stacklevel=2,
469
+ )
470
+ return self.tables_container.get(name=name, strict=False)
471
+
472
+ def get_table_as(
473
+ self,
474
+ name: str,
475
+ table_cls: type[TableType],
476
+ backend: TableBackend | None = None,
477
+ ) -> TableType:
478
+ """Get a table from the image as a specific type.
479
+
480
+ Args:
481
+ name (str): The name of the table.
482
+ table_cls (type[TableType]): The type of the table.
483
+ backend (TableBackend | None): The backend to use. If None,
484
+ the default backend is used.
388
485
  """
389
- if check_type is None:
390
- table = self.tables_container.get(name, strict=False)
391
- return table
392
-
393
- table = self.tables_container.get(name, strict=True)
394
- match check_type:
395
- case "roi_table":
396
- if not isinstance(table, RoiTable):
397
- raise NgioValueError(
398
- f"Table '{name}' is not a ROI table. Found type: {table.type()}"
399
- )
400
- return table
401
- case "masking_roi_table":
402
- if not isinstance(table, MaskingRoiTable):
403
- raise NgioValueError(
404
- f"Table '{name}' is not a masking ROI table. "
405
- f"Found type: {table.type()}"
406
- )
407
- return table
408
-
409
- case "generic_roi_table":
410
- if not isinstance(table, GenericRoiTable):
411
- raise NgioValueError(
412
- f"Table '{name}' is not a generic ROI table. "
413
- f"Found type: {table.type()}"
414
- )
415
- return table
416
-
417
- case "feature_table":
418
- if not isinstance(table, FeatureTable):
419
- raise NgioValueError(
420
- f"Table '{name}' is not a feature table. "
421
- f"Found type: {table.type()}"
422
- )
423
- return table
424
- case _:
425
- raise NgioValueError(f"Unknown check_type: {check_type}")
486
+ return self.tables_container.get_as(
487
+ name=name,
488
+ table_cls=table_cls,
489
+ backend=backend,
490
+ )
426
491
 
427
492
  def build_image_roi_table(self, name: str = "image") -> RoiTable:
428
493
  """Compute the ROI table for an image."""
@@ -436,7 +501,7 @@ class OmeZarrContainer:
436
501
  self,
437
502
  name: str,
438
503
  table: Table,
439
- backend: str | None = None,
504
+ backend: TableBackend = "anndata",
440
505
  overwrite: bool = False,
441
506
  ) -> None:
442
507
  """Add a table to the image."""
@@ -446,7 +511,10 @@ class OmeZarrContainer:
446
511
 
447
512
  def list_labels(self) -> list[str]:
448
513
  """List all labels in the image."""
449
- return self.labels_container.list()
514
+ label_container = self._get_labels_container()
515
+ if label_container is None:
516
+ return []
517
+ return label_container.list()
450
518
 
451
519
  def get_label(
452
520
  self,
@@ -499,9 +567,7 @@ class OmeZarrContainer:
499
567
  if masking_table_name is None:
500
568
  masking_table = masking_label.build_masking_roi_table()
501
569
  else:
502
- masking_table = self.get_table(
503
- masking_table_name, check_type="masking_roi_table"
504
- )
570
+ masking_table = self.get_masking_roi_table(name=masking_table_name)
505
571
 
506
572
  return MaskedLabel(
507
573
  group_handler=label._group_handler,
@@ -566,7 +632,7 @@ def open_ome_zarr_container(
566
632
  handler = ZarrGroupHandler(store=store, cache=cache, mode=mode)
567
633
  return OmeZarrContainer(
568
634
  group_handler=handler,
569
- validate_arrays=validate_arrays,
635
+ validate_paths=validate_arrays,
570
636
  )
571
637
 
572
638
 
@@ -187,10 +187,6 @@ class GenericMetaHandler(
187
187
 
188
188
  raise NgioValueError(f"Could not load metadata: {meta_or_error}")
189
189
 
190
- def safe_load_meta(self) -> _image_meta | ConverterError:
191
- """Load the metadata from the store."""
192
- return self._load_meta(return_error=True)
193
-
194
190
  def _write_meta(self, meta) -> None:
195
191
  """Write the metadata to the store."""
196
192
  _meta = self._meta_exporter(metadata=meta)
@@ -217,6 +213,10 @@ class ImageMetaHandler(
217
213
  return meta
218
214
  raise NgioValueError(f"Could not load metadata: {meta}")
219
215
 
216
+ def safe_load_meta(self) -> NgioImageMeta | ConverterError:
217
+ """Load the metadata from the store."""
218
+ return self._load_meta(return_error=True)
219
+
220
220
 
221
221
  class LabelMetaHandler(
222
222
  GenericMetaHandler[NgioLabelMeta, LabelMetaImporter, LabelMetaExporter]
@@ -230,6 +230,10 @@ class LabelMetaHandler(
230
230
  return meta
231
231
  raise NgioValueError(f"Could not load metadata: {meta}")
232
232
 
233
+ def safe_load_meta(self) -> NgioLabelMeta | ConverterError:
234
+ """Load the metadata from the store."""
235
+ return self._load_meta(return_error=True)
236
+
233
237
 
234
238
  ###########################################################################
235
239
  #
@@ -267,10 +271,6 @@ class GenericHCSMetaHandler(Generic[_hcs_meta, _hcs_meta_importer, _hcs_meta_exp
267
271
 
268
272
  raise NgioValueError(f"Could not load metadata: {meta_or_error}")
269
273
 
270
- def safe_load_meta(self) -> _hcs_meta | ConverterError:
271
- """Load the metadata from the store."""
272
- return self._load_meta(return_error=True)
273
-
274
274
  def _write_meta(self, meta) -> None:
275
275
  _meta = self._meta_exporter(metadata=meta)
276
276
  self._group_handler.write_attrs(_meta)
@@ -295,6 +295,10 @@ class WellMetaHandler(
295
295
  return meta
296
296
  raise NgioValueError(f"Could not load metadata: {meta}")
297
297
 
298
+ def safe_load_meta(self) -> NgioWellMeta | ConverterError:
299
+ """Load the metadata from the store."""
300
+ return self._load_meta(return_error=True)
301
+
298
302
 
299
303
  class PlateMetaHandler(
300
304
  GenericHCSMetaHandler[NgioPlateMeta, PlateMetaImporter, PlateMetaExporter]
@@ -308,6 +312,10 @@ class PlateMetaHandler(
308
312
  return meta
309
313
  raise NgioValueError(f"Could not load metadata: {meta}")
310
314
 
315
+ def safe_load_meta(self) -> NgioPlateMeta | ConverterError:
316
+ """Load the metadata from the store."""
317
+ return self._load_meta(return_error=True)
318
+
311
319
 
312
320
  ###########################################################################
313
321
  #
@@ -329,33 +329,35 @@ class ChannelsMeta(BaseModel):
329
329
  @classmethod
330
330
  def default_init(
331
331
  cls,
332
- labels: Collection[str] | int,
333
- wavelength_id: Collection[str] | None = None,
334
- colors: Collection[str | NgioColors] | None = None,
335
- start: Collection[int | float] | int | float | None = None,
336
- end: Collection[int | float] | int | float | None = None,
337
- active: Collection[bool] | None = None,
332
+ labels: Collection[str | None] | int,
333
+ wavelength_id: Collection[str | None] | None = None,
334
+ colors: Collection[str | NgioColors | None] | None = None,
335
+ start: Collection[int | float | None] | int | float | None = None,
336
+ end: Collection[int | float | None] | int | float | None = None,
337
+ active: Collection[bool | None] | None = None,
338
338
  data_type: Any = np.uint16,
339
339
  **omero_kwargs: dict,
340
340
  ) -> "ChannelsMeta":
341
341
  """Create a ChannelsMeta object with the default unit.
342
342
 
343
343
  Args:
344
- labels(Collection[str] | int): The list of channels names in the image.
345
- If an integer is provided, the channels will be named "channel_i".
346
- wavelength_id(Collection[str] | None): The wavelength ID of the channel.
347
- If None, the wavelength ID will be the same as the channel name.
348
- colors(Collection[str, NgioColors] | None): The list of colors for the
349
- channels. If None, the colors will be random.
350
- start(Collection[int | float] | int | float | None): The start value of the
351
- channel. If None, the start value will be the minimum value of the
352
- data type.
353
- end(Collection[int | float] | int | float | None): The end value of the
354
- channel. If None, the end value will be the maximum value of the
355
- data type.
344
+ labels(Collection[str | None] | int): The list of channels names
345
+ in the image. If an integer is provided, the channels will be
346
+ named "channel_i".
347
+ wavelength_id(Collection[str | None] | None): The wavelength ID of the
348
+ channel. If None, the wavelength ID will be the same as the
349
+ channel name.
350
+ colors(Collection[str | NgioColors | None] | None): The list of
351
+ colors for the channels. If None, the colors will be random.
352
+ start(Collection[int | float | None] | int | float | None): The start
353
+ value of the channel. If None, the start value will be the
354
+ minimum value of the data type.
355
+ end(Collection[int | float | None] | int | float | None): The end
356
+ value of the channel. If None, the end value will be the
357
+ maximum value of the data type.
356
358
  data_type(Any): The data type of the channel. Will be used to set the
357
359
  min and max values of the channel.
358
- active (Collection[bool] | None):active(bool): Whether the channel should
360
+ active (Collection[bool | None] | None): Whether the channel should
359
361
  be shown by default.
360
362
  omero_kwargs(dict): Extra fields to store in the omero attributes.
361
363
  """
@@ -366,25 +368,35 @@ class ChannelsMeta(BaseModel):
366
368
  labels = _check_unique(labels)
367
369
 
368
370
  _wavelength_id: Collection[str | None] = [None] * len(labels)
369
- if isinstance(wavelength_id, Collection):
371
+ if wavelength_id is None:
372
+ _wavelength_id: Collection[str | None] = [None] * len(labels)
373
+ else:
370
374
  _wavelength_id = _check_elements(wavelength_id, str)
371
375
  _wavelength_id = _check_unique(wavelength_id)
372
376
 
373
- _colors: Collection[str | NgioColors | None] = [None] * len(labels)
374
- if isinstance(colors, Collection):
377
+ if colors is None:
378
+ _colors = [NgioColors.semi_random_pick(label) for label in labels]
379
+ else:
375
380
  _colors = _check_elements(colors, str | NgioColors)
376
381
 
377
- _start: Collection[int | float | None] = [None] * len(labels)
378
- if isinstance(start, Collection):
382
+ if start is None:
383
+ _start = [None] * len(labels)
384
+ elif isinstance(start, int | float):
385
+ _start = [start] * len(labels)
386
+ else:
379
387
  _start = _check_elements(start, (int, float))
380
388
 
381
- _end: Collection[int | float | None] = [None] * len(labels)
382
- if isinstance(end, Collection):
389
+ if end is None:
390
+ _end = [None] * len(labels)
391
+ elif isinstance(end, int | float):
392
+ _end = [end] * len(labels)
393
+ else:
383
394
  _end = _check_elements(end, (int, float))
384
395
 
385
- _active: Collection[bool] = [True] * len(labels)
386
- if isinstance(active, Collection):
387
- _active = _check_elements(active, bool)
396
+ if active is None:
397
+ _active = [True] * len(labels)
398
+ else:
399
+ _active = _check_elements(active, (bool,))
388
400
 
389
401
  all_lengths = [
390
402
  len(labels),