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.
- ngio/__init__.py +2 -5
- ngio/common/__init__.py +6 -11
- ngio/common/_masking_roi.py +54 -34
- ngio/common/_pyramid.py +87 -321
- ngio/common/_roi.py +330 -258
- ngio/experimental/iterators/_feature.py +3 -3
- ngio/experimental/iterators/_rois_utils.py +11 -10
- ngio/hcs/_plate.py +136 -192
- ngio/images/_abstract_image.py +35 -539
- ngio/images/_create.py +283 -0
- ngio/images/_create_synt_container.py +43 -40
- ngio/images/_image.py +251 -517
- ngio/images/_label.py +172 -249
- ngio/images/_masked_image.py +2 -2
- ngio/images/_ome_zarr_container.py +241 -644
- ngio/io_pipes/_io_pipes.py +9 -9
- ngio/io_pipes/_io_pipes_masked.py +7 -7
- ngio/io_pipes/_io_pipes_roi.py +6 -6
- ngio/io_pipes/_io_pipes_types.py +3 -3
- ngio/io_pipes/_match_shape.py +8 -6
- ngio/io_pipes/_ops_slices_utils.py +5 -8
- ngio/ome_zarr_meta/__init__.py +18 -29
- ngio/ome_zarr_meta/_meta_handlers.py +708 -392
- ngio/ome_zarr_meta/ngio_specs/__init__.py +0 -4
- ngio/ome_zarr_meta/ngio_specs/_axes.py +51 -152
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +22 -13
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +91 -129
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +68 -57
- ngio/ome_zarr_meta/v04/__init__.py +1 -5
- ngio/ome_zarr_meta/v04/{_v04_spec.py → _v04_spec_utils.py} +85 -54
- ngio/ome_zarr_meta/v05/__init__.py +1 -5
- ngio/ome_zarr_meta/v05/{_v05_spec.py → _v05_spec_utils.py} +87 -64
- ngio/resources/__init__.py +1 -1
- ngio/resources/resource_model.py +1 -1
- ngio/tables/_tables_container.py +27 -85
- ngio/tables/backends/_anndata.py +8 -58
- ngio/tables/backends/_anndata_utils.py +6 -1
- ngio/tables/backends/_csv.py +19 -3
- ngio/tables/backends/_json.py +13 -10
- ngio/tables/backends/_non_zarr_backends.py +196 -0
- ngio/tables/backends/_parquet.py +31 -3
- ngio/tables/v1/_roi_table.py +27 -44
- ngio/utils/__init__.py +12 -8
- ngio/utils/_datasets.py +0 -6
- ngio/utils/_logger.py +50 -0
- ngio/utils/_zarr_utils.py +250 -292
- {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/METADATA +6 -13
- ngio-0.5.0a1.dist-info/RECORD +88 -0
- {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/WHEEL +1 -1
- ngio/images/_create_utils.py +0 -406
- ngio/tables/backends/_py_arrow_backends.py +0 -222
- ngio/utils/_cache.py +0 -48
- ngio-0.5.0.dist-info/RECORD +0 -88
- {ngio-0.5.0.dist-info → ngio-0.5.0a1.dist-info}/licenses/LICENSE +0 -0
ngio/common/_roi.py
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"""Region of interest (ROI) metadata.
|
|
2
2
|
|
|
3
|
-
These are the interfaces
|
|
3
|
+
These are the interfaces bwteen the ROI tables / masking ROI tables and
|
|
4
4
|
the ImageLikeHandler.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
from warnings import warn
|
|
9
9
|
|
|
10
|
-
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
11
|
|
|
12
|
-
from ngio.
|
|
12
|
+
from ngio.common._dimensions import Dimensions
|
|
13
|
+
from ngio.ome_zarr_meta.ngio_specs import DefaultSpaceUnit, PixelSize, SpaceUnits
|
|
13
14
|
from ngio.utils import NgioValueError
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def
|
|
17
|
+
def _world_to_raster(value: float, pixel_size: float, eps: float = 1e-6) -> float:
|
|
17
18
|
raster_value = value / pixel_size
|
|
18
19
|
|
|
19
20
|
# If the value is very close to an integer, round it
|
|
@@ -25,291 +26,362 @@ def world_to_pixel(value: float, pixel_size: float, eps: float = 1e-6) -> float:
|
|
|
25
26
|
return raster_value
|
|
26
27
|
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
-
|
|
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
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
def
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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)}")
|
|
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)
|
|
83
43
|
|
|
84
|
-
def __repr__(self) -> str:
|
|
85
|
-
return f"{self.axis_name}: {self.start}->{self.end}"
|
|
86
44
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return None
|
|
91
|
-
return self.start + self.length
|
|
45
|
+
def _raster_to_world(value: int | float, pixel_size: float) -> float:
|
|
46
|
+
"""Convert to world coordinates."""
|
|
47
|
+
return value * pixel_size
|
|
92
48
|
|
|
93
|
-
def to_slice(self) -> slice:
|
|
94
|
-
return slice(self.start, self.end)
|
|
95
49
|
|
|
96
|
-
|
|
97
|
-
if self.axis_name != other.axis_name:
|
|
98
|
-
raise NgioValueError(
|
|
99
|
-
f"{msg}: Cannot operate on RoiSlices with different axis names"
|
|
100
|
-
)
|
|
50
|
+
T = TypeVar("T", int, float)
|
|
101
51
|
|
|
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)
|
|
131
52
|
|
|
132
|
-
|
|
133
|
-
|
|
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)
|
|
53
|
+
class GenericRoi(BaseModel):
|
|
54
|
+
"""A generic Region of Interest (ROI) model."""
|
|
140
55
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
147
67
|
|
|
148
|
-
|
|
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)
|
|
68
|
+
model_config = ConfigDict(extra="allow")
|
|
152
69
|
|
|
70
|
+
def intersection(self, other: "GenericRoi") -> "GenericRoi | None":
|
|
71
|
+
"""Calculate the intersection of this ROI with another ROI."""
|
|
72
|
+
return roi_intersection(self, other)
|
|
153
73
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
159
83
|
|
|
160
|
-
|
|
84
|
+
t_str = f"t={t_start}->{t_end}"
|
|
161
85
|
|
|
162
|
-
|
|
163
|
-
|
|
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 = ""
|
|
86
|
+
if self.z is not None:
|
|
87
|
+
z_start = self.z
|
|
174
88
|
else:
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
name_str = ""
|
|
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
|
|
179
92
|
else:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@classmethod
|
|
184
|
-
def from_values(
|
|
185
|
-
cls,
|
|
186
|
-
slices: Mapping[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
|
-
)
|
|
93
|
+
z_end = None
|
|
94
|
+
z_str = f"z={z_start}->{z_end}"
|
|
198
95
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
label_str = f", label={self.label}"
|
|
101
|
+
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})"
|
|
204
105
|
|
|
205
106
|
def get_name(self) -> str:
|
|
107
|
+
"""Get the name of the ROI, or a default if not set."""
|
|
206
108
|
if self.name is not None:
|
|
207
109
|
return self.name
|
|
208
|
-
|
|
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
|
-
)
|
|
110
|
+
return self._nice_str()
|
|
242
111
|
|
|
243
|
-
|
|
244
|
-
|
|
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."
|
|
245
160
|
)
|
|
246
|
-
if out_slices is None:
|
|
247
|
-
return None
|
|
248
161
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
254
190
|
|
|
255
|
-
|
|
256
|
-
|
|
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:
|
|
257
194
|
raise NgioValueError(
|
|
258
|
-
"
|
|
259
|
-
"other in world space"
|
|
195
|
+
"Cannot calculate intersection of ROIs with different labels."
|
|
260
196
|
)
|
|
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)
|
|
261
235
|
|
|
262
|
-
|
|
263
|
-
|
|
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,
|
|
264
256
|
)
|
|
265
|
-
if out_slices is None:
|
|
266
|
-
raise NgioValueError("Roi union failed: could not compute union")
|
|
267
257
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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,
|
|
272
267
|
)
|
|
273
268
|
|
|
274
|
-
|
|
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"})
|
|
269
|
+
return self.to_roi_pixels(pixel_size=pixel_size)
|
|
297
270
|
|
|
298
|
-
def
|
|
299
|
-
|
|
300
|
-
return self.model_copy()
|
|
271
|
+
def zoom(self, zoom_factor: float = 1) -> "Roi":
|
|
272
|
+
"""Zoom the ROI by a factor.
|
|
301
273
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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)
|
|
306
281
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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)
|
|
301
|
+
|
|
302
|
+
if self.z is None:
|
|
303
|
+
z = None
|
|
304
|
+
else:
|
|
305
|
+
z = _raster_to_world(self.z, pixel_size.z)
|
|
306
|
+
|
|
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)
|
|
316
|
+
|
|
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
|
+
)
|
|
312
337
|
|
|
313
|
-
def to_slicing_dict(self, pixel_size: PixelSize
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
@@ -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
|
|
7
|
+
from ngio.common import Roi, RoiPixels
|
|
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]
|
|
22
|
-
DaskPipeType: TypeAlias = tuple[da.Array, da.Array, Roi]
|
|
21
|
+
NumpyPipeType: TypeAlias = tuple[np.ndarray, np.ndarray, Roi | RoiPixels]
|
|
22
|
+
DaskPipeType: TypeAlias = tuple[da.Array, da.Array, Roi | RoiPixels]
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class NumpyFeatureGetter(DataGetter[NumpyPipeType]):
|