ngio 0.5.0b7__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ngio/images/_image.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Generic class to handle Image-like data in a OME-NGFF file."""
2
2
 
3
+ import logging
3
4
  from collections.abc import Mapping, Sequence
4
5
  from typing import Any, Literal
5
6
 
@@ -27,26 +28,28 @@ from ngio.ome_zarr_meta import (
27
28
  from ngio.ome_zarr_meta.ngio_specs import (
28
29
  Channel,
29
30
  ChannelsMeta,
30
- ChannelVisualisation,
31
31
  DefaultSpaceUnit,
32
32
  DefaultTimeUnit,
33
33
  NgffVersions,
34
34
  SpaceUnits,
35
35
  TimeUnits,
36
36
  )
37
+ from ngio.ome_zarr_meta.ngio_specs._axes import AxesSetup
37
38
  from ngio.utils import (
38
39
  NgioValueError,
39
40
  StoreOrGroup,
40
41
  ZarrGroupHandler,
41
42
  )
42
43
 
44
+ logger = logging.getLogger(f"ngio:{__name__}")
45
+
43
46
 
44
47
  class ChannelSelectionModel(BaseModel):
45
48
  """Model for channel selection.
46
49
 
47
50
  This model is used to select a channel by label, wavelength ID, or index.
48
51
 
49
- Args:
52
+ Properties:
50
53
  identifier (str): Unique identifier for the channel.
51
54
  This can be a channel label, wavelength ID, or index.
52
55
  mode (Literal["label", "wavelength_id", "index"]): Specifies how to
@@ -105,7 +108,7 @@ class Image(AbstractImage):
105
108
  self,
106
109
  group_handler: ZarrGroupHandler,
107
110
  path: str,
108
- meta_handler: ImageMetaHandler | None,
111
+ meta_handler: ImageMetaHandler,
109
112
  ) -> None:
110
113
  """Initialize the Image at a single level.
111
114
 
@@ -115,8 +118,6 @@ class Image(AbstractImage):
115
118
  meta_handler: The image metadata handler.
116
119
 
117
120
  """
118
- if meta_handler is None:
119
- meta_handler = ImageMetaHandler(group_handler)
120
121
  super().__init__(
121
122
  group_handler=group_handler, path=path, meta_handler=meta_handler
122
123
  )
@@ -401,76 +402,133 @@ class Image(AbstractImage):
401
402
 
402
403
 
403
404
  class ImagesContainer:
404
- """A class to handle the /labels group in an OME-NGFF file."""
405
+ """A class to handle the /images group in an OME-NGFF file."""
405
406
 
406
- def __init__(self, group_handler: ZarrGroupHandler) -> None:
407
- """Initialize the LabelGroupHandler."""
407
+ def __init__(
408
+ self,
409
+ group_handler: ZarrGroupHandler,
410
+ axes_setup: AxesSetup | None,
411
+ version: NgffVersions | None = None,
412
+ validate_paths: bool = True,
413
+ ) -> None:
414
+ """Initialize the ImagesContainer."""
408
415
  self._group_handler = group_handler
409
- self._meta_handler = ImageMetaHandler(group_handler)
416
+ self._meta_handler = ImageMetaHandler(
417
+ group_handler=group_handler, axes_setup=axes_setup, version=version
418
+ )
419
+ if validate_paths:
420
+ for level_path in self._meta_handler.get_meta().paths:
421
+ self.get(path=level_path)
410
422
 
411
423
  @property
412
424
  def meta(self) -> NgioImageMeta:
413
425
  """Return the metadata."""
414
426
  return self._meta_handler.get_meta()
415
427
 
428
+ @property
429
+ def channels_meta(self) -> ChannelsMeta:
430
+ """Return the channels metadata."""
431
+ return self.get().channels_meta
432
+
433
+ @property
434
+ def axes_setup(self) -> AxesSetup:
435
+ """Return the axes setup."""
436
+ return self.meta.axes_handler.axes_setup
437
+
438
+ @property
439
+ def level_paths(self) -> list[str]:
440
+ """Return the paths of the levels in the image."""
441
+ return self.meta.paths
442
+
443
+ @property
444
+ def levels_paths(self) -> list[str]:
445
+ """Deprecated: use 'level_paths' instead."""
446
+ logger.warning(
447
+ "'levels_paths' is deprecated and will be removed in ngio=0.6. "
448
+ "Please use 'level_paths' instead."
449
+ )
450
+ return self.level_paths
451
+
416
452
  @property
