ngio 0.2.0a2__py3-none-any.whl → 0.5.0b4__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 +40 -12
- ngio/common/__init__.py +16 -32
- ngio/common/_dimensions.py +270 -48
- ngio/common/_masking_roi.py +153 -0
- ngio/common/_pyramid.py +267 -73
- ngio/common/_roi.py +290 -66
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +54 -22
- 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 +17 -58
- ngio/hcs/_plate.py +1354 -0
- ngio/images/__init__.py +30 -9
- ngio/images/_abstract_image.py +968 -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 +417 -0
- ngio/images/_masked_image.py +531 -0
- ngio/images/_ome_zarr_container.py +1235 -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 +39 -15
- ngio/ome_zarr_meta/_meta_handlers.py +490 -96
- ngio/ome_zarr_meta/ngio_specs/__init__.py +24 -10
- ngio/ome_zarr_meta/ngio_specs/_axes.py +268 -234
- ngio/ome_zarr_meta/ngio_specs/_channels.py +125 -41
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +42 -87
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +536 -2
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +202 -198
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +72 -34
- ngio/ome_zarr_meta/v04/__init__.py +21 -5
- ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
- ngio/ome_zarr_meta/v04/{_v04_spec_utils.py → _v04_spec.py} +151 -90
- 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 +20 -4
- ngio/tables/_abstract_table.py +270 -0
- ngio/tables/_tables_container.py +449 -0
- ngio/tables/backends/__init__.py +50 -1
- ngio/tables/backends/_abstract_backend.py +200 -31
- ngio/tables/backends/_anndata.py +139 -0
- ngio/tables/backends/_anndata_utils.py +10 -114
- 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 +162 -38
- ngio/tables/backends/_utils.py +608 -0
- ngio/tables/v1/__init__.py +19 -4
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +79 -115
- ngio/tables/v1/_generic_table.py +21 -90
- ngio/tables/v1/_roi_table.py +486 -137
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +16 -14
- ngio/utils/_cache.py +48 -0
- ngio/utils/_datasets.py +121 -13
- ngio/utils/_fractal_fsspec_store.py +42 -0
- ngio/utils/_zarr_utils.py +374 -218
- ngio-0.5.0b4.dist-info/METADATA +147 -0
- ngio-0.5.0b4.dist-info/RECORD +88 -0
- {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/WHEEL +1 -1
- ngio/common/_array_pipe.py +0 -160
- ngio/common/_axes_transforms.py +0 -63
- ngio/common/_common_types.py +0 -5
- ngio/common/_slicer.py +0 -97
- ngio/images/abstract_image.py +0 -240
- ngio/images/create.py +0 -251
- ngio/images/image.py +0 -389
- ngio/images/label.py +0 -236
- ngio/images/omezarr_container.py +0 -535
- ngio/ome_zarr_meta/_generic_handlers.py +0 -320
- ngio/ome_zarr_meta/v04/_meta_handlers.py +0 -54
- ngio/tables/_validators.py +0 -192
- ngio/tables/backends/_anndata_v1.py +0 -75
- ngio/tables/backends/_json_v1.py +0 -56
- ngio/tables/tables_container.py +0 -300
- ngio/tables/v1/_masking_roi_table.py +0 -175
- ngio/utils/_logger.py +0 -29
- ngio-0.2.0a2.dist-info/METADATA +0 -95
- ngio-0.2.0a2.dist-info/RECORD +0 -53
- {ngio-0.2.0a2.dist-info → ngio-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
ngio/tables/v1/_roi_table.py
CHANGED
|
@@ -4,16 +4,30 @@ This class follows the roi_table specification at:
|
|
|
4
4
|
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import warnings
|
|
7
8
|
from collections.abc import Iterable
|
|
8
9
|
from typing import Literal
|
|
10
|
+
from uuid import uuid4
|
|
9
11
|
|
|
10
12
|
import pandas as pd
|
|
11
13
|
from pydantic import BaseModel
|
|
12
14
|
|
|
13
|
-
from ngio.common import
|
|
14
|
-
from ngio.tables.
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
)
|
|
17
31
|
|
|
18
32
|
REQUIRED_COLUMNS = [
|
|
19
33
|
"x_micrometer",
|
|
@@ -24,100 +38,274 @@ REQUIRED_COLUMNS = [
|
|
|
24
38
|
"len_z_micrometer",
|
|
25
39
|
]
|
|
26
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
|
+
]
|
|
27
50
|
|
|
28
51
|
ORIGIN_COLUMNS = [
|
|
29
52
|
"x_micrometer_original",
|
|
30
53
|
"y_micrometer_original",
|
|
54
|
+
"z_micrometer_original",
|
|
31
55
|
]
|
|
32
56
|
|
|
33
57
|
TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
|
|
34
58
|
|
|
35
|
-
|
|
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
|
+
)
|
|
36
83
|
|
|
37
84
|
|
|
38
|
-
def _dataframe_to_rois(
|
|
85
|
+
def _dataframe_to_rois(
|
|
86
|
+
dataframe: pd.DataFrame,
|
|
87
|
+
required_columns: list[str] = REQUIRED_COLUMNS,
|
|
88
|
+
) -> dict[str, Roi]:
|
|
39
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
|
+
|
|
40
108
|
rois = {}
|
|
41
|
-
for
|
|
109
|
+
for row in dataframe.itertuples(index=True):
|
|
42
110
|
# check if optional columns are present
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,
|
|
59
138
|
)
|
|
60
139
|
rois[roi.name] = roi
|
|
61
140
|
return rois
|
|
62
141
|
|
|
63
142
|
|
|
64
|
-
def _rois_to_dataframe(rois: dict[str,
|
|
143
|
+
def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
|
|
65
144
|
"""Convert a list of WorldCooROI objects to a DataFrame."""
|
|
66
145
|
data = []
|
|
67
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.")
|
|
68
167
|
row = {
|
|
69
|
-
index_key: roi.
|
|
70
|
-
"x_micrometer":
|
|
71
|
-
"y_micrometer":
|
|
72
|
-
"z_micrometer":
|
|
73
|
-
"len_x_micrometer":
|
|
74
|
-
"len_y_micrometer":
|
|
75
|
-
"len_z_micrometer":
|
|
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,
|
|
76
175
|
}
|
|
77
176
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if
|
|
81
|
-
|
|
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
|
|
82
181
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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]
|
|
86
189
|
data.append(row)
|
|
190
|
+
|
|
87
191
|
dataframe = pd.DataFrame(data)
|
|
88
|
-
dataframe = dataframe
|
|
192
|
+
dataframe = normalize_pandas_df(dataframe, index_key=index_key)
|
|
89
193
|
return dataframe
|
|
90
194
|
|
|
91
195
|
|
|
92
|
-
class
|
|
93
|
-
"""
|
|
196
|
+
class RoiDictWrapper:
|
|
197
|
+
"""A wrapper for a dictionary of ROIs to provide a consistent interface."""
|
|
94
198
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
98
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)
|
|
99
213
|
|
|
100
|
-
|
|
101
|
-
|
|
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)
|
|
102
217
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.")
|
|
107
222
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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)
|
|
112
242
|
|
|
113
|
-
|
|
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
|
|
114
291
|
if rois is not None:
|
|
115
|
-
self.
|
|
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})"
|
|
116
304
|
|
|
117
305
|
@staticmethod
|
|
118
|
-
def
|
|
306
|
+
def table_type() -> str:
|
|
119
307
|
"""Return the type of the table."""
|
|
120
|
-
return "
|
|
308
|
+
return "generic_roi_table"
|
|
121
309
|
|
|
122
310
|
@staticmethod
|
|
123
311
|
def version() -> Literal["1"]:
|
|
@@ -125,102 +313,263 @@ class RoiTableV1:
|
|
|
125
313
|
return "1"
|
|
126
314
|
|
|
127
315
|
@property
|
|
128
|
-
def
|
|
129
|
-
"""Return the
|
|
130
|
-
if self.
|
|
131
|
-
return
|
|
132
|
-
return self._table_backend.backend_name()
|
|
316
|
+
def table_data(self) -> TabularData:
|
|
317
|
+
"""Return the table."""
|
|
318
|
+
if self._rois is None:
|
|
319
|
+
return super().table_data
|
|
133
320
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
index_type="str",
|
|
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,
|
|
154
340
|
)
|
|
155
|
-
|
|
341
|
+
self._table_data = table_data
|
|
342
|
+
self._rois = rois
|
|
343
|
+
return None
|
|
156
344
|
|
|
157
|
-
if not
|
|
345
|
+
if self._table_data is not None and not refresh:
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
if self._table_backend is None:
|
|
158
349
|
raise NgioValueError(
|
|
159
|
-
"The
|
|
350
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
160
351
|
)
|
|
161
352
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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([])
|
|
165
393
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,
|
|
169
416
|
required_columns=REQUIRED_COLUMNS,
|
|
170
|
-
optional_columns=OPTIONAL_COLUMNS,
|
|
171
417
|
)
|
|
172
|
-
|
|
173
|
-
return table
|
|
418
|
+
return cls(rois=rois.to_list(), meta=meta)
|
|
174
419
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
179
440
|
) -> None:
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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,
|
|
186
462
|
)
|
|
187
|
-
|
|
188
|
-
self._table_backend = backend
|
|
463
|
+
return table
|
|
189
464
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
465
|
+
@staticmethod
|
|
466
|
+
def table_type() -> Literal["roi_table"]:
|
|
467
|
+
"""Return the type of the table."""
|
|
468
|
+
return "roi_table"
|
|
193
469
|
|
|
194
|
-
def get(self, roi_name: str) -> WorldCooROI:
|
|
195
|
-
"""Get an ROI from the table."""
|
|
196
|
-
if roi_name not in self._rois:
|
|
197
|
-
raise NgioValueError(f"ROI {roi_name} not found in the table.")
|
|
198
|
-
return self._rois[roi_name]
|
|
199
470
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if isinstance(roi, WorldCooROI):
|
|
203
|
-
roi = [roi]
|
|
471
|
+
class RegionMeta(BaseModel):
|
|
472
|
+
"""Metadata for the region."""
|
|
204
473
|
|
|
205
|
-
|
|
206
|
-
if _roi.name in self._rois:
|
|
207
|
-
raise NgioValueError(f"ROI {_roi.name} already exists in the table.")
|
|
208
|
-
self._rois[_roi.name] = _roi
|
|
474
|
+
path: str
|
|
209
475
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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):
|
|
213
550
|
raise NgioValueError(
|
|
214
|
-
"
|
|
215
|
-
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
551
|
+
"The metadata of the table is not of type MaskingRoiTableV1Meta."
|
|
216
552
|
)
|
|
553
|
+
return self._meta
|
|
217
554
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
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
|