ngio 0.2.9__py3-none-any.whl → 0.3.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/common/__init__.py +16 -0
- ngio/common/_table_ops.py +471 -0
- ngio/hcs/plate.py +430 -72
- ngio/images/ome_zarr_container.py +99 -68
- ngio/ome_zarr_meta/_meta_handlers.py +16 -8
- ngio/ome_zarr_meta/ngio_specs/_axes.py +7 -4
- ngio/tables/__init__.py +13 -1
- ngio/tables/abstract_table.py +269 -0
- ngio/tables/backends/__init__.py +20 -0
- ngio/tables/backends/_abstract_backend.py +58 -80
- ngio/tables/backends/{_anndata_v1.py → _anndata.py} +5 -1
- ngio/tables/backends/_csv.py +35 -0
- ngio/tables/backends/{_json_v1.py → _json.py} +4 -1
- ngio/tables/backends/{_csv_v1.py → _non_zarr_backends.py} +61 -27
- ngio/tables/backends/_parquet.py +47 -0
- ngio/tables/backends/_table_backends.py +39 -18
- ngio/tables/backends/_utils.py +147 -1
- ngio/tables/tables_container.py +180 -92
- ngio/tables/v1/__init__.py +19 -3
- ngio/tables/v1/_condition_table.py +71 -0
- ngio/tables/v1/_feature_table.py +63 -129
- ngio/tables/v1/_generic_table.py +21 -159
- ngio/tables/v1/_roi_table.py +285 -201
- ngio/utils/_fractal_fsspec_store.py +29 -0
- {ngio-0.2.9.dist-info → ngio-0.3.0a1.dist-info}/METADATA +4 -3
- {ngio-0.2.9.dist-info → ngio-0.3.0a1.dist-info}/RECORD +28 -24
- ngio/tables/_validators.py +0 -108
- {ngio-0.2.9.dist-info → ngio-0.3.0a1.dist-info}/WHEEL +0 -0
- {ngio-0.2.9.dist-info → ngio-0.3.0a1.dist-info}/licenses/LICENSE +0 -0
ngio/tables/v1/_roi_table.py
CHANGED
|
@@ -5,17 +5,30 @@ https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
# Import _type to avoid name conflict with table.type
|
|
8
|
-
from builtins import type as _type
|
|
9
8
|
from collections.abc import Iterable
|
|
10
|
-
from
|
|
9
|
+
from functools import cache
|
|
10
|
+
from typing import Literal
|
|
11
11
|
|
|
12
12
|
import pandas as pd
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
15
15
|
from ngio.common import Roi
|
|
16
|
-
from ngio.tables.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
ngio_logger,
|
|
31
|
+
)
|
|
19
32
|
|
|
20
33
|
REQUIRED_COLUMNS = [
|
|
21
34
|
"x_micrometer",
|
|
@@ -26,6 +39,10 @@ REQUIRED_COLUMNS = [
|
|
|
26
39
|
"len_z_micrometer",
|
|
27
40
|
]
|
|
28
41
|
|
|
42
|
+
#####################
|
|
43
|
+
# Optional columns are not validated at the moment
|
|
44
|
+
# only a warning is raised if non optional columns are present
|
|
45
|
+
#####################
|
|
29
46
|
|
|
30
47
|
ORIGIN_COLUMNS = [
|
|
31
48
|
"x_micrometer_original",
|
|
@@ -34,36 +51,95 @@ ORIGIN_COLUMNS = [
|
|
|
34
51
|
|
|
35
52
|
TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
PLATE_COLUMNS = ["plate_name", "row", "column", "path", "acquisition"]
|
|
38
55
|
|
|
56
|
+
INDEX_COLUMNS = [
|
|
57
|
+
"FieldIndex",
|
|
58
|
+
"label",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS + PLATE_COLUMNS + INDEX_COLUMNS
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@cache
|
|
65
|
+
def _check_optional_columns(col_name: str) -> None:
|
|
66
|
+
"""Check if the column name is in the optional columns."""
|
|
67
|
+
if col_name not in OPTIONAL_COLUMNS:
|
|
68
|
+
ngio_logger.warning(
|
|
69
|
+
f"Column {col_name} is not in the optional columns. "
|
|
70
|
+
f"Standard optional columns are: {OPTIONAL_COLUMNS}."
|
|
71
|
+
)
|
|
39
72
|
|
|
40
|
-
|
|
73
|
+
|
|
74
|
+
def _dataframe_to_rois(
|
|
75
|
+
dataframe: pd.DataFrame,
|
|
76
|
+
required_columns: list[str] | None = None,
|
|
77
|
+
) -> dict[str, Roi]:
|
|
41
78
|
"""Convert a DataFrame to a WorldCooROI object."""
|
|
79
|
+
if required_columns is None:
|
|
80
|
+
required_columns = REQUIRED_COLUMNS
|
|
81
|
+
|
|
82
|
+
# Validate the columns of the DataFrame
|
|
83
|
+
_required_columns = set(dataframe.columns).intersection(set(required_columns))
|
|
84
|
+
if len(_required_columns) != len(required_columns):
|
|
85
|
+
raise NgioTableValidationError(
|
|
86
|
+
f"Could not find required columns: {_required_columns} in the table."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
extra_columns = set(dataframe.columns).difference(set(required_columns))
|
|
90
|
+
|
|
91
|
+
for col in extra_columns:
|
|
92
|
+
_check_optional_columns(col)
|
|
93
|
+
|
|
94
|
+
extras = {}
|
|
95
|
+
|
|
42
96
|
rois = {}
|
|
43
|
-
for
|
|
97
|
+
for row in dataframe.itertuples(index=True):
|
|
44
98
|
# check if optional columns are present
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
translation = {col: row.get(col, None) for col in TRANSLATION_COLUMNS}
|
|
48
|
-
translation = dict(filter(lambda x: x[1] is not None, translation.items()))
|
|
99
|
+
if len(extra_columns) > 0:
|
|
100
|
+
extras = {col: getattr(row, col, None) for col in extra_columns}
|
|
49
101
|
|
|
50
102
|
roi = Roi(
|
|
51
|
-
name=str(
|
|
52
|
-
x=row
|
|
53
|
-
y=row
|
|
54
|
-
z=row
|
|
55
|
-
x_length=row
|
|
56
|
-
y_length=row
|
|
57
|
-
z_length=row
|
|
103
|
+
name=str(row.Index),
|
|
104
|
+
x=row.x_micrometer, # type: ignore
|
|
105
|
+
y=row.y_micrometer, # type: ignore
|
|
106
|
+
z=row.z_micrometer, # type: ignore
|
|
107
|
+
x_length=row.len_x_micrometer, # type: ignore
|
|
108
|
+
y_length=row.len_y_micrometer, # type: ignore
|
|
109
|
+
z_length=row.len_z_micrometer, # type: ignore
|
|
58
110
|
unit="micrometer", # type: ignore
|
|
59
|
-
**
|
|
60
|
-
**translation,
|
|
111
|
+
**extras,
|
|
61
112
|
)
|
|
62
113
|
rois[roi.name] = roi
|
|
63
114
|
return rois
|
|
64
115
|
|
|
65
116
|
|
|
66
|
-
def
|
|
117
|
+
def _table_to_rois(
|
|
118
|
+
table: TabularData,
|
|
119
|
+
index_key: str | None = None,
|
|
120
|
+
index_type: Literal["int", "str"] | None = None,
|
|
121
|
+
required_columns: list[str] | None = None,
|
|
122
|
+
) -> tuple[pd.DataFrame, dict[str, Roi]]:
|
|
123
|
+
"""Convert a table to a dictionary of ROIs.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
table: The table to convert.
|
|
127
|
+
index_key: The column name to use as the index of the DataFrame.
|
|
128
|
+
index_type: The type of the index column in the DataFrame.
|
|
129
|
+
required_columns: The required columns in the DataFrame.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A dictionary of ROIs.
|
|
133
|
+
"""
|
|
134
|
+
dataframe = convert_to_pandas(
|
|
135
|
+
table,
|
|
136
|
+
index_key=index_key,
|
|
137
|
+
index_type=index_type,
|
|
138
|
+
)
|
|
139
|
+
return dataframe, _dataframe_to_rois(dataframe, required_columns=required_columns)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
|
|
67
143
|
"""Convert a list of WorldCooROI objects to a DataFrame."""
|
|
68
144
|
data = []
|
|
69
145
|
for roi in rois.values():
|
|
@@ -78,153 +154,110 @@ def _rois_to_dataframe(rois: dict[str, Roi], index_key: str) -> pd.DataFrame:
|
|
|
78
154
|
}
|
|
79
155
|
|
|
80
156
|
extra = roi.model_extra or {}
|
|
81
|
-
for col in
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for col in TRANSLATION_COLUMNS:
|
|
86
|
-
if col in extra:
|
|
87
|
-
row[col] = extra[col]
|
|
157
|
+
for col in extra:
|
|
158
|
+
_check_optional_columns(col)
|
|
159
|
+
row[col] = extra[col]
|
|
88
160
|
data.append(row)
|
|
89
161
|
dataframe = pd.DataFrame(data)
|
|
90
|
-
dataframe = dataframe
|
|
162
|
+
dataframe = normalize_pandas_df(dataframe, index_key=index_key)
|
|
91
163
|
return dataframe
|
|
92
164
|
|
|
93
165
|
|
|
94
|
-
class
|
|
95
|
-
"""Metadata for the ROI table."""
|
|
96
|
-
|
|
97
|
-
fractal_table_version: Literal["1"] = "1"
|
|
98
|
-
type: Literal["roi_table"] = "roi_table"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class RegionMeta(BaseModel):
|
|
102
|
-
"""Metadata for the region."""
|
|
103
|
-
|
|
104
|
-
path: str
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class MaskingRoiTableV1Meta(BackendMeta):
|
|
108
|
-
"""Metadata for the ROI table."""
|
|
109
|
-
|
|
110
|
-
fractal_table_version: Literal["1"] = "1"
|
|
111
|
-
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
112
|
-
region: RegionMeta | None = None
|
|
113
|
-
instance_key: str = "label"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
_roi_meta = TypeVar("_roi_meta", RoiTableV1Meta, MaskingRoiTableV1Meta)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class _GenericRoiTableV1(Generic[_roi_meta]):
|
|
120
|
-
"""Class to a non-specific table."""
|
|
121
|
-
|
|
122
|
-
_meta: _roi_meta
|
|
123
|
-
|
|
166
|
+
class GenericRoiTableV1(AbstractBaseTable):
|
|
124
167
|
def __init__(
|
|
125
|
-
self,
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
rois: Iterable[Roi] | None = None,
|
|
171
|
+
meta: BackendMeta,
|
|
126
172
|
) -> None:
|
|
127
|
-
|
|
128
|
-
if meta is None:
|
|
129
|
-
raise NgioValueError("Metadata must be provided.")
|
|
130
|
-
self._meta = meta
|
|
131
|
-
self._table_backend = None
|
|
173
|
+
table = None
|
|
132
174
|
|
|
133
|
-
self._rois =
|
|
175
|
+
self._rois: dict[str, Roi] | None = None
|
|
134
176
|
if rois is not None:
|
|
177
|
+
self._rois = {}
|
|
135
178
|
self.add(rois)
|
|
179
|
+
table = _rois_to_dataframe(self._rois, index_key=meta.index_key)
|
|
180
|
+
|
|
181
|
+
super().__init__(table_data=table, meta=meta)
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
"""Return a string representation of the table."""
|
|
185
|
+
rois = self.rois()
|
|
186
|
+
prop = f"num_rois={len(rois)}"
|
|
187
|
+
class_name = self.__class__.__name__
|
|
188
|
+
return f"{class_name}({prop})"
|
|
136
189
|
|
|
137
190
|
@staticmethod
|
|
138
|
-
def
|
|
191
|
+
def table_type() -> str:
|
|
139
192
|
"""Return the type of the table."""
|
|
140
|
-
|
|
193
|
+
return "generic_roi_table"
|
|
141
194
|
|
|
142
195
|
@staticmethod
|
|
143
196
|
def version() -> Literal["1"]:
|
|
144
197
|
"""Return the version of the fractal table."""
|
|
145
198
|
return "1"
|
|
146
199
|
|
|
147
|
-
@
|
|
148
|
-
def
|
|
149
|
-
"""Return the
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
@staticmethod
|
|
153
|
-
def _index_type() -> Literal["int", "str"]:
|
|
154
|
-
"""Return the index type of the table."""
|
|
155
|
-
raise NotImplementedError
|
|
200
|
+
@property
|
|
201
|
+
def table_data(self) -> TabularData:
|
|
202
|
+
"""Return the table."""
|
|
203
|
+
if self._rois is None:
|
|
204
|
+
return super().table_data
|
|
156
205
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
raise NotImplementedError
|
|
206
|
+
if len(self.rois()) > 0:
|
|
207
|
+
self._table_data = _rois_to_dataframe(self._rois, index_key=self.index_key)
|
|
208
|
+
return super().table_data
|
|
161
209
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if
|
|
210
|
+
def set_table_data(
|
|
211
|
+
self, table_data: TabularData | None = None, refresh: bool = False
|
|
212
|
+
) -> None:
|
|
213
|
+
if table_data is not None:
|
|
214
|
+
if not isinstance(table_data, TabularData):
|
|
215
|
+
raise NgioValueError(
|
|
216
|
+
"The table must be a pandas DataFrame, polars LazyFrame, "
|
|
217
|
+
" or AnnData object."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
table_data, rois = _table_to_rois(
|
|
221
|
+
table_data,
|
|
222
|
+
index_key=self.index_key,
|
|
223
|
+
index_type=self.index_type,
|
|
224
|
+
required_columns=REQUIRED_COLUMNS,
|
|
225
|
+
)
|
|
226
|
+
self._table_data = table_data
|
|
227
|
+
self._rois = rois
|
|
166
228
|
return None
|
|
167
|
-
return self._table_backend.backend_name()
|
|
168
229
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
cls, handler: ZarrGroupHandler, backend_name: str | None = None
|
|
172
|
-
) -> "_GenericRoiTableV1":
|
|
173
|
-
"""Create a new ROI table from a Zarr store."""
|
|
174
|
-
meta = cls._meta_type()(**handler.load_attrs())
|
|
175
|
-
|
|
176
|
-
if backend_name is None:
|
|
177
|
-
backend = ImplementedTableBackends().get_backend(
|
|
178
|
-
backend_name=meta.backend,
|
|
179
|
-
group_handler=handler,
|
|
180
|
-
index_key=cls._index_key(),
|
|
181
|
-
index_type=cls._index_type(),
|
|
182
|
-
)
|
|
183
|
-
else:
|
|
184
|
-
backend = ImplementedTableBackends().get_backend(
|
|
185
|
-
backend_name=backend_name,
|
|
186
|
-
group_handler=handler,
|
|
187
|
-
index_key=cls._index_key(),
|
|
188
|
-
index_type=cls._index_type(),
|
|
189
|
-
)
|
|
190
|
-
meta.backend = backend_name
|
|
230
|
+
if self._table_data is not None and not refresh:
|
|
231
|
+
return None
|
|
191
232
|
|
|
192
|
-
if
|
|
233
|
+
if self._table_backend is None:
|
|
193
234
|
raise NgioValueError(
|
|
194
|
-
"The
|
|
235
|
+
"The table does not have a DataFrame in memory nor a backend."
|
|
195
236
|
)
|
|
196
237
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
dataframe = backend.load_as_pandas_df()
|
|
203
|
-
dataframe = validate_columns(
|
|
204
|
-
dataframe,
|
|
238
|
+
table_data, rois = _table_to_rois(
|
|
239
|
+
self._table_backend.load(),
|
|
240
|
+
index_key=self.index_key,
|
|
241
|
+
index_type=self.index_type,
|
|
205
242
|
required_columns=REQUIRED_COLUMNS,
|
|
206
|
-
optional_columns=OPTIONAL_COLUMNS,
|
|
207
243
|
)
|
|
208
|
-
|
|
209
|
-
|
|
244
|
+
self._table_data = table_data
|
|
245
|
+
self._rois = rois
|
|
210
246
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
group_handler=handler,
|
|
220
|
-
index_key=self._index_key(),
|
|
221
|
-
index_type=self._index_type(),
|
|
222
|
-
)
|
|
223
|
-
self._meta.backend = backend_name
|
|
224
|
-
self._table_backend = backend
|
|
247
|
+
def _check_rois(self) -> None:
|
|
248
|
+
"""Load the ROIs from the table.
|
|
249
|
+
|
|
250
|
+
If the ROIs are already loaded, do nothing.
|
|
251
|
+
If the ROIs are not loaded, load them from the table.
|
|
252
|
+
"""
|
|
253
|
+
if self._rois is None:
|
|
254
|
+
self._rois = _dataframe_to_rois(self.dataframe)
|
|
225
255
|
|
|
226
256
|
def rois(self) -> list[Roi]:
|
|
227
257
|
"""List all ROIs in the table."""
|
|
258
|
+
self._check_rois()
|
|
259
|
+
if self._rois is None:
|
|
260
|
+
return []
|
|
228
261
|
return list(self._rois.values())
|
|
229
262
|
|
|
230
263
|
def add(self, roi: Roi | Iterable[Roi], overwrite: bool = False) -> None:
|
|
@@ -237,31 +270,49 @@ class _GenericRoiTableV1(Generic[_roi_meta]):
|
|
|
237
270
|
if isinstance(roi, Roi):
|
|
238
271
|
roi = [roi]
|
|
239
272
|
|
|
273
|
+
self._check_rois()
|
|
274
|
+
if self._rois is None:
|
|
275
|
+
self._rois = {}
|
|
276
|
+
|
|
240
277
|
for _roi in roi:
|
|
241
278
|
if not overwrite and _roi.name in self._rois:
|
|
242
279
|
raise NgioValueError(f"ROI {_roi.name} already exists in the table.")
|
|
243
280
|
self._rois[_roi.name] = _roi
|
|
244
281
|
|
|
245
|
-
def
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
251
|
-
)
|
|
282
|
+
def get(self, roi_name: str) -> Roi:
|
|
283
|
+
"""Get an ROI from the table."""
|
|
284
|
+
self._check_rois()
|
|
285
|
+
if self._rois is None:
|
|
286
|
+
self._rois = {}
|
|
252
287
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
288
|
+
if roi_name not in self._rois:
|
|
289
|
+
raise NgioValueError(f"ROI {roi_name} not found in the table.")
|
|
290
|
+
return self._rois[roi_name]
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def from_table_data(
|
|
294
|
+
cls, table_data: TabularData, meta: BackendMeta
|
|
295
|
+
) -> "GenericRoiTableV1":
|
|
296
|
+
"""Create a new ROI table from a table data."""
|
|
297
|
+
_, rois = _table_to_rois(
|
|
298
|
+
table=table_data,
|
|
299
|
+
index_key=meta.index_key,
|
|
300
|
+
index_type=meta.index_type,
|
|
256
301
|
required_columns=REQUIRED_COLUMNS,
|
|
257
|
-
optional_columns=OPTIONAL_COLUMNS,
|
|
258
|
-
)
|
|
259
|
-
self._table_backend.write(
|
|
260
|
-
dataframe, metadata=self._meta.model_dump(exclude_none=True), mode="pandas"
|
|
261
302
|
)
|
|
303
|
+
return cls(rois=rois.values(), meta=meta)
|
|
304
|
+
|
|
262
305
|
|
|
306
|
+
class RoiTableV1Meta(BackendMeta):
|
|
307
|
+
"""Metadata for the ROI table."""
|
|
263
308
|
|
|
264
|
-
|
|
309
|
+
table_version: Literal["1"] = "1"
|
|
310
|
+
type: Literal["roi_table"] = "roi_table"
|
|
311
|
+
index_key: str | None = "FieldIndex"
|
|
312
|
+
index_type: Literal["str", "int"] | None = "str"
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class RoiTableV1(GenericRoiTableV1):
|
|
265
316
|
"""Class to handle fractal ROI tables.
|
|
266
317
|
|
|
267
318
|
To know more about the ROI table format, please refer to the
|
|
@@ -269,43 +320,57 @@ class RoiTableV1(_GenericRoiTableV1[RoiTableV1Meta]):
|
|
|
269
320
|
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
270
321
|
"""
|
|
271
322
|
|
|
272
|
-
def __init__(
|
|
323
|
+
def __init__(
|
|
324
|
+
self, rois: Iterable[Roi] | None = None, *, meta: RoiTableV1Meta | None = None
|
|
325
|
+
) -> None:
|
|
273
326
|
"""Create a new ROI table."""
|
|
274
|
-
|
|
327
|
+
if meta is None:
|
|
328
|
+
meta = RoiTableV1Meta()
|
|
275
329
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
330
|
+
if meta.index_key is None:
|
|
331
|
+
meta.index_key = "FieldIndex"
|
|
332
|
+
|
|
333
|
+
if meta.index_type is None:
|
|
334
|
+
meta.index_type = "str"
|
|
335
|
+
super().__init__(meta=meta, rois=rois)
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def from_handler(
|
|
339
|
+
cls,
|
|
340
|
+
handler: ZarrGroupHandler,
|
|
341
|
+
backend: TableBackend | None = None,
|
|
342
|
+
) -> "RoiTableV1":
|
|
343
|
+
table = cls._from_handler(
|
|
344
|
+
handler=handler,
|
|
345
|
+
backend=backend,
|
|
346
|
+
meta_model=RoiTableV1Meta,
|
|
347
|
+
)
|
|
348
|
+
return table
|
|
280
349
|
|
|
281
350
|
@staticmethod
|
|
282
|
-
def
|
|
351
|
+
def table_type() -> Literal["roi_table"]:
|
|
283
352
|
"""Return the type of the table."""
|
|
284
353
|
return "roi_table"
|
|
285
354
|
|
|
286
|
-
@staticmethod
|
|
287
|
-
def _index_key() -> str:
|
|
288
|
-
"""Return the index key of the table."""
|
|
289
|
-
return "FieldIndex"
|
|
290
355
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"""Return the index type of the table."""
|
|
294
|
-
return "str"
|
|
356
|
+
class RegionMeta(BaseModel):
|
|
357
|
+
"""Metadata for the region."""
|
|
295
358
|
|
|
296
|
-
|
|
297
|
-
def _meta_type() -> _type[RoiTableV1Meta]:
|
|
298
|
-
"""Return the metadata type of the table."""
|
|
299
|
-
return RoiTableV1Meta
|
|
359
|
+
path: str
|
|
300
360
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
361
|
+
|
|
362
|
+
class MaskingRoiTableV1Meta(BackendMeta):
|
|
363
|
+
"""Metadata for the ROI table."""
|
|
364
|
+
|
|
365
|
+
table_version: Literal["1"] = "1"
|
|
366
|
+
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
367
|
+
region: RegionMeta | None = None
|
|
368
|
+
instance_key: str = "label"
|
|
369
|
+
index_key: str | None = "label"
|
|
370
|
+
index_type: Literal["int", "str"] | None = "int"
|
|
306
371
|
|
|
307
372
|
|
|
308
|
-
class MaskingRoiTableV1(
|
|
373
|
+
class MaskingRoiTableV1(GenericRoiTableV1):
|
|
309
374
|
"""Class to handle fractal ROI tables.
|
|
310
375
|
|
|
311
376
|
To know more about the ROI table format, please refer to the
|
|
@@ -316,54 +381,73 @@ class MaskingRoiTableV1(_GenericRoiTableV1[MaskingRoiTableV1Meta]):
|
|
|
316
381
|
def __init__(
|
|
317
382
|
self,
|
|
318
383
|
rois: Iterable[Roi] | None = None,
|
|
384
|
+
*,
|
|
319
385
|
reference_label: str | None = None,
|
|
386
|
+
meta: MaskingRoiTableV1Meta | None = None,
|
|
320
387
|
) -> None:
|
|
321
388
|
"""Create a new ROI table."""
|
|
322
|
-
meta
|
|
389
|
+
if meta is None:
|
|
390
|
+
meta = MaskingRoiTableV1Meta()
|
|
391
|
+
|
|
323
392
|
if reference_label is not None:
|
|
324
393
|
meta.region = RegionMeta(path=reference_label)
|
|
325
|
-
|
|
394
|
+
|
|
395
|
+
if meta.index_key is None:
|
|
396
|
+
meta.index_key = "label"
|
|
397
|
+
|
|
398
|
+
if meta.index_type is None:
|
|
399
|
+
meta.index_type = "int"
|
|
400
|
+
meta.instance_key = meta.index_key
|
|
401
|
+
super().__init__(meta=meta, rois=rois)
|
|
326
402
|
|
|
327
403
|
def __repr__(self) -> str:
|
|
328
404
|
"""Return a string representation of the table."""
|
|
329
|
-
|
|
405
|
+
rois = self.rois()
|
|
330
406
|
if self.reference_label is not None:
|
|
331
|
-
prop
|
|
407
|
+
prop = f"num_rois={len(rois)}, reference_label={self.reference_label}"
|
|
408
|
+
else:
|
|
409
|
+
prop = f"num_rois={len(rois)}"
|
|
332
410
|
return f"MaskingRoiTableV1({prop})"
|
|
333
411
|
|
|
412
|
+
@classmethod
|
|
413
|
+
def from_handler(
|
|
414
|
+
cls,
|
|
415
|
+
handler: ZarrGroupHandler,
|
|
416
|
+
backend: TableBackend | None = None,
|
|
417
|
+
) -> "MaskingRoiTableV1":
|
|
418
|
+
table = cls._from_handler(
|
|
419
|
+
handler=handler,
|
|
420
|
+
backend=backend,
|
|
421
|
+
meta_model=MaskingRoiTableV1Meta,
|
|
422
|
+
)
|
|
423
|
+
return table
|
|
424
|
+
|
|
334
425
|
@staticmethod
|
|
335
|
-
def
|
|
426
|
+
def table_type() -> Literal["masking_roi_table"]:
|
|
336
427
|
"""Return the type of the table."""
|
|
337
428
|
return "masking_roi_table"
|
|
338
429
|
|
|
339
|
-
@
|
|
340
|
-
def
|
|
341
|
-
"""Return the
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return "int"
|
|
348
|
-
|
|
349
|
-
@staticmethod
|
|
350
|
-
def _meta_type() -> _type[MaskingRoiTableV1Meta]:
|
|
351
|
-
"""Return the metadata type of the table."""
|
|
352
|
-
return MaskingRoiTableV1Meta
|
|
430
|
+
@property
|
|
431
|
+
def meta(self) -> MaskingRoiTableV1Meta:
|
|
432
|
+
"""Return the metadata of the table."""
|
|
433
|
+
if not isinstance(self._meta, MaskingRoiTableV1Meta):
|
|
434
|
+
raise NgioValueError(
|
|
435
|
+
"The metadata of the table is not of type MaskingRoiTableV1Meta."
|
|
436
|
+
)
|
|
437
|
+
return self._meta
|
|
353
438
|
|
|
354
439
|
@property
|
|
355
440
|
def reference_label(self) -> str | None:
|
|
356
441
|
"""Return the reference label."""
|
|
357
|
-
path = self.
|
|
442
|
+
path = self.meta.region
|
|
358
443
|
if path is None:
|
|
359
444
|
return None
|
|
445
|
+
|
|
360
446
|
path = path.path
|
|
361
447
|
path = path.split("/")[-1]
|
|
362
448
|
return path
|
|
363
449
|
|
|
364
|
-
def get(self, label: int) -> Roi:
|
|
450
|
+
def get(self, label: int | str) -> Roi: # type: ignore
|
|
365
451
|
"""Get an ROI from the table."""
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
raise KeyError(f"ROI {_label} not found in the table.")
|
|
369
|
-
return self._rois[_label]
|
|
452
|
+
roi_name = str(label)
|
|
453
|
+
return super().get(roi_name)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import fsspec.implementations.http
|
|
2
|
+
from aiohttp import ClientResponseError
|
|
3
|
+
|
|
4
|
+
from ngio.utils import NgioValueError
|
|
2
5
|
|
|
3
6
|
|
|
4
7
|
def fractal_fsspec_store(
|
|
@@ -9,5 +12,31 @@ def fractal_fsspec_store(
|
|
|
9
12
|
if fractal_token is not None:
|
|
10
13
|
client_kwargs["headers"] = {"Authorization": f"Bearer {fractal_token}"}
|
|
11
14
|
fs = fsspec.implementations.http.HTTPFileSystem(client_kwargs=client_kwargs)
|
|
15
|
+
|
|
12
16
|
store = fs.get_mapper(url)
|
|
17
|
+
|
|
18
|
+
possible_keys = [".zgroup", ".zarray"]
|
|
19
|
+
for key in possible_keys:
|
|
20
|
+
try:
|
|
21
|
+
value = store.get(key)
|
|
22
|
+
if value is not None:
|
|
23
|
+
break
|
|
24
|
+
except ClientResponseError as e:
|
|
25
|
+
if e.status == 401 and fractal_token is None:
|
|
26
|
+
raise NgioValueError(
|
|
27
|
+
"No auto token is provided. You need a valid "
|
|
28
|
+
f"'fractal_token' to access: {url}."
|
|
29
|
+
) from e
|
|
30
|
+
elif e.status == 401 and fractal_token is not None:
|
|
31
|
+
raise NgioValueError(
|
|
32
|
+
f"The 'fractal_token' provided is invalid for: {url}."
|
|
33
|
+
) from e
|
|
34
|
+
else:
|
|
35
|
+
raise e
|
|
36
|
+
else:
|
|
37
|
+
raise NgioValueError(
|
|
38
|
+
f"Store {url} can not be read. Possible problems are: \n"
|
|
39
|
+
"- The url does not exist. \n"
|
|
40
|
+
f"- The url is not a valid .zarr. \n"
|
|
41
|
+
)
|
|
13
42
|
return store
|