ngio 0.5.0a2__py3-none-any.whl → 0.5.0a3__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 (47) hide show
  1. ngio/__init__.py +2 -2
  2. ngio/common/__init__.py +11 -6
  3. ngio/common/_masking_roi.py +12 -41
  4. ngio/common/_pyramid.py +206 -76
  5. ngio/common/_roi.py +257 -329
  6. ngio/experimental/iterators/_feature.py +3 -3
  7. ngio/experimental/iterators/_rois_utils.py +10 -11
  8. ngio/hcs/_plate.py +50 -43
  9. ngio/images/_abstract_image.py +417 -35
  10. ngio/images/_create_synt_container.py +35 -42
  11. ngio/images/_create_utils.py +423 -0
  12. ngio/images/_image.py +154 -176
  13. ngio/images/_label.py +144 -119
  14. ngio/images/_ome_zarr_container.py +361 -196
  15. ngio/io_pipes/_io_pipes.py +9 -9
  16. ngio/io_pipes/_io_pipes_masked.py +7 -7
  17. ngio/io_pipes/_io_pipes_roi.py +6 -6
  18. ngio/io_pipes/_io_pipes_types.py +3 -3
  19. ngio/io_pipes/_match_shape.py +5 -4
  20. ngio/io_pipes/_ops_slices_utils.py +8 -5
  21. ngio/ome_zarr_meta/__init__.py +15 -18
  22. ngio/ome_zarr_meta/_meta_handlers.py +334 -713
  23. ngio/ome_zarr_meta/ngio_specs/_axes.py +1 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +13 -22
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +54 -61
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +14 -68
  27. ngio/ome_zarr_meta/v04/__init__.py +1 -1
  28. ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +16 -61
  29. ngio/ome_zarr_meta/v05/__init__.py +1 -1
  30. ngio/ome_zarr_meta/v05/{_v05_spec_utils.py → _v05_spec.py} +18 -61
  31. ngio/tables/_tables_container.py +2 -4
  32. ngio/tables/backends/_anndata.py +57 -8
  33. ngio/tables/backends/_anndata_utils.py +1 -6
  34. ngio/tables/backends/_csv.py +3 -19
  35. ngio/tables/backends/_json.py +10 -13
  36. ngio/tables/backends/_parquet.py +3 -31
  37. ngio/tables/backends/_py_arrow_backends.py +222 -0
  38. ngio/tables/v1/_roi_table.py +41 -24
  39. ngio/utils/__init__.py +4 -12
  40. ngio/utils/_zarr_utils.py +160 -53
  41. {ngio-0.5.0a2.dist-info → ngio-0.5.0a3.dist-info}/METADATA +6 -2
  42. {ngio-0.5.0a2.dist-info → ngio-0.5.0a3.dist-info}/RECORD +44 -45
  43. {ngio-0.5.0a2.dist-info → ngio-0.5.0a3.dist-info}/WHEEL +1 -1
  44. ngio/images/_create.py +0 -287
  45. ngio/tables/backends/_non_zarr_backends.py +0 -196
  46. ngio/utils/_logger.py +0 -50
  47. {ngio-0.5.0a2.dist-info → ngio-0.5.0a3.dist-info}/licenses/LICENSE +0 -0
ngio/common/_roi.py CHANGED
@@ -4,17 +4,16 @@ These are the interfaces bwteen the ROI tables / masking ROI tables and
4
4
  the ImageLikeHandler.