417
453
  def levels(self) -> int:
418
454
  """Return the number of levels in the image."""
419
- return self._meta_handler.get_meta().levels
455
+ return self.meta.levels
420
456
 
421
457
  @property
422
- def levels_paths(self) -> list[str]:
423
- """Return the paths of the levels in the image."""
424
- return self._meta_handler.get_meta().paths
458
+ def is_3d(self) -> bool:
459
+ """Return True if the image is 3D."""
460
+ return self.get().is_3d
425
461
 
426
462
  @property
427
- def num_channels(self) -> int:
428
- """Return the number of channels."""
429
- image = self.get()
430
- return image.num_channels
463
+ def is_2d(self) -> bool:
464
+ """Return True if the image is 2D."""
465
+ return self.get().is_2d
466
+
467
+ @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
491
+
492
+ @property
493
+ def time_unit(self) -> str | None:
494
+ """Return the time unit of the image."""
495
+ return self.meta.time_unit
431
496
 
432
497
  @property
433
498
  def channel_labels(self) -> list[str]:
434
499
  """Return the channels of the image."""
435
- image = self.get()
436
- return image.channel_labels
500
+ return self.get().channel_labels
437
501
 
438
502
  @property
439
503
  def wavelength_ids(self) -> list[str | None]:
440
- """Return the wavelength of the image."""
441
- image = self.get()
442
- return image.wavelength_ids
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
443
511
 
444
512
  def get_channel_idx(
445
513
  self, channel_label: str | None = None, wavelength_id: str | None = None
446
514
  ) -> int:
