ngio 0.4.7__py3-none-any.whl → 0.5.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.
- ngio/__init__.py +5 -2
- ngio/common/__init__.py +11 -6
- ngio/common/_masking_roi.py +34 -54
- ngio/common/_pyramid.py +322 -75
- ngio/common/_roi.py +258 -330
- ngio/experimental/iterators/_feature.py +3 -3
- ngio/experimental/iterators/_rois_utils.py +10 -11
- ngio/hcs/_plate.py +192 -136
- ngio/images/_abstract_image.py +539 -35
- ngio/images/_create_synt_container.py +45 -47
- ngio/images/_create_utils.py +406 -0
- ngio/images/_image.py +524 -248
- ngio/images/_label.py +257 -180
- ngio/images/_masked_image.py +2 -2
- ngio/images/_ome_zarr_container.py +658 -255
- 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 +6 -8
- ngio/io_pipes/_ops_slices_utils.py +8 -5
- ngio/ome_zarr_meta/__init__.py +29 -18
- ngio/ome_zarr_meta/_meta_handlers.py +402 -689
- ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +152 -51
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +13 -22
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +129 -91
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +69 -69
- ngio/ome_zarr_meta/v04/__init__.py +5 -1
- ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +55 -86
- ngio/ome_zarr_meta/v05/__init__.py +27 -0
- ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v05/_v05_spec.py +495 -0
- ngio/resources/__init__.py +1 -1
- ngio/resources/resource_model.py +1 -1
- ngio/tables/_tables_container.py +82 -24
- ngio/tables/backends/_abstract_backend.py +7 -0
- ngio/tables/backends/_anndata.py +60 -7
- ngio/tables/backends/_anndata_utils.py +2 -4
- ngio/tables/backends/_csv.py +3 -19
- ngio/tables/backends/_json.py +10 -13
- ngio/tables/backends/_parquet.py +3 -31
- ngio/tables/backends/_py_arrow_backends.py +222 -0
- ngio/tables/backends/_utils.py +1 -1
- ngio/tables/v1/_roi_table.py +41 -24
- ngio/utils/__init__.py +8 -12
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +6 -0
- ngio/utils/_zarr_utils.py +354 -236
- {ngio-0.4.7.dist-info → ngio-0.5.0.dist-info}/METADATA +13 -6
- ngio-0.5.0.dist-info/RECORD +88 -0
- ngio/images/_create.py +0 -276
- ngio/tables/backends/_non_zarr_backends.py +0 -196
- ngio/utils/_logger.py +0 -50
- ngio-0.4.7.dist-info/RECORD +0 -85
- {ngio-0.4.7.dist-info → ngio-0.5.0.dist-info}/WHEEL +0 -0
- {ngio-0.4.7.dist-info → ngio-0.5.0.dist-info}/licenses/LICENSE +0 -0
ngio/common/_roi.py
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
"""Region of interest (ROI) metadata.
|
|
2
2
|
|
|
3
|
-
These are the interfaces
|
|
3
|
+
These are the interfaces between the ROI tables / masking ROI tables and
|
|
4
4
|
the ImageLikeHandler.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
7
|
+
from collections.abc import Callable, Mapping
|
|
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.
|
|
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
|
|
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
|
|
30
|
-
|
|
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
|
|
37
|
-
if
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
160
|
+
model_config = ConfigDict(extra="allow")
|
|
85
161
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if
|
|
91
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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: 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
|
+
)
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
340
|
-
|
|
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
|
|
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
|
|
22
|
-
DaskPipeType: TypeAlias = tuple[da.Array, da.Array, Roi
|
|
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]):
|