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.
Files changed (84) hide show
  1. ngio/__init__.py +31 -5
  2. ngio/common/__init__.py +44 -0
  3. ngio/common/_array_pipe.py +160 -0
  4. ngio/common/_axes_transforms.py +63 -0
  5. ngio/common/_common_types.py +5 -0
  6. ngio/common/_dimensions.py +113 -0
  7. ngio/common/_pyramid.py +222 -0
  8. ngio/{core/roi.py → common/_roi.py} +22 -23
  9. ngio/common/_slicer.py +97 -0
  10. ngio/{pipes/_zoom_utils.py → common/_zoom.py} +2 -78
  11. ngio/hcs/__init__.py +60 -0
  12. ngio/images/__init__.py +23 -0
  13. ngio/images/abstract_image.py +240 -0
  14. ngio/images/create.py +251 -0
  15. ngio/images/image.py +383 -0
  16. ngio/images/label.py +96 -0
  17. ngio/images/omezarr_container.py +512 -0
  18. ngio/ome_zarr_meta/__init__.py +35 -0
  19. ngio/ome_zarr_meta/_generic_handlers.py +320 -0
  20. ngio/ome_zarr_meta/_meta_handlers.py +142 -0
  21. ngio/ome_zarr_meta/ngio_specs/__init__.py +63 -0
  22. ngio/ome_zarr_meta/ngio_specs/_axes.py +481 -0
  23. ngio/ome_zarr_meta/ngio_specs/_channels.py +378 -0
  24. ngio/ome_zarr_meta/ngio_specs/_dataset.py +134 -0
  25. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +5 -0
  26. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +434 -0
  27. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +84 -0
  28. ngio/ome_zarr_meta/v04/__init__.py +11 -0
  29. ngio/ome_zarr_meta/v04/_meta_handlers.py +54 -0
  30. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +412 -0
  31. ngio/tables/__init__.py +21 -5
  32. ngio/tables/_validators.py +192 -0
  33. ngio/tables/backends/__init__.py +8 -0
  34. ngio/tables/backends/_abstract_backend.py +71 -0
  35. ngio/tables/backends/_anndata_utils.py +194 -0
  36. ngio/tables/backends/_anndata_v1.py +75 -0
  37. ngio/tables/backends/_json_v1.py +56 -0
  38. ngio/tables/backends/_table_backends.py +102 -0
  39. ngio/tables/tables_container.py +300 -0
  40. ngio/tables/v1/__init__.py +6 -5
  41. ngio/tables/v1/_feature_table.py +161 -0
  42. ngio/tables/v1/_generic_table.py +99 -182
  43. ngio/tables/v1/_masking_roi_table.py +175 -0
  44. ngio/tables/v1/_roi_table.py +226 -0
  45. ngio/utils/__init__.py +23 -10
  46. ngio/utils/_datasets.py +51 -0
  47. ngio/utils/_errors.py +10 -4
  48. ngio/utils/_zarr_utils.py +378 -0
  49. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/METADATA +18 -39
  50. ngio-0.2.0a1.dist-info/RECORD +53 -0
  51. ngio/core/__init__.py +0 -7
  52. ngio/core/dimensions.py +0 -122
  53. ngio/core/image_handler.py +0 -228
  54. ngio/core/image_like_handler.py +0 -549
  55. ngio/core/label_handler.py +0 -410
  56. ngio/core/ngff_image.py +0 -387
  57. ngio/core/utils.py +0 -287
  58. ngio/io/__init__.py +0 -19
  59. ngio/io/_zarr.py +0 -88
  60. ngio/io/_zarr_array_utils.py +0 -0
  61. ngio/io/_zarr_group_utils.py +0 -60
  62. ngio/iterators/__init__.py +0 -1
  63. ngio/ngff_meta/__init__.py +0 -27
  64. ngio/ngff_meta/fractal_image_meta.py +0 -1267
  65. ngio/ngff_meta/meta_handler.py +0 -92
  66. ngio/ngff_meta/utils.py +0 -235
  67. ngio/ngff_meta/v04/__init__.py +0 -6
  68. ngio/ngff_meta/v04/specs.py +0 -158
  69. ngio/ngff_meta/v04/zarr_utils.py +0 -376
  70. ngio/pipes/__init__.py +0 -7
  71. ngio/pipes/_slicer_transforms.py +0 -176
  72. ngio/pipes/_transforms.py +0 -33
  73. ngio/pipes/data_pipe.py +0 -52
  74. ngio/tables/_ad_reader.py +0 -80
  75. ngio/tables/_utils.py +0 -301
  76. ngio/tables/tables_group.py +0 -252
  77. ngio/tables/v1/feature_tables.py +0 -182
  78. ngio/tables/v1/masking_roi_tables.py +0 -243
  79. ngio/tables/v1/roi_tables.py +0 -285
  80. ngio/utils/_common_types.py +0 -5
  81. ngio/utils/_pydantic_utils.py +0 -52
  82. ngio-0.1.6.dist-info/RECORD +0 -44
  83. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/WHEEL +0 -0
  84. {ngio-0.1.6.dist-info → ngio-0.2.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,201 +1,118 @@
1
- """Implementation of a Generic Table class."""
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.core.utils import State
12
- from ngio.tables._ad_reader import custom_read_zarr
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 BaseTable:
57
- """A base table class to be used for table operations in all other tables."""
10
+ class GenericTableMeta(BaseModel):
11
+ """Metadata for the ROI table."""
58
12
 
59
- def __init__(
60
- self,
61
- group: zarr.Group,
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
- This is in general the path of the NGFFImage.
96
- """
97
- return str(self._table_group.store.path)
18
+ class GenericTable:
19
+ """Class to a non-specific table.
98
20
 
99
- @property
100
- def group_path(self) -> str:
101
- """Return the path of the group.
21
+ This can be used to load any table that does not have
22
+ a specific definition.
23
+ """
102
24
 
103
- This is the path of the group containing the table.
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
- root = self.root_path
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 table(self) -> pd.DataFrame:
113
- """Return the ROI table as a DataFrame."""
114
- return self._table
115
-
116
- @table.setter
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 group(self) -> zarr.Group:
156
- """Return the group of the table."""
157
- return self._table_group
158
-
159
- @property
160
- def index_key(self) -> str:
161
- """Return the index key of the table."""
162
- return self._index_key
163
-
164
- @property
165
- def index_type(self) -> Literal["int", "str"]:
166
- """Return the index type of the table."""
167
- return self._index_type
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
- @property
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
- table = self.table
188
- table = validate_table(
189
- table_df=table,
190
- index_key=self.index_key,
191
- index_type=self.index_type,
192
- validators=self._validators,
193
- )
194
- write_table_ad(
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
+ )