ngio 0.3.4__py3-none-any.whl → 0.4.0__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 (73) hide show
  1. ngio/__init__.py +7 -2
  2. ngio/common/__init__.py +5 -52
  3. ngio/common/_dimensions.py +270 -55
  4. ngio/common/_masking_roi.py +38 -10
  5. ngio/common/_pyramid.py +51 -30
  6. ngio/common/_roi.py +269 -82
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +49 -19
  9. ngio/experimental/__init__.py +5 -0
  10. ngio/experimental/iterators/__init__.py +15 -0
  11. ngio/experimental/iterators/_abstract_iterator.py +390 -0
  12. ngio/experimental/iterators/_feature.py +189 -0
  13. ngio/experimental/iterators/_image_processing.py +130 -0
  14. ngio/experimental/iterators/_mappers.py +48 -0
  15. ngio/experimental/iterators/_rois_utils.py +127 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/_plate.py +41 -36
  18. ngio/images/__init__.py +22 -1
  19. ngio/images/_abstract_image.py +403 -176
  20. ngio/images/_create.py +31 -15
  21. ngio/images/_create_synt_container.py +138 -0
  22. ngio/images/_image.py +452 -63
  23. ngio/images/_label.py +56 -30
  24. ngio/images/_masked_image.py +387 -129
  25. ngio/images/_ome_zarr_container.py +237 -67
  26. ngio/{common → images}/_table_ops.py +41 -41
  27. ngio/io_pipes/__init__.py +75 -0
  28. ngio/io_pipes/_io_pipes.py +361 -0
  29. ngio/io_pipes/_io_pipes_masked.py +488 -0
  30. ngio/io_pipes/_io_pipes_roi.py +152 -0
  31. ngio/io_pipes/_io_pipes_types.py +56 -0
  32. ngio/io_pipes/_match_shape.py +376 -0
  33. ngio/io_pipes/_ops_axes.py +344 -0
  34. ngio/io_pipes/_ops_slices.py +446 -0
  35. ngio/io_pipes/_ops_slices_utils.py +196 -0
  36. ngio/io_pipes/_ops_transforms.py +104 -0
  37. ngio/io_pipes/_zoom_transform.py +175 -0
  38. ngio/ome_zarr_meta/__init__.py +4 -2
  39. ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -10
  40. ngio/ome_zarr_meta/ngio_specs/_axes.py +186 -175
  41. ngio/ome_zarr_meta/ngio_specs/_channels.py +55 -18
  42. ngio/ome_zarr_meta/ngio_specs/_dataset.py +48 -122
  43. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +6 -15
  44. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +38 -87
  45. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +17 -1
  46. ngio/ome_zarr_meta/v04/_v04_spec_utils.py +34 -31
  47. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  48. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  49. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  50. ngio/resources/__init__.py +55 -0
  51. ngio/resources/resource_model.py +36 -0
  52. ngio/tables/backends/_abstract_backend.py +5 -6
  53. ngio/tables/backends/_anndata.py +1 -2
  54. ngio/tables/backends/_anndata_utils.py +3 -3
  55. ngio/tables/backends/_non_zarr_backends.py +1 -1
  56. ngio/tables/backends/_table_backends.py +0 -1
  57. ngio/tables/backends/_utils.py +3 -3
  58. ngio/tables/v1/_roi_table.py +165 -70
  59. ngio/transforms/__init__.py +5 -0
  60. ngio/transforms/_zoom.py +19 -0
  61. ngio/utils/__init__.py +2 -3
  62. ngio/utils/_datasets.py +5 -0
  63. ngio/utils/_logger.py +19 -0
  64. ngio/utils/_zarr_utils.py +6 -6
  65. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/METADATA +24 -22
  66. ngio-0.4.0.dist-info/RECORD +85 -0
  67. ngio/common/_array_pipe.py +0 -288
  68. ngio/common/_axes_transforms.py +0 -64
  69. ngio/common/_common_types.py +0 -5
  70. ngio/common/_slicer.py +0 -96
  71. ngio-0.3.4.dist-info/RECORD +0 -61
  72. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/WHEEL +0 -0
  73. {ngio-0.3.4.dist-info → ngio-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,55 @@
1
+ """Predefined resources for testing and demonstration purposes."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from ngio.resources.resource_model import LabelsInfo, SampleInfo
7
+
8
+ resources = Path(__file__).parent.resolve()
9
+
10
+ _resources = {
11
+ "Cardiomyocyte": SampleInfo(
12
+ img_path=resources
13
+ / "20200812-CardiomyocyteDifferentiation14-Cycle1_B03"
14
+ / "raw.jpg",
15
+ labels=[
16
+ LabelsInfo(
17
+ name="nuclei",
18
+ label_path=resources
19
+ / "20200812-CardiomyocyteDifferentiation14-Cycle1_B03"
20
+ / "nuclei.png",
21
+ create_masking_table=False,
22
+ ensure_unique_labels=True,
23
+ ),
24
+ LabelsInfo(
25
+ name="nuclei_mask",
26
+ label_path=resources
27
+ / "20200812-CardiomyocyteDifferentiation14-Cycle1_B03"
28
+ / "mask.png",
29
+ create_masking_table=True,
30
+ ensure_unique_labels=False,
31
+ dtype="uint8",
32
+ ),
33
+ ],
34
+ xy_pixelsize=0.325,
35
+ z_spacing=1.0,
36
+ time_spacing=1.0,
37
+ name="Cardiomyocyte Differentiation",
38
+ info="20200812-CardiomyocyteDifferentiation14-Cycle1_B03",
39
+ )
40
+ }
41
+
42
+ AVAILABLE_SAMPLES = Literal["Cardiomyocyte"]
43
+
44
+
45
+ def get_sample_info(name: AVAILABLE_SAMPLES) -> SampleInfo:
46
+ """Get a predefined resource by name."""
47
+ image_info = _resources.get(name)
48
+ if image_info is None:
49
+ raise ValueError(
50
+ f"Sample '{name}' not found. Available samples: {_resources.keys()}"
51
+ )
52
+ return image_info
53
+
54
+
55
+ __all__ = ["AVAILABLE_SAMPLES", "LabelsInfo", "SampleInfo"]
@@ -0,0 +1,36 @@
1
+ """Models for programmatic description of image resources."""
2
+
3
+ from pathlib import Path
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from ngio.ome_zarr_meta.ngio_specs import (
8
+ DefaultSpaceUnit,
9
+ DefaultTimeUnit,
10
+ SpaceUnits,
11
+ TimeUnits,
12
+ )
13
+
14
+
15
+ class LabelsInfo(BaseModel):
16
+ """Metadata for a label image."""
17
+
18
+ name: str
19
+ label_path: Path
20
+ ensure_unique_labels: bool = True
21
+ create_masking_table: bool = False
22
+ dtype: str = "uint32"
23
+
24
+
25
+ class SampleInfo(BaseModel):
26
+ """Metadata necessary to create an OME-Ngff from image files."""
27
+
28
+ img_path: Path
29
+ labels: list[LabelsInfo] = Field(default_factory=list)
30
+ xy_pixelsize: float
31
+ z_spacing: float = 1.0
32
+ time_spacing: float = 1.0
33
+ space_unit: SpaceUnits = DefaultSpaceUnit
34
+ time_unit: TimeUnits = DefaultTimeUnit
35
+ name: str | None = None
36
+ info: str | None = None
@@ -210,7 +210,6 @@ class AbstractTableBackend(ABC):
210
210
  self,
211
211
  table_data: TabularData,
212
212
  metadata: dict | None = None,
213
- mode: Literal["pandas", "anndata", "polars"] | None = None,
214
213
  ) -> None:
215
214
  """Serialize the table to the store, and write the metadata.
216
215
 
@@ -218,11 +217,11 @@ class AbstractTableBackend(ABC):
218
217
  Based on the explicit mode or the type of the table,
219
218
  it will call the appropriate write method.
220
219
  """
221
- if mode == "pandas" or isinstance(table_data, DataFrame):
222
- self.write_from_pandas(table_data) # type: ignore[arg-type]
223
- elif mode == "anndata" or isinstance(table_data, AnnData):
224
- self.write_from_anndata(table_data) # type: ignore[arg-type]
225
- elif mode == "polars" or isinstance(table_data, PolarsDataFrame | LazyFrame):
220
+ if isinstance(table_data, DataFrame):
221
+ self.write_from_pandas(table_data)
222
+ elif isinstance(table_data, AnnData):
223
+ self.write_from_anndata(table_data)
224
+ elif isinstance(table_data, PolarsDataFrame | LazyFrame):
226
225
  self.write_from_polars(table_data)
227
226
  else:
228
227
  raise NgioValueError(
@@ -58,7 +58,7 @@ class AnnDataBackend(AbstractTableBackend):
58
58
  "Please make sure to use a compatible "
59
59
  "store like a zarr.DirectoryStore."
60
60
  )
61
- table.write_zarr(full_url) # type: ignore
61
+ table.write_zarr(full_url) # type: ignore (AnnData writer requires a str path)
62
62
 
63
63
  def write_from_pandas(self, table: DataFrame) -> None:
64
64
  """Serialize the table from a pandas DataFrame."""
@@ -77,7 +77,6 @@ class AnnDataBackend(AbstractTableBackend):
77
77
  self.write_from_anndata(anndata)
78
78
 
79
79
 
80
-
81
80
  class AnnDataBackendV1(AnnDataBackend):
82
81
  """A wrapper for the AnnData backend that for backwards compatibility."""
83
82
 
@@ -17,11 +17,11 @@ from ngio.utils import (
17
17
  )
18
18
 
19
19
  if TYPE_CHECKING:
20
- from collections.abc import Callable, Collection
20
+ from collections.abc import Callable, Sequence
21
21
 
22
22
 
23
23
  def custom_anndata_read_zarr(
24
- store: StoreOrGroup, elem_to_read: Collection[str] | None = None
24
+ store: StoreOrGroup, elem_to_read: Sequence[str] | None = None
25
25
  ) -> AnnData:
26
26
  """Read from a hierarchical Zarr array store.
27
27
 
@@ -31,7 +31,7 @@ def custom_anndata_read_zarr(
31
31
 
32
32
  Args:
33
33
  store (StoreOrGroup): A store or group to read the AnnData from.
34
- elem_to_read (Collection[str] | None): The elements to read from the store.
34
+ elem_to_read (Sequence[str] | None): The elements to read from the store.
35
35
  """
36
36
  group = open_group_wrapper(store=store, mode="r")
37
37
 
@@ -80,7 +80,7 @@ class NonZarrBaseBackend(AbstractTableBackend):
80
80
  """Load the table from an FS store."""
81
81
  full_url = self._group_handler.full_url
82
82
  parquet_path = f"{full_url}/{self.table_name}"
83
- store_fs = self._group_handler.store.fs # type: ignore
83
+ store_fs = self._group_handler.store.fs # type: ignore (in this context, store_fs is a fs.FSStore)
84
84
  with store_fs.open(parquet_path, "rb") as f:
85
85
  dataframe = reader(f)
86
86
  return dataframe
@@ -127,7 +127,6 @@ class TableBackendProtocol(Protocol):
127
127
  self,
128
128
  table_data: DataFrame | AnnData | PolarsDataFrame | LazyFrame,
129
129
  metadata: dict[str, str] | None = None,
130
- mode: Literal["pandas", "anndata", "polars"] | None = None,
131
130
  ) -> None:
132
131
  """This is a generic write method.
133
132
 
@@ -126,10 +126,10 @@ def _check_for_mixed_types(series: pd.Series) -> None:
126
126
  Raises:
127
127
  NgioTableValidationError: If the column has mixed types.
128
128
  """
129
- if series.apply(type).nunique() > 1: # type: ignore
129
+ if series.apply(type).nunique() > 1: # type: ignore (type lint fails here)
130
130
  raise NgioTableValidationError(
131
131
  f"Column {series.name} has mixed types: "
132
- f"{series.apply(type).unique()}. " # type: ignore
132
+ f"{series.apply(type).unique()}. " # type: ignore (type lint fails here)
133
133
  "Type of all elements must be the same."
134
134
  )
135
135
 
@@ -186,7 +186,7 @@ def normalize_pandas_df(
186
186
  pandas_df = _validate_index_key_df(pandas_df, index_key)
187
187
  pandas_df = _validate_cast_index_dtype_df(pandas_df, index_type)
188
188
  if pandas_df.index.name is not None:
189
- index_key = pandas_df.index.name
189
+ index_key = str(pandas_df.index.name)
190
190
 
191
191
  if reset_index and pandas_df.index.name is not None:
192
192
  pandas_df = pandas_df.reset_index()
@@ -4,10 +4,9 @@ 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 _type to avoid name conflict with table.type
8
7
  from collections.abc import Iterable
9
- from functools import cache
10
8
  from typing import Literal
9
+ from uuid import uuid4
11
10
 
12
11
  import pandas as pd
13
12
  from pydantic import BaseModel
@@ -27,7 +26,7 @@ from ngio.utils import (
27
26
  NgioTableValidationError,
28
27
  NgioValueError,
29
28
  ZarrGroupHandler,
30
- ngio_logger,
29
+ ngio_warn,
31
30
  )
32
31
 
33
32
  REQUIRED_COLUMNS = [
@@ -44,6 +43,11 @@ REQUIRED_COLUMNS = [
44
43
  # only a warning is raised if non optional columns are present
45
44
  #####################
46
45
 
46
+ TIME_COLUMNS = [
47
+ "t_second",
48
+ "len_t_second",
49
+ ]
50
+
47
51
  ORIGIN_COLUMNS = [
48
52
  "x_micrometer_original",
49
53
  "y_micrometer_original",
@@ -70,24 +74,17 @@ INDEX_COLUMNS = [
70
74
  OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS + PLATE_COLUMNS + INDEX_COLUMNS
71
75
 
72
76
 
73
- @cache
74
77
  def _check_optional_columns(col_name: str) -> None:
75
78
  """Check if the column name is in the optional columns."""
76
- if col_name not in OPTIONAL_COLUMNS:
77
- ngio_logger.warning(
78
- f"Column {col_name} is not in the optional columns. "
79
- f"Standard optional columns are: {OPTIONAL_COLUMNS}."
80
- )
79
+ if col_name not in OPTIONAL_COLUMNS + TIME_COLUMNS:
80
+ ngio_warn(f"Column {col_name} is not in the optional columns.")
81
81
 
82
82
 
83
83
  def _dataframe_to_rois(
84
84
  dataframe: pd.DataFrame,
85
- required_columns: list[str] | None = None,
85
+ required_columns: list[str] = REQUIRED_COLUMNS,
86
86
  ) -> dict[str, Roi]:
87
87
  """Convert a DataFrame to a WorldCooROI object."""
88
- if required_columns is None:
89
- required_columns = REQUIRED_COLUMNS
90
-
91
88
  # Validate the columns of the DataFrame
92
89
  _required_columns = set(dataframe.columns).intersection(set(required_columns))
93
90
  if len(_required_columns) != len(required_columns):
@@ -95,11 +92,15 @@ def _dataframe_to_rois(
95
92
  f"Could not find required columns: {_required_columns} in the table."
96
93
  )
97
94
 
98
- extra_columns = set(dataframe.columns).difference(set(required_columns))
95
+ extra_columns = set(dataframe.columns).difference(
96
+ set(required_columns + TIME_COLUMNS)
97
+ )
99
98
 
100
99
  for col in extra_columns:
101
100
  _check_optional_columns(col)
102
101
 
102
+ label_is_index = True if dataframe.index.name == "label" else False
103
+
103
104
  extras = {}
104
105
 
105
106
  rois = {}
@@ -108,85 +109,173 @@ def _dataframe_to_rois(
108
109
  if len(extra_columns) > 0:
109
110
  extras = {col: getattr(row, col, None) for col in extra_columns}
110
111
 
112
+ z_micrometer = getattr(row, "z_micrometer", None)
113
+ z_length_micrometer = getattr(row, "len_z_micrometer", None)
114
+
115
+ t_second = getattr(row, "t_second", None)
116
+ t_length_second = getattr(row, "len_t_second", None)
117
+
118
+ if label_is_index:
119
+ label = int(row.Index) # type: ignore (type can not be known here, but should be castable to int)
120
+ else:
121
+ label = getattr(row, "label", None)
122
+
111
123
  roi = Roi(
112
124
  name=str(row.Index),
113
- x=row.x_micrometer, # type: ignore
114
- y=row.y_micrometer, # type: ignore
115
- z=row.z_micrometer, # type: ignore
116
- x_length=row.len_x_micrometer, # type: ignore
117
- y_length=row.len_y_micrometer, # type: ignore
118
- z_length=row.len_z_micrometer, # type: ignore
119
- unit="micrometer", # type: ignore
125
+ x=row.x_micrometer, # type: ignore (type can not be known here)
126
+ y=row.y_micrometer, # type: ignore (type can not be known here)
127
+ z=z_micrometer,
128
+ t=t_second,
129
+ x_length=row.len_x_micrometer, # type: ignore (type can not be known here)
130
+ y_length=row.len_y_micrometer, # type: ignore (type can not be known here)
131
+ z_length=z_length_micrometer,
132
+ t_length=t_length_second,
133
+ unit="micrometer",
134
+ label=label,
120
135
  **extras,
121
136
  )
122
137
  rois[roi.name] = roi
123
138
  return rois
124
139
 
125
140
 
126
- def _table_to_rois(
127
- table: TabularData,
128
- index_key: str | None = None,
129
- index_type: Literal["int", "str"] | None = None,
130
- required_columns: list[str] | None = None,
131
- ) -> tuple[pd.DataFrame, dict[str, Roi]]:
132
- """Convert a table to a dictionary of ROIs.
133
-
134
- Args:
135
- table: The table to convert.
136
- index_key: The column name to use as the index of the DataFrame.
137
- index_type: The type of the index column in the DataFrame.
138
- required_columns: The required columns in the DataFrame.
139
-
140
- Returns:
141
- A dictionary of ROIs.
142
- """
143
- dataframe = convert_to_pandas(
144
- table,
145
- index_key=index_key,
146
- index_type=index_type,
147
- )
148
- return dataframe, _dataframe_to_rois(dataframe, required_columns=required_columns)
149
-
150
-
151
141
  def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
152
142
  """Convert a list of WorldCooROI objects to a DataFrame."""
153
143
  data = []
154
144
  for roi in rois.values():
145
+ # This normalization is necessary for backward compatibility
146
+ z_micrometer = roi.z if roi.z is not None else 0.0
147
+ len_z_micrometer = roi.z_length if roi.z_length is not None else 1.0
148
+
155
149
  row = {
156
- index_key: roi.name,
150
+ index_key: roi.get_name(),
157
151
  "x_micrometer": roi.x,
158
152
  "y_micrometer": roi.y,
159
- "z_micrometer": roi.z,
153
+ "z_micrometer": z_micrometer,
160
154
  "len_x_micrometer": roi.x_length,
161
155
  "len_y_micrometer": roi.y_length,
162
- "len_z_micrometer": roi.z_length,
156
+ "len_z_micrometer": len_z_micrometer,
163
157
  }
164
158
 
159
+ if roi.t is not None:
160
+ row["t_second"] = roi.t
161
+
162
+ if roi.t_length is not None:
163
+ row["len_t_second"] = roi.t_length
164
+
165
+ if roi.label is not None and index_key != "label":
166
+ row["label"] = roi.label
167
+
165
168
  extra = roi.model_extra or {}
166
169
  for col in extra:
167
170
  _check_optional_columns(col)
168
171
  row[col] = extra[col]
169
172
  data.append(row)
173
+
170
174
  dataframe = pd.DataFrame(data)
171
175
  dataframe = normalize_pandas_df(dataframe, index_key=index_key)
172
176
  return dataframe
173
177
 
174
178
 
179
+ class RoiDictWrapper:
180
+ """A wrapper for a dictionary of ROIs to provide a consistent interface."""
181
+
182
+ def __init__(self, rois: Iterable[Roi]) -> None:
183
+ self._rois_by_name = {}
184
+ self._rois_by_label = {}
185
+ for roi in rois:
186
+ name = roi.get_name()
187
+ if name in self._rois_by_name:
188
+ name = f"{name}_{uuid4().hex[:8]}"
189
+ self._rois_by_name[name] = roi
190
+ if roi.label is not None:
191
+ self._rois_by_label[roi.label] = roi
192
+
193
+ def get_by_name(self, name: str, default: Roi | None = None) -> Roi | None:
194
+ """Get an ROI by its name."""
195
+ return self._rois_by_name.get(name, default)
196
+
197
+ def get_by_label(self, label: int, default: Roi | None = None) -> Roi | None:
198
+ """Get an ROI by its label."""
199
+ return self._rois_by_label.get(label, default)
200
+
201
+ def _add_roi(self, roi: Roi, overwrite: bool = False) -> None:
202
+ """Add an ROI to the wrapper."""
203
+ if roi.name in self._rois_by_name and not overwrite:
204
+ raise NgioValueError(f"ROI with name {roi.name} already exists.")
205
+
206
+ self._rois_by_name[roi.name] = roi
207
+ if roi.label is not None:
208
+ self._rois_by_label[roi.label] = roi
209
+
210
+ def add_rois(self, rois: Roi | Iterable[Roi], overwrite: bool = False) -> None:
211
+ """Add ROIs to the wrapper."""
212
+ if isinstance(rois, Roi):
213
+ rois = [rois]
214
+
215
+ for roi in rois:
216
+ self._add_roi(roi, overwrite=overwrite)
217
+
218
+ def to_list(self) -> list[Roi]:
219
+ """Return the list of ROIs."""
220
+ return list(self._rois_by_name.values())
221
+
222
+ def to_dataframe(self, index_key: str | None = None) -> pd.DataFrame:
223
+ """Convert the ROIs to a DataFrame."""
224
+ return _rois_to_dataframe(self._rois_by_name, index_key=index_key)
225
+
226
+ @classmethod
227
+ def from_dataframe(
228
+ cls, dataframe: pd.DataFrame, required_columns: list[str] = REQUIRED_COLUMNS
229
+ ) -> "RoiDictWrapper":
230
+ """Create a RoiDictWrapper from a DataFrame."""
231
+ rois = _dataframe_to_rois(dataframe, required_columns=required_columns)
232
+ return cls(rois.values())
233
+
234
+
235
+ def _table_to_rois(
236
+ table: TabularData,
237
+ index_key: str | None = None,
238
+ index_type: Literal["int", "str"] | None = None,
239
+ required_columns: list[str] = REQUIRED_COLUMNS,
240
+ ) -> tuple[pd.DataFrame, RoiDictWrapper]:
241
+ """Convert a table to a dictionary of ROIs.
242
+
243
+ Args:
244
+ table: The table to convert.
245
+ index_key: The column name to use as the index of the DataFrame.
246
+ index_type: The type of the index column in the DataFrame.
247
+ required_columns: The required columns in the DataFrame.
248
+
249
+ Returns:
250
+ A tuple containing the DataFrame and a RoiDictWrapper with the ROIs.
251
+ """
252
+ dataframe = convert_to_pandas(
253
+ table,
254
+ index_key=index_key,
255
+ index_type=index_type,
256
+ )
257
+ roi_dict_wrapper = RoiDictWrapper.from_dataframe(
258
+ dataframe, required_columns=required_columns
259
+ )
260
+ return dataframe, roi_dict_wrapper
261
+
262
+
175
263
  class GenericRoiTableV1(AbstractBaseTable):
176
264
  def __init__(
177
265
  self,
178
266
  *,
179
267
  rois: Iterable[Roi] | None = None,
180
268
  meta: BackendMeta,
269
+ required_columns: list[str] = REQUIRED_COLUMNS,
181
270
  ) -> None:
182
271
  table = None
183
272
 
184
- self._rois: dict[str, Roi] | None = None
273
+ self._rois: RoiDictWrapper | None = None
185
274
  if rois is not None:
186
- self._rois = {}
187
- self.add(rois)
188
- table = _rois_to_dataframe(self._rois, index_key=meta.index_key)
275
+ self._rois = RoiDictWrapper(rois)
276
+ table = self._rois.to_dataframe(index_key=meta.index_key)
189
277
 
278
+ self._required_columns = required_columns
190
279
  super().__init__(table_data=table, meta=meta)
191
280
 
192
281
  def __repr__(self) -> str:
@@ -213,7 +302,7 @@ class GenericRoiTableV1(AbstractBaseTable):
213
302
  return super().table_data
214
303
 
215
304
  if len(self.rois()) > 0:
216
- self._table_data = _rois_to_dataframe(self._rois, index_key=self.index_key)
305
+ self._table_data = self._rois.to_dataframe(index_key=self.meta.index_key)
217
306
  return super().table_data
218
307
 
219
308
  def set_table_data(
@@ -260,14 +349,16 @@ class GenericRoiTableV1(AbstractBaseTable):
260
349
  If the ROIs are not loaded, load them from the table.
261
350
  """
262
351
  if self._rois is None:
263
- self._rois = _dataframe_to_rois(self.dataframe)
352
+ self._rois = RoiDictWrapper.from_dataframe(
353
+ self.dataframe, required_columns=self._required_columns
354
+ )
264
355
 
265
356
  def rois(self) -> list[Roi]:
266
357
  """List all ROIs in the table."""
267
358
  self._check_rois()
268
359
  if self._rois is None:
269
360
  return []
270
- return list(self._rois.values())
361
+ return self._rois.to_list()
271
362
 
272
363
  def add(self, roi: Roi | Iterable[Roi], overwrite: bool = False) -> None:
273
364
  """Append ROIs to the current table.
@@ -281,22 +372,20 @@ class GenericRoiTableV1(AbstractBaseTable):
281
372
 
282
373
  self._check_rois()
283
374
  if self._rois is None:
284
- self._rois = {}
375
+ self._rois = RoiDictWrapper([])
285
376
 
286
- for _roi in roi:
287
- if not overwrite and _roi.name in self._rois:
288
- raise NgioValueError(f"ROI {_roi.name} already exists in the table.")
289
- self._rois[_roi.name] = _roi
377
+ self._rois.add_rois(roi, overwrite=overwrite)
290
378
 
291
379
  def get(self, roi_name: str) -> Roi:
292
380
  """Get an ROI from the table."""
293
381
  self._check_rois()
294
382
  if self._rois is None:
295
- self._rois = {}
383
+ self._rois = RoiDictWrapper([])
296
384
 
297
- if roi_name not in self._rois:
298
- raise NgioValueError(f"ROI {roi_name} not found in the table.")
299
- return self._rois[roi_name]
385
+ roi = self._rois.get_by_name(roi_name)
386
+ if roi is None:
387
+ raise NgioValueError(f"ROI with name {roi_name} not found in the table.")
388
+ return roi
300
389
 
301
390
  @classmethod
302
391
  def from_table_data(
@@ -309,7 +398,7 @@ class GenericRoiTableV1(AbstractBaseTable):
309
398
  index_type=meta.index_type,
310
399
  required_columns=REQUIRED_COLUMNS,
311
400
  )
312
- return cls(rois=rois.values(), meta=meta)
401
+ return cls(rois=rois.to_list(), meta=meta)
313
402
 
314
403
 
315
404
  class RoiTableV1Meta(BackendMeta):
@@ -457,7 +546,13 @@ class MaskingRoiTableV1(GenericRoiTableV1):
457
546
  path = path.split("/")[-1]
458
547
  return path
459
548
 
460
- def get(self, label: int | str) -> Roi: # type: ignore
461
- """Get an ROI from the table."""
462
- roi_name = str(label)
463
- return super().get(roi_name)
549
+ def get_label(self, label: int) -> Roi:
550
+ """Get an ROI by label."""
551
+ self._check_rois()
552
+ if self._rois is None:
553
+ self._rois = RoiDictWrapper([])
554
+ roi = self._rois.get_by_label(label)
555
+
556
+ if roi is None:
557
+ raise NgioValueError(f"ROI with label {label} not found.")
558
+ return roi
@@ -0,0 +1,5 @@
1
+ """Concrete IO transformations."""
2
+
3
+ from ngio.transforms._zoom import ZoomTransform
4
+
5
+ __all__ = ["ZoomTransform"]
@@ -0,0 +1,19 @@
1
+ from ngio.common._zoom import (
2
+ InterpolationOrder,
3
+ )
4
+ from ngio.images._abstract_image import AbstractImage
5
+ from ngio.io_pipes._zoom_transform import BaseZoomTransform
6
+
7
+
8
+ class ZoomTransform(BaseZoomTransform):
9
+ def __init__(
10
+ self,
11
+ input_image: AbstractImage,
12
+ target_image: AbstractImage,
13
+ order: InterpolationOrder = "nearest",
14
+ ) -> None:
15
+ super().__init__(
16
+ input_dimensions=input_image.dimensions,
17
+ target_dimensions=target_image.dimensions,
18
+ order=order,
19
+ )
ngio/utils/__init__.py CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  import os
4
4
 
5
- from ngio.common._common_types import ArrayLike
6
5
  from ngio.utils._datasets import (
7
6
  download_ome_zarr_dataset,
8
7
  list_ome_zarr_datasets,
@@ -16,7 +15,7 @@ from ngio.utils._errors import (
16
15
  NgioValueError,
17
16
  )
18
17
  from ngio.utils._fractal_fsspec_store import fractal_fsspec_store
19
- from ngio.utils._logger import ngio_logger, set_logger_level
18
+ from ngio.utils._logger import ngio_logger, ngio_warn, set_logger_level
20
19
  from ngio.utils._zarr_utils import (
21
20
  AccessModeLiteral,
22
21
  StoreOrGroup,
@@ -29,7 +28,6 @@ set_logger_level(os.getenv("NGIO_LOGGER_LEVEL", "WARNING"))
29
28
  __all__ = [
30
29
  # Zarr
31
30
  "AccessModeLiteral",
32
- "ArrayLike",
33
31
  # Errors
34
32
  "NgioFileExistsError",
35
33
  "NgioFileNotFoundError",
@@ -44,6 +42,7 @@ __all__ = [
44
42
  "list_ome_zarr_datasets",
45
43
  # Logger
46
44
  "ngio_logger",
45
+ "ngio_warn",
47
46
  "open_group_wrapper",
48
47
  "print_datasets_infos",
49
48
  "set_logger_level",
ngio/utils/_datasets.py CHANGED
@@ -38,6 +38,11 @@ class UnzipAndRename(pooch.Unzip):
38
38
  return None
39
39
 
40
40
  tmp_dir = Path(extract_dir) / "tmp"
41
+ # If tmp_dir exists, remove it
42
+ if tmp_dir.exists():
43
+ shutil.rmtree(tmp_dir, ignore_errors=True)
44
+ tmp_dir.mkdir(parents=True, exist_ok=True)
45
+
41
46
  super()._extract_file(fname, tmp_dir)
42
47
 
43
48
  list_extracted_dirs = tmp_dir.iterdir()
ngio/utils/_logger.py CHANGED
@@ -1,4 +1,6 @@
1
1
  import logging
2
+ import time
3
+ from functools import cache
2
4
 
3
5
  from ngio.utils._errors import NgioValueError
4
6
 
@@ -29,3 +31,20 @@ def set_logger_level(level: str) -> None:
29
31
  raise NgioValueError(f"Invalid log level: {level}")
30
32
 
31
33
  ngio_logger.setLevel(level)
34
+
35
+
36
+ @cache
37
+ def _warn(message: str, ttl_hash: int) -> None:
38
+ """Log a warning message with a time-to-live (TTL) hash."""
39
+ ngio_logger.warning(message, stacklevel=3)
40
+
41
+
42
+ def ngio_warn(message: str, cooldown: int = 2) -> None:
43
+ """Log a warning message.
44
+
45
+ Args:
46
+ message: The warning message to log.
47
+ cooldown: The cooldown period in seconds to avoid repeated logging.
48
+ """
49
+ ttl_hash = time.time() // cooldown
50
+ _warn(message, ttl_hash)