ngio 0.5.0b6__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 +69 -0
- ngio/common/__init__.py +28 -0
- ngio/common/_dimensions.py +335 -0
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +408 -0
- ngio/common/_roi.py +315 -0
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +188 -0
- ngio/experimental/__init__.py +5 -0
- ngio/experimental/iterators/__init__.py +15 -0
- ngio/experimental/iterators/_abstract_iterator.py +390 -0
- ngio/experimental/iterators/_feature.py +189 -0
- ngio/experimental/iterators/_image_processing.py +130 -0
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_rois_utils.py +126 -0
- ngio/experimental/iterators/_segmentation.py +235 -0
- ngio/hcs/__init__.py +19 -0
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +44 -0
- ngio/images/_abstract_image.py +967 -0
- ngio/images/_create_synt_container.py +132 -0
- ngio/images/_create_utils.py +423 -0
- ngio/images/_image.py +926 -0
- ngio/images/_label.py +411 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1237 -0
- ngio/images/_table_ops.py +471 -0
- ngio/io_pipes/__init__.py +75 -0
- ngio/io_pipes/_io_pipes.py +361 -0
- ngio/io_pipes/_io_pipes_masked.py +488 -0
- ngio/io_pipes/_io_pipes_roi.py +146 -0
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_match_shape.py +377 -0
- ngio/io_pipes/_ops_axes.py +344 -0
- ngio/io_pipes/_ops_slices.py +411 -0
- ngio/io_pipes/_ops_slices_utils.py +199 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +180 -0
- ngio/ome_zarr_meta/__init__.py +65 -0
- ngio/ome_zarr_meta/_meta_handlers.py +536 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
- ngio/ome_zarr_meta/v04/__init__.py +27 -0
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
- 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 +511 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
- ngio/resources/__init__.py +55 -0
- ngio/resources/resource_model.py +36 -0
- ngio/tables/__init__.py +43 -0
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +57 -0
- ngio/tables/backends/_abstract_backend.py +240 -0
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +90 -0
- ngio/tables/backends/_csv.py +19 -0
- ngio/tables/backends/_json.py +92 -0
- ngio/tables/backends/_parquet.py +19 -0
- ngio/tables/backends/_py_arrow_backends.py +222 -0
- ngio/tables/backends/_table_backends.py +226 -0
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +23 -0
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +125 -0
- ngio/tables/v1/_generic_table.py +49 -0
- ngio/tables/v1/_roi_table.py +575 -0
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +45 -0
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +165 -0
- ngio/utils/_errors.py +37 -0
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +534 -0
- ngio-0.5.0b6.dist-info/METADATA +148 -0
- ngio-0.5.0b6.dist-info/RECORD +88 -0
- ngio-0.5.0b6.dist-info/WHEEL +4 -0
- ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
"""Implementation of the ROI Table class.
|
|
2
|
+
|
|
3
|
+
This class follows the roi_table specification at:
|
|
4
|
+
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import warnings
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from typing import Literal
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
from ngio.common import Roi
|
|
16
|
+
from ngio.tables._abstract_table import (
|
|
17
|
+
AbstractBaseTable,
|
|
18
|
+
TabularData,
|
|
19
|
+
)
|
|
20
|
+
from ngio.tables.backends import (
|
|
21
|
+
BackendMeta,
|
|
22
|
+
TableBackend,
|
|
23
|
+
convert_to_pandas,
|
|
24
|
+
normalize_pandas_df,
|
|
25
|
+
)
|
|
26
|
+
from ngio.utils import (
|
|
27
|
+
NgioTableValidationError,
|
|
28
|
+
NgioValueError,
|
|
29
|
+
ZarrGroupHandler,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
REQUIRED_COLUMNS = [
|
|
33
|
+
"x_micrometer",
|
|
34
|
+
"y_micrometer",
|
|
35
|
+
"z_micrometer",
|
|
36
|
+
"len_x_micrometer",
|
|
37
|
+
"len_y_micrometer",
|
|
38
|
+
"len_z_micrometer",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
#####################
|
|
42
|
+
# Optional columns are not validated at the moment
|
|
43
|
+
# only a warning is raised if non optional columns are present
|
|
44
|
+
#####################
|
|
45
|
+
|
|
46
|
+
TIME_COLUMNS = [
|
|
47
|
+
"t_second",
|
|
48
|
+
"len_t_second",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
ORIGIN_COLUMNS = [
|
|
52
|
+
"x_micrometer_original",
|
|
53
|
+
"y_micrometer_original",
|
|
54
|
+
"z_micrometer_original",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
|
|
58
|
+
|
|
59
|
+
PLATE_COLUMNS = [
|
|
60
|
+
"plate_name",
|
|
61
|
+
"row",
|
|
62
|
+
"column",
|
|
63
|
+
"path_in_well",
|
|
64
|
+
"path_in_plate",
|
|
65
|
+
"acquisition_id",
|
|
66
|
+
"acquisition_name",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
INDEX_COLUMNS = [
|
|
70
|
+
"FieldIndex",
|
|
71
|
+
"label",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS + PLATE_COLUMNS + INDEX_COLUMNS
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _check_optional_columns(col_name: str) -> None:
|
|
78
|
+
"""Check if the column name is in the optional columns."""
|
|
79
|
+
if col_name not in OPTIONAL_COLUMNS + TIME_COLUMNS:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
f"Column {col_name} is not in the optional columns.", stacklevel=2
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _dataframe_to_rois(
|
|
86
|
+
dataframe: pd.DataFrame,
|
|
87
|
+
required_columns: list[str] = REQUIRED_COLUMNS,
|
|
88
|
+
) -> dict[str, Roi]:
|
|
89
|
+
"""Convert a DataFrame to a WorldCooROI object."""
|
|
90
|
+
# Validate the columns of the DataFrame
|
|
91
|
+
_missing_columns = set(required_columns).difference(set(dataframe.columns))
|
|
92
|
+
if len(_missing_columns) != 0:
|
|
93
|
+
raise NgioTableValidationError(
|
|
94
|
+
f"Could not find required columns: {_missing_columns} in the table."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
extra_columns = set(dataframe.columns).difference(
|
|
98
|
+
set(required_columns + TIME_COLUMNS)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
for col in extra_columns:
|
|
102
|
+
_check_optional_columns(col)
|
|
103
|
+
|
|
104
|
+
label_is_index = True if dataframe.index.name == "label" else False
|
|
105
|
+
|
|
106
|
+
extras = {}
|
|
107
|
+
|
|
108
|
+
rois = {}
|
|
109
|
+
for row in dataframe.itertuples(index=True):
|
|
110
|
+
# check if optional columns are present
|
|
111
|
+
if len(extra_columns) > 0:
|
|
112
|
+
extras = {col: getattr(row, col, None) for col in extra_columns}
|
|
113
|
+
|
|
114
|
+
z_micrometer = getattr(row, "z_micrometer", None)
|
|
115
|
+
z_length_micrometer = getattr(row, "len_z_micrometer", None)
|
|
116
|
+
|
|
117
|
+
t_second = getattr(row, "t_second", None)
|
|
118
|
+
t_length_second = getattr(row, "len_t_second", None)
|
|
119
|
+
|
|
120
|
+
if label_is_index:
|
|
121
|
+
label = int(row.Index) # type: ignore (type can not be known here, but should be castable to int)
|
|
122
|
+
else:
|
|
123
|
+
label = getattr(row, "label", None)
|
|
124
|
+
|
|
125
|
+
slices = {
|
|
126
|
+
"x": (row.x_micrometer, row.len_x_micrometer),
|
|
127
|
+
"y": (row.y_micrometer, row.len_y_micrometer),
|
|
128
|
+
"z": (z_micrometer, z_length_micrometer),
|
|
129
|
+
}
|
|
130
|
+
if t_second is not None or t_length_second is not None:
|
|
131
|
+
slices["t"] = (t_second, t_length_second)
|
|
132
|
+
roi = Roi.from_values(
|
|
133
|
+
name=str(row.Index),
|
|
134
|
+
slices=slices,
|
|
135
|
+
space="world",
|
|
136
|
+
label=label,
|
|
137
|
+
**extras,
|
|
138
|
+
)
|
|
139
|
+
rois[roi.name] = roi
|
|
140
|
+
return rois
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
|
|
144
|
+
"""Convert a list of WorldCooROI objects to a DataFrame."""
|
|
145
|
+
data = []
|
|
146
|
+
for roi in rois.values():
|
|
147
|
+
# This normalization is necessary for backward compatibility
|
|
148
|
+
if roi.space != "world":
|
|
149
|
+
raise NotImplementedError(
|
|
150
|
+
"Only ROIs in world coordinates can be serialized."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
z_slice = roi.get("z")
|
|
154
|
+
if z_slice is None:
|
|
155
|
+
z_micrometer = 0.0
|
|
156
|
+
len_z_micrometer = 1.0
|
|
157
|
+
else:
|
|
158
|
+
z_micrometer = z_slice.start if z_slice.start is not None else 0.0
|
|
159
|
+
len_z_micrometer = z_slice.length if z_slice.length is not None else 1.0
|
|
160
|
+
|
|
161
|
+
x_slice = roi.get("x")
|
|
162
|
+
if x_slice is None:
|
|
163
|
+
raise NgioValueError("ROI is missing 'x' slice.")
|
|
164
|
+
y_slice = roi.get("y")
|
|
165
|
+
if y_slice is None:
|
|
166
|
+
raise NgioValueError("ROI is missing 'y' slice.")
|
|
167
|
+
row = {
|
|
168
|
+
index_key: roi.get_name(),
|
|
169
|
+
"x_micrometer": x_slice.start if x_slice.start is not None else 0.0,
|
|
170
|
+
"y_micrometer": y_slice.start if y_slice.start is not None else 0.0,
|
|
171
|
+
"z_micrometer": z_micrometer,
|
|
172
|
+
"len_x_micrometer": x_slice.length if x_slice.length is not None else 1.0,
|
|
173
|
+
"len_y_micrometer": y_slice.length if y_slice.length is not None else 1.0,
|
|
174
|
+
"len_z_micrometer": len_z_micrometer,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
t_slice = roi.get("t")
|
|
178
|
+
if t_slice is not None:
|
|
179
|
+
row["t_second"] = t_slice.start if t_slice.start is not None else 0.0
|
|
180
|
+
row["len_t_second"] = t_slice.length if t_slice.length is not None else 1.0
|
|
181
|
+
|
|
182
|
+
if roi.label is not None and index_key != "label":
|
|
183
|
+
row["label"] = roi.label
|
|
184
|
+
|
|
185
|
+
extra = roi.model_extra or {}
|
|
186
|
+
for col in extra:
|
|
187
|
+
_check_optional_columns(col)
|
|
188
|
+
row[col] = extra[col]
|
|
189
|
+
data.append(row)
|
|
190
|
+
|
|
191
|
+
dataframe = pd.DataFrame(data)
|
|
192
|
+
dataframe = normalize_pandas_df(dataframe, index_key=index_key)
|
|
193
|
+
return dataframe
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class RoiDictWrapper:
|
|
197
|
+
"""A wrapper for a dictionary of ROIs to provide a consistent interface."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, rois: Iterable[Roi]) -> None:
|
|
200
|
+
self._rois_by_name = {}
|
|
201
|
+
self._rois_by_label = {}
|
|
202
|
+
for roi in rois:
|
|
203
|
+
name = roi.name
|
|
204
|
+
if name in self._rois_by_name:
|
|
205
|
+
name = f"{name}_{uuid4().hex[:8]}"
|
|
206
|
+
self._rois_by_name[name] = roi
|
|
207
|
+
if roi.label is not None:
|
|
208
|
+
self._rois_by_label[roi.label] = roi
|
|
209
|
+
|
|
210
|
+
def get_by_name(self, name: str, default: Roi | None = None) -> Roi | None:
|
|
211
|
+
"""Get an ROI by its name."""
|
|
212
|
+
return self._rois_by_name.get(name, default)
|
|
213
|
+
|
|
214
|
+
def get_by_label(self, label: int, default: Roi | None = None) -> Roi | None:
|
|
215
|
+
"""Get an ROI by its label."""
|
|
216
|
+
return self._rois_by_label.get(label, default)
|
|
217
|
+
|
|
218
|
+
def _add_roi(self, roi: Roi, overwrite: bool = False) -> None:
|
|
219
|
+
"""Add an ROI to the wrapper."""
|
|
220
|
+
if roi.name in self._rois_by_name and not overwrite:
|
|
221
|
+
raise NgioValueError(f"ROI with name {roi.name} already exists.")
|
|
222
|
+
|
|
223
|
+
self._rois_by_name[roi.name] = roi
|
|
224
|
+
if roi.label is not None:
|
|
225
|
+
self._rois_by_label[roi.label] = roi
|
|
226
|
+
|
|
227
|
+
def add_rois(self, rois: Roi | Iterable[Roi], overwrite: bool = False) -> None:
|
|
228
|
+
"""Add ROIs to the wrapper."""
|
|
229
|
+
if isinstance(rois, Roi):
|
|
230
|
+
rois = [rois]
|
|
231
|
+
|
|
232
|
+
for roi in rois:
|
|
233
|
+
self._add_roi(roi, overwrite=overwrite)
|
|
234
|
+
|
|
235
|
+
def to_list(self) -> list[Roi]:
|
|
236
|
+
"""Return the list of ROIs."""
|
|
237
|
+
return list(self._rois_by_name.values())
|
|
238
|
+
|
|
239
|
+
def to_dataframe(self, index_key: str | None = None) -> pd.DataFrame:
|
|
240
|
+
"""Convert the ROIs to a DataFrame."""
|
|
241
|
+
return _rois_to_dataframe(self._rois_by_name, index_key=index_key)
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def from_dataframe(
|
|
245
|
+
cls, dataframe: pd.DataFrame, required_columns: list[str] = REQUIRED_COLUMNS
|
|
246
|
+
) -> "RoiDictWrapper":
|
|
247
|
+
"""Create a RoiDictWrapper from a DataFrame."""
|
|
248
|
+
rois = _dataframe_to_rois(dataframe, required_columns=required_columns)
|
|
249
|
+
return cls(rois.values())
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _table_to_rois(
|
|
253
|
+
table: TabularData,
|
|
254
|
+
index_key: str | None = None,
|
|
255
|
+
index_type: Literal["int", "str"] | None = None,
|
|
256
|
+
required_columns: list[str] = REQUIRED_COLUMNS,
|
|
257
|
+
) -> tuple[pd.DataFrame, RoiDictWrapper]:
|
|
258
|
+
"""Convert a table to a dictionary of ROIs.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
table: The table to convert.
|
|
262
|
+
index_key: The column name to use as the index of the DataFrame.
|
|
263
|
+
index_type: The type of the index column in the DataFrame.
|
|
264
|
+
required_columns: The required columns in the DataFrame.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A tuple containing the DataFrame and a RoiDictWrapper with the ROIs.
|
|
268
|
+
"""
|
|
269
|
+
dataframe = convert_to_pandas(
|
|
270
|
+
table,
|
|
271
|
+
index_key=index_key,
|
|
272
|
+
index_type=index_type,
|
|
273
|
+
)
|
|
274
|
+
roi_dict_wrapper = RoiDictWrapper.from_dataframe(
|
|
275
|
+
dataframe, required_columns=required_columns
|
|
276
|
+
)
|
|
277
|
+
return dataframe, roi_dict_wrapper
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class GenericRoiTableV1(AbstractBaseTable):
|
|
281
|
+
def __init__(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
rois: Iterable[Roi] | None = None,
|
|
285
|
+
meta: BackendMeta,
|
|
286
|
+
required_columns: list[str] = REQUIRED_COLUMNS,
|
|
287
|
+
) -> None:
|
|
288
|
+
table = None
|
|
289
|
+
|
|
290
|
+
self._rois: RoiDictWrapper | None = None
|
|
291
|
+
if rois is not None:
|
|
292
|
+
self._rois = RoiDictWrapper(rois)
|
|
293
|
+
table = self._rois.to_dataframe(index_key=meta.index_key)
|
|
294
|
+
|
|
295
|
+
self._required_columns = required_columns
|
|
296
|
+
super().__init__(table_data=table, meta=meta)
|
|
297
|
+
|
|
298
|
+
def __repr__(self) -> str:
|
|
299
|
+
"""Return a string representation of the table."""
|
|
300
|
+
rois = self.rois()
|
|
301
|
+
prop = f"num_rois={len(rois)}"
|
|
302
|
+
class_name = self.__class__.__name__
|
|
303
|
+
return f"{class_name}({prop})"
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def table_type() -> str:
|
|
307
|
+
"""Return the type of the table."""
|
|
308
|
+
return "generic_roi_table"
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def version() -> Literal["1"]:
|
|
312
|
+
"""Return the version of the fractal table."""
|
|
313
|
+
return "1"
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def table_data(self) -> TabularData:
|
|
317
|
+
"""Return the table."""
|
|
318
|
+
if self._rois is None:
|
|
319
|
+
return super().table_data
|
|
320
|
+
|
|
321
|
+
if len(self.rois()) > 0:
|
|
322
|
+
self._table_data = self._rois.to_dataframe(index_key=self.meta.index_key)
|
|
323
|
+
return super().table_data
|
|
324
|
+
|
|
325
|
+
def set_table_data(
|
|
326
|
+
self, table_data: TabularData | None = None, refresh: bool = False
|
|
327
|
+
) -> None:
|
|
328
|
+
if table_data is not None:
|
|
329
|
+
if not isinstance(table_data, TabularData):
|
|
330
|
+
raise NgioValueError(
|
|
331
|
+
"The table must be a pandas DataFrame, polars LazyFrame, "
|
|
332
|
+
" or AnnData object."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
table_data, rois = _table_to_rois(
|
|
336
|
+
table_data,
|
|
337
|
+
index_key=self.index_key,
|
|
338
|
+
index_type=self.index_type,
|
|
339
|
+
required_columns=REQUIRED_COLUMNS,
|
|
340
|
+
)
|
|
341
|
+
self._table_data = table_data
|
|
342
|
+
self._rois = rois
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
if self._table_data is not None and not refresh:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
if self._table_backend is None:
|
|
349
|
+
raise NgioValueError(
|
|
350
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
table_data, rois = _table_to_rois(
|
|
354
|
+
self._table_backend.load(),
|
|
355
|
+
index_key=self.index_key,
|
|
356
|
+
index_type=self.index_type,
|
|
357
|
+
required_columns=REQUIRED_COLUMNS,
|
|
358
|
+
)
|
|
359
|
+
self._table_data = table_data
|
|
360
|
+
self._rois = rois
|
|
361
|
+
|
|
362
|
+
def _check_rois(self) -> None:
|
|
363
|
+
"""Load the ROIs from the table.
|
|
364
|
+
|
|
365
|
+
If the ROIs are already loaded, do nothing.
|
|
366
|
+
If the ROIs are not loaded, load them from the table.
|
|
367
|
+
"""
|
|
368
|
+
if self._rois is None:
|
|
369
|
+
self._rois = RoiDictWrapper.from_dataframe(
|
|
370
|
+
self.dataframe, required_columns=self._required_columns
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def rois(self) -> list[Roi]:
|
|
374
|
+
"""List all ROIs in the table."""
|
|
375
|
+
self._check_rois()
|
|
376
|
+
if self._rois is None:
|
|
377
|
+
return []
|
|
378
|
+
return self._rois.to_list()
|
|
379
|
+
|
|
380
|
+
def add(self, roi: Roi | Iterable[Roi], overwrite: bool = False) -> None:
|
|
381
|
+
"""Append ROIs to the current table.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
roi: A single ROI or a list of ROIs to add to the table.
|
|
385
|
+
overwrite: If True, overwrite existing ROIs with the same name.
|
|
386
|
+
"""
|
|
387
|
+
if isinstance(roi, Roi):
|
|
388
|
+
roi = [roi]
|
|
389
|
+
|
|
390
|
+
self._check_rois()
|
|
391
|
+
if self._rois is None:
|
|
392
|
+
self._rois = RoiDictWrapper([])
|
|
393
|
+
|
|
394
|
+
self._rois.add_rois(roi, overwrite=overwrite)
|
|
395
|
+
|
|
396
|
+
def get(self, roi_name: str) -> Roi:
|
|
397
|
+
"""Get an ROI from the table."""
|
|
398
|
+
self._check_rois()
|
|
399
|
+
if self._rois is None:
|
|
400
|
+
self._rois = RoiDictWrapper([])
|
|
401
|
+
|
|
402
|
+
roi = self._rois.get_by_name(roi_name)
|
|
403
|
+
if roi is None:
|
|
404
|
+
raise NgioValueError(f"ROI with name {roi_name} not found in the table.")
|
|
405
|
+
return roi
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def from_table_data(
|
|
409
|
+
cls, table_data: TabularData, meta: BackendMeta
|
|
410
|
+
) -> "GenericRoiTableV1":
|
|
411
|
+
"""Create a new ROI table from a table data."""
|
|
412
|
+
_, rois = _table_to_rois(
|
|
413
|
+
table=table_data,
|
|
414
|
+
index_key=meta.index_key,
|
|
415
|
+
index_type=meta.index_type,
|
|
416
|
+
required_columns=REQUIRED_COLUMNS,
|
|
417
|
+
)
|
|
418
|
+
return cls(rois=rois.to_list(), meta=meta)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
class RoiTableV1Meta(BackendMeta):
|
|
422
|
+
"""Metadata for the ROI table."""
|
|
423
|
+
|
|
424
|
+
table_version: Literal["1"] = "1"
|
|
425
|
+
type: Literal["roi_table"] = "roi_table"
|
|
426
|
+
index_key: str | None = "FieldIndex"
|
|
427
|
+
index_type: Literal["str", "int"] | None = "str"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class RoiTableV1(GenericRoiTableV1):
|
|
431
|
+
"""Class to handle fractal ROI tables.
|
|
432
|
+
|
|
433
|
+
To know more about the ROI table format, please refer to the
|
|
434
|
+
specification at:
|
|
435
|
+
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
def __init__(
|
|
439
|
+
self, rois: Iterable[Roi] | None = None, *, meta: RoiTableV1Meta | None = None
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Create a new ROI table."""
|
|
442
|
+
if meta is None:
|
|
443
|
+
meta = RoiTableV1Meta()
|
|
444
|
+
|
|
445
|
+
if meta.index_key is None:
|
|
446
|
+
meta.index_key = "FieldIndex"
|
|
447
|
+
|
|
448
|
+
if meta.index_type is None:
|
|
449
|
+
meta.index_type = "str"
|
|
450
|
+
super().__init__(meta=meta, rois=rois)
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
def from_handler(
|
|
454
|
+
cls,
|
|
455
|
+
handler: ZarrGroupHandler,
|
|
456
|
+
backend: TableBackend | None = None,
|
|
457
|
+
) -> "RoiTableV1":
|
|
458
|
+
table = cls._from_handler(
|
|
459
|
+
handler=handler,
|
|
460
|
+
backend=backend,
|
|
461
|
+
meta_model=RoiTableV1Meta,
|
|
462
|
+
)
|
|
463
|
+
return table
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def table_type() -> Literal["roi_table"]:
|
|
467
|
+
"""Return the type of the table."""
|
|
468
|
+
return "roi_table"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class RegionMeta(BaseModel):
|
|
472
|
+
"""Metadata for the region."""
|
|
473
|
+
|
|
474
|
+
path: str
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class MaskingRoiTableV1Meta(BackendMeta):
|
|
478
|
+
"""Metadata for the ROI table."""
|
|
479
|
+
|
|
480
|
+
table_version: Literal["1"] = "1"
|
|
481
|
+
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
482
|
+
region: RegionMeta | None = None
|
|
483
|
+
instance_key: str = "label"
|
|
484
|
+
index_key: str | None = "label"
|
|
485
|
+
index_type: Literal["int", "str"] | None = "int"
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class MaskingRoiTableV1(GenericRoiTableV1):
|
|
489
|
+
"""Class to handle fractal ROI tables.
|
|
490
|
+
|
|
491
|
+
To know more about the ROI table format, please refer to the
|
|
492
|
+
specification at:
|
|
493
|
+
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
494
|
+
"""
|
|
495
|
+
|
|
496
|
+
def __init__(
|
|
497
|
+
self,
|
|
498
|
+
rois: Iterable[Roi] | None = None,
|
|
499
|
+
*,
|
|
500
|
+
reference_label: str | None = None,
|
|
501
|
+
meta: MaskingRoiTableV1Meta | None = None,
|
|
502
|
+
) -> None:
|
|
503
|
+
"""Create a new ROI table."""
|
|
504
|
+
if meta is None:
|
|
505
|
+
meta = MaskingRoiTableV1Meta()
|
|
506
|
+
|
|
507
|
+
if reference_label is not None:
|
|
508
|
+
path = f"../labels/{reference_label}"
|
|
509
|
+
meta.region = RegionMeta(path=path)
|
|
510
|
+
|
|
511
|
+
if meta.index_key is None:
|
|
512
|
+
meta.index_key = "label"
|
|
513
|
+
|
|
514
|
+
if meta.index_type is None:
|
|
515
|
+
meta.index_type = "int"
|
|
516
|
+
meta.instance_key = meta.index_key
|
|
517
|
+
super().__init__(meta=meta, rois=rois)
|
|
518
|
+
|
|
519
|
+
def __repr__(self) -> str:
|
|
520
|
+
"""Return a string representation of the table."""
|
|
521
|
+
rois = self.rois()
|
|
522
|
+
if self.reference_label is not None:
|
|
523
|
+
prop = f"num_rois={len(rois)}, reference_label={self.reference_label}"
|
|
524
|
+
else:
|
|
525
|
+
prop = f"num_rois={len(rois)}"
|
|
526
|
+
return f"MaskingRoiTableV1({prop})"
|
|
527
|
+
|
|
528
|
+
@classmethod
|
|
529
|
+
def from_handler(
|
|
530
|
+
cls,
|
|
531
|
+
handler: ZarrGroupHandler,
|
|
532
|
+
backend: TableBackend | None = None,
|
|
533
|
+
) -> "MaskingRoiTableV1":
|
|
534
|
+
table = cls._from_handler(
|
|
535
|
+
handler=handler,
|
|
536
|
+
backend=backend,
|
|
537
|
+
meta_model=MaskingRoiTableV1Meta,
|
|
538
|
+
)
|
|
539
|
+
return table
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def table_type() -> Literal["masking_roi_table"]:
|
|
543
|
+
"""Return the type of the table."""
|
|
544
|
+
return "masking_roi_table"
|
|
545
|
+
|
|
546
|
+
@property
|
|
547
|
+
def meta(self) -> MaskingRoiTableV1Meta:
|
|
548
|
+
"""Return the metadata of the table."""
|
|
549
|
+
if not isinstance(self._meta, MaskingRoiTableV1Meta):
|
|
550
|
+
raise NgioValueError(
|
|
551
|
+
"The metadata of the table is not of type MaskingRoiTableV1Meta."
|
|
552
|
+
)
|
|
553
|
+
return self._meta
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def reference_label(self) -> str | None:
|
|
557
|
+
"""Return the reference label."""
|
|
558
|
+
path = self.meta.region
|
|
559
|
+
if path is None:
|
|
560
|
+
return None
|
|
561
|
+
|
|
562
|
+
path = path.path
|
|
563
|
+
path = path.split("/")[-1]
|
|
564
|
+
return path
|
|
565
|
+
|
|
566
|
+
def get_label(self, label: int) -> Roi:
|
|
567
|
+
"""Get an ROI by label."""
|
|
568
|
+
self._check_rois()
|
|
569
|
+
if self._rois is None:
|
|
570
|
+
self._rois = RoiDictWrapper([])
|
|
571
|
+
roi = self._rois.get_by_label(label)
|
|
572
|
+
|
|
573
|
+
if roi is None:
|
|
574
|
+
raise NgioValueError(f"ROI with label {label} not found.")
|
|
575
|
+
return roi
|
ngio/transforms/_zoom.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from ngio.common._zoom import (
|
|
2
|
+
InterpolationOrder,
|
|
3
|
+
)
|
|
4
|
+
from ngio.images._abstract_image import AbstractImage
|
|
5
|
+
from ngio.io_pipes._zoom_transform import BaseZoomTransform
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ZoomTransform(BaseZoomTransform):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
input_image: AbstractImage,
|
|
12
|
+
target_image: AbstractImage,
|
|
13
|
+
order: InterpolationOrder = "nearest",
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(
|
|
16
|
+
input_dimensions=input_image.dimensions,
|
|
17
|
+
target_dimensions=target_image.dimensions,
|
|
18
|
+
order=order,
|
|
19
|
+
)
|
ngio/utils/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Various utilities for the ngio package."""
|
|
2
|
+
|
|
3
|
+
from ngio.utils._datasets import (
|
|
4
|
+
download_ome_zarr_dataset,
|
|
5
|
+
list_ome_zarr_datasets,
|
|
6
|
+
print_datasets_infos,
|
|
7
|
+
)
|
|
8
|
+
from ngio.utils._errors import (
|
|
9
|
+
NgioError,
|
|
10
|
+
NgioFileExistsError,
|
|
11
|
+
NgioFileNotFoundError,
|
|
12
|
+
NgioTableValidationError,
|
|
13
|
+
NgioValidationError,
|
|
14
|
+
NgioValueError,
|
|
15
|
+
)
|
|
16
|
+
from ngio.utils._fractal_fsspec_store import fractal_fsspec_store
|
|
17
|
+
from ngio.utils._zarr_utils import (
|
|
18
|
+
AccessModeLiteral,
|
|
19
|
+
NgioCache,
|
|
20
|
+
NgioSupportedStore,
|
|
21
|
+
StoreOrGroup,
|
|
22
|
+
ZarrGroupHandler,
|
|
23
|
+
copy_group,
|
|
24
|
+
open_group_wrapper,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AccessModeLiteral",
|
|
29
|
+
"NgioCache",
|
|
30
|
+
"NgioError",
|
|
31
|
+
"NgioFileExistsError",
|
|
32
|
+
"NgioFileNotFoundError",
|
|
33
|
+
"NgioSupportedStore",
|
|
34
|
+
"NgioTableValidationError",
|
|
35
|
+
"NgioValidationError",
|
|
36
|
+
"NgioValueError",
|
|
37
|
+
"StoreOrGroup",
|
|
38
|
+
"ZarrGroupHandler",
|
|
39
|
+
"copy_group",
|
|
40
|
+
"download_ome_zarr_dataset",
|
|
41
|
+
"fractal_fsspec_store",
|
|
42
|
+
"list_ome_zarr_datasets",
|
|
43
|
+
"open_group_wrapper",
|
|
44
|
+
"print_datasets_infos",
|
|
45
|
+
]
|
ngio/utils/_cache.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Generic, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NgioCache(Generic[T]):
|
|
7
|
+
"""A simple cache for NGIO objects."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, use_cache: bool = True):
|
|
10
|
+
self._cache: dict[str, T] = {}
|
|
11
|
+
self._use_cache = use_cache
|
|
12
|
+
|
|
13
|
+
def _cache_sanity_check(self) -> None:
|
|
14
|
+
if len(self._cache) > 0:
|
|
15
|
+
raise RuntimeError(
|
|
16
|
+
"Cache is disabled, but cache contains items. "
|
|
17
|
+
"This indicates a logic error."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def use_cache(self) -> bool:
|
|
22
|
+
return self._use_cache
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def cache(self) -> dict[str, T]:
|
|
26
|
+
return self._cache
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_empty(self) -> bool:
|
|
30
|
+
return len(self._cache) == 0
|
|
31
|
+
|
|
32
|
+
def get(self, key: str, default: T | None = None) -> T | None:
|
|
33
|
+
if not self._use_cache:
|
|
34
|
+
self._cache_sanity_check()
|
|
35
|
+
return default
|
|
36
|
+
return self._cache.get(key, default)
|
|
37
|
+
|
|
38
|
+
def set(self, key: str, value: T, overwrite: bool = True) -> None:
|
|
39
|
+
if not self._use_cache:
|
|
40
|
+
self._cache_sanity_check()
|
|
41
|
+
return
|
|
42
|
+
self._cache[key] = value
|
|
43
|
+
|
|
44
|
+
def clear(self) -> None:
|
|
45
|
+
if not self._use_cache:
|
|
46
|
+
self._cache_sanity_check()
|
|
47
|
+
return
|
|
48
|
+
self._cache.clear()
|