ngio 0.1.6__py3-none-any.whl → 0.2.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ngio/__init__.py +31 -5
- ngio/common/__init__.py +44 -0
- ngio/common/_array_pipe.py +160 -0
- ngio/common/_axes_transforms.py +63 -0
- ngio/common/_common_types.py +5 -0
- ngio/common/_dimensions.py +113 -0
- ngio/common/_pyramid.py +222 -0
- ngio/{core/roi.py → common/_roi.py} +22 -23
- ngio/common/_slicer.py +97 -0
- ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
- ngio/hcs/__init__.py +60 -0
- ngio/images/__init__.py +23 -0
- ngio/images/abstract_image.py +240 -0
- ngio/images/create.py +251 -0
- ngio/images/image.py +383 -0
- ngio/images/label.py +96 -0
- ngio/images/omezarr_container.py +512 -0
- ngio/ome_zarr_meta/__init__.py +35 -0
- ngio/ome_zarr_meta/_generic_handlers.py +320 -0
- ngio/ome_zarr_meta/_meta_handlers.py +142 -0
- ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
- ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
- ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
- ngio/ome_zarr_meta/v04/__init__.py +11 -0
- ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
- ngio/tables/__init__.py +21 -5
- ngio/tables/_validators.py +192 -0
- ngio/tables/backends/__init__.py +8 -0
- ngio/tables/backends/_abstract_backend.py +71 -0
- ngio/tables/backends/_anndata_utils.py +194 -0
- ngio/tables/backends/_anndata_v1.py +75 -0
- ngio/tables/backends/_json_v1.py +56 -0
- ngio/tables/backends/_table_backends.py +102 -0
- ngio/tables/tables_container.py +300 -0
- ngio/tables/v1/__init__.py +6 -5
- ngio/tables/v1/_feature_table.py +161 -0
- ngio/tables/v1/_generic_table.py +99 -182
- ngio/tables/v1/_masking_roi_table.py +175 -0
- ngio/tables/v1/_roi_table.py +226 -0
- ngio/utils/__init__.py +23 -10
- ngio/utils/_datasets.py +51 -0
- ngio/utils/_errors.py +10 -4
- ngio/utils/_zarr_utils.py +378 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/METADATA +18 -39
- ngio-0.2.0a1.dist-info/RECORD +53 -0
- ngio/core/__init__.py +0 -7
- ngio/core/dimensions.py +0 -122
- ngio/core/image_handler.py +0 -228
- ngio/core/image_like_handler.py +0 -549
- ngio/core/label_handler.py +0 -410
- ngio/core/ngff_image.py +0 -387
- ngio/core/utils.py +0 -287
- ngio/io/__init__.py +0 -19
- ngio/io/_zarr.py +0 -88
- ngio/io/_zarr_array_utils.py +0 -0
- ngio/io/_zarr_group_utils.py +0 -60
- ngio/iterators/__init__.py +0 -1
- ngio/ngff_meta/__init__.py +0 -27
- ngio/ngff_meta/fractal_image_meta.py +0 -1267
- ngio/ngff_meta/meta_handler.py +0 -92
- ngio/ngff_meta/utils.py +0 -235
- ngio/ngff_meta/v04/__init__.py +0 -6
- ngio/ngff_meta/v04/specs.py +0 -158
- ngio/ngff_meta/v04/zarr_utils.py +0 -376
- ngio/pipes/__init__.py +0 -7
- ngio/pipes/_slicer_transforms.py +0 -176
- ngio/pipes/_transforms.py +0 -33
- ngio/pipes/data_pipe.py +0 -52
- ngio/tables/_ad_reader.py +0 -80
- ngio/tables/_utils.py +0 -301
- ngio/tables/tables_group.py +0 -252
- ngio/tables/v1/feature_tables.py +0 -182
- ngio/tables/v1/masking_roi_tables.py +0 -243
- ngio/tables/v1/roi_tables.py +0 -285
- ngio/utils/_common_types.py +0 -5
- ngio/utils/_pydantic_utils.py +0 -52
- ngio-0.1.6.dist-info/RECORD +0 -44
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/WHEEL +0 -0
- {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/licenses/LICENSE +0 -0
ngio/tables/v1/_generic_table.py
CHANGED
|
@@ -1,201 +1,118 @@
|
|
|
1
|
-
"""Implementation of a
|
|
1
|
+
"""Implementation of a generic table class."""
|
|
2
2
|
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Literal
|
|
5
|
-
|
|
6
|
-
import anndata as ad
|
|
7
3
|
import pandas as pd
|
|
8
|
-
import zarr
|
|
9
4
|
from pydantic import BaseModel
|
|
10
5
|
|
|
11
|
-
from ngio.
|
|
12
|
-
from ngio.
|
|
13
|
-
from ngio.tables._utils import Validator, table_ad_to_df, table_df_to_ad, validate_table
|
|
14
|
-
|
|
15
|
-
REQUIRED_COLUMNS = [
|
|
16
|
-
"x_micrometer",
|
|
17
|
-
"y_micrometer",
|
|
18
|
-
"z_micrometer",
|
|
19
|
-
"len_x_micrometer",
|
|
20
|
-
"len_y_micrometer",
|
|
21
|
-
"len_z_micrometer",
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def write_table_ad(
|
|
26
|
-
group: zarr.Group,
|
|
27
|
-
table: pd.DataFrame,
|
|
28
|
-
index_key: str,
|
|
29
|
-
index_type: Literal["int", "str"],
|
|
30
|
-
meta: BaseModel,
|
|
31
|
-
validators: list[Validator] | None = None,
|
|
32
|
-
) -> None:
|
|
33
|
-
"""Write a table to a Zarr group.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
group (zarr.Group): The group to write the table to.
|
|
37
|
-
table (pd.DataFrame): The table to write.
|
|
38
|
-
index_key (str): The column name to use as the index of the DataFrame.
|
|
39
|
-
index_type (str): The type of the index column in the DataFrame.
|
|
40
|
-
meta (BaseModel): The metadata of the table.
|
|
41
|
-
validators (list[Validator]): A list of functions to further validate the
|
|
42
|
-
table.
|
|
43
|
-
"""
|
|
44
|
-
ad_table = table_df_to_ad(
|
|
45
|
-
table,
|
|
46
|
-
index_key=index_key,
|
|
47
|
-
index_type=index_type,
|
|
48
|
-
validators=validators,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
group_path = Path(group.store.path) / group.path
|
|
52
|
-
ad_table.write_zarr(group_path)
|
|
53
|
-
group.attrs.update(meta.model_dump(exclude=None))
|
|
6
|
+
from ngio.tables.backends import ImplementedTableBackends
|
|
7
|
+
from ngio.utils import ZarrGroupHandler
|
|
54
8
|
|
|
55
9
|
|
|
56
|
-
class
|
|
57
|
-
"""
|
|
10
|
+
class GenericTableMeta(BaseModel):
|
|
11
|
+
"""Metadata for the ROI table."""
|
|
58
12
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
index_key: str,
|
|
63
|
-
index_type: Literal["int", "str"],
|
|
64
|
-
validators: list[Validator] | None = None,
|
|
65
|
-
):
|
|
66
|
-
"""Initialize the class from an existing group.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
group (zarr.Group): The group containing the
|
|
70
|
-
ROI table.
|
|
71
|
-
index_key (str): The column name to use as the index of the DataFrame.
|
|
72
|
-
index_type (str): The type of the index column in the DataFrame.
|
|
73
|
-
validators (list[Validator]): A list of functions to further validate the
|
|
74
|
-
table.
|
|
75
|
-
"""
|
|
76
|
-
self._table_group = group
|
|
77
|
-
self._index_key = index_key
|
|
78
|
-
self._index_type = index_type
|
|
79
|
-
self._validators = validators
|
|
80
|
-
|
|
81
|
-
table_ad = custom_read_zarr(store=group)
|
|
82
|
-
|
|
83
|
-
self._table = table_ad_to_df(
|
|
84
|
-
table_ad=table_ad,
|
|
85
|
-
index_key=self._index_key,
|
|
86
|
-
index_type=self._index_type,
|
|
87
|
-
validators=self._validators,
|
|
88
|
-
)
|
|
89
|
-
self.state = State.CONSOLIDATED
|
|
13
|
+
fractal_table_version: str | None = None
|
|
14
|
+
type: str | None = None
|
|
15
|
+
backend: str | None = None
|
|
90
16
|
|
|
91
|
-
@property
|
|
92
|
-
def root_path(self) -> str:
|
|
93
|
-
"""Return the path of the root group.
|
|
94
17
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return str(self._table_group.store.path)
|
|
18
|
+
class GenericTable:
|
|
19
|
+
"""Class to a non-specific table.
|
|
98
20
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
21
|
+
This can be used to load any table that does not have
|
|
22
|
+
a specific definition.
|
|
23
|
+
"""
|
|
102
24
|
|
|
103
|
-
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
dataframe: pd.DataFrame,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the GenericTable."""
|
|
30
|
+
self._meta = GenericTableMeta()
|
|
31
|
+
self._dataframe = dataframe
|
|
32
|
+
self._table_backend = None
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def type() -> str:
|
|
36
|
+
"""Return the type of the table."""
|
|
37
|
+
return "generic"
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def version() -> str:
|
|
41
|
+
"""The generic table does not have a version.
|
|
42
|
+
|
|
43
|
+
Since does not follow a specific schema.
|
|
104
44
|
"""
|
|
105
|
-
|
|
106
|
-
if root.endswith("/"):
|
|
107
|
-
root = root[:-1]
|
|
108
|
-
|
|
109
|
-
return f"{root}/{self._table_group.path}"
|
|
45
|
+
return "1"
|
|
110
46
|
|
|
111
47
|
@property
|
|
112
|
-
def
|
|
113
|
-
"""Return the
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def table(self, table: pd.DataFrame) -> None:
|
|
118
|
-
raise NotImplementedError(
|
|
119
|
-
"Setting the table directly is not supported. "
|
|
120
|
-
"Please use the 'set_table' method."
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
def set_table(self, table: pd.DataFrame) -> None:
|
|
124
|
-
table = validate_table(
|
|
125
|
-
table_df=table,
|
|
126
|
-
index_key=self.index_key,
|
|
127
|
-
index_type=self.index_type,
|
|
128
|
-
validators=self._validators,
|
|
129
|
-
)
|
|
130
|
-
self._table = table
|
|
131
|
-
|
|
132
|
-
def as_anndata(self) -> ad.AnnData:
|
|
133
|
-
"""Return the ROI table as an AnnData object."""
|
|
134
|
-
return table_df_to_ad(
|
|
135
|
-
self.table, index_key=self.index_key, index_type=self.index_type
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
def from_anndata(self, table_ad: ad.AnnData) -> None:
|
|
139
|
-
"""Return the ROI table as an AnnData object."""
|
|
140
|
-
table = table_ad_to_df(
|
|
141
|
-
table_ad=table_ad,
|
|
142
|
-
index_key=self.index_key,
|
|
143
|
-
index_type=self.index_type,
|
|
144
|
-
validators=self._validators,
|
|
145
|
-
)
|
|
146
|
-
# Don't use the setter to avoid re-validating the table
|
|
147
|
-
self._table = table
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
def index(self) -> list[int | str]:
|
|
151
|
-
"""Return a list of all the labels in the table."""
|
|
152
|
-
return self.table.index.tolist()
|
|
48
|
+
def backend_name(self) -> str | None:
|
|
49
|
+
"""Return the name of the backend."""
|
|
50
|
+
if self._table_backend is None:
|
|
51
|
+
return None
|
|
52
|
+
return self._table_backend.backend_name()
|
|
153
53
|
|
|
154
54
|
@property
|
|
155
|
-
def
|
|
156
|
-
"""Return the
|
|
157
|
-
return self.
|
|
158
|
-
|
|
159
|
-
@
|
|
160
|
-
def
|
|
161
|
-
"""
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@
|
|
165
|
-
def
|
|
166
|
-
|
|
167
|
-
|
|
55
|
+
def dataframe(self) -> pd.DataFrame:
|
|
56
|
+
"""Return the table as a DataFrame."""
|
|
57
|
+
return self._dataframe
|
|
58
|
+
|
|
59
|
+
@dataframe.setter
|
|
60
|
+
def dataframe(self, dataframe: pd.DataFrame) -> None:
|
|
61
|
+
"""Set the table as a DataFrame."""
|
|
62
|
+
self._dataframe = dataframe
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _from_handler(
|
|
66
|
+
cls, handler: ZarrGroupHandler, backend_name: str | None = None
|
|
67
|
+
) -> "GenericTable":
|
|
68
|
+
"""Create a new ROI table from a Zarr group handler."""
|
|
69
|
+
meta = GenericTableMeta(**handler.load_attrs())
|
|
70
|
+
if backend_name is None:
|
|
71
|
+
backend = ImplementedTableBackends().get_backend(
|
|
72
|
+
backend_name=meta.backend,
|
|
73
|
+
group_handler=handler,
|
|
74
|
+
index_key=None,
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
backend = ImplementedTableBackends().get_backend(
|
|
78
|
+
backend_name=backend_name,
|
|
79
|
+
group_handler=handler,
|
|
80
|
+
index_key=None,
|
|
81
|
+
)
|
|
82
|
+
meta.backend = backend_name
|
|
83
|
+
|
|
84
|
+
if not backend.implements_dataframe:
|
|
85
|
+
raise ValueError("The backend does not implement the dataframe protocol.")
|
|
86
|
+
|
|
87
|
+
dataframe = backend.load_as_dataframe()
|
|
88
|
+
|
|
89
|
+
table = cls(dataframe)
|
|
90
|
+
table._meta = meta
|
|
91
|
+
table._table_backend = backend
|
|
92
|
+
return table
|
|
93
|
+
|
|
94
|
+
def _set_backend(
|
|
95
|
+
self,
|
|
96
|
+
handler: ZarrGroupHandler,
|
|
97
|
+
backend_name: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Set the backend of the table."""
|
|
100
|
+
backend = ImplementedTableBackends().get_backend(
|
|
101
|
+
backend_name=backend_name,
|
|
102
|
+
group_handler=handler,
|
|
103
|
+
index_key=None,
|
|
104
|
+
)
|
|
105
|
+
self._meta.backend = backend_name
|
|
106
|
+
self._table_backend = backend
|
|
168
107
|
|
|
169
|
-
|
|
170
|
-
def validators(self) -> list[Validator] | None:
|
|
171
|
-
"""Return the validators of the table."""
|
|
172
|
-
return self._validators
|
|
173
|
-
|
|
174
|
-
@validators.setter
|
|
175
|
-
def validators(self, validators: list[Validator] | None) -> None:
|
|
176
|
-
"""Set the validators of the table."""
|
|
177
|
-
self._validators = validators
|
|
178
|
-
|
|
179
|
-
def add_validator(self, validator: Validator) -> None:
|
|
180
|
-
"""Add a validator to the table."""
|
|
181
|
-
if self._validators is None:
|
|
182
|
-
self._validators = []
|
|
183
|
-
self._validators.append(validator)
|
|
184
|
-
|
|
185
|
-
def consolidate(self, meta: BaseModel) -> None:
|
|
108
|
+
def consolidate(self) -> None:
|
|
186
109
|
"""Write the current state of the table to the Zarr file."""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
group=self._table_group,
|
|
196
|
-
table=self.table,
|
|
197
|
-
index_key=self.index_key,
|
|
198
|
-
index_type=self.index_type,
|
|
199
|
-
meta=meta,
|
|
200
|
-
validators=self._validators,
|
|
110
|
+
if self._table_backend is None:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"No backend set for the table. "
|
|
113
|
+
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._table_backend.write_from_dataframe(
|
|
117
|
+
self._dataframe, metadata=self._meta.model_dump(exclude_none=True)
|
|
201
118
|
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Implementation of the Masking 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
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from ngio.common import WorldCooROI
|
|
13
|
+
from ngio.tables._validators import validate_columns
|
|
14
|
+
from ngio.tables.backends import ImplementedTableBackends
|
|
15
|
+
from ngio.tables.v1._roi_table import (
|
|
16
|
+
OPTIONAL_COLUMNS,
|
|
17
|
+
REQUIRED_COLUMNS,
|
|
18
|
+
_dataframe_to_rois,
|
|
19
|
+
_rois_to_dataframe,
|
|
20
|
+
)
|
|
21
|
+
from ngio.utils import NgioValueError, ZarrGroupHandler
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegionMeta(BaseModel):
|
|
25
|
+
"""Metadata for the region."""
|
|
26
|
+
|
|
27
|
+
path: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MaskingROITableV1Meta(BaseModel):
|
|
31
|
+
"""Metadata for the ROI table."""
|
|
32
|
+
|
|
33
|
+
fractal_table_version: Literal["1"] = "1"
|
|
34
|
+
type: Literal["masking_roi_table"] = "masking_roi_table"
|
|
35
|
+
backend: str | None = None
|
|
36
|
+
region: RegionMeta | None = None
|
|
37
|
+
instance_key: str = "label"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MaskingROITableV1:
|
|
41
|
+
"""Class to handle fractal ROI tables.
|
|
42
|
+
|
|
43
|
+
To know more about the ROI table format, please refer to the
|
|
44
|
+
specification at:
|
|
45
|
+
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
rois: Iterable[WorldCooROI] | None = None,
|
|
51
|
+
reference_label: str | None = None,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Create a new ROI table."""
|
|
54
|
+
if reference_label is None:
|
|
55
|
+
self._meta = MaskingROITableV1Meta()
|
|
56
|
+
else:
|
|
57
|
+
path = f"../labels/{reference_label}"
|
|
58
|
+
self._meta = MaskingROITableV1Meta(region=RegionMeta(path=path))
|
|
59
|
+
self._table_backend = None
|
|
60
|
+
|
|
61
|
+
self._rois = {}
|
|
62
|
+
if rois is not None:
|
|
63
|
+
self.add(rois)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def type() -> Literal["masking_roi_table"]:
|
|
67
|
+
"""Return the type of the table."""
|
|
68
|
+
return "masking_roi_table"
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def version() -> Literal["1"]:
|
|
72
|
+
"""Return the version of the fractal table."""
|
|
73
|
+
return "1"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def backend_name(self) -> str | None:
|
|
77
|
+
"""Return the name of the backend."""
|
|
78
|
+
if self._table_backend is None:
|
|
79
|
+
return None
|
|
80
|
+
return self._table_backend.backend_name()
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def _from_handler(
|
|
84
|
+
cls, handler: ZarrGroupHandler, backend_name: str | None = None
|
|
85
|
+
) -> "MaskingROITableV1":
|
|
86
|
+
"""Create a new ROI table from a Zarr store."""
|
|
87
|
+
meta = MaskingROITableV1Meta(**handler.load_attrs())
|
|
88
|
+
|
|
89
|
+
if backend_name is None:
|
|
90
|
+
backend = ImplementedTableBackends().get_backend(
|
|
91
|
+
backend_name=meta.backend,
|
|
92
|
+
group_handler=handler,
|
|
93
|
+
index_key="label",
|
|
94
|
+
index_type="int",
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
backend = ImplementedTableBackends().get_backend(
|
|
98
|
+
backend_name=backend_name,
|
|
99
|
+
group_handler=handler,
|
|
100
|
+
index_key="label",
|
|
101
|
+
index_type="int",
|
|
102
|
+
)
|
|
103
|
+
meta.backend = backend_name
|
|
104
|
+
|
|
105
|
+
if not backend.implements_dataframe:
|
|
106
|
+
raise NgioValueError(
|
|
107
|
+
"The backend does not implement the dataframe protocol."
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
table = cls()
|
|
111
|
+
table._meta = meta
|
|
112
|
+
table._table_backend = backend
|
|
113
|
+
|
|
114
|
+
dataframe = backend.load_as_dataframe()
|
|
115
|
+
dataframe = validate_columns(
|
|
116
|
+
dataframe,
|
|
117
|
+
required_columns=REQUIRED_COLUMNS,
|
|
118
|
+
optional_columns=OPTIONAL_COLUMNS,
|
|
119
|
+
)
|
|
120
|
+
table._rois = _dataframe_to_rois(dataframe)
|
|
121
|
+
return table
|
|
122
|
+
|
|
123
|
+
def _set_backend(
|
|
124
|
+
self,
|
|
125
|
+
handler: ZarrGroupHandler,
|
|
126
|
+
backend_name: str | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Set the backend of the table."""
|
|
129
|
+
backend = ImplementedTableBackends().get_backend(
|
|
130
|
+
backend_name=backend_name,
|
|
131
|
+
group_handler=handler,
|
|
132
|
+
index_key="label",
|
|
133
|
+
index_type="int",
|
|
134
|
+
)
|
|
135
|
+
self._meta.backend = backend_name
|
|
136
|
+
self._table_backend = backend
|
|
137
|
+
|
|
138
|
+
def rois(self) -> list[WorldCooROI]:
|
|
139
|
+
"""List all ROIs in the table."""
|
|
140
|
+
return list(self._rois.values())
|
|
141
|
+
|
|
142
|
+
def get(self, label: int) -> WorldCooROI:
|
|
143
|
+
"""Get an ROI from the table."""
|
|
144
|
+
_label = str(label)
|
|
145
|
+
if _label not in self._rois:
|
|
146
|
+
raise KeyError(f"ROI {_label} not found in the table.")
|
|
147
|
+
return self._rois[_label]
|
|
148
|
+
|
|
149
|
+
def add(self, roi: WorldCooROI | Iterable[WorldCooROI]) -> None:
|
|
150
|
+
"""Append ROIs to the current table."""
|
|
151
|
+
if isinstance(roi, WorldCooROI):
|
|
152
|
+
roi = [roi]
|
|
153
|
+
|
|
154
|
+
for _roi in roi:
|
|
155
|
+
if _roi.name in self._rois:
|
|
156
|
+
raise NgioValueError(f"ROI {_roi.name} already exists in the table.")
|
|
157
|
+
self._rois[_roi.name] = _roi
|
|
158
|
+
|
|
159
|
+
def consolidate(self) -> None:
|
|
160
|
+
"""Write the current state of the table to the Zarr file."""
|
|
161
|
+
if self._table_backend is None:
|
|
162
|
+
raise NgioValueError(
|
|
163
|
+
"No backend set for the table. "
|
|
164
|
+
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
dataframe = _rois_to_dataframe(self._rois, index_key="label")
|
|
168
|
+
dataframe = validate_columns(
|
|
169
|
+
dataframe,
|
|
170
|
+
required_columns=REQUIRED_COLUMNS,
|
|
171
|
+
optional_columns=OPTIONAL_COLUMNS,
|
|
172
|
+
)
|
|
173
|
+
self._table_backend.write_from_dataframe(
|
|
174
|
+
dataframe, metadata=self._meta.model_dump(exclude_none=True)
|
|
175
|
+
)
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
from collections.abc import Iterable
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ngio.common import WorldCooROI
|
|
14
|
+
from ngio.tables._validators import validate_columns
|
|
15
|
+
from ngio.tables.backends import ImplementedTableBackends
|
|
16
|
+
from ngio.utils import NgioValueError, ZarrGroupHandler
|
|
17
|
+
|
|
18
|
+
REQUIRED_COLUMNS = [
|
|
19
|
+
"x_micrometer",
|
|
20
|
+
"y_micrometer",
|
|
21
|
+
"z_micrometer",
|
|
22
|
+
"len_x_micrometer",
|
|
23
|
+
"len_y_micrometer",
|
|
24
|
+
"len_z_micrometer",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
ORIGIN_COLUMNS = [
|
|
29
|
+
"x_micrometer_original",
|
|
30
|
+
"y_micrometer_original",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
|
|
34
|
+
|
|
35
|
+
OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _dataframe_to_rois(dataframe: pd.DataFrame) -> dict[str, WorldCooROI]:
|
|
39
|
+
"""Convert a DataFrame to a WorldCooROI object."""
|
|
40
|
+
rois = {}
|
|
41
|
+
for key, row in dataframe.iterrows():
|
|
42
|
+
# check if optional columns are present
|
|
43
|
+
origin = {col: row.get(col, None) for col in ORIGIN_COLUMNS}
|
|
44
|
+
origin = dict(filter(lambda x: x[1] is not None, origin.items()))
|
|
45
|
+
translation = {col: row.get(col, None) for col in TRANSLATION_COLUMNS}
|
|
46
|
+
translation = dict(filter(lambda x: x[1] is not None, translation.items()))
|
|
47
|
+
|
|
48
|
+
roi = WorldCooROI(
|
|
49
|
+
name=str(key),
|
|
50
|
+
x=row["x_micrometer"],
|
|
51
|
+
y=row["y_micrometer"],
|
|
52
|
+
z=row["z_micrometer"],
|
|
53
|
+
x_length=row["len_x_micrometer"],
|
|
54
|
+
y_length=row["len_y_micrometer"],
|
|
55
|
+
z_length=row["len_z_micrometer"],
|
|
56
|
+
unit="micrometer", # type: ignore
|
|
57
|
+
**origin,
|
|
58
|
+
**translation,
|
|
59
|
+
)
|
|
60
|
+
rois[roi.name] = roi
|
|
61
|
+
return rois
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _rois_to_dataframe(rois: dict[str, WorldCooROI], index_key: str) -> pd.DataFrame:
|
|
65
|
+
"""Convert a list of WorldCooROI objects to a DataFrame."""
|
|
66
|
+
data = []
|
|
67
|
+
for roi in rois.values():
|
|
68
|
+
row = {
|
|
69
|
+
index_key: roi.name,
|
|
70
|
+
"x_micrometer": roi.x,
|
|
71
|
+
"y_micrometer": roi.y,
|
|
72
|
+
"z_micrometer": roi.z,
|
|
73
|
+
"len_x_micrometer": roi.x_length,
|
|
74
|
+
"len_y_micrometer": roi.y_length,
|
|
75
|
+
"len_z_micrometer": roi.z_length,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
extra = roi.model_extra or {}
|
|
79
|
+
for col in ORIGIN_COLUMNS:
|
|
80
|
+
if col in extra:
|
|
81
|
+
row[col] = extra[col]
|
|
82
|
+
|
|
83
|
+
for col in TRANSLATION_COLUMNS:
|
|
84
|
+
if col in extra:
|
|
85
|
+
row[col] = extra[col]
|
|
86
|
+
data.append(row)
|
|
87
|
+
dataframe = pd.DataFrame(data)
|
|
88
|
+
dataframe = dataframe.set_index(index_key)
|
|
89
|
+
return dataframe
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ROITableV1Meta(BaseModel):
|
|
93
|
+
"""Metadata for the ROI table."""
|
|
94
|
+
|
|
95
|
+
fractal_table_version: Literal["1"] = "1"
|
|
96
|
+
type: Literal["roi_table"] = "roi_table"
|
|
97
|
+
backend: str | None = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class RoiTableV1:
|
|
101
|
+
"""Class to handle fractal ROI tables.
|
|
102
|
+
|
|
103
|
+
To know more about the ROI table format, please refer to the
|
|
104
|
+
specification at:
|
|
105
|
+
https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, rois: Iterable[WorldCooROI] | None = None) -> None:
|
|
109
|
+
"""Create a new ROI table."""
|
|
110
|
+
self._meta = ROITableV1Meta()
|
|
111
|
+
self._table_backend = None
|
|
112
|
+
|
|
113
|
+
self._rois = {}
|
|
114
|
+
if rois is not None:
|
|
115
|
+
self.add(rois)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def type() -> Literal["roi_table"]:
|
|
119
|
+
"""Return the type of the table."""
|
|
120
|
+
return "roi_table"
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def version() -> Literal["1"]:
|
|
124
|
+
"""Return the version of the fractal table."""
|
|
125
|
+
return "1"
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def backend_name(self) -> str | None:
|
|
129
|
+
"""Return the name of the backend."""
|
|
130
|
+
if self._table_backend is None:
|
|
131
|
+
return None
|
|
132
|
+
return self._table_backend.backend_name()
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _from_handler(
|
|
136
|
+
cls, handler: ZarrGroupHandler, backend_name: str | None = None
|
|
137
|
+
) -> "RoiTableV1":
|
|
138
|
+
"""Create a new ROI table from a Zarr store."""
|
|
139
|
+
meta = ROITableV1Meta(**handler.load_attrs())
|
|
140
|
+
|
|
141
|
+
if backend_name is None:
|
|
142
|
+
backend = ImplementedTableBackends().get_backend(
|
|
143
|
+
backend_name=meta.backend,
|
|
144
|
+
group_handler=handler,
|
|
145
|
+
index_key="FieldIndex",
|
|
146
|
+
index_type="str",
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
backend = ImplementedTableBackends().get_backend(
|
|
150
|
+
backend_name=backend_name,
|
|
151
|
+
group_handler=handler,
|
|
152
|
+
index_key="FieldIndex",
|
|
153
|
+
index_type="str",
|
|
154
|
+
)
|
|
155
|
+
meta.backend = backend_name
|
|
156
|
+
|
|
157
|
+
if not backend.implements_dataframe:
|
|
158
|
+
raise NgioValueError(
|
|
159
|
+
"The backend does not implement the dataframe protocol."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
table = cls()
|
|
163
|
+
table._meta = meta
|
|
164
|
+
table._table_backend = backend
|
|
165
|
+
|
|
166
|
+
dataframe = backend.load_as_dataframe()
|
|
167
|
+
dataframe = validate_columns(
|
|
168
|
+
dataframe,
|
|
169
|
+
required_columns=REQUIRED_COLUMNS,
|
|
170
|
+
optional_columns=OPTIONAL_COLUMNS,
|
|
171
|
+
)
|
|
172
|
+
table._rois = _dataframe_to_rois(dataframe)
|
|
173
|
+
return table
|
|
174
|
+
|
|
175
|
+
def _set_backend(
|
|
176
|
+
self,
|
|
177
|
+
handler: ZarrGroupHandler,
|
|
178
|
+
backend_name: str | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Set the backend of the table."""
|
|
181
|
+
backend = ImplementedTableBackends().get_backend(
|
|
182
|
+
backend_name=backend_name,
|
|
183
|
+
group_handler=handler,
|
|
184
|
+
index_key="FieldIndex",
|
|
185
|
+
index_type="str",
|
|
186
|
+
)
|
|
187
|
+
self._meta.backend = backend_name
|
|
188
|
+
self._table_backend = backend
|
|
189
|
+
|
|
190
|
+
def rois(self) -> list[WorldCooROI]:
|
|
191
|
+
"""List all ROIs in the table."""
|
|
192
|
+
return list(self._rois.values())
|
|
193
|
+
|
|
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
|
+
|
|
200
|
+
def add(self, roi: WorldCooROI | Iterable[WorldCooROI]) -> None:
|
|
201
|
+
"""Append ROIs to the current table."""
|
|
202
|
+
if isinstance(roi, WorldCooROI):
|
|
203
|
+
roi = [roi]
|
|
204
|
+
|
|
205
|
+
for _roi in roi:
|
|
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
|
|
209
|
+
|
|
210
|
+
def consolidate(self) -> None:
|
|
211
|
+
"""Write the current state of the table to the Zarr file."""
|
|
212
|
+
if self._table_backend is None:
|
|
213
|
+
raise NgioValueError(
|
|
214
|
+
"No backend set for the table. "
|
|
215
|
+
"Please add the table to a OME-Zarr Image before calling consolidate."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
dataframe = _rois_to_dataframe(self._rois, index_key="FieldIndex")
|
|
219
|
+
dataframe = validate_columns(
|
|
220
|
+
dataframe,
|
|
221
|
+
required_columns=REQUIRED_COLUMNS,
|
|
222
|
+
optional_columns=OPTIONAL_COLUMNS,
|
|
223
|
+
)
|
|
224
|
+
self._table_backend.write_from_dataframe(
|
|
225
|
+
dataframe, metadata=self._meta.model_dump(exclude_none=True)
|
|
226
|
+
)
|