ngio 0.5.0__py3-none-any.whl → 0.5.0a1__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 (54) hide show
  1. ngio/__init__.py +2 -5
  2. ngio/common/__init__.py +6 -11
  3. ngio/common/_masking_roi.py +54 -34
  4. ngio/common/_pyramid.py +87 -321
  5. ngio/common/_roi.py +330 -258
  6. ngio/experimental/iterators/_feature.py +3 -3
  7. ngio/experimental/iterators/_rois_utils.py +11 -10
  8. ngio/hcs/_plate.py +136 -192
  9. ngio/images/_abstract_image.py +35 -539
  10. ngio/images/_create.py +283 -0
  11. ngio/images/_create_synt_container.py +43 -40
  12. ngio/images/_image.py +251 -517
  13. ngio/images/_label.py +172 -249
  14. ngio/images/_masked_image.py +2 -2
  15. ngio/images/_ome_zarr_container.py +241 -644
  16. ngio/io_pipes/_io_pipes.py +9 -9
  17. ngio/io_pipes/_io_pipes_masked.py +7 -7
  18. ngio/io_pipes/_io_pipes_roi.py +6 -6
  19. ngio/io_pipes/_io_pipes_types.py +3 -3
  20. ngio/io_pipes/_match_shape.py +8 -6
  21. ngio/io_pipes/_ops_slices_utils.py +5 -8
  22. ngio/ome_zarr_meta/__init__.py +18 -29
  23. ngio/ome_zarr_meta/_meta_handlers.py +708 -392
  24. ngio/ome_zarr_meta/ngio_specs/__init__.py +0 -4
  25. ngio/ome_zarr_meta/ngio_specs/_axes.py +51 -152
  26. ngio/ome_zarr_meta/ngio_specs/_dataset.py +22 -13
  27. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +91 -129
  28. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +68 -57
  29. ngio/ome_zarr_meta/v04/__init__.py +1 -5
  30. ngio/ome_zarr_meta/v04/{_v04_spec.py → _v04_spec_utils.py} +85 -54
  31. ngio/ome_zarr_meta/v05/__init__.py +1 -5
  32. ngio/ome_zarr_meta/v05/{_v05_spec.py → _v05_spec_utils.py} +87 -64
  33. ngio/resources/__init__.py +1 -1
  34. ngio/resources/resource_model.py +1 -1
  35. ngio/tables/_tables_container.py +27 -85
  36. ngio/tables/backends/_anndata.py +8 -58
  37. ngio/tables/backends/_anndata_utils.py +6 -1
  38. ngio/tables/backends/_csv.py +19 -3
  39. ngio/tables/backends/_json.py +13 -10
  40. ngio/tables/backends/_non_zarr_backends.py +196 -0
  41. ngio/tables/backends/_parquet.py +31 -3
  42. ngio/tables/v1/_roi_table.py +27 -44
  43. ngio/utils/__init__.py +12 -8
  44. ngio/utils/_datasets.py +0 -6
  45. ngio/utils/_logger.py +50 -0
  46. ngio/utils/_zarr_utils.py +250 -292
  47. {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/METADATA +6 -13
  48. ngio-0.5.0a1.dist-info/RECORD +88 -0
  49. {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/WHEEL +1 -1
  50. ngio/images/_create_utils.py +0 -406
  51. ngio/tables/backends/_py_arrow_backends.py +0 -222
  52. ngio/utils/_cache.py +0 -48
  53. ngio-0.5.0.dist-info/RECORD +0 -88
  54. {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/licenses/LICENSE +0 -0
ngio/images/_image.py CHANGED
@@ -1,8 +1,7 @@
1
1
  """Generic class to handle Image-like data in a OME-NGFF file."""
2
2
 
3
- import warnings
4
- from collections.abc import Mapping, Sequence
5
- from typing import Any, Literal
3
+ from collections.abc import Sequence
4
+ from typing import Literal
6
5
 
7
6
  import dask.array as da
8
7
  import numpy as np
@@ -13,9 +12,10 @@ from ngio.common import (
13
12
  Dimensions,
14
13
  InterpolationOrder,
15
14
  Roi,
15
+ RoiPixels,
16
16
  )
17
- from ngio.common._pyramid import ChunksLike, ShardsLike
18
- from ngio.images._abstract_image import AbstractImage, abstract_derive
17
+ from ngio.images._abstract_image import AbstractImage
18
+ from ngio.images._create import create_empty_image_container
19
19
  from ngio.io_pipes import (
20
20
  SlicingInputType,
21
21
  TransformProtocol,
@@ -24,22 +24,24 @@ from ngio.ome_zarr_meta import (
24
24
  ImageMetaHandler,
25
25
  NgioImageMeta,
26
26
  PixelSize,
27
+ find_image_meta_handler,
27
28
  )
28
29
  from ngio.ome_zarr_meta.ngio_specs import (
29
30
  Channel,
30
31
  ChannelsMeta,
32
+ ChannelVisualisation,
31
33
  DefaultSpaceUnit,
32
34
  DefaultTimeUnit,
33
35
  NgffVersions,
34
36
  SpaceUnits,
35
37
  TimeUnits,
36
38
  )
37
- from ngio.ome_zarr_meta.ngio_specs._axes import AxesSetup
38
39
  from ngio.utils import (
39
- NgioValueError,
40
+ NgioValidationError,
40
41
  StoreOrGroup,
41
42
  ZarrGroupHandler,
42
43
  )
44
+ from ngio.utils._zarr_utils import find_dimension_separator
43
45
 
44
46
 
45
47
  class ChannelSelectionModel(BaseModel):
@@ -47,7 +49,7 @@ class ChannelSelectionModel(BaseModel):
47
49
 
48
50
  This model is used to select a channel by label, wavelength ID, or index.
49
51
 
50
- Properties:
52
+ Args:
51
53
  identifier (str): Unique identifier for the channel.
52
54
  This can be a channel label, wavelength ID, or index.
53
55
  mode (Literal["label", "wavelength_id", "index"]): Specifies how to
@@ -88,7 +90,7 @@ def _check_channel_meta(meta: NgioImageMeta, dimension: Dimensions) -> ChannelsM
88
90
  return ChannelsMeta.default_init(labels=c_dim)
89
91
 
90
92
  if len(meta.channels_meta.channels) != c_dim:
91
- raise NgioValueError(
93
+ raise NgioValidationError(
92
94
  "The number of channels does not match the image. "
93
95
  f"Expected {len(meta.channels_meta.channels)} channels, got {c_dim}."
94
96
  )
@@ -96,7 +98,7 @@ def _check_channel_meta(meta: NgioImageMeta, dimension: Dimensions) -> ChannelsM
96
98
  return meta.channels_meta
97
99
 
98
100
 
99
- class Image(AbstractImage):
101
+ class Image(AbstractImage[ImageMetaHandler]):
100
102
  """A class to handle a single image (or level) in an OME-Zarr image.
101
103
 
102
104
  This class is meant to be subclassed by specific image types.
@@ -106,7 +108,7 @@ class Image(AbstractImage):
106
108
  self,
107
109
  group_handler: ZarrGroupHandler,
108
110
  path: str,
109
- meta_handler: ImageMetaHandler,
111
+ meta_handler: ImageMetaHandler | None,
110
112
  ) -> None:
111
113
  """Initialize the Image at a single level.
112
114
 
@@ -116,22 +118,16 @@ class Image(AbstractImage):
116
118
  meta_handler: The image metadata handler.
117
119
 
118
120
  """
121
+ if meta_handler is None:
122
+ meta_handler = find_image_meta_handler(group_handler)
119
123
  super().__init__(
120
124
  group_handler=group_handler, path=path, meta_handler=meta_handler
121
125
  )
122
126
 
123
- @property
124
- def meta_handler(self) -> ImageMetaHandler:
125
- """Return the metadata handler."""
126
- assert isinstance(self._meta_handler, ImageMetaHandler)
127
- return self._meta_handler
128
-
129
127
  @property
130
128
  def meta(self) -> NgioImageMeta:
131
129
  """Return the metadata."""
132
- meta = self.meta_handler.get_meta()
133
- assert isinstance(meta, NgioImageMeta)
134
- return meta
130
+ return self._meta_handler.meta
135
131
 
136
132
  @property
137
133
  def channels_meta(self) -> ChannelsMeta:
@@ -191,7 +187,7 @@ class Image(AbstractImage):
191
187
 
192
188
  def get_roi_as_numpy(
193
189
  self,
194
- roi: Roi,
190
+ roi: Roi | RoiPixels,
195
191
  channel_selection: ChannelSlicingInputType = None,
196
192
  axes_order: Sequence[str] | None = None,
197
193
  transforms: Sequence[TransformProtocol] | None = None,
@@ -245,7 +241,7 @@ class Image(AbstractImage):
245
241
 
246
242
  def get_roi_as_dask(
247
243
  self,
248
- roi: Roi,
244
+ roi: Roi | RoiPixels,
249
245
  channel_selection: ChannelSlicingInputType = None,
250
246
  axes_order: Sequence[str] | None = None,
251
247
  transforms: Sequence[TransformProtocol] | None = None,
@@ -302,7 +298,7 @@ class Image(AbstractImage):
302
298
 
303
299
  def get_roi(
304
300
  self,
305
- roi: Roi,
301
+ roi: Roi | RoiPixels,
306
302
  channel_selection: ChannelSlicingInputType = None,
307
303
  axes_order: Sequence[str] | None = None,
308
304
  transforms: Sequence[TransformProtocol] | None = None,
@@ -362,7 +358,7 @@ class Image(AbstractImage):
362
358
 
363
359
  def set_roi(
364
360
  self,
365
- roi: Roi,
361
+ roi: Roi | RoiPixels,
366
362
  patch: np.ndarray | da.Array,
367
363
  channel_selection: ChannelSlicingInputType = None,
368
364
  axes_order: Sequence[str] | None = None,
@@ -400,135 +396,67 @@ class Image(AbstractImage):
400
396
 
401
397
 
402
398
  class ImagesContainer:
403
- """A class to handle the /images group in an OME-NGFF file."""
399
+ """A class to handle the /labels group in an OME-NGFF file."""
404
400
 
405
- def __init__(
406
- self,
407
- group_handler: ZarrGroupHandler,
408
- axes_setup: AxesSetup | None,
409
- version: NgffVersions | None = None,
410
- validate_paths: bool = True,
411
- ) -> None:
412
- """Initialize the ImagesContainer."""
401
+ def __init__(self, group_handler: ZarrGroupHandler) -> None:
402
+ """Initialize the LabelGroupHandler."""
413
403
  self._group_handler = group_handler
414
- self._meta_handler = ImageMetaHandler(
415
- group_handler=group_handler, axes_setup=axes_setup, version=version
416
- )
417
- if validate_paths:
418
- for level_path in self._meta_handler.get_meta().paths:
419
- self.get(path=level_path)
404
+ self._meta_handler = find_image_meta_handler(group_handler)
420
405
 
421
406
  @property
422
407
  def meta(self) -> NgioImageMeta:
423
408
  """Return the metadata."""
424
- return self._meta_handler.get_meta()
425
-
426
- @property
427
- def channels_meta(self) -> ChannelsMeta:
428
- """Return the channels metadata."""
429
- return self.get().channels_meta
430
-
431
- @property
432
- def axes_setup(self) -> AxesSetup:
433
- """Return the axes setup."""
434
- return self.meta.axes_handler.axes_setup
435
-
436
- @property
437
- def level_paths(self) -> list[str]:
438
- """Return the paths of the levels in the image."""
439
- return self.meta.paths
440
-
441
- @property
442
- def levels_paths(self) -> list[str]:
443
- """Deprecated: use 'level_paths' instead."""
444
- warnings.warn(
445
- "'levels_paths' is deprecated and will be removed in ngio=0.6. "
446
- "Please use 'level_paths' instead.",
447
- DeprecationWarning,
448
- stacklevel=2,
449
- )
450
- return self.level_paths
409
+ return self._meta_handler.meta
451
410
 
452
411
  @property
453
412
  def levels(self) -> int:
454
413
  """Return the number of levels in the image."""
455
- return self.meta.levels
456
-
457
- @property
458
- def is_3d(self) -> bool:
459
- """Return True if the image is 3D."""
460
- return self.get().is_3d
461
-
462
- @property
463
- def is_2d(self) -> bool:
464
- """Return True if the image is 2D."""
465
- return self.get().is_2d
414
+ return self._meta_handler.meta.levels
466
415
 
467
416
  @property
468
- def is_time_series(self) -> bool:
469
- """Return True if the image is a time series."""
470
- return self.get().is_time_series
471
-
472
- @property
473
- def is_2d_time_series(self) -> bool:
474
- """Return True if the image is a 2D time series."""
475
- return self.get().is_2d_time_series
476
-
477
- @property
478
- def is_3d_time_series(self) -> bool:
479
- """Return True if the image is a 3D time series."""
480
- return self.get().is_3d_time_series
481
-
482
- @property
483
- def is_multi_channels(self) -> bool:
484
- """Return True if the image is multichannel."""
485
- return self.get().is_multi_channels
486
-
487
- @property
488
- def space_unit(self) -> str | None:
489
- """Return the space unit of the image."""
490
- return self.meta.space_unit
417
+ def levels_paths(self) -> list[str]:
418
+ """Return the paths of the levels in the image."""
419
+ return self._meta_handler.meta.paths
491
420
 
492
421
  @property
493
- def time_unit(self) -> str | None:
494
- """Return the time unit of the image."""
495
- return self.meta.time_unit
422
+ def num_channels(self) -> int:
423
+ """Return the number of channels."""
424
+ image = self.get()
425
+ return image.num_channels
496
426
 
497
427
  @property
498
428
  def channel_labels(self) -> list[str]:
499
429
  """Return the channels of the image."""
500
- return self.get().channel_labels
430
+ image = self.get()
431
+ return image.channel_labels
501
432
 
502
433
  @property
503
434
  def wavelength_ids(self) -> list[str | None]:
504
- """Return the list of wavelength of the image."""
505
- return self.get().wavelength_ids
506
-
507
- @property
508
- def num_channels(self) -> int:
509
- """Return the number of channels."""
510
- return self.get().num_channels
435
+ """Return the wavelength of the image."""
436
+ image = self.get()
437
+ return image.wavelength_ids
511
438
 
512
439
  def get_channel_idx(
513
440
  self, channel_label: str | None = None, wavelength_id: str | None = None
514
441
  ) -> int:
515
- """Get the index of a channel by its label or wavelength ID."""
516
- return self.channels_meta.get_channel_idx(
442
+ """Get the index of a channel by label or wavelength ID.
443
+
444
+ Args:
445
+ channel_label (str | None): The label of the channel.
446
+ If None a wavelength ID must be provided.
447
+ wavelength_id (str | None): The wavelength ID of the channel.
448
+ If None a channel label must be provided.
449
+
450
+ Returns:
451
+ int: The index of the channel.
452
+
453
+ """
454
+ image = self.get()
455
+ return image.get_channel_idx(
517
456
  channel_label=channel_label, wavelength_id=wavelength_id
518
457
  )
519
458
 
520
- def _set_channel_meta(
521
- self,
522
- channels_meta: ChannelsMeta | None = None,
523
- ) -> None:
524
- """Set the channels metadata."""
525
- if channels_meta is None:
526
- channels_meta = ChannelsMeta.default_init(labels=self.num_channels)
527
- meta = self.meta
528
- meta.set_channels_meta(channels_meta)
529
- self._meta_handler.update_meta(meta)
530
-
531
- def _set_channel_meta_legacy(
459
+ def set_channel_meta(
532
460
  self,
533
461
  labels: Sequence[str | None] | int | None = None,
534
462
  wavelength_id: Sequence[str | None] | None = None,
@@ -564,22 +492,32 @@ class ImagesContainer:
564
492
  ref_image = self.get(path=low_res_dataset.path)
565
493
 
566
494
  if start is not None and end is None:
567
- raise NgioValueError("If start is provided, end must be provided as well.")
495
+ raise NgioValidationError(
496
+ "If start is provided, end must be provided as well."
497
+ )
568
498
  if end is not None and start is None:
569
- raise NgioValueError("If end is provided, start must be provided as well.")
499
+ raise NgioValidationError(
500
+ "If end is provided, start must be provided as well."
501
+ )
570
502
 
571
503
  if start is not None and percentiles is not None:
572
- raise NgioValueError(
504
+ raise NgioValidationError(
573
505
  "If start and end are provided, percentiles must be None."
574
506
  )
575
507
 
508
+ if percentiles is not None:
509
+ start, end = compute_image_percentile(
510
+ ref_image,
511
+ start_percentile=percentiles[0],
512
+ end_percentile=percentiles[1],
513
+ )
576
514
  elif start is not None and end is not None:
577
515
  if len(start) != len(end):
578
- raise NgioValueError(
516
+ raise NgioValidationError(
579
517
  "The start and end lists must have the same length."
580
518
  )
581
519
  if len(start) != self.num_channels:
582
- raise NgioValueError(
520
+ raise NgioValidationError(
583
521
  "The start and end lists must have the same length as "
584
522
  "the number of channels."
585
523
  )
@@ -603,212 +541,44 @@ class ImagesContainer:
603
541
  data_type=ref_image.dtype,
604
542
  **omero_kwargs,
605
543
  )
606
- self._set_channel_meta(channel_meta)
607
- if percentiles is not None:
608
- self.set_channel_windows_with_percentiles(percentiles=percentiles)
609
-
610
- def set_channel_meta(
611
- self,
612
- channel_meta: ChannelsMeta | None = None,
613
- labels: Sequence[str | None] | int | None = None,
614
- wavelength_id: Sequence[str | None] | None = None,
615
- start: Sequence[float | None] | None = None,
616
- end: Sequence[float | None] | None = None,
617
- percentiles: tuple[float, float] | None = None,
618
- colors: Sequence[str | None] | None = None,
619
- active: Sequence[bool | None] | None = None,
620
- **omero_kwargs: dict,
621
- ) -> None:
622
- """Create a ChannelsMeta object with the default unit.
623
-
624
- Args:
625
- channel_meta (ChannelsMeta | None): The channels metadata to set.
626
- If none, it will fall back to the deprecated parameters.
627
- labels(Sequence[str | None] | int): Deprecated. The list of channels names
628
- in the image. If an integer is provided, the channels will
629
- be named "channel_i".
630
- wavelength_id(Sequence[str | None]): Deprecated. The wavelength ID of the
631
- channel. If None, the wavelength ID will be the same as
632
- the channel name.
633
- start(Sequence[float | None]): Deprecated. The start value for each channel.
634
- If None, the start value will be computed from the image.
635
- end(Sequence[float | None]): Deprecated. The end value for each channel.
636
- If None, the end value will be computed from the image.
637
- percentiles(tuple[float, float] | None): Deprecated. The start and end
638
- percentiles for each channel. If None, the percentiles will
639
- not be computed.
640
- colors(Sequence[str | None]): Deprecated. The list of colors for the
641
- channels. If None, the colors will be random.
642
- active (Sequence[bool | None]): Deprecated. Whether the channel should
643
- be shown by default.
644
- omero_kwargs(dict): Deprecated. Extra fields to store in the omero
645
- attributes.
646
- """
647
- _is_legacy = any(
648
- param is not None
649
- for param in [
650
- labels,
651
- wavelength_id,
652
- start,
653
- end,
654
- percentiles,
655
- colors,
656
- active,
657
- ]
658
- )
659
- if _is_legacy:
660
- warnings.warn(
661
- "The following parameters are deprecated and will be removed in "
662
- "ngio=0.6: labels, wavelength_id, start, end, percentiles, "
663
- "colors, active, omero_kwargs. Please use the "
664
- "'channel_meta' parameter instead.",
665
- DeprecationWarning,
666
- stacklevel=2,
667
- )
668
- self._set_channel_meta_legacy(
669
- labels=labels,
670
- wavelength_id=wavelength_id,
671
- start=start,
672
- end=end,
673
- percentiles=percentiles,
674
- colors=colors,
675
- active=active,
676
- **omero_kwargs,
677
- )
678
- return None
679
- self._set_channel_meta(channel_meta)
680
544
 
681
- def set_channel_labels(
682
- self,
683
- labels: Sequence[str],
684
- ) -> None:
685
- """Update the labels of the channels.
686
-
687
- Args:
688
- labels (Sequence[str]): The new labels for the channels.
689
- """
690
- channels_meta = self.channels_meta
691
- if len(labels) != len(channels_meta.channels):
692
- raise NgioValueError(
693
- "The number of labels must match the number of channels."
694
- )
695
- new_channels = []
696
- for label, ch in zip(labels, channels_meta.channels, strict=True):
697
- channel = ch.model_copy(update={"label": label})
698
- new_channels.append(channel)
699
- new_meta = channels_meta.model_copy(update={"channels": new_channels})
700
- self._set_channel_meta(new_meta)
701
-
702
- def set_channel_colors(
703
- self,
704
- colors: Sequence[str],
705
- ) -> None:
706
- """Update the colors of the channels.
707
-
708
- Args:
709
- colors (Sequence[str]): The new colors for the channels.
710
- """
711
- channel_meta = self.channels_meta
712
- if len(colors) != len(channel_meta.channels):
713
- raise NgioValueError(
714
- "The number of colors must match the number of channels."
715
- )
716
- new_channels = []
717
- for color, ch in zip(colors, channel_meta.channels, strict=True):
718
- ch_visualisation = ch.channel_visualisation.model_copy(
719
- update={"color": color}
720
- )
721
- channel = ch.model_copy(update={"channel_visualisation": ch_visualisation})
722
- new_channels.append(channel)
723
- new_meta = channel_meta.model_copy(update={"channels": new_channels})
724
- self._set_channel_meta(new_meta)
545
+ meta = self.meta
546
+ meta.set_channels_meta(channel_meta)
547
+ self._meta_handler.write_meta(meta)
725
548
 
726
549
  def set_channel_percentiles(
727
550
  self,
728
551
  start_percentile: float = 0.1,
729
552
  end_percentile: float = 99.9,
730
553
  ) -> None:
731
- """Deprecated: Update the channel windows using percentiles.
554
+ """Update the percentiles of the channels."""
555
+ if self.meta._channels_meta is None:
556
+ raise NgioValidationError("The channels meta is not initialized.")
732
557
 
733
- Args:
734
- start_percentile (float): The start percentile.
735
- end_percentile (float): The end percentile.
736
- """
737
- warnings.warn(
738
- "The 'set_channel_percentiles' method is deprecated and will be removed in "
739
- "ngio=0.6. Please use 'set_channel_windows_with_percentiles' instead.",
740
- DeprecationWarning,
741
- stacklevel=2,
742
- )
743
- self.set_channel_windows_with_percentiles(
744
- percentiles=(start_percentile, end_percentile)
558
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
559
+ ref_image = self.get(path=low_res_dataset.path)
560
+ starts, ends = compute_image_percentile(
561
+ ref_image, start_percentile=start_percentile, end_percentile=end_percentile
745
562
  )
746
563
 
747
- def set_channel_windows(
748
- self,
749
- starts_ends: Sequence[tuple[float, float]],
750
- min_max: Sequence[tuple[float, float]] | None = None,
751
- ) -> None:
752
- """Update the channel windows.
753
-
754
- These values are used by viewers to set the display
755
- range of each channel.
756
-
757
- Args:
758
- starts_ends (Sequence[tuple[float, float]]): The start and end values
759
- for each channel.
760
- min_max (Sequence[tuple[float, float]] | None): The min and max values
761
- for each channel. If None, the min and max values will not be updated.
762
- """
763
- current_channels = self.channels_meta.channels
764
- if len(starts_ends) != len(current_channels):
765
- raise NgioValueError(
766
- "The number of start-end pairs must match the number of channels."
767
- )
768
- if min_max is not None and len(min_max) != len(current_channels):
769
- raise NgioValueError(
770
- "The number of min-max pairs must match the number of channels."
771
- )
772
- if min_max is None:
773
- min_max_ = [None] * len(current_channels)
774
- else:
775
- min_max_ = list(min_max)
776
564
  channels = []
777
- for se, mm, ch in zip(
778
- starts_ends, min_max_, self.channels_meta.channels, strict=True
779
- ):
780
- updates = {"start": se[0], "end": se[1]}
781
- if mm is not None:
782
- updates.update({"min": mm[0], "max": mm[1]})
783
- channel_visualisation = ch.channel_visualisation.model_copy(update=updates)
784
- channel = ch.model_copy(
785
- update={"channel_visualisation": channel_visualisation}
565
+ for c, channel in enumerate(self.meta._channels_meta.channels):
566
+ new_v = ChannelVisualisation(
567
+ start=starts[c],
568
+ end=ends[c],
569
+ **channel.channel_visualisation.model_dump(exclude={"start", "end"}),
570
+ )
571
+ new_c = Channel(
572
+ channel_visualisation=new_v,
573
+ **channel.model_dump(exclude={"channel_visualisation"}),
786
574
  )
787
- channels.append(channel)
575
+ channels.append(new_c)
576
+
788
577
  new_meta = ChannelsMeta(channels=channels)
578
+
789
579
  meta = self.meta
790
580
  meta.set_channels_meta(new_meta)
791
- self._meta_handler.update_meta(meta)
792
-
793
- def set_channel_windows_with_percentiles(
794
- self,
795
- percentiles: tuple[float, float] | list[tuple[float, float]] = (0.1, 99.9),
796
- ) -> None:
797
- """Update the channel windows using percentiles.
798
-
799
- Args:
800
- percentiles (tuple[float, float] | list[tuple[float, float]]):
801
- The start and end percentiles for each channel.
802
- If a single tuple is provided,
803
- the same percentiles will be used for all channels.
804
- """
805
- if self.meta._channels_meta is None:
806
- raise NgioValueError("The channels meta is not initialized.")
807
-
808
- low_res_dataset = self.meta.get_lowest_resolution_dataset()
809
- ref_image = self.get(path=low_res_dataset.path)
810
- starts_ends = compute_image_percentile(ref_image, percentiles=percentiles)
811
- self.set_channel_windows(starts_ends=starts_ends)
581
+ self._meta_handler.write_meta(meta)
812
582
 
813
583
  def set_axes_unit(
814
584
  self,
@@ -821,127 +591,64 @@ class ImagesContainer:
821
591
  space_unit (SpaceUnits): The space unit of the image.
822
592
  time_unit (TimeUnits): The time unit of the image.
823
593
  """
824
- self.get().set_axes_unit(space_unit=space_unit, time_unit=time_unit)
825
-
826
- def set_axes_names(
827
- self,
828
- axes_names: Sequence[str],
829
- ) -> None:
830
- """Set the axes names of the image.
831
-
832
- Args:
833
- axes_names (Sequence[str]): The axes names of the image.
834
- """
835
- image = self.get()
836
- image.set_axes_names(axes_names=axes_names)
837
- self._meta_handler._axes_setup = image.meta.axes_handler.axes_setup
838
-
839
- def set_name(
840
- self,
841
- name: str,
842
- ) -> None:
843
- """Set the name of the image in the metadata.
844
-
845
- This does not change the group name or any paths.
846
-
847
- Args:
848
- name (str): The name of the image.
849
- """
850
- self.get().set_name(name=name)
594
+ meta = self.meta
595
+ meta = meta.to_units(space_unit=space_unit, time_unit=time_unit)
596
+ self._meta_handler.write_meta(meta)
851
597
 
852
598
  def derive(
853
599
  self,
854
600
  store: StoreOrGroup,
855
601
  ref_path: str | None = None,
856
- # Metadata parameters
857
602
  shape: Sequence[int] | None = None,
858
- pixelsize: float | tuple[float, float] | None = None,
859
- z_spacing: float | None = None,
860
- time_spacing: float | None = None,
603
+ labels: Sequence[str] | None = None,
604
+ pixel_size: PixelSize | None = None,
605
+ axes_names: Sequence[str] | None = None,
861
606
  name: str | None = None,
862
- translation: Sequence[float] | None = None,
863
- channels_meta: Sequence[str | Channel] | None = None,
864
- channels_policy: Literal["same", "squeeze", "singleton"] | int = "same",
607
+ chunks: Sequence[int] | None = None,
608
+ dtype: str | None = None,
609
+ dimension_separator: Literal[".", "/"] | None = None,
610
+ compressors: CompressorLike | None = None,
865
611
  ngff_version: NgffVersions | None = None,
866
- # Zarr Array parameters
867
- chunks: ChunksLike | None = None,
868
- shards: ShardsLike | None = None,
869
- dtype: str = "uint16",
870
- dimension_separator: Literal[".", "/"] = "/",
871
- compressors: CompressorLike = "auto",
872
- extra_array_kwargs: Mapping[str, Any] | None = None,
873
612
  overwrite: bool = False,
874
- # Deprecated arguments
875
- labels: Sequence[str] | None = None,
876
- pixel_size: PixelSize | None = None,
877
613
  ) -> "ImagesContainer":
878
614
  """Create an empty OME-Zarr image from an existing image.
879
615
 
880
- If a kwarg is not provided, the value from the reference image will be used.
881
-
882
616
  Args:
883
617
  store (StoreOrGroup): The Zarr store or group to create the image in.
884
- ref_path (str | None): The path to the reference image in the image
885
- container.
618
+ ref_path (str | None): The path to the reference image in
619
+ the image container.
886
620
  shape (Sequence[int] | None): The shape of the new image.
887
- pixelsize (float | tuple[float, float] | None): The pixel size of the new
888
- image.
889
- z_spacing (float | None): The z spacing of the new image.
890
- time_spacing (float | None): The time spacing of the new image.
621
+ labels (Sequence[str] | None): The labels of the new image.
622
+ pixel_size (PixelSize | None): The pixel size of the new image.
623
+ axes_names (Sequence[str] | None): The axes names of the new image.
891
624
  name (str | None): The name of the new image.
892
- translation (Sequence[float] | None): The translation for each axis
893
- at the highest resolution level. Defaults to None.
894
- channels_meta (Sequence[str | Channel] | None): The channels metadata
895
- of the new image.
896
- channels_policy (Literal["same", "squeeze", "singleton"] | int):
897
- Possible policies:
898
- - If "squeeze", the channels axis will be removed (no matter its size).
899
- - If "same", the channels axis will be kept as is (if it exists).
900
- - If "singleton", the channels axis will be set to size 1.
901
- - If an integer is provided, the channels axis will be changed to have
902
- that size.
903
- ngff_version (NgffVersions | None): The NGFF version to use.
904
- chunks (ChunksLike | None): The chunk shape of the new image.
905
- shards (ShardsLike | None): The shard shape of the new image.
625
+ chunks (Sequence[int] | Literal["auto"]): The chunk shape of the new image.
626
+ dimension_separator (DIMENSION_SEPARATOR | None): The separator to use for
627
+ dimensions. If None it will use the same as the reference image.
628
+ compressors: The compressor to use. If None it will use
629
+ the same as the reference image.
906
630
  dtype (str | None): The data type of the new image.
907
- dimension_separator (Literal[".", "/"] | None): The separator to use for
908
- dimensions.
909
- compressors (CompressorLike | None): The compressors to use.
910
- extra_array_kwargs (Mapping[str, Any] | None): Extra arguments to pass to
911
- the zarr array creation.
631
+ ngff_version (NgffVersions): The NGFF version to use.
912
632
  overwrite (bool): Whether to overwrite an existing image.
913
- labels (Sequence[str] | None): The labels of the new image.
914
- This argument is deprecated please use channels_meta instead.
915
- pixel_size (PixelSize | None): The pixel size of the new image.
916
- This argument is deprecated please use pixelsize, z_spacing,
917
- and time_spacing instead.
918
633
 
919
634
  Returns:
920
- ImagesContainer: The new derived image.
921
-
635
+ ImagesContainer: The new image
922
636
  """
923
637
  return derive_image_container(
924
638
  image_container=self,
925
639
  store=store,
926
640
  ref_path=ref_path,
927
641
  shape=shape,
928
- pixelsize=pixelsize,
929
- z_spacing=z_spacing,
930
- time_spacing=time_spacing,
642
+ labels=labels,
643
+ pixel_size=pixel_size,
644
+ axes_names=axes_names,
931
645
  name=name,
932
- translation=translation,
933
- channels_meta=channels_meta,
934
- channels_policy=channels_policy,
935
- ngff_version=ngff_version,
936
646
  chunks=chunks,
937
- shards=shards,
938
647
  dtype=dtype,
939
648
  dimension_separator=dimension_separator,
940
649
  compressors=compressors,
941
- extra_array_kwargs=extra_array_kwargs,
650
+ ngff_version=ngff_version,
942
651
  overwrite=overwrite,
943
- labels=labels,
944
- pixel_size=pixel_size,
945
652
  )
946
653
 
947
654
  def get(
@@ -960,7 +667,7 @@ class ImagesContainer:
960
667
  closest pixel size level will be returned.
961
668
 
962
669
  """
963
- dataset = self._meta_handler.get_meta().get_dataset(
670
+ dataset = self._meta_handler.meta.get_dataset(
964
671
  path=path, pixel_size=pixel_size, strict=strict
965
672
  )
966
673
  return Image(
@@ -972,53 +679,34 @@ class ImagesContainer:
972
679
 
973
680
  def compute_image_percentile(
974
681
  image: Image,
975
- percentiles: tuple[float, float] | list[tuple[float, float]] = (0.1, 99.9),
976
- ) -> list[tuple[float, float]]:
682
+ start_percentile: float = 0.1,
683
+ end_percentile: float = 99.9,
684
+ ) -> tuple[list[float], list[float]]:
977
685
  """Compute the start and end percentiles for each channel of an image.
978
686
 
979
687
  Args:
980
688
  image: The image to compute the percentiles for.
981
- percentiles: The start and end percentiles for each channel.
982
- If a single tuple is provided, the same percentiles will be used
983
- for all channels.
689
+ start_percentile: The start percentile to compute.
690
+ end_percentile: The end percentile to compute.
984
691
 
985
692
  Returns:
986
693
  A tuple containing the start and end percentiles for each channel.
987
694
  """
988
- num_channels = image.num_channels
989
- # handle the case where a single tuple is provided
990
- if isinstance(percentiles, tuple):
991
- if len(percentiles) != 2:
992
- raise NgioValueError(
993
- "Percentiles must be a tuple of two floats: "
994
- "(start_percentile, end_percentile) or "
995
- "a list of such tuples with length equal to the number of channels."
996
- )
997
- if not isinstance(percentiles[0], float) or not isinstance(
998
- percentiles[1], float
999
- ):
1000
- raise NgioValueError(
1001
- "Percentiles must be a tuple of two floats: "
1002
- "(start_percentile, end_percentile) or "
1003
- "a list of such tuples with length equal to the number of channels."
1004
- )
1005
- percentiles = [percentiles] * num_channels
695
+ starts, ends = [], []
696
+ for c in range(image.num_channels):
697
+ if image.num_channels == 1:
698
+ data = image.get_as_dask()
699
+ else:
700
+ data = image.get_as_dask(c=c)
1006
701
 
1007
- if len(percentiles) != num_channels:
1008
- raise NgioValueError(
1009
- "If a list of percentiles is provided, its length must be equal "
1010
- "to the number of channels."
1011
- )
1012
- starts_and_ends = []
1013
- for c_idx, (start_percentile, end_percentile) in enumerate(percentiles):
1014
- data = image.get_as_dask(c=c_idx)
1015
702
  data = da.ravel(data)
1016
703
  # remove all the zeros
1017
704
  mask = data > 1e-16
1018
705
  data = data[mask]
1019
706
  _data = data.compute()
1020
707
  if _data.size == 0:
1021
- starts_and_ends.append((0.0, 0.0))
708
+ starts.append(0.0)
709
+ ends.append(0.0)
1022
710
  continue
1023
711
 
1024
712
  # compute the percentiles
@@ -1026,107 +714,153 @@ def compute_image_percentile(
1026
714
  data, [start_percentile, end_percentile], method="nearest"
1027
715
  ).compute() # type: ignore (return type is a tuple of floats)
1028
716
 
1029
- starts_and_ends.append((float(_s_perc), float(_e_perc)))
1030
- return starts_and_ends
717
+ starts.append(float(_s_perc))
718
+ ends.append(float(_e_perc))
719
+ return starts, ends
1031
720
 
1032
721
 
1033
722
  def derive_image_container(
1034
- *,
1035
723
  image_container: ImagesContainer,
1036
724
  store: StoreOrGroup,
1037
725
  ref_path: str | None = None,
1038
- # Metadata parameters
1039
726
  shape: Sequence[int] | None = None,
1040
- pixelsize: float | tuple[float, float] | None = None,
1041
- z_spacing: float | None = None,
1042
- time_spacing: float | None = None,
727
+ labels: Sequence[str] | None = None,
728
+ pixel_size: PixelSize | None = None,
729
+ axes_names: Sequence[str] | None = None,
1043
730
  name: str | None = None,
1044
- translation: Sequence[float] | None = None,
1045
- channels_policy: Literal["same", "squeeze", "singleton"] | int = "same",
1046
- channels_meta: Sequence[str | Channel] | None = None,
1047
- ngff_version: NgffVersions | None = None,
1048
- # Zarr Array parameters
1049
- chunks: ChunksLike | None = None,
1050
- shards: ShardsLike | None = None,
731
+ chunks: Sequence[int] | None = None,
1051
732
  dtype: str | None = None,
1052
733
  dimension_separator: Literal[".", "/"] | None = None,
1053
734
  compressors: CompressorLike | None = None,
1054
- extra_array_kwargs: Mapping[str, Any] | None = None,
735
+ ngff_version: NgffVersions | None = None,
1055
736
  overwrite: bool = False,
1056
- # Deprecated arguments
1057
- labels: Sequence[str] | None = None,
1058
- pixel_size: PixelSize | None = None,
1059
737
  ) -> ImagesContainer:
1060
- """Derive a new OME-Zarr image container from an existing image.
1061
-
1062
- If a kwarg is not provided, the value from the reference image will be used.
738
+ """Create an empty OME-Zarr image from an existing image.
1063
739
 
1064
740
  Args:
1065
- image_container (ImagesContainer): The image container to derive the new image
1066
- from.
741
+ image_container (ImagesContainer): The image container to derive the new image.
1067
742
  store (StoreOrGroup): The Zarr store or group to create the image in.
1068
743
  ref_path (str | None): The path to the reference image in the image container.
1069
744
  shape (Sequence[int] | None): The shape of the new image.
1070
- pixelsize (float | tuple[float, float] | None): The pixel size of the new image.
1071
- z_spacing (float | None): The z spacing of the new image.
1072
- time_spacing (float | None): The time spacing of the new image.
745
+ labels (Sequence[str] | None): The labels of the new image.
746
+ pixel_size (PixelSize | None): The pixel size of the new image.
747
+ axes_names (Sequence[str] | None): The axes names of the new image.
1073
748
  name (str | None): The name of the new image.
1074
- translation (Sequence[float] | None): The translation for each axis
1075
- at the highest resolution level. Defaults to None.
1076
- channels_policy (Literal["squeeze", "same", "singleton"] | int): Possible
1077
- policies:
1078
- - If "squeeze", the channels axis will be removed (no matter its size).
1079
- - If "same", the channels axis will be kept as is (if it exists).
1080
- - If "singleton", the channels axis will be set to size 1.
1081
- - If an integer is provided, the channels axis will be changed to have
1082
- that size.
1083
- channels_meta (Sequence[str | Channel] | None): The channels metadata
1084
- of the new image.
1085
- ngff_version (NgffVersions | None): The NGFF version to use.
1086
- chunks (ChunksLike | None): The chunk shape of the new image.
1087
- shards (ShardsLike | None): The shard shape of the new image.
749
+ chunks (Sequence[int] | None): The chunk shape of the new image.
750
+ dimension_separator (DIMENSION_SEPARATOR | None): The separator to use for
751
+ dimensions. If None it will use the same as the reference image.
752
+ compressors (CompressorLike | None): The compressors to use. If None it will use
753
+ the same as the reference image.
754
+ ngff_version (NgffVersions): The NGFF version to use.
1088
755
  dtype (str | None): The data type of the new image.
1089
- dimension_separator (Literal[".", "/"] | None): The separator to use for
1090
- dimensions.
1091
- compressors (CompressorLike | None): The compressors to use.
1092
- extra_array_kwargs (Mapping[str, Any] | None): Extra arguments to pass to
1093
- the zarr array creation.
1094
- overwrite (bool): Whether to overwrite an existing image. Defaults to False.
1095
- labels (Sequence[str] | None): Deprecated. This argument is deprecated,
1096
- please use channels_meta instead.
1097
- pixel_size (PixelSize | None): Deprecated. The pixel size of the new image.
1098
- This argument is deprecated, please use pixelsize, z_spacing,
1099
- and time_spacing instead.
756
+ overwrite (bool): Whether to overwrite an existing image.
1100
757
 
1101
758
  Returns:
1102
- ImagesContainer: The new derived image container.
759
+ ImagesContainer: The new image
1103
760
 
1104
761
  """
1105
- ref_image = image_container.get(path=ref_path)
1106
- group_handler, axes_setup = abstract_derive(
1107
- ref_image=ref_image,
1108
- meta_type=NgioImageMeta,
762
+ if ref_path is None:
763
+ ref_image = image_container.get()
764
+ else:
765
+ ref_image = image_container.get(path=ref_path)
766
+
767
+ ref_meta = ref_image.meta
768
+
769
+ if shape is None:
770
+ shape = ref_image.shape
771
+
772
+ if pixel_size is None:
773
+ pixel_size = ref_image.pixel_size
774
+
775
+ if axes_names is None:
776
+ axes_names = ref_meta.axes_handler.axes_names
777
+
778
+ if len(axes_names) != len(shape):
779
+ raise NgioValidationError(
780
+ "The axes names of the new image does not match the reference image."
781
+ f"Got {axes_names} for shape {shape}."
782
+ )
783
+
784
+ if chunks is None:
785
+ chunks = ref_image.chunks
786
+
787
+ if len(chunks) != len(shape):
788
+ raise NgioValidationError(
789
+ "The chunks of the new image does not match the reference image."
790
+ f"Got {chunks} for shape {shape}."
791
+ )
792
+
793
+ if name is None:
794
+ name = ref_meta.name
795
+
796
+ if dtype is None:
797
+ dtype = ref_image.dtype
798
+
799
+ if dimension_separator is None:
800
+ dimension_separator = find_dimension_separator(ref_image.zarr_array)
801
+
802
+ if compressors is None:
803
+ compressors = ref_image.zarr_array.compressors # type: ignore
804
+
805
+ if ngff_version is None:
806
+ ngff_version = ref_meta.version
807
+
808
+ handler = create_empty_image_container(
1109
809
  store=store,
1110
810
  shape=shape,
1111
- pixelsize=pixelsize,
1112
- z_spacing=z_spacing,
1113
- time_spacing=time_spacing,
811
+ pixelsize=pixel_size.x,
812
+ z_spacing=pixel_size.z,
813
+ time_spacing=pixel_size.t,
814
+ levels=ref_meta.paths,
815
+ yx_scaling_factor=ref_meta.yx_scaling(),
816
+ z_scaling_factor=ref_meta.z_scaling(),
817
+ time_unit=pixel_size.time_unit,
818
+ space_unit=pixel_size.space_unit,
819
+ axes_names=axes_names,
1114
820
  name=name,
1115
- translation=translation,
1116
- channels_meta=channels_meta,
1117
- channels_policy=channels_policy,
1118
- ngff_version=ngff_version,
1119
821
  chunks=chunks,
1120
- shards=shards,
1121
822
  dtype=dtype,
1122
823
  dimension_separator=dimension_separator,
1123
824
  compressors=compressors,
1124
- extra_array_kwargs=extra_array_kwargs,
1125
825
  overwrite=overwrite,
1126
- labels=labels,
1127
- pixel_size=pixel_size,
826
+ version=ngff_version,
1128
827
  )
1129
- return ImagesContainer(group_handler=group_handler, axes_setup=axes_setup)
828
+ image_container = ImagesContainer(handler)
829
+
830
+ if ref_image.num_channels == image_container.num_channels:
831
+ _labels = ref_image.channel_labels
832
+ wavelength_id = ref_image.wavelength_ids
833
+
834
+ channel_meta = ref_image.channels_meta
835
+ colors = [c.channel_visualisation.color for c in channel_meta.channels]
836
+ active = [c.channel_visualisation.active for c in channel_meta.channels]
837
+ start = [c.channel_visualisation.start for c in channel_meta.channels]
838
+ end = [c.channel_visualisation.end for c in channel_meta.channels]
839
+ else:
840
+ _labels = None
841
+ wavelength_id = None
842
+ colors = None
843
+ active = None
844
+ start = None
845
+ end = None
846
+
847
+ if labels is not None:
848
+ if len(labels) != image_container.num_channels:
849
+ raise NgioValidationError(
850
+ "The number of labels does not match the number of channels."
851
+ )
852
+ _labels = labels
853
+
854
+ image_container.set_channel_meta(
855
+ labels=_labels,
856
+ wavelength_id=wavelength_id,
857
+ percentiles=None,
858
+ colors=colors,
859
+ active=active,
860
+ start=start,
861
+ end=end,
862
+ )
863
+ return image_container
1130
864
 
1131
865
 
1132
866
  def _parse_str_or_model(
@@ -1135,9 +869,9 @@ def _parse_str_or_model(
1135
869
  """Parse a string or ChannelSelectionModel to an integer channel index."""
1136
870
  if isinstance(channel_selection, int):
1137
871
  if channel_selection < 0:
1138
- raise NgioValueError("Channel index must be a non-negative integer.")
872
+ raise NgioValidationError("Channel index must be a non-negative integer.")
1139
873
  if channel_selection >= image.num_channels:
1140
- raise NgioValueError(
874
+ raise NgioValidationError(
1141
875
  "Channel index must be less than the number "
1142
876
  f"of channels ({image.num_channels})."
1143
877
  )
@@ -1151,11 +885,11 @@ def _parse_str_or_model(
1151
885
  )
1152
886
  elif channel_selection.mode == "wavelength_id":
1153
887
  return image.get_channel_idx(
1154
- wavelength_id=str(channel_selection.identifier)
888
+ channel_label=str(channel_selection.identifier)
1155
889
  )
1156
890
  elif channel_selection.mode == "index":
1157
891
  return int(channel_selection.identifier)
1158
- raise NgioValueError(
892
+ raise NgioValidationError(
1159
893
  "Invalid channel selection type. "
1160
894
  f"{channel_selection} is of type {type(channel_selection)} ",
1161
895
  "supported types are str, ChannelSelectionModel, and int.",
@@ -1174,7 +908,7 @@ def _parse_channel_selection(
1174
908
  elif isinstance(channel_selection, Sequence):
1175
909
  _sequence = [_parse_str_or_model(image, cs) for cs in channel_selection]
1176
910
  return {"c": _sequence}
1177
- raise NgioValueError(
911
+ raise NgioValidationError(
1178
912
  f"Invalid channel selection type {type(channel_selection)}. "
1179
913
  "Supported types are int, str, ChannelSelectionModel, and Sequence."
1180
914
  )
@@ -1188,7 +922,7 @@ def add_channel_selection_to_slicing_dict(
1188
922
  """Add channel selection information to the slicing dictionary."""
1189
923
  channel_info = _parse_channel_selection(image, channel_selection)
1190
924
  if "c" in slicing_dict and channel_info:
1191
- raise NgioValueError(
925
+ raise NgioValidationError(
1192
926
  "Both channel_selection and 'c' in slicing_kwargs are provided. "
1193
927
  "Which channel selection should be used is ambiguous. "
1194
928
  "Please provide only one."