ngio 0.5.0b6__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.
@@ -1,6 +1,6 @@
1
1
  """Generic class to handle Image-like data in a OME-NGFF file."""
2
2
 
3
- import warnings
3
+ import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from collections.abc import Mapping, Sequence
6
6
  from typing import Any, Literal
@@ -16,9 +16,15 @@ from ngio.common import (
16
16
  Roi,
17
17
  consolidate_pyramid,
18
18
  )
19
- from ngio.common._pyramid import ChunksLike, ShardsLike, shapes_from_scaling_factors
19
+ from ngio.common._pyramid import (
20
+ ChunksLike,
21
+ ShardsLike,
22
+ compute_scales_from_shapes,
23
+ compute_shapes_from_scaling_factors,
24
+ )
20
25
  from ngio.images._create_utils import (
21
26
  _image_or_label_meta,
27
+ compute_base_scale,
22
28
  init_image_like_from_shapes,
23
29
  )
24
30
  from ngio.io_pipes import (
@@ -46,6 +52,13 @@ from ngio.ome_zarr_meta.ngio_specs import (
46
52
  NgffVersions,
47
53
  NgioLabelMeta,
48
54
  )
55
+ from ngio.ome_zarr_meta.ngio_specs._axes import (
56
+ AxesSetup,
57
+ DefaultSpaceUnit,
58
+ DefaultTimeUnit,
59
+ SpaceUnits,
60
+ TimeUnits,
61
+ )
49
62
  from ngio.tables import RoiTable
50
63
  from ngio.utils import (
51
64
  NgioFileExistsError,
@@ -55,6 +68,8 @@ from ngio.utils import (
55
68
  )
56
69
  from ngio.utils._zarr_utils import find_dimension_separator
57
70
 
71
+ logger = logging.getLogger(f"ngio:{__name__}")
72
+
58
73
 
59
74
  class AbstractImage(ABC):
60
75
  """A class to handle a single image (or level) in an OME-Zarr image.
@@ -130,6 +145,11 @@ class AbstractImage(ABC):
130
145
  """Return the axes handler of the image."""
131
146
  return self.dataset.axes_handler
132
147
 
148
+ @property
149
+ def axes_setup(self) -> AxesSetup:
150
+ """Return the axes setup of the image."""
151
+ return self.axes_handler.axes_setup
152
+
133
153
  @property
134
154
  def axes(self) -> tuple[str, ...]:
135
155
  """Return the axes of the image."""
@@ -199,6 +219,47 @@ class AbstractImage(ABC):
199
219
  """Return True if the image has the given axis."""
200
220
  return self.axes_handler.has_axis(axis)
201
221
 
222
+ def set_axes_unit(
223
+ self,
224
+ space_unit: SpaceUnits = DefaultSpaceUnit,
225
+ time_unit: TimeUnits = DefaultTimeUnit,
226
+ ) -> None:
227
+ """Set the axes unit of the image.
228
+
229
+ Args:
230
+ space_unit (SpaceUnits): The space unit of the image.
231
+ time_unit (TimeUnits): The time unit of the image.
232
+ """
233
+ meta = self._meta_handler.get_meta()
234
+ meta = meta.to_units(space_unit=space_unit, time_unit=time_unit)
235
+ self._meta_handler.update_meta(meta) # type: ignore
236
+
237
+ def set_axes_names(self, axes_names: Sequence[str]) -> None:
238
+ """Set the axes names of the label.
239
+
240
+ Args:
241
+ axes_names (Sequence[str]): The axes names to set.
242
+ """
243
+ meta = self._meta_handler.get_meta()
244
+ meta = meta.rename_axes(axes_names=axes_names)
245
+ self._meta_handler._axes_setup = meta.axes_handler.axes_setup
246
+ self._meta_handler.update_meta(meta) # type: ignore
247
+
248
+ def set_name(
249
+ self,
250
+ name: str,
251
+ ) -> None:
252
+ """Set the name of the image in the metadata.
253
+
254
+ This does not change the group name or any paths.
255
+
256
+ Args:
257
+ name (str): The name of the image.
258
+ """
259
+ meta = self._meta_handler.get_meta()
260
+ meta = meta.rename_image(name=name)
261
+ self._meta_handler.update_meta(meta) # type: ignore
262
+
202
263
  def _get_as_numpy(
203
264
  self,
204
265
  axes_order: Sequence[str] | None = None,
@@ -602,32 +663,39 @@ def consolidate_image(
602
663
 
603
664
  def _shapes_from_ref_image(
604
665
  ref_image: AbstractImage,
605
- ) -> list[tuple[int, ...]]:
666
+ ) -> tuple[list[tuple[int, ...]], list[tuple[float, ...]]]:
606
667
  """Rebuild base shape based on a new shape."""
607
- paths = ref_image.meta.paths
668
+ meta = ref_image.meta
669
+ paths = meta.paths
608
670
  index_path = paths.index(ref_image.path)
609
671
  sub_paths = paths[index_path:]
610
672
  group_handler = ref_image._group_handler
611
- shapes = []
673
+ shapes, scales = [], []
612
674
  for path in sub_paths:
613
675
  zarr_array = group_handler.get_array(path)
614
676
  shapes.append(zarr_array.shape)
677
+ scales.append(meta.get_dataset(path=path).scale)
615
678
  if len(shapes) == len(paths):
616
- return shapes
679
+ return shapes, scales
617
680
  missing_levels = len(paths) - len(shapes)
618
- extended_shapes = shapes_from_scaling_factors(
681
+ extended_shapes = compute_shapes_from_scaling_factors(
619
682
  base_shape=shapes[-1],
620
683
  scaling_factors=ref_image.meta.scaling_factor(),
621
684
  num_levels=missing_levels + 1,
622
685
  )
623
686
  shapes.extend(extended_shapes[1:])
624
- return shapes
687
+ extended_scales = compute_scales_from_shapes(
688
+ shapes=extended_shapes,
689
+ base_scale=scales[-1],
690
+ )
691
+ scales.extend(extended_scales[1:])
692
+ return shapes, scales
625
693
 
626
694
 
627
695
  def _shapes_from_new_shape(
628
696
  ref_image: AbstractImage,
629
697
  shape: Sequence[int],
630
- ) -> list[tuple[int, ...]]:
698
+ ) -> tuple[list[tuple[int, ...]], list[tuple[float, ...]]]:
631
699
  """Rebuild pyramid shapes based on a new base shape."""
632
700
  if len(shape) != len(ref_image.shape):
633
701
  raise NgioValueError(
@@ -637,27 +705,33 @@ def _shapes_from_new_shape(
637
705
  base_shape = tuple(shape)
638
706
  scaling_factors = ref_image.meta.scaling_factor()
639
707
  num_levels = len(ref_image.meta.paths)
640
- return shapes_from_scaling_factors(
708
+ shapes = compute_shapes_from_scaling_factors(
641
709
  base_shape=base_shape,
642
710
  scaling_factors=scaling_factors,
643
711
  num_levels=num_levels,
644
712
  )
713
+ scales = compute_scales_from_shapes(
714
+ shapes=shapes,
715
+ base_scale=ref_image.dataset.scale,
716
+ )
717
+ return shapes, scales
645
718
 
646
719
 
647
720
  def _compute_pyramid_shapes(
648
721
  ref_image: AbstractImage,
649
722
  shape: Sequence[int] | None,
650
- ) -> list[tuple[int, ...]]:
723
+ ) -> tuple[list[tuple[int, ...]], list[tuple[float, ...]]]:
651
724
  """Rebuild pyramid shapes based on a new base shape."""
652
725
  if shape is None:
653
726
  return _shapes_from_ref_image(ref_image=ref_image)
654
727
  return _shapes_from_new_shape(ref_image=ref_image, shape=shape)
655
728
 
656
729
 
657
- def _check_chunks_and_shards_compatibility(
730
+ def _check_len_compatibility(
658
731
  ref_shape: tuple[int, ...],
659
732
  chunks: ChunksLike,
660
733
  shards: ShardsLike | None,
734
+ translation: Sequence[float] | None = None,
661
735
  ) -> None:
662
736
  """Check if the chunks and shards are compatible with the reference shape.
663
737
 
@@ -665,6 +739,7 @@ def _check_chunks_and_shards_compatibility(
665
739
  ref_shape: The reference shape.
666
740
  chunks: The chunks to check.
667
741
  shards: The shards to check.
742
+ translation: The translation to check.
668
743
  """
669
744
  if chunks != "auto":
670
745
  if len(chunks) != len(ref_shape):
@@ -676,6 +751,12 @@ def _check_chunks_and_shards_compatibility(
676
751
  raise NgioValueError(
677
752
  "The length of the shards must be the same as the number of dimensions."
678
753
  )
754
+ if translation is not None:
755
+ if len(translation) != len(ref_shape):
756
+ raise NgioValueError(
757
+ "The length of the translation must be the same as the number of "
758
+ "dimensions."
759
+ )
679
760
 
680
761
 
681
762
  def _apply_channel_policy(
@@ -685,7 +766,16 @@ def _apply_channel_policy(
685
766
  axes: tuple[str, ...],
686
767
  chunks: ChunksLike,
687
768
  shards: ShardsLike | None,
688
- ) -> tuple[list[tuple[int, ...]], tuple[str, ...], ChunksLike, ShardsLike | None]:
769
+ translation: Sequence[float],
770
+ scales: list[tuple[float, ...]] | tuple[float, ...],
771
+ ) -> tuple[
772
+ list[tuple[int, ...]],
773
+ tuple[str, ...],
774
+ ChunksLike,
775
+ ShardsLike | None,
776
+ tuple[float, ...],
777
+ list[tuple[float, ...]] | tuple[float, ...],
778
+ ]:
689
779
  """Apply the channel policy to the shapes and axes.
690
780
 
691
781
  Args:
@@ -695,12 +785,15 @@ def _apply_channel_policy(
695
785
  axes: The axes of the image.
696
786
  chunks: The chunks of the image.
697
787
  shards: The shards of the image.
788
+ translation: The translation of the image.
789
+ scales: The scales of the image.
698
790
 
699
791
  Returns:
700
792
  The new shapes and axes after applying the channel policy.
701
793
  """
794
+ translation = tuple(translation)
702
795
  if channels_policy == "same":
703
- return shapes, axes, chunks, shards
796
+ return shapes, axes, chunks, shards, translation, scales
704
797
 
705
798
  if channels_policy == "singleton":
706
799
  # Treat 'singleton' as setting channel size to 1
@@ -709,7 +802,7 @@ def _apply_channel_policy(
709
802
  channel_index = ref_image.axes_handler.get_index("c")
710
803
  if channel_index is None:
711
804
  if channels_policy == "squeeze":
712
- return shapes, axes, chunks, shards
805
+ return shapes, axes, chunks, shards, translation, scales
713
806
  raise NgioValueError(
714
807
  f"Cannot apply channel policy {channels_policy=} to an image "
715
808
  "without channels axis."
@@ -719,6 +812,15 @@ def _apply_channel_policy(
719
812
  for shape in shapes:
720
813
  new_shape = shape[:channel_index] + shape[channel_index + 1 :]
721
814
  new_shapes.append(new_shape)
815
+
816
+ if isinstance(scales, tuple):
817
+ new_scales = scales[:channel_index] + scales[channel_index + 1 :]
818
+ else:
819
+ new_scales = []
820
+ for scale in scales:
821
+ new_scale = scale[:channel_index] + scale[channel_index + 1 :]
822
+ new_scales.append(new_scale)
823
+
722
824
  new_axes = axes[:channel_index] + axes[channel_index + 1 :]
723
825
  if chunks == "auto":
724
826
  new_chunks: ChunksLike = "auto"
@@ -728,7 +830,9 @@ def _apply_channel_policy(
728
830
  new_shards: ShardsLike | None = shards
729
831
  else:
730
832
  new_shards = shards[:channel_index] + shards[channel_index + 1 :]
731
- return new_shapes, new_axes, new_chunks, new_shards
833
+
834
+ translation = translation[:channel_index] + translation[channel_index + 1 :]
835
+ return new_shapes, new_axes, new_chunks, new_shards, translation, new_scales
732
836
  elif isinstance(channels_policy, int):
733
837
  new_shapes = []
734
838
  for shape in shapes:
@@ -738,7 +842,7 @@ def _apply_channel_policy(
738
842
  *shape[channel_index + 1 :],
739
843
  )
740
844
  new_shapes.append(new_shape)
741
- return new_shapes, axes, chunks, shards
845
+ return new_shapes, axes, chunks, shards, translation, scales
742
846
  else:
743
847
  raise NgioValueError(
744
848
  f"Invalid channels policy: {channels_policy}. "
@@ -783,6 +887,35 @@ def _check_channels_meta_compatibility(
783
887
  return channels_meta_
784
888
 
785
889
 
890
+ def adapt_scales(
891
+ scales: list[tuple[float, ...]],
892
+ pixelsize: float | tuple[float, float] | None,
893
+ z_spacing: float | None,
894
+ time_spacing: float | None,
895
+ ref_image: AbstractImage,
896
+ ) -> list[tuple[float, ...]] | tuple[float, ...]:
897
+ if pixelsize is None and z_spacing is None and time_spacing is None:
898
+ return scales
899
+ pixel_size = ref_image.pixel_size
900
+ if pixelsize is None:
901
+ pixelsize = (pixel_size.y, pixel_size.x)
902
+ if z_spacing is None:
903
+ z_spacing = pixel_size.z
904
+ else:
905
+ z_spacing = z_spacing
906
+ if time_spacing is None:
907
+ time_spacing = pixel_size.t
908
+ else:
909
+ time_spacing = time_spacing
910
+ base_scale = compute_base_scale(
911
+ pixelsize=pixelsize,
912
+ z_spacing=z_spacing,
913
+ time_spacing=time_spacing,
914
+ axes_handler=ref_image.axes_handler,
915
+ )
916
+ return base_scale
917
+
918
+
786
919
  def abstract_derive(
787
920
  *,
788
921
  ref_image: AbstractImage,
@@ -795,6 +928,7 @@ def abstract_derive(
795
928
  z_spacing: float | None = None,
796
929
  time_spacing: float | None = None,
797
930
  name: str | None = None,
931
+ translation: Sequence[float] | None = None,
798
932
  channels_policy: Literal["squeeze", "same", "singleton"] | int = "same",
799
933
  channels_meta: Sequence[str | Channel] | None = None,
800
934
  ngff_version: NgffVersions | None = None,
@@ -808,7 +942,7 @@ def abstract_derive(
808
942
  # Deprecated arguments
809
943
  labels: Sequence[str] | None = None,
810
944
  pixel_size: PixelSize | None = None,
811
- ) -> ZarrGroupHandler:
945
+ ) -> tuple[ZarrGroupHandler, AxesSetup]:
812
946
  """Create an empty OME-Zarr image from an existing image.
813
947
 
814
948
  If a kwarg is not provided, the value from the reference image will be used.
@@ -823,6 +957,8 @@ def abstract_derive(
823
957
  z_spacing (float | None): The z spacing of the new image.
824
958
  time_spacing (float | None): The time spacing of the new image.
825
959
  name (str | None): The name of the new image.
960
+ translation (Sequence[float] | None): The translation for each axis
961
+ at the highest resolution level. Defaults to None.
826
962
  channels_policy (Literal["squeeze", "same", "singleton"] | int):
827
963
  Possible policies:
828
964
  - If "squeeze", the channels axis will be removed (no matter its size).
@@ -853,54 +989,34 @@ def abstract_derive(
853
989
  """
854
990
  # TODO: remove in ngio 0.6
855
991
  if labels is not None:
856
- warnings.warn(
992
+ logger.warning(
857
993
  "The 'labels' argument is deprecated and will be removed in "
858
- "a future release.",
859
- DeprecationWarning,
860
- stacklevel=2,
994
+ "ngio=0.6. Please use 'channels_meta' instead."
861
995
  )
862
996
  channels_meta = list(labels)
863
997
  if pixel_size is not None:
864
- warnings.warn(
998
+ logger.warning(
865
999
  "The 'pixel_size' argument is deprecated and will be removed in "
866
- "a future release.",
867
- DeprecationWarning,
868
- stacklevel=2,
1000
+ "ngio=0.6. Please use 'pixelsize', 'z_spacing', and 'time_spacing'"
1001
+ "instead."
869
1002
  )
870
- pixelsize_ = (pixel_size.y, pixel_size.x)
871
- z_spacing_ = pixel_size.z
872
- time_spacing_ = pixel_size.t
873
- else:
874
- if pixelsize is None:
875
- pixelsize_ = (ref_image.pixel_size.y, ref_image.pixel_size.x)
876
- else:
877
- pixelsize_ = pixelsize
878
-
879
- if z_spacing is None:
880
- z_spacing_ = ref_image.pixel_size.z
881
- else:
882
- z_spacing_ = z_spacing
883
-
884
- if time_spacing is None:
885
- time_spacing_ = ref_image.pixel_size.t
886
- else:
887
- time_spacing_ = time_spacing
1003
+ pixelsize = (pixel_size.y, pixel_size.x)
1004
+ # End of deprecated arguments handling
888
1005
  ref_meta = ref_image.meta
889
1006
 
890
- shapes = _compute_pyramid_shapes(
1007
+ shapes, scales = _compute_pyramid_shapes(
891
1008
  shape=shape,
892
1009
  ref_image=ref_image,
893
1010
  )
894
1011
  ref_shape = next(iter(shapes))
895
1012
 
896
- if pixelsize is None:
897
- pixelsize = (ref_image.pixel_size.y, ref_image.pixel_size.x)
898
-
899
- if z_spacing is None:
900
- z_spacing = ref_image.pixel_size.z
901
-
902
- if time_spacing is None:
903
- time_spacing = ref_image.pixel_size.t
1013
+ scales = adapt_scales(
1014
+ scales=scales,
1015
+ pixelsize=pixelsize,
1016
+ z_spacing=z_spacing,
1017
+ time_spacing=time_spacing,
1018
+ ref_image=ref_image,
1019
+ )
904
1020
 
905
1021
  if name is None:
906
1022
  name = ref_meta.name
@@ -914,27 +1030,33 @@ def abstract_derive(
914
1030
  if compressors is None:
915
1031
  compressors = ref_image.zarr_array.compressors # type: ignore
916
1032
 
1033
+ if translation is None:
1034
+ translation = ref_image.dataset.translation
1035
+
917
1036
  if chunks is None:
918
1037
  chunks = ref_image.zarr_array.chunks
919
1038
  if shards is None:
920
1039
  shards = ref_image.zarr_array.shards
921
1040
 
922
- _check_chunks_and_shards_compatibility(
1041
+ _check_len_compatibility(
923
1042
  ref_shape=ref_shape,
924
1043
  chunks=chunks,
925
1044
  shards=shards,
1045
+ translation=translation,
926
1046
  )
927
1047
 
928
1048
  if ngff_version is None:
929
1049
  ngff_version = ref_meta.version
930
1050
 
931
- shapes, axes, chunks, shards = _apply_channel_policy(
1051
+ shapes, axes, chunks, shards, translation, scales = _apply_channel_policy(
932
1052
  ref_image=ref_image,
933
1053
  channels_policy=channels_policy,
934
1054
  shapes=shapes,
935
1055
  axes=ref_image.axes,
936
1056
  chunks=chunks,
937
1057
  shards=shards,
1058
+ translation=translation,
1059
+ scales=scales,
938
1060
  )
939
1061
  channels_meta_ = _check_channels_meta_compatibility(
940
1062
  meta_type=meta_type,
@@ -942,18 +1064,18 @@ def abstract_derive(
942
1064
  channels_meta=channels_meta,
943
1065
  )
944
1066
 
945
- handler = init_image_like_from_shapes(
1067
+ handler, axes_setup = init_image_like_from_shapes(
946
1068
  store=store,
947
1069
  meta_type=meta_type,
948
1070
  shapes=shapes,
949
- pixelsize=pixelsize_,
950
- z_spacing=z_spacing_,
951
- time_spacing=time_spacing_,
1071
+ base_scale=scales,
952
1072
  levels=ref_meta.paths,
1073
+ translation=translation,
953
1074
  time_unit=ref_image.time_unit,
954
1075
  space_unit=ref_image.space_unit,
955
1076
  axes_names=axes,
956
1077
  name=name,
1078
+ axes_setup=ref_image.axes_setup,
957
1079
  channels_meta=channels_meta_,
958
1080
  chunks=chunks,
959
1081
  shards=shards,
@@ -964,4 +1086,4 @@ def abstract_derive(
964
1086
  ngff_version=ngff_version,
965
1087
  extra_array_kwargs=extra_array_kwargs,
966
1088
  )
967
- return handler
1089
+ return handler, axes_setup
@@ -30,6 +30,7 @@ def create_synthetic_ome_zarr(
30
30
  shape: Sequence[int],
31
31
  reference_sample: AVAILABLE_SAMPLES | SampleInfo = "Cardiomyocyte",
32
32
  levels: int | list[str] = 5,
33
+ translation: Sequence[float] | None = None,
33
34
  table_backend: TableBackend = DefaultTableBackend,
34
35
  scaling_factors: Sequence[float] | Literal["auto"] = "auto",
35
36
  axes_names: Sequence[str] | None = None,
@@ -51,6 +52,8 @@ def create_synthetic_ome_zarr(
51
52
  Defaults to "Cardiomyocyte".
52
53
  levels (int | list[str]): The number of levels in the pyramid or a list of
53
54
  level names. Defaults to 5.
55
+ translation (Sequence[float] | None): The translation for each axis
56
+ at the highest resolution level. Defaults to None.
54
57
  table_backend (TableBackend): Table backend to be used to store tables.
55
58
  Defaults to DefaultTableBackend.
56
59
  scaling_factors (Sequence[float] | Literal["auto"]): The down-scaling factors
@@ -82,10 +85,11 @@ def create_synthetic_ome_zarr(
82
85
  ome_zarr = create_ome_zarr_from_array(
83
86
  store=store,
84
87
  array=raw,
85
- pixelsize=sample_info.xy_pixelsize,
88
+ pixelsize=sample_info.pixelsize,
86
89
  z_spacing=sample_info.z_spacing,
87
90
  time_spacing=sample_info.time_spacing,
88
91
  levels=levels,
92
+ translation=translation,
89
93
  space_unit=sample_info.space_unit,
90
94
  time_unit=sample_info.time_unit,
91
95
  axes_names=axes_names,