447
- """Get the index of a channel by label or wavelength ID.
448
-
449
- Args:
450
- channel_label (str | None): The label of the channel.
451
- If None a wavelength ID must be provided.
452
- wavelength_id (str | None): The wavelength ID of the channel.
453
- If None a channel label must be provided.
454
-
455
- Returns:
456
- int: The index of the channel.
457
-
458
- """
459
- image = self.get()
460
- return image.get_channel_idx(
515
+ """Get the index of a channel by its label or wavelength ID."""
516
+ return self.channels_meta.get_channel_idx(
461
517
  channel_label=channel_label, wavelength_id=wavelength_id
462
518
  )
463
519
 
464
520
  def _set_channel_meta(
465
521
  self,
466
- channels_meta: ChannelsMeta,
522
+ channels_meta: ChannelsMeta | None = None,
467
523
  ) -> None:
468
524
  """Set the channels metadata."""
525
+ if channels_meta is None:
526
+ channels_meta = ChannelsMeta.default_init(labels=self.num_channels)
469
527
  meta = self.meta
470
528
  meta.set_channels_meta(channels_meta)
471
529
  self._meta_handler.update_meta(meta)
472
530
 
473
- def set_channel_meta(
531
+ def _set_channel_meta_legacy(
474
532
  self,
475
533
  labels: Sequence[str | None] | int | None = None,
476
534
  wavelength_id: Sequence[str | None] | None = None,
@@ -515,12 +573,6 @@ class ImagesContainer:
515
573
  "If start and end are provided, percentiles must be None."
516
574
  )
517
575
 
518
- if percentiles is not None:
519
- start, end = compute_image_percentile(
520
- ref_image,
521
- start_percentile=percentiles[0],
522
- end_percentile=percentiles[1],
523
- )
524
576
  elif start is not None and end is not None:
525
577
  if len(start) != len(end):
526
578
  raise NgioValueError(
@@ -552,41 +604,205 @@ class ImagesContainer:
552
604
  **omero_kwargs,
553
605
  )
554
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
+ logger.warning(
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
+ )
666
+ self._set_channel_meta_legacy(
667
+ labels=labels,
668
+ wavelength_id=wavelength_id,
669
+ start=start,
670
+ end=end,
671
+ percentiles=percentiles,
672
+ colors=colors,
673
+ active=active,
674
+ **omero_kwargs,
675
+ )
676
+ return None
677
+ self._set_channel_meta(channel_meta)
678
+
679
+ def set_channel_labels(
680
+ self,
681
+ labels: Sequence[str],
682
+ ) -> None:
683
+ """Update the labels of the channels.
684
+
685
+ Args:
686
+ labels (Sequence[str]): The new labels for the channels.
687
+ """
688
+ channels_meta = self.channels_meta
689
+ if len(labels) != len(channels_meta.channels):
690
+ raise NgioValueError(
691
+ "The number of labels must match the number of channels."
692
+ )
693
+ new_channels = []
694
+ for label, ch in zip(labels, channels_meta.channels, strict=True):
695
+ channel = ch.model_copy(update={"label": label})
696
+ new_channels.append(channel)
697
+ new_meta = channels_meta.model_copy(update={"channels": new_channels})
698
+ self._set_channel_meta(new_meta)
699
+
700
+ def set_channel_colors(
701
+ self,
702
+ colors: Sequence[str],
703
+ ) -> None:
704
+ """Update the colors of the channels.
705
+
706
+ Args:
707
+ colors (Sequence[str]): The new colors for the channels.
708
+ """
709
+ channel_meta = self.channels_meta
710
+ if len(colors) != len(channel_meta.channels):
711
+ raise NgioValueError(
712
+ "The number of colors must match the number of channels."
713
+ )
714
+ new_channels = []
715
+ for color, ch in zip(colors, channel_meta.channels, strict=True):
716
+ ch_visualisation = ch.channel_visualisation.model_copy(
717
+ update={"color": color}
718
+ )
719
+ channel = ch.model_copy(update={"channel_visualisation": ch_visualisation})
720
+ new_channels.append(channel)
721
+ new_meta = channel_meta.model_copy(update={"channels": new_channels})
722
+ self._set_channel_meta(new_meta)
555
723
 
556
724
  def set_channel_percentiles(
557
725
  self,
558
726
  start_percentile: float = 0.1,
559
727
  end_percentile: float = 99.9,
560
728
  ) -> None:
561
- """Update the percentiles of the channels."""
562
- if self.meta._channels_meta is None:
563
- raise NgioValueError("The channels meta is not initialized.")
729
+ """Deprecated: Update the channel windows using percentiles.
564
730
 
565
- low_res_dataset = self.meta.get_lowest_resolution_dataset()
566
- ref_image = self.get(path=low_res_dataset.path)
567
- starts, ends = compute_image_percentile(
568
- ref_image, start_percentile=start_percentile, end_percentile=end_percentile
731
+ Args:
732
+ start_percentile (float): The start percentile.
733
+ end_percentile (float): The end percentile.
734
+ """
735
+ logger.warning(
736
+ "The 'set_channel_percentiles' method is deprecated and will be removed in "
737
+ "ngio=0.6. Please use 'set_channel_windows_with_percentiles' instead."
738
+ )
739
+ self.set_channel_windows_with_percentiles(
740
+ percentiles=(start_percentile, end_percentile)
569
741
  )
570
742
 
571
- channels = []
572
- for c, channel in enumerate(self.meta._channels_meta.channels):
573
- new_v = ChannelVisualisation(
574
- start=starts[c],
575
- end=ends[c],
576
- **channel.channel_visualisation.model_dump(exclude={"start", "end"}),
743
+ def set_channel_windows(
744
+ self,
745
+ starts_ends: Sequence[tuple[float, float]],
746
+ min_max: Sequence[tuple[float, float]] | None = None,
747
+ ) -> None:
748
+ """Update the channel windows.
749
+
750
+ These values are used by viewers to set the display
751
+ range of each channel.
752
+
753
+ Args:
754
+ starts_ends (Sequence[tuple[float, float]]): The start and end values
755
+ for each channel.
756
+ min_max (Sequence[tuple[float, float]] | None): The min and max values
757
+ for each channel. If None, the min and max values will not be updated.
758
+ """
759
+ current_channels = self.channels_meta.channels
760
+ if len(starts_ends) != len(current_channels):
761
+ raise NgioValueError(
762
+ "The number of start-end pairs must match the number of channels."
577
763
  )
578
- new_c = Channel(
579
- channel_visualisation=new_v,
580
- **channel.model_dump(exclude={"channel_visualisation"}),
764
+ if min_max is not None and len(min_max) != len(current_channels):
765
+ raise NgioValueError(
766
+ "The number of min-max pairs must match the number of channels."
581
767
  )
582
- channels.append(new_c)
583
-
768
+ if min_max is None:
769
+ min_max_ = [None] * len(current_channels)
770
+ else:
771
+ min_max_ = list(min_max)
772
+ channels = []
773
+ for se, mm, ch in zip(
774
+ starts_ends, min_max_, self.channels_meta.channels, strict=True
775
+ ):
776
+ updates = {"start": se[0], "end": se[1]}
777
+ if mm is not None:
778
+ updates.update({"min": mm[0], "max": mm[1]})
779
+ channel_visualisation = ch.channel_visualisation.model_copy(update=updates)
780
+ channel = ch.model_copy(
781
+ update={"channel_visualisation": channel_visualisation}
782
+ )
783
+ channels.append(channel)
584
784
  new_meta = ChannelsMeta(channels=channels)
585
-
586
785
  meta = self.meta
587
786
  meta.set_channels_meta(new_meta)
588
787
  self._meta_handler.update_meta(meta)
589
788
 
789
+ def set_channel_windows_with_percentiles(
790
+ self,
791
+ percentiles: tuple[float, float] | list[tuple[float, float]] = (0.1, 99.9),
792
+ ) -> None:
793
+ """Update the channel windows using percentiles.
794
+
795
+ Args:
796
+ percentiles (tuple[float, float] | list[tuple[float, float]]):
797
+ The start and end percentiles for each channel.
798
+ If a single tuple is provided,
799
+ the same percentiles will be used for all channels.
800
+ """
801
+ low_res_dataset = self.meta.get_lowest_resolution_dataset()
802
+ ref_image = self.get(path=low_res_dataset.path)
803
+ starts_ends = compute_image_percentile(ref_image, percentiles=percentiles)
804
+ self.set_channel_windows(starts_ends=starts_ends)
805
+
590
806
  def set_axes_unit(
591
807
  self,
592
808
  space_unit: SpaceUnits = DefaultSpaceUnit,
@@ -598,9 +814,33 @@ class ImagesContainer:
598
814
  space_unit (SpaceUnits): The space unit of the image.
599
815
  time_unit (TimeUnits): The time unit of the image.
600
816
  """
601
- meta = self.meta
602
- meta = meta.to_units(space_unit=space_unit, time_unit=time_unit)
603
- self._meta_handler.update_meta(meta)
817
+ self.get().set_axes_unit(space_unit=space_unit, time_unit=time_unit)
818
+
819
+ def set_axes_names(
820
+ self,
821
+ axes_names: Sequence[str],
822
+ ) -> None:
823
+ """Set the axes names of the image.
824
+
825
+ Args:
826
+ axes_names (Sequence[str]): The axes names of the image.
827
+ """
828
+ image = self.get()
829
+ image.set_axes_names(axes_names=axes_names)
830
+ self._meta_handler._axes_setup = image.meta.axes_handler.axes_setup
831
+
832
+ def set_name(
833
+ self,
834
+ name: str,
835
+ ) -> None:
836
+ """Set the name of the image in the metadata.
837
+
838
+ This does not change the group name or any paths.
839
+
840
+ Args:
841
+ name (str): The name of the image.
842
+ """
843
+ self.get().set_name(name=name)
604
844
 
605
845
  def derive(
606
846
  self,
@@ -633,8 +873,6 @@ class ImagesContainer:
633
873
  If a kwarg is not provided, the value from the reference image will be used.
634
874
 
635
875
  Args:
636
- image_container (ImagesContainer): The image container to derive the new
637
- image.
638
876
  store (StoreOrGroup): The Zarr store or group to create the image in.
639
877
  ref_path (str | None): The path to the reference image in the image
640
878
  container.
@@ -727,34 +965,53 @@ class ImagesContainer:
727
965
 
728
966
  def compute_image_percentile(
729
967
  image: Image,
730
- start_percentile: float = 0.1,
731
- end_percentile: float = 99.9,
732
- ) -> tuple[list[float], list[float]]:
968
+ percentiles: tuple[float, float] | list[tuple[float, float]] = (0.1, 99.9),
969
+ ) -> list[tuple[float, float]]:
733
970
  """Compute the start and end percentiles for each channel of an image.
734
971
 
735
972
  Args:
736
973
  image: The image to compute the percentiles for.
737
- start_percentile: The start percentile to compute.
738
- end_percentile: The end percentile to compute.
974
+ percentiles: The start and end percentiles for each channel.
975
+ If a single tuple is provided, the same percentiles will be used
976
+ for all channels.
739
977
 
740
978
  Returns:
741
979
  A tuple containing the start and end percentiles for each channel.
742
980
  """
743
- starts, ends = [], []
744
- for c in range(image.num_channels):
745
- if image.num_channels == 1:
746
- data = image.get_as_dask()
747
- else:
748
- data = image.get_as_dask(c=c)
981
+ num_channels = image.num_channels
982
+ # handle the case where a single tuple is provided
983
+ if isinstance(percentiles, tuple):
984
+ if len(percentiles) != 2:
985
+ raise NgioValueError(
986
+ "Percentiles must be a tuple of two floats: "
987
+ "(start_percentile, end_percentile) or "
988
+ "a list of such tuples with length equal to the number of channels."
989
+ )
990
+ if not isinstance(percentiles[0], float) or not isinstance(
991
+ percentiles[1], float
992
+ ):
993
+ raise NgioValueError(
994
+ "Percentiles must be a tuple of two floats: "
995
+ "(start_percentile, end_percentile) or "
996
+ "a list of such tuples with length equal to the number of channels."
997
+ )
998
+ percentiles = [percentiles] * num_channels
749
999
 
1000
+ if len(percentiles) != num_channels:
1001
+ raise NgioValueError(
1002
+ "If a list of percentiles is provided, its length must be equal "
1003
+ "to the number of channels."
1004
+ )
1005
+ starts_and_ends = []
1006
+ for c_idx, (start_percentile, end_percentile) in enumerate(percentiles):
1007
+ data = image.get_as_dask(c=c_idx)
750
1008
  data = da.ravel(data)
751
1009
  # remove all the zeros
752
1010
  mask = data > 1e-16
753
1011
  data = data[mask]
754
1012
  _data = data.compute()
755
1013
  if _data.size == 0:
756
- starts.append(0.0)
757
- ends.append(0.0)
1014
+ starts_and_ends.append((0.0, 0.0))
758
1015
  continue
759
1016
 
760
1017
  # compute the percentiles
@@ -762,9 +1019,8 @@ def compute_image_percentile(
762
1019
  data, [start_percentile, end_percentile], method="nearest"
763
1020
  ).compute() # type: ignore (return type is a tuple of floats)
764
1021
 
765
- starts.append(float(_s_perc))
766
- ends.append(float(_e_perc))
767
- return starts, ends
1022
+ starts_and_ends.append((float(_s_perc), float(_e_perc)))
1023
+ return starts_and_ends
768
1024
 
769
1025
 
770
1026
  def derive_image_container(
@@ -840,7 +1096,7 @@ def derive_image_container(
840
1096
 
841
1097
  """
842
1098
  ref_image = image_container.get(path=ref_path)
843
- group_handler = abstract_derive(
1099
+ group_handler, axes_setup = abstract_derive(
844
1100
  ref_image=ref_image,
845
1101
  meta_type=NgioImageMeta,
846
1102
  store=store,
@@ -863,7 +1119,7 @@ def derive_image_container(
863
1119
  labels=labels,
864
1120
  pixel_size=pixel_size,
865
1121
  )
866
- return ImagesContainer(group_handler=group_handler)
1122
+ return ImagesContainer(group_handler=group_handler, axes_setup=axes_setup)
867
1123
 
868
1124
 
869
1125
  def _parse_str_or_model(