ngio 0.3.4__py3-none-any.whl → 0.4.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 (73) hide show
  1. ngio/__init__.py +7 -2
  2. ngio/common/__init__.py +5 -52
  3. ngio/common/_dimensions.py +270 -55
  4. ngio/common/_masking_roi.py +38 -10
  5. ngio/common/_pyramid.py +51 -30
  6. ngio/common/_roi.py +269 -82
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +49 -19
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +127 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/_plate.py +41 -36
  18. ngio/images/__init__.py +22 -1
  19. ngio/images/_abstract_image.py +403 -176
  20. ngio/images/_create.py +31 -15
  21. ngio/images/_create_synt_container.py +138 -0
  22. ngio/images/_image.py +452 -63
  23. ngio/images/_label.py +56 -30
  24. ngio/images/_masked_image.py +387 -129
  25. ngio/images/_ome_zarr_container.py +237 -67
  26. ngio/{common → images}/_table_ops.py +41 -41
  27. ngio/io_pipes/__init__.py +75 -0
  28. ngio/io_pipes/_io_pipes.py +361 -0
  29. ngio/io_pipes/_io_pipes_masked.py +488 -0
  30. ngio/io_pipes/_io_pipes_roi.py +152 -0
  31. ngio/io_pipes/_io_pipes_types.py +56 -0
  32. ngio/io_pipes/_match_shape.py +376 -0
  33. ngio/io_pipes/_ops_axes.py +344 -0
  34. ngio/io_pipes/_ops_slices.py +446 -0
  35. ngio/io_pipes/_ops_slices_utils.py +196 -0
  36. ngio/io_pipes/_ops_transforms.py +104 -0
  37. ngio/io_pipes/_zoom_transform.py +175 -0
  38. ngio/ome_zarr_meta/__init__.py +4 -2
  39. ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -10
  40. ngio/ome_zarr_meta/ngio_specs/_axes.py +186 -175
  41. ngio/ome_zarr_meta/ngio_specs/_channels.py +55 -18
  42. ngio/ome_zarr_meta/ngio_specs/_dataset.py +48 -122
  43. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +6 -15
  44. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +38 -87
  45. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +17 -1
  46. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +34 -31
  47. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  48. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  49. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  50. ngio/resources/__init__.py +55 -0
  51. ngio/resources/resource_model.py +36 -0
  52. ngio/tables/backends/_abstract_backend.py +5 -6
  53. ngio/tables/backends/_anndata.py +1 -2
  54. ngio/tables/backends/_anndata_utils.py +3 -3
  55. ngio/tables/backends/_non_zarr_backends.py +1 -1
  56. ngio/tables/backends/_table_backends.py +0 -1
  57. ngio/tables/backends/_utils.py +3 -3
  58. ngio/tables/v1/_roi_table.py +165 -70
  59. ngio/transforms/__init__.py +5 -0
  60. ngio/transforms/_zoom.py +19 -0
  61. ngio/utils/__init__.py +2 -3
  62. ngio/utils/_datasets.py +5 -0
  63. ngio/utils/_logger.py +19 -0
  64. ngio/utils/_zarr_utils.py +6 -6
  65. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/METADATA +24 -22
  66. ngio-0.4.0.dist-info/RECORD +85 -0
  67. ngio/common/_array_pipe.py +0 -288
  68. ngio/common/_axes_transforms.py +0 -64
  69. ngio/common/_common_types.py +0 -5
  70. ngio/common/_slicer.py +0 -96
  71. ngio-0.3.4.dist-info/RECORD +0 -61
  72. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/WHEEL +0 -0
  73. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/licenses/LICENSE +0 -0
ngio/common/_pyramid.py CHANGED
@@ -1,12 +1,18 @@
1
1
  import math
2
- from collections.abc import Collection
2
+ from collections.abc import Callable, Sequence
3
3
  from typing import Literal
4
4
 
5
5
  import dask.array as da
6
6
  import numpy as np
7
7
  import zarr
8
+ from zarr.types import DIMENSION_SEPARATOR
8
9
 
9
- from ngio.common._zoom import _zoom_inputs_check, dask_zoom, numpy_zoom
10
+ from ngio.common._zoom import (
11
+ InterpolationOrder,
12
+ _zoom_inputs_check,
13
+ dask_zoom,
14
+ numpy_zoom,
15
+ )
10
16
  from ngio.utils import (
11
17
  AccessModeLiteral,
12
18
  NgioValueError,
@@ -18,7 +24,7 @@ from ngio.utils import (
18
24
  def _on_disk_numpy_zoom(
19
25
  source: zarr.Array,
20
26
  target: zarr.Array,
21
- order: Literal[0, 1, 2] = 1,
27
+ order: InterpolationOrder,
22
28
  ) -> None:
23
29
  target[...] = numpy_zoom(source[...], target_shape=target.shape, order=order)
24
30
 
@@ -26,7 +32,7 @@ def _on_disk_numpy_zoom(
26
32
  def _on_disk_dask_zoom(
27
33
  source: zarr.Array,
28
34
  target: zarr.Array,
29
- order: Literal[0, 1, 2] = 1,
35
+ order: InterpolationOrder,
30
36
  ) -> None:
31
37
  source_array = da.from_zarr(source)
32
38
  target_array = dask_zoom(source_array, target_shape=target.shape, order=order)
@@ -39,18 +45,18 @@ def _on_disk_dask_zoom(
39
45
  def _on_disk_coarsen(
40
46
  source: zarr.Array,
41
47
  target: zarr.Array,
42
- _order: Literal[0, 1] = 1,
43
- aggregation_function: np.ufunc | None = None,
48
+ order: InterpolationOrder = "linear",
49
+ aggregation_function: Callable | None = None,
44
50
  ) -> None:
45
51
  """Apply a coarsening operation from a source zarr array to a target zarr array.
46
52
 
47
53
  Args:
48
54
  source (zarr.Array): The source array to coarsen.
49
55
  target (zarr.Array): The target array to save the coarsened result to.
50
- _order (Literal[0, 1]): The order of interpolation is not really implemented
56
+ order (InterpolationOrder): The order of interpolation is not really implemented
51
57
  for coarsening, but it is kept for compatibility with the zoom function.
52
- _order=1 -> linear interpolation ~ np.mean
53
- _order=0 -> nearest interpolation ~ np.max
58
+ order="linear" -> linear interpolation ~ np.mean
59
+ order="nearest" -> nearest interpolation ~ np.max
54
60
  aggregation_function (np.ufunc): The aggregation function to use.
55
61
  """
56
62
  source_array = da.from_zarr(source)
@@ -64,13 +70,15 @@ def _on_disk_coarsen(
64
70
  )
65
71
 
66
72
  if aggregation_function is None:
67
- if _order == 1:
73
+ if order == "linear":
68
74
  aggregation_function = np.mean
69
- elif _order == 0:
75
+ elif order == "nearest":
70
76
  aggregation_function = np.max
77
+ elif order == "cubic":
78
+ raise NgioValueError("Cubic interpolation is not supported for coarsening.")
71
79
  else:
72
80
  raise NgioValueError(
73
- f"Aggregation function must be provided for order {_order}"
81
+ f"Aggregation function must be provided for order {order}"
74
82
  )
75
83
 
76
84
  coarsening_setup = {}
@@ -96,7 +104,7 @@ def _on_disk_coarsen(
96
104
  def on_disk_zoom(
97
105
  source: zarr.Array,
98
106
  target: zarr.Array,
99
- order: Literal[0, 1, 2] = 1,
107
+ order: InterpolationOrder = "linear",
100
108
  mode: Literal["dask", "numpy", "coarsen"] = "dask",
101
109
  ) -> None:
102
110
  """Apply a zoom operation from a source zarr array to a target zarr array.
@@ -104,7 +112,7 @@ def on_disk_zoom(
104
112
  Args:
105
113
  source (zarr.Array): The source array to zoom.
106
114
  target (zarr.Array): The target array to save the zoomed result to.
107
- order (Literal[0, 1, 2]): The order of interpolation. Defaults to 1.
115
+ order (InterpolationOrder): The order of interpolation. Defaults to "linear".
108
116
  mode (Literal["dask", "numpy", "coarsen"]): The mode to use. Defaults to "dask".
109
117
  """
110
118
  if not isinstance(source, zarr.Array):
@@ -132,7 +140,7 @@ def on_disk_zoom(
132
140
 
133
141
  def _find_closest_arrays(
134
142
  processed: list[zarr.Array], to_be_processed: list[zarr.Array]
135
- ) -> tuple[int, int]:
143
+ ) -> tuple[np.intp, np.intp]:
136
144
  dist_matrix = np.zeros((len(processed), len(to_be_processed)))
137
145
  for i, arr_to_proc in enumerate(to_be_processed):
138
146
  for j, proc_arr in enumerate(processed):
@@ -147,13 +155,15 @@ def _find_closest_arrays(
147
155
  )
148
156
  )
149
157
 
150
- return np.unravel_index(dist_matrix.argmin(), dist_matrix.shape)
158
+ indices = np.unravel_index(dist_matrix.argmin(), dist_matrix.shape)
159
+ assert len(indices) == 2, "Indices must be of length 2"
160
+ return indices
151
161
 
152
162
 
153
163
  def consolidate_pyramid(
154
164
  source: zarr.Array,
155
165
  targets: list[zarr.Array],
156
- order: Literal[0, 1, 2] = 1,
166
+ order: InterpolationOrder = "linear",
157
167
  mode: Literal["dask", "numpy", "coarsen"] = "dask",
158
168
  ) -> None:
159
169
  """Consolidate the Zarr array."""
@@ -175,14 +185,25 @@ def consolidate_pyramid(
175
185
  processed.append(target_image)
176
186
 
177
187
 
188
+ def _maybe_int(value: float | int) -> float | int:
189
+ """Convert a float to an int if it is an integer."""
190
+ if isinstance(value, int):
191
+ return value
192
+ if value.is_integer():
193
+ return int(value)
194
+ return value
195
+
196
+
178
197
  def init_empty_pyramid(
179
198
  store: StoreOrGroup,
180
199
  paths: list[str],
181
- ref_shape: Collection[int],
182
- scaling_factors: Collection[float],
183
- chunks: Collection[int] | None = None,
200
+ ref_shape: Sequence[int],
201
+ scaling_factors: Sequence[float],
202
+ chunks: Sequence[int] | None = None,
184
203
  dtype: str = "uint16",
185
204
  mode: AccessModeLiteral = "a",
205
+ dimension_separator: DIMENSION_SEPARATOR = "/",
206
+ compressor="default",
186
207
  ) -> None:
187
208
  # Return the an Image object
188
209
  if chunks is not None and len(chunks) != len(ref_shape):
@@ -198,6 +219,10 @@ def init_empty_pyramid(
198
219
  "The shape and scaling factor must have the same number of dimensions."
199
220
  )
200
221
 
222
+ # Ensure scaling factors are int if possible
223
+ # To reduce the risk of floating point issues
224
+ scaling_factors = [_maybe_int(s) for s in scaling_factors]
225
+
201
226
  root_group = open_group_wrapper(store, mode=mode)
202
227
 
203
228
  for path in paths:
@@ -211,22 +236,18 @@ def init_empty_pyramid(
211
236
  shape=ref_shape,
212
237
  dtype=dtype,
213
238
  chunks=chunks,
214
- dimension_separator="/",
239
+ dimension_separator=dimension_separator,
215
240
  overwrite=True,
241
+ compressor=compressor,
216
242
  )
217
243
 
218
- # Todo redo this with when a proper build of pyramid is implemented
219
- _shape = []
220
- for s, sc in zip(ref_shape, scaling_factors, strict=True):
221
- if math.floor(s / sc) % 2 == 0:
222
- _shape.append(math.floor(s / sc))
223
- else:
224
- _shape.append(math.ceil(s / sc))
244
+ _shape = [
245
+ math.floor(s / sc) for s, sc in zip(ref_shape, scaling_factors, strict=True)
246
+ ]
225
247
  ref_shape = _shape
226
248
 
227
249
  if chunks is None:
228
250
  chunks = new_arr.chunks
229
- if chunks is None:
230
- raise NgioValueError("Something went wrong with the chunks")
251
+ assert chunks is not None
231
252
  chunks = [min(c, s) for c, s in zip(chunks, ref_shape, strict=True)]
232
253
  return None
ngio/common/_roi.py CHANGED
@@ -4,62 +4,242 @@ These are the interfaces bwteen the ROI tables / masking ROI tables and
4
4
  the ImageLikeHandler.
5
5
  """
6
6
 
7
- from collections.abc import Iterable
7
+ from typing import TypeVar
8
+ from warnings import warn
8
9
 
9
- import numpy as np
10
- from pydantic import BaseModel, ConfigDict, Field
10
+ from pydantic import BaseModel, ConfigDict
11
11
 
12
12
  from ngio.common._dimensions import Dimensions
13
13
  from ngio.ome_zarr_meta.ngio_specs import DefaultSpaceUnit, PixelSize, SpaceUnits
14
14
  from ngio.utils import NgioValueError
15
15
 
16
16
 
17
- def _to_raster(value: float, pixel_size: float, max_shape: int) -> int:
18
- """Convert to raster coordinates."""
19
- round_value = int(np.round(value / pixel_size))
20
- # Ensure the value is within the image shape boundaries
21
- return max(0, min(round_value, max_shape))
17
+ def _to_raster(value: float, length: float, pixel_size: float) -> tuple[float, float]:
18
+ raster_value = value / pixel_size
19
+ raster_length = length / pixel_size
20
+ return raster_value, raster_length
22
21
 
23
22
 
24
- def _to_world(value: int, pixel_size: float) -> float:
23
+ def _to_slice(start: float | None, length: float | None) -> slice:
24
+ if length is not None:
25
+ assert start is not None
26
+ end = start + length
27
+ else:
28
+ end = None
29
+ return slice(start, end)
30
+
31
+
32
+ def _to_world(value: int | float, pixel_size: float) -> float:
25
33
  """Convert to world coordinates."""
26
34
  return value * pixel_size
27
35
 
28
36
 
29
- class Roi(BaseModel):
30
- """Region of interest (ROI) metadata."""
37
+ T = TypeVar("T", int, float)
38
+
31
39
 
32
- name: str
40
+ class GenericRoi(BaseModel):
41
+ """A generic Region of Interest (ROI) model."""
42
+
43
+ name: str | None = None
44
+ x: float
45
+ y: float
46
+ z: float | None = None
47
+ t: float | None = None
33
48
  x_length: float
34
49
  y_length: float
35
- z_length: float = 1.0
36
- x: float = 0.0
37
- y: float = 0.0
38
- z: float = 0.0
39
- unit: SpaceUnits | str | None = Field(DefaultSpaceUnit, repr=False)
50
+ z_length: float | None = None
51
+ t_length: float | None = None
52
+ label: int | None = None
53
+ unit: SpaceUnits | str | None = None
40
54
 
41
55
  model_config = ConfigDict(extra="allow")
42
56
 
43
- def to_pixel_roi(
44
- self, pixel_size: PixelSize, dimensions: Dimensions
45
- ) -> "RoiPixels":
57
+ def intersection(self, other: "GenericRoi") -> "GenericRoi | None":
58
+ """Calculate the intersection of this ROI with another ROI."""
59
+ return roi_intersection(self, other)
60
+
61
+ def _nice_str(self) -> str:
62
+ if self.t is not None:
63
+ t_str = f"t={self.t}->{self.t_length}"
64
+ else:
65
+ t_str = "t=None"
66
+ if self.z is not None:
67
+ z_str = f"z={self.z}->{self.z_length}"
68
+ else:
69
+ z_str = "z=None"
70
+
71
+ y_str = f"y={self.y}->{self.y_length}"
72
+ x_str = f"x={self.x}->{self.x_length}"
73
+
74
+ if self.label is not None:
75
+ label_str = f", label={self.label}"
76
+ else:
77
+ label_str = ""
78
+ cls_name = self.__class__.__name__
79
+ return f"{cls_name}({t_str}, {z_str}, {y_str}, {x_str}{label_str})"
80
+
81
+ def get_name(self) -> str:
82
+ """Get the name of the ROI, or a default if not set."""
83
+ if self.name is not None:
84
+ return self.name
85
+ return self._nice_str()
86
+
87
+ def __repr__(self) -> str:
88
+ return self._nice_str()
89
+
90
+ def __str__(self) -> str:
91
+ return self._nice_str()
92
+
93
+
94
+ def _1d_intersection(
95
+ a: T | None, a_length: T | None, b: T | None, b_length: T | None
96
+ ) -> tuple[T | None, T | None]:
97
+ """Calculate the intersection of two 1D intervals."""
98
+ if a is None:
99
+ if b is not None and b_length is not None:
100
+ return b, b_length
101
+ return None, None
102
+ if b is None:
103
+ if a is not None and a_length is not None:
104
+ return a, a_length
105
+ return None, None
106
+
107
+ assert (
108
+ a is not None
109
+ and a_length is not None
110
+ and b is not None
111
+ and b_length is not None
112
+ )
113
+ start = max(a, b)
114
+ end = min(a + a_length, b + b_length)
115
+ length = end - start
116
+
117
+ if length <= 0:
118
+ return None, None
119
+
120
+ return start, length
121
+
122
+
123
+ def roi_intersection(ref_roi: GenericRoi, other_roi: GenericRoi) -> GenericRoi | None:
124
+ """Calculate the intersection of two ROIs."""
125
+ if (
126
+ ref_roi.unit is not None
127
+ and other_roi.unit is not None
128
+ and ref_roi.unit != other_roi.unit
129
+ ):
130
+ raise NgioValueError(
131
+ "Cannot calculate intersection of ROIs with different units."
132
+ )
133
+
134
+ x, x_length = _1d_intersection(
135
+ ref_roi.x, ref_roi.x_length, other_roi.x, other_roi.x_length
136
+ )
137
+ if x is None and x_length is None:
138
+ # No intersection
139
+ return None
140
+ assert x is not None and x_length is not None
141
+
142
+ y, y_length = _1d_intersection(
143
+ ref_roi.y, ref_roi.y_length, other_roi.y, other_roi.y_length
144
+ )
145
+ if y is None and y_length is None:
146
+ # No intersection
147
+ return None
148
+ assert y is not None and y_length is not None
149
+
150
+ z, z_length = _1d_intersection(
151
+ ref_roi.z, ref_roi.z_length, other_roi.z, other_roi.z_length
152
+ )
153
+ t, t_length = _1d_intersection(
154
+ ref_roi.t, ref_roi.t_length, other_roi.t, other_roi.t_length
155
+ )
156
+
157
+ if (z_length is not None and z_length <= 0) or (
158
+ t_length is not None and t_length <= 0
159
+ ):
160
+ # No intersection
161
+ return None
162
+
163
+ # Find label
164
+ if ref_roi.label is not None and other_roi.label is not None:
165
+ if ref_roi.label != other_roi.label:
166
+ raise NgioValueError(
167
+ "Cannot calculate intersection of ROIs with different labels."
168
+ )
169
+ label = ref_roi.label or other_roi.label
170
+
171
+ if ref_roi.name is not None and other_roi.name is not None:
172
+ name = f"{ref_roi.name}:{other_roi.name}"
173
+ else:
174
+ name = ref_roi.name or other_roi.name
175
+
176
+ cls_ref = ref_roi.__class__
177
+ return cls_ref(
178
+ name=name,
179
+ x=x,
180
+ y=y,
181
+ z=z,
182
+ t=t,
183
+ x_length=x_length,
184
+ y_length=y_length,
185
+ z_length=z_length,
186
+ t_length=t_length,
187
+ unit=ref_roi.unit,
188
+ label=label,
189
+ )
190
+
191
+
192
+ class Roi(GenericRoi):
193
+ x: float = 0.0
194
+ y: float = 0.0
195
+ unit: SpaceUnits | str | None = DefaultSpaceUnit
196
+
197
+ def to_roi_pixels(self, pixel_size: PixelSize) -> "RoiPixels":
46
198
  """Convert to raster coordinates."""
47
- dim_x = dimensions.get("x")
48
- dim_y = dimensions.get("y")
49
- # Will default to 1 if z does not exist
50
- dim_z = dimensions.get("z", strict=False)
199
+ x, x_length = _to_raster(self.x, self.x_length, pixel_size.x)
200
+ y, y_length = _to_raster(self.y, self.y_length, pixel_size.y)
201
+
202
+ if self.z is None:
203
+ z, z_length = None, None
204
+ else:
205
+ assert self.z_length is not None
206
+ z, z_length = _to_raster(self.z, self.z_length, pixel_size.z)
207
+
208
+ if self.t is None:
209
+ t, t_length = None, None
210
+ else:
211
+ assert self.t_length is not None
212
+ t, t_length = _to_raster(self.t, self.t_length, pixel_size.t)
213
+ extra_dict = self.model_extra if self.model_extra else {}
51
214
 
52
215
  return RoiPixels(
53
216
  name=self.name,
54
- x=_to_raster(self.x, pixel_size.x, dim_x),
55
- y=_to_raster(self.y, pixel_size.y, dim_y),
56
- z=_to_raster(self.z, pixel_size.z, dim_z),
57
- x_length=_to_raster(self.x_length, pixel_size.x, dim_x),
58
- y_length=_to_raster(self.y_length, pixel_size.y, dim_y),
59
- z_length=_to_raster(self.z_length, pixel_size.z, dim_z),
60
- **self.model_extra,
217
+ x=x,
218
+ y=y,
219
+ z=z,
220
+ t=t,
221
+ x_length=x_length,
222
+ y_length=y_length,
223
+ z_length=z_length,
224
+ t_length=t_length,
225
+ label=self.label,
226
+ unit=self.unit,
227
+ **extra_dict,
61
228
  )
62
229
 
230
+ def to_pixel_roi(
231
+ self, pixel_size: PixelSize, dimensions: Dimensions | None = None
232
+ ) -> "RoiPixels":
233
+ """Convert to raster coordinates."""
234
+ warn(
235
+ "to_pixel_roi is deprecated and will be removed in a future release. "
236
+ "Use to_roi_pixels instead.",
237
+ DeprecationWarning,
238
+ stacklevel=2,
239
+ )
240
+
241
+ return self.to_roi_pixels(pixel_size=pixel_size)
242
+
63
243
  def zoom(self, zoom_factor: float = 1) -> "Roi":
64
244
  """Zoom the ROI by a factor.
65
245
 
@@ -72,38 +252,67 @@ class Roi(BaseModel):
72
252
  return zoom_roi(self, zoom_factor)
73
253
 
74
254
 
75
- class RoiPixels(BaseModel):
76
- """Region of interest (ROI) metadata."""
255
+ class RoiPixels(GenericRoi):
256
+ """Region of interest (ROI) in pixel coordinates."""
77
257
 
78
- name: str
79
- x: int
80
- y: int
81
- z: int
82
- x_length: int
83
- y_length: int
84
- z_length: int
85
- model_config = ConfigDict(extra="allow")
258
+ x: float = 0
259
+ y: float = 0
260
+ unit: SpaceUnits | str | None = None
86
261
 
87
- def to_roi(self, pixel_size: PixelSize) -> Roi:
88
- """Convert to world coordinates."""
262
+ def to_roi(self, pixel_size: PixelSize) -> "Roi":
263
+ """Convert to raster coordinates."""
264
+ x = _to_world(self.x, pixel_size.x)
265
+ x_length = _to_world(self.x_length, pixel_size.x)
266
+ y = _to_world(self.y, pixel_size.y)
267
+ y_length = _to_world(self.y_length, pixel_size.y)
268
+
269
+ if self.z is None:
270
+ z = None
271
+ else:
272
+ z = _to_world(self.z, pixel_size.z)
273
+
274
+ if self.z_length is None:
275
+ z_length = None
276
+ else:
277
+ z_length = _to_world(self.z_length, pixel_size.z)
278
+
279
+ if self.t is None:
280
+ t = None
281
+ else:
282
+ t = _to_world(self.t, pixel_size.t)
283
+
284
+ if self.t_length is None:
285
+ t_length = None
286
+ else:
287
+ t_length = _to_world(self.t_length, pixel_size.t)
288
+
289
+ extra_dict = self.model_extra if self.model_extra else {}
89
290
  return Roi(
90
291
  name=self.name,
91
- x=_to_world(self.x, pixel_size.x),
92
- y=_to_world(self.y, pixel_size.y),
93
- z=_to_world(self.z, pixel_size.z),
94
- x_length=_to_world(self.x_length, pixel_size.x),
95
- y_length=_to_world(self.y_length, pixel_size.y),
96
- z_length=_to_world(self.z_length, pixel_size.z),
97
- unit=pixel_size.space_unit,
98
- **self.model_extra,
292
+ x=x,
293
+ y=y,
294
+ z=z,
295
+ t=t,
296
+ x_length=x_length,
297
+ y_length=y_length,
298
+ z_length=z_length,
299
+ t_length=t_length,
300
+ label=self.label,
301
+ unit=self.unit,
302
+ **extra_dict,
99
303
  )
100
304
 
101
- def to_slices(self) -> dict[str, slice]:
102
- """Return the slices for the ROI."""
305
+ def to_slicing_dict(self) -> dict[str, slice]:
306
+ """Convert to a slicing dictionary."""
307
+ x_slice = _to_slice(self.x, self.x_length)
308
+ y_slice = _to_slice(self.y, self.y_length)
309
+ z_slice = _to_slice(self.z, self.z_length)
310
+ t_slice = _to_slice(self.t, self.t_length)
103
311
  return {
104
- "x": slice(self.x, self.x + self.x_length),
105
- "y": slice(self.y, self.y + self.y_length),
106
- "z": slice(self.z, self.z + self.z_length),
312
+ "x": x_slice,
313
+ "y": y_slice,
314
+ "z": z_slice,
315
+ "t": t_slice,
107
316
  }
108
317
 
109
318
 
@@ -118,7 +327,7 @@ def zoom_roi(roi: Roi, zoom_factor: float = 1) -> Roi:
118
327
  If the zoom factor is 1 the ROI will not be changed.
119
328
  """
120
329
  if zoom_factor <= 0:
121
- raise ValueError("Zoom factor must be greater than 0.")
330
+ raise NgioValueError("Zoom factor must be greater than 0.")
122
331
 
123
332
  # the zoom factor needs to be rescaled
124
333
  # from the range [-1, inf) to [0, inf)
@@ -134,34 +343,12 @@ def zoom_roi(roi: Roi, zoom_factor: float = 1) -> Roi:
134
343
  x=new_x,
135
344
  y=new_y,
136
345
  z=roi.z,
346
+ t=roi.t,
137
347
  x_length=roi.x_length + diff_x,
138
348
  y_length=roi.y_length + diff_y,
139
349
  z_length=roi.z_length,
350
+ t_length=roi.t_length,
351
+ label=roi.label,
140
352
  unit=roi.unit,
141
353
  )
142
-
143
354
  return new_roi
144
-
145
-
146
- def roi_to_slice_kwargs(
147
- roi: Roi,
148
- pixel_size: PixelSize,
149
- dimensions: Dimensions,
150
- **slice_kwargs: slice | int | Iterable[int],
151
- ) -> dict[str, slice | int | Iterable[int]]:
152
- """Convert a WorldCooROI to slice_kwargs."""
153
- raster_roi = roi.to_pixel_roi(
154
- pixel_size=pixel_size, dimensions=dimensions
155
- ).to_slices()
156
-
157
- if not dimensions.has_axis(axis_name="z"):
158
- raster_roi.pop("z")
159
-
160
- for key in slice_kwargs.keys():
161
- if key in raster_roi:
162
- raise NgioValueError(
163
- f"Key {key} is already in the slice_kwargs. "
164
- "Ambiguous which one to use: "
165
- f"{key}={slice_kwargs[key]} or roi_{key}={raster_roi[key]}"
166
- )
167
- return {**raster_roi, **slice_kwargs}