5
5
  """
6
6
 
7
- from typing import TypeVar
8
- from warnings import warn
7
+ from collections.abc import Callable
8
+ from typing import Literal, Self
9
9
 
10
- from pydantic import BaseModel, ConfigDict
10
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
11
11
 
12
- from ngio.common._dimensions import Dimensions
13
- from ngio.ome_zarr_meta.ngio_specs import DefaultSpaceUnit, PixelSize, SpaceUnits
12
+ from ngio.ome_zarr_meta import PixelSize
14
13
  from ngio.utils import NgioValueError
15
14
 
16
15
 
17
- def _world_to_raster(value: float, pixel_size: float, eps: float = 1e-6) -> float:
16
+ def world_to_pixel(value: float, pixel_size: float, eps: float = 1e-6) -> float:
18
17
  raster_value = value / pixel_size
19
18
 
20
19
  # If the value is very close to an integer, round it
@@ -26,362 +25,291 @@ def _world_to_raster(value: float, pixel_size: float, eps: float = 1e-6) -> floa
26
25
  return raster_value
27
26
 
28
27
 
29
- def _to_raster(value: float, length: float, pixel_size: float) -> tuple[float, float]:
30
- """Convert to raster coordinates."""
31
- raster_value = _world_to_raster(value, pixel_size)
32
- raster_length = _world_to_raster(length, pixel_size)
33
- return raster_value, raster_length
28
+ def pixel_to_world(value: float, pixel_size: float) -> float:
29
+ return value * pixel_size
34
30
 
35
31
 
36
- def _to_slice(start: float | None, length: float | None) -> slice:
37
- if length is not None:
38
- assert start is not None
39
- end = start + length
40
- else:
41
- end = None
42
- return slice(start, end)
32
+ def _join_roi_names(name1: str | None, name2: str | None) -> str | None:
33
+ if name1 is not None and name2 is not None:
34
+ if name1 == name2:
35
+ return name1
36
+ return f"{name1}:{name2}"
37
+ return name1 or name2
38
+
39
+
40
+ def _join_roi_labels(label1: int | None, label2: int | None) -> int | None:
41
+ if label1 is not None and label2 is not None:
42
+ if label1 == label2:
43
+ return label1
44
+ raise NgioValueError("Cannot join ROIs with different labels")
45
+ return label1 or label2
46
+
47
+
48
+ class RoiSlice(BaseModel):
49
+ axis_name: str
50
+ start: float | None = Field(default=None)
51
+ length: float | None = Field(default=None, ge=0)
52
+
53
+ model_config = ConfigDict(extra="forbid")
54
+
55
+ @classmethod
56
+ def _from_slice(
57
+ cls,
58
+ axis_name: str,
59
+ selection: slice,
60
+ ) -> "RoiSlice":
61
+ start = selection.start
62
+ length = (
63
+ None
64
+ if selection.stop is None or selection.start is None
65
+ else selection.stop - selection.start
66
+ )
67
+ return cls(axis_name=axis_name, start=start, length=length)
68
+
69
+ @classmethod
70
+ def from_value(
71
+ cls,
72
+ axis_name: str,
73
+ value: float | tuple[float | None, float | None] | slice,
74
+ ) -> "RoiSlice":
75
+ if isinstance(value, slice):
76
+ return cls._from_slice(axis_name=axis_name, selection=value)
77
+ elif isinstance(value, tuple):
78
+ return cls(axis_name=axis_name, start=value[0], length=value[1])
79
+ elif isinstance(value, int | float):
80
+ return cls(axis_name=axis_name, start=value, length=1)
81
+ else:
82
+ raise TypeError(f"Unsupported type for slice value: {type(value)}")
43
83
 
84
+ def __repr__(self) -> str:
85
+ return f"{self.axis_name}: {self.start}->{self.end}"
44
86
 
45
- def _raster_to_world(value: int | float, pixel_size: float) -> float:
46
- """Convert to world coordinates."""
47
- return value * pixel_size
87
+ @property
88
+ def end(self) -> float | None:
89
+ if self.start is None or self.length is None:
90
+ return None
91
+ return self.start + self.length
48
92
 
93
+ def to_slice(self) -> slice:
94
+ return slice(self.start, self.end)
49
95
 
50
- T = TypeVar("T", int, float)
96
+ def _is_compatible(self, other: "RoiSlice", msg: str) -> None:
97
+ if self.axis_name != other.axis_name:
98
+ raise NgioValueError(
99
+ f"{msg}: Cannot operate on RoiSlices with different axis names"
100
+ )
51
101
 
102
+ def union(self, other: "RoiSlice") -> "RoiSlice":
103
+ self._is_compatible(other, "RoiSlice union failed")
104
+ start = min(self.start or 0, other.start or 0)
105
+ end = max(self.end or float("inf"), other.end or float("inf"))
106
+ length = end - start if end > start else 0
107
+ if length == float("inf"):
108
+ length = None
109
+ return RoiSlice(axis_name=self.axis_name, start=start, length=length)
110
+
111
+ def intersection(self, other: "RoiSlice") -> "RoiSlice | None":
112
+ self._is_compatible(other, "RoiSlice intersection failed")
113
+ start = max(self.start or 0, other.start or 0)
114
+ end = min(self.end or float("inf"), other.end or float("inf"))
115
+ if end <= start:
116
+ # No intersection
117
+ return None
118
+ length = end - start
119
+ if length == float("inf"):
120
+ length = None
121
+ return RoiSlice(axis_name=self.axis_name, start=start, length=length)
122
+
123
+ def to_world(self, pixel_size: float) -> "RoiSlice":
124
+ start = (
125
+ pixel_to_world(self.start, pixel_size) if self.start is not None else None
126
+ )
127
+ length = (
128
+ pixel_to_world(self.length, pixel_size) if self.length is not None else None
129
+ )
130
+ return RoiSlice(axis_name=self.axis_name, start=start, length=length)
52
131
 
53
- class GenericRoi(BaseModel):
54
- """A generic Region of Interest (ROI) model."""
132
+ def to_pixel(self, pixel_size: float) -> "RoiSlice":
133
+ start = (
134
+ world_to_pixel(self.start, pixel_size) if self.start is not None else None
135
+ )
136
+ length = (
137
+ world_to_pixel(self.length, pixel_size) if self.length is not None else None
138
+ )
139
+ return RoiSlice(axis_name=self.axis_name, start=start, length=length)
55
140
 
56
- name: str | None = None
57
- x: float
58
- y: float
59
- z: float | None = None
60
- t: float | None = None
61
- x_length: float
62
- y_length: float
63
- z_length: float | None = None
64
- t_length: float | None = None
65
- label: int | None = None
66
- unit: SpaceUnits | str | None = None
141
+ def zoom(self, zoom_factor: float = 1.0) -> "RoiSlice":
142
+ if zoom_factor <= 0:
143
+ raise NgioValueError("Zoom factor must be greater than 0")
144
+ zoom_factor -= 1.0
145
+ if self.length is None:
146
+ return self
67
147
 
68
- model_config = ConfigDict(extra="allow")
148
+ diff_length = self.length * zoom_factor
149
+ length = self.length + diff_length
150
+ start = max((self.start or 0) - (diff_length / 2), 0)
151
+ return RoiSlice(axis_name=self.axis_name, start=start, length=length)
69
152
 
70
- def intersection(self, other: "GenericRoi") -> "GenericRoi | None":
71
- """Calculate the intersection of this ROI with another ROI."""
72
- return roi_intersection(self, other)
73
153
 
74
- def _nice_str(self) -> str:
75
- if self.t is not None:
76
- t_start = self.t
77
- else:
78
- t_start = None
79
- if self.t_length is not None and t_start is not None:
80
- t_end = t_start + self.t_length
81
- else:
82
- t_end = None
154
+ class Roi(BaseModel):
155
+ name: str | None
156
+ slices: list[RoiSlice] = Field(min_length=2)
157
+ label: int | None = Field(default=None, ge=0)
158
+ space: Literal["world", "pixel"] = "world"
83
159
 
84
- t_str = f"t={t_start}->{t_end}"
160
+ model_config = ConfigDict(extra="allow")
85
161
 
86
- if self.z is not None:
87
- z_start = self.z
88
- else:
89
- z_start = None
90
- if self.z_length is not None and z_start is not None:
91
- z_end = z_start + self.z_length
162
+ @field_validator("slices")
163
+ @classmethod
164
+ def validate_no_duplicate_axes(cls, v: list[RoiSlice]) -> list[RoiSlice]:
165
+ axis_names = [s.axis_name for s in v]
166
+ if len(axis_names) != len(set(axis_names)):
167
+ raise NgioValueError("Roi slices must have unique axis names")
168
+ return v
169
+
170
+ def _nice_repr__(self) -> str:
171
+ slices_repr = ", ".join(repr(s) for s in self.slices)
172
+ if self.label is None:
173
+ label_str = ""
92
174
  else:
93
- z_end = None
94
- z_str = f"z={z_start}->{z_end}"
95
-
96
- y_str = f"y={self.y}->{self.y + self.y_length}"
97
- x_str = f"x={self.x}->{self.x + self.x_length}"
98
-
99
- if self.label is not None:
100
175
  label_str = f", label={self.label}"
176
+
177
+ if self.name is None:
178
+ name_str = ""
101
179
  else:
102
- label_str = ""
103
- cls_name = self.__class__.__name__
104
- return f"{cls_name}({t_str}, {z_str}, {y_str}, {x_str}{label_str})"
180
+ name_str = f"name={self.name}, "
181
+ return f"Roi({name_str}{slices_repr}{label_str}, space={self.space})"
182
+
183
+ @classmethod
184
+ def from_values(
185
+ cls,
186
+ slices: dict[str, float | tuple[float | None, float | None] | slice],
187
+ name: str | None,
188
+ label: int | None = None,
189
+ space: Literal["world", "pixel"] = "world",
190
+ **kwargs,
191
+ ) -> Self:
192
+ _slices = []
193
+ for axis, _slice in slices.items():
194
+ _slices.append(RoiSlice.from_value(axis_name=axis, value=_slice))
195
+ return cls.model_construct(
196
+ name=name, slices=_slices, label=label, space=space, **kwargs
197
+ )
198
+
199
+ def get(self, axis_name: str) -> RoiSlice | None:
200
+ for roi_slice in self.slices:
201
+ if roi_slice.axis_name == axis_name:
202
+ return roi_slice
203
+ return None
105
204
 
106
205
  def get_name(self) -> str:
107
- """Get the name of the ROI, or a default if not set."""
108
206
  if self.name is not None:
109
207
  return self.name
110
- return self._nice_str()
208
+ if self.label is not None:
209
+ return str(self.label)
210
+ return self._nice_repr__()
211
+
212
+ @staticmethod
213
+ def _apply_sym_ops(
214
+ self_slices: list[RoiSlice],
215
+ other_slices: list[RoiSlice],
216
+ op: Callable[[RoiSlice, RoiSlice], RoiSlice | None],
217
+ ) -> list[RoiSlice] | None:
218
+ self_axis_dict = {s.axis_name: s for s in self_slices}
219
+ other_axis_dict = {s.axis_name: s for s in other_slices}
220
+ common_axis_names = self_axis_dict.keys() | other_axis_dict.keys()
221
+ new_slices = []
222
+ for axis_name in common_axis_names:
223
+ slice_a = self_axis_dict.get(axis_name)
224
+ slice_b = other_axis_dict.get(axis_name)
225
+ if slice_a is not None and slice_b is not None:
226
+ result = op(slice_a, slice_b)
227
+ if result is None:
228
+ return None
229
+ new_slices.append(result)
230
+ elif slice_a is not None:
231
+ new_slices.append(slice_a)
232
+ elif slice_b is not None:
233
+ new_slices.append(slice_b)
234
+ return new_slices
235
+
236
+ def intersection(self, other: Self) -> Self | None:
237
+ if self.space != other.space:
238
+ raise NgioValueError(
239
+ "Roi intersection failed: One ROI is in pixel space and the "
240
+ "other in world space"
241
+ )
111
242
 
112
- def __repr__(self) -> str:
113
- return self._nice_str()
114
-
115
- def __str__(self) -> str:
116
- return self._nice_str()
117
-
118
- def to_slicing_dict(self, pixel_size: PixelSize) -> dict[str, slice]:
119
- raise NotImplementedError
120
-
121
-
122
- def _1d_intersection(
123
- a: T | None, a_length: T | None, b: T | None, b_length: T | None
124
- ) -> tuple[T | None, T | None]:
125
- """Calculate the intersection of two 1D intervals."""
126
- if a is None:
127
- if b is not None and b_length is not None:
128
- return b, b_length
129
- return None, None
130
- if b is None:
131
- if a is not None and a_length is not None:
132
- return a, a_length
133
- return None, None
134
-
135
- assert (
136
- a is not None
137
- and a_length is not None
138
- and b is not None
139
- and b_length is not None
140
- )
141
- start = max(a, b)
142
- end = min(a + a_length, b + b_length)
143
- length = end - start
144
-
145
- if length <= 0:
146
- return None, None
147
-
148
- return start, length
149
-
150
-
151
- def roi_intersection(ref_roi: GenericRoi, other_roi: GenericRoi) -> GenericRoi | None:
152
- """Calculate the intersection of two ROIs."""
153
- if (
154
- ref_roi.unit is not None
155
- and other_roi.unit is not None
156
- and ref_roi.unit != other_roi.unit
157
- ):
158
- raise NgioValueError(
159
- "Cannot calculate intersection of ROIs with different units."
243
+ out_slices = self._apply_sym_ops(
244
+ self.slices, other.slices, op=lambda a, b: a.intersection(b)
160
245
  )
246
+ if out_slices is None:
247
+ return None
161
248
 
162
- x, x_length = _1d_intersection(
163
- ref_roi.x, ref_roi.x_length, other_roi.x, other_roi.x_length
164
- )
165
- if x is None and x_length is None:
166
- # No intersection
167
- return None
168
- assert x is not None and x_length is not None
169
-
170
- y, y_length = _1d_intersection(
171
- ref_roi.y, ref_roi.y_length, other_roi.y, other_roi.y_length
172
- )
173
- if y is None and y_length is None:
174
- # No intersection
175
- return None
176
- assert y is not None and y_length is not None
177
-
178
- z, z_length = _1d_intersection(
179
- ref_roi.z, ref_roi.z_length, other_roi.z, other_roi.z_length
180
- )
181
- t, t_length = _1d_intersection(
182
- ref_roi.t, ref_roi.t_length, other_roi.t, other_roi.t_length
183
- )
184
-
185
- if (z_length is not None and z_length <= 0) or (
186
- t_length is not None and t_length <= 0
187
- ):
188
- # No intersection
189
- return None
249
+ name = _join_roi_names(self.name, other.name)
250
+ label = _join_roi_labels(self.label, other.label)
251
+ return self.model_copy(
252
+ update={"name": name, "slices": out_slices, "label": label}
253
+ )
190
254
 
191
- # Find label
192
- if ref_roi.label is not None and other_roi.label is not None:
193
- if ref_roi.label != other_roi.label:
255
+ def union(self, other: Self) -> Self:
256
+ if self.space != other.space:
194
257
  raise NgioValueError(
195
- "Cannot calculate intersection of ROIs with different labels."
258
+ "Roi union failed: One ROI is in pixel space and the "
259
+ "other in world space"
196
260
  )
197
- label = ref_roi.label or other_roi.label
198
-
199
- if ref_roi.name is not None and other_roi.name is not None:
200
- name = f"{ref_roi.name}:{other_roi.name}"
201
- else:
202
- name = ref_roi.name or other_roi.name
203
-
204
- cls_ref = ref_roi.__class__
205
- return cls_ref(
206
- name=name,
207
- x=x,
208
- y=y,
209
- z=z,
210
- t=t,
211
- x_length=x_length,
212
- y_length=y_length,
213
- z_length=z_length,
214
- t_length=t_length,
215
- unit=ref_roi.unit,
216
- label=label,
217
- )
218
-
219
-
220
- class Roi(GenericRoi):
221
- x: float = 0.0
222
- y: float = 0.0
223
- unit: SpaceUnits | str | None = DefaultSpaceUnit
224
-
225
- def to_roi_pixels(self, pixel_size: PixelSize) -> "RoiPixels":
226
- """Convert to raster coordinates."""
227
- x, x_length = _to_raster(self.x, self.x_length, pixel_size.x)
228
- y, y_length = _to_raster(self.y, self.y_length, pixel_size.y)
229
-
230
- if self.z is None:
231
- z, z_length = None, None
232
- else:
233
- assert self.z_length is not None
234
- z, z_length = _to_raster(self.z, self.z_length, pixel_size.z)
235
261
 
236
- if self.t is None:
237
- t, t_length = None, None
238
- else:
239
- assert self.t_length is not None
240
- t, t_length = _to_raster(self.t, self.t_length, pixel_size.t)
241
- extra_dict = self.model_extra if self.model_extra else {}
242
-
243
- return RoiPixels(
244
- name=self.name,
245
- x=x,
246
- y=y,
247
- z=z,
248
- t=t,
249
- x_length=x_length,
250
- y_length=y_length,
251
- z_length=z_length,
252
- t_length=t_length,
253
- label=self.label,
254
- unit=self.unit,
255
- **extra_dict,
262
+ out_slices = self._apply_sym_ops(
263
+ self.slices, other.slices, op=lambda a, b: a.union(b)
256
264
  )
265
+ if out_slices is None:
266
+ raise NgioValueError("Roi union failed: could not compute union")
257
267
 
258
- def to_pixel_roi(
259
- self, pixel_size: PixelSize, dimensions: Dimensions | None = None
260
- ) -> "RoiPixels":
261
- """Convert to raster coordinates."""
262
- warn(
263
- "to_pixel_roi is deprecated and will be removed in a future release. "
264
- "Use to_roi_pixels instead.",
265
- DeprecationWarning,
266
- stacklevel=2,
268
+ name = _join_roi_names(self.name, other.name)
269
+ label = _join_roi_labels(self.label, other.label)
270
+ return self.model_copy(
271
+ update={"name": name, "slices": out_slices, "label": label}
267
272
  )
268
273
 
269
- return self.to_roi_pixels(pixel_size=pixel_size)
270
-
271
- def zoom(self, zoom_factor: float = 1) -> "Roi":
272
- """Zoom the ROI by a factor.
273
-
274
- Args:
275
- zoom_factor: The zoom factor. If the zoom factor
276
- is less than 1 the ROI will be zoomed in.
277
- If the zoom factor is greater than 1 the ROI will be zoomed out.
278
- If the zoom factor is 1 the ROI will not be changed.
279
- """
280
- return zoom_roi(self, zoom_factor)
281
-
282
- def to_slicing_dict(self, pixel_size: PixelSize) -> dict[str, slice]:
283
- """Convert to a slicing dictionary."""
284
- roi_pixels = self.to_roi_pixels(pixel_size)
285
- return roi_pixels.to_slicing_dict(pixel_size)
286
-
287
-
288
- class RoiPixels(GenericRoi):
289
- """Region of interest (ROI) in pixel coordinates."""
290
-
291
- x: float = 0
292
- y: float = 0
293
- unit: SpaceUnits | str | None = None
294
-
295
- def to_roi(self, pixel_size: PixelSize) -> "Roi":
296
- """Convert to raster coordinates."""
297
- x = _raster_to_world(self.x, pixel_size.x)
298
- x_length = _raster_to_world(self.x_length, pixel_size.x)
299
- y = _raster_to_world(self.y, pixel_size.y)
300
- y_length = _raster_to_world(self.y_length, pixel_size.y)
274
+ def zoom(
275
+ self, zoom_factor: float = 1.0, axes: tuple[str, ...] = ("x", "y")
276
+ ) -> Self:
277
+ new_slices = []
278
+ for roi_slice in self.slices:
279
+ if roi_slice.axis_name in axes:
280
+ new_slices.append(roi_slice.zoom(zoom_factor=zoom_factor))
281
+ else:
282
+ new_slices.append(roi_slice)
283
+ return self.model_copy(update={"slices": new_slices})
284
+
285
+ def to_world(self, pixel_size: PixelSize | None = None) -> Self:
286
+ if self.space == "world":
287
+ return self.model_copy()
288
+ if pixel_size is None:
289
+ raise NgioValueError(
290
+ "Pixel sizes must be provided to convert ROI from pixel to world"
291
+ )
292
+ new_slices = []
293
+ for roi_slice in self.slices:
294
+ pixel_size_ = pixel_size.get(roi_slice.axis_name, default=1.0)
295
+ new_slices.append(roi_slice.to_world(pixel_size=pixel_size_))
296
+ return self.model_copy(update={"slices": new_slices, "space": "world"})
301
297
 
302
- if self.z is None:
303
- z = None
304
- else:
305
- z = _raster_to_world(self.z, pixel_size.z)
298
+ def to_pixel(self, pixel_size: PixelSize | None = None) -> Self:
299
+ if self.space == "pixel":
300
+ return self.model_copy()
306
301
 
307
- if self.z_length is None:
308
- z_length = None
309
- else:
310
- z_length = _raster_to_world(self.z_length, pixel_size.z)
311
-
312
- if self.t is None:
313
- t = None
314
- else:
315
- t = _raster_to_world(self.t, pixel_size.t)
302
+ if pixel_size is None:
303
+ raise NgioValueError(
304
+ "Pixel sizes must be provided to convert ROI from world to pixel"
305
+ )
316
306
 
317
- if self.t_length is None:
318
- t_length = None
319
- else:
320
- t_length = _raster_to_world(self.t_length, pixel_size.t)
321
-
322
- extra_dict = self.model_extra if self.model_extra else {}
323
- return Roi(
324
- name=self.name,
325
- x=x,
326
- y=y,
327
- z=z,
328
- t=t,
329
- x_length=x_length,
330
- y_length=y_length,
331
- z_length=z_length,
332
- t_length=t_length,
333
- label=self.label,
334
- unit=self.unit,
335
- **extra_dict,
336
- )
307
+ new_slices = []
308
+ for roi_slice in self.slices:
309
+ pixel_size_ = pixel_size.get(roi_slice.axis_name, default=1.0)
310
+ new_slices.append(roi_slice.to_pixel(pixel_size=pixel_size_))
311
+ return self.model_copy(update={"slices": new_slices, "space": "pixel"})
337
312
 
338
- def to_slicing_dict(self, pixel_size: PixelSize) -> dict[str, slice]:
339
- """Convert to a slicing dictionary."""
340
- x_slice = _to_slice(self.x, self.x_length)
341
- y_slice = _to_slice(self.y, self.y_length)
342
- z_slice = _to_slice(self.z, self.z_length)
343
- t_slice = _to_slice(self.t, self.t_length)
344
- return {
345
- "x": x_slice,
346
- "y": y_slice,
347
- "z": z_slice,
348
- "t": t_slice,
349
- }
350
-
351
-
352
- def zoom_roi(roi: Roi, zoom_factor: float = 1) -> Roi:
353
- """Zoom the ROI by a factor.
354
-
355
- Args:
356
- roi: The ROI to zoom.
357
- zoom_factor: The zoom factor. If the zoom factor
358
- is less than 1 the ROI will be zoomed in.
359
- If the zoom factor is greater than 1 the ROI will be zoomed out.
360
- If the zoom factor is 1 the ROI will not be changed.
361
- """
362
- if zoom_factor <= 0:
363
- raise NgioValueError("Zoom factor must be greater than 0.")
364
-
365
- # the zoom factor needs to be rescaled
366
- # from the range [-1, inf) to [0, inf)
367
- zoom_factor -= 1
368
- diff_x = roi.x_length * zoom_factor
369
- diff_y = roi.y_length * zoom_factor
370
-
371
- new_x = max(roi.x - diff_x / 2, 0)
372
- new_y = max(roi.y - diff_y / 2, 0)
373
-
374
- new_roi = Roi(
375
- name=roi.name,
376
- x=new_x,
377
- y=new_y,
378
- z=roi.z,
379
- t=roi.t,
380
- x_length=roi.x_length + diff_x,
381
- y_length=roi.y_length + diff_y,
382
- z_length=roi.z_length,
383
- t_length=roi.t_length,
384
- label=roi.label,
385
- unit=roi.unit,
386
- )
387
- return new_roi
313
+ def to_slicing_dict(self, pixel_size: PixelSize | None = None) -> dict[str, slice]:
314
+ roi = self.to_pixel(pixel_size=pixel_size)
315
+ return {roi_slice.axis_name: roi_slice.to_slice() for roi_slice in roi.slices}
@@ -4,7 +4,7 @@ from typing import TypeAlias
4
4
  import dask.array as da
5
5
  import numpy as np
6
6
 
7
- from ngio.common import Roi, RoiPixels
7
+ from ngio.common import Roi
8
8
  from ngio.experimental.iterators._abstract_iterator import AbstractIteratorBuilder
9
9
  from ngio.images import Image, Label
10
10
  from ngio.images._image import (
@@ -18,8 +18,8 @@ from ngio.io_pipes import (
18
18
  TransformProtocol,
19
19
  )
20
20
 
21
- NumpyPipeType: TypeAlias = tuple[np.ndarray, np.ndarray, Roi | RoiPixels]
22
- DaskPipeType: TypeAlias = tuple[da.Array, da.Array, Roi | RoiPixels]
21
+ NumpyPipeType: TypeAlias = tuple[np.ndarray, np.ndarray, Roi]
22
+ DaskPipeType: TypeAlias = tuple[da.Array, da.Array, Roi]
23
23
 
24
24
 
25
25
  class NumpyFeatureGetter(DataGetter[NumpyPipeType]):