ngio 0.5.0b6__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 (88) hide show
  1. ngio/__init__.py +69 -0
  2. ngio/common/__init__.py +28 -0
  3. ngio/common/_dimensions.py +335 -0
  4. ngio/common/_masking_roi.py +153 -0
  5. ngio/common/_pyramid.py +408 -0
  6. ngio/common/_roi.py +315 -0
  7. ngio/common/_synt_images_utils.py +101 -0
  8. ngio/common/_zoom.py +188 -0
  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 +126 -0
  16. ngio/experimental/iterators/_segmentation.py +235 -0
  17. ngio/hcs/__init__.py +19 -0
  18. ngio/hcs/_plate.py +1354 -0
  19. ngio/images/__init__.py +44 -0
  20. ngio/images/_abstract_image.py +967 -0
  21. ngio/images/_create_synt_container.py +132 -0
  22. ngio/images/_create_utils.py +423 -0
  23. ngio/images/_image.py +926 -0
  24. ngio/images/_label.py +411 -0
  25. ngio/images/_masked_image.py +531 -0
  26. ngio/images/_ome_zarr_container.py +1237 -0
  27. ngio/images/_table_ops.py +471 -0
  28. ngio/io_pipes/__init__.py +75 -0
  29. ngio/io_pipes/_io_pipes.py +361 -0
  30. ngio/io_pipes/_io_pipes_masked.py +488 -0
  31. ngio/io_pipes/_io_pipes_roi.py +146 -0
  32. ngio/io_pipes/_io_pipes_types.py +56 -0
  33. ngio/io_pipes/_match_shape.py +377 -0
  34. ngio/io_pipes/_ops_axes.py +344 -0
  35. ngio/io_pipes/_ops_slices.py +411 -0
  36. ngio/io_pipes/_ops_slices_utils.py +199 -0
  37. ngio/io_pipes/_ops_transforms.py +104 -0
  38. ngio/io_pipes/_zoom_transform.py +180 -0
  39. ngio/ome_zarr_meta/__init__.py +65 -0
  40. ngio/ome_zarr_meta/_meta_handlers.py +536 -0
  41. ngio/ome_zarr_meta/ngio_specs/__init__.py +77 -0
  42. ngio/ome_zarr_meta/ngio_specs/_axes.py +515 -0
  43. ngio/ome_zarr_meta/ngio_specs/_channels.py +462 -0
  44. ngio/ome_zarr_meta/ngio_specs/_dataset.py +89 -0
  45. ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +539 -0
  46. ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +438 -0
  47. ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +122 -0
  48. ngio/ome_zarr_meta/v04/__init__.py +27 -0
  49. ngio/ome_zarr_meta/v04/_custom_models.py +18 -0
  50. ngio/ome_zarr_meta/v04/_v04_spec.py +473 -0
  51. ngio/ome_zarr_meta/v05/__init__.py +27 -0
  52. ngio/ome_zarr_meta/v05/_custom_models.py +18 -0
  53. ngio/ome_zarr_meta/v05/_v05_spec.py +511 -0
  54. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
  55. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
  56. ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
  57. ngio/resources/__init__.py +55 -0
  58. ngio/resources/resource_model.py +36 -0
  59. ngio/tables/__init__.py +43 -0
  60. ngio/tables/_abstract_table.py +270 -0
  61. ngio/tables/_tables_container.py +449 -0
  62. ngio/tables/backends/__init__.py +57 -0
  63. ngio/tables/backends/_abstract_backend.py +240 -0
  64. ngio/tables/backends/_anndata.py +139 -0
  65. ngio/tables/backends/_anndata_utils.py +90 -0
  66. ngio/tables/backends/_csv.py +19 -0
  67. ngio/tables/backends/_json.py +92 -0
  68. ngio/tables/backends/_parquet.py +19 -0
  69. ngio/tables/backends/_py_arrow_backends.py +222 -0
  70. ngio/tables/backends/_table_backends.py +226 -0
  71. ngio/tables/backends/_utils.py +608 -0
  72. ngio/tables/v1/__init__.py +23 -0
  73. ngio/tables/v1/_condition_table.py +71 -0
  74. ngio/tables/v1/_feature_table.py +125 -0
  75. ngio/tables/v1/_generic_table.py +49 -0
  76. ngio/tables/v1/_roi_table.py +575 -0
  77. ngio/transforms/__init__.py +5 -0
  78. ngio/transforms/_zoom.py +19 -0
  79. ngio/utils/__init__.py +45 -0
  80. ngio/utils/_cache.py +48 -0
  81. ngio/utils/_datasets.py +165 -0
  82. ngio/utils/_errors.py +37 -0
  83. ngio/utils/_fractal_fsspec_store.py +42 -0
  84. ngio/utils/_zarr_utils.py +534 -0
  85. ngio-0.5.0b6.dist-info/METADATA +148 -0
  86. ngio-0.5.0b6.dist-info/RECORD +88 -0
  87. ngio-0.5.0b6.dist-info/WHEEL +4 -0
  88. ngio-0.5.0b6.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,575 @@
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
+ import warnings
8
+ from collections.abc import Iterable
9
+ from typing import Literal
10
+ from uuid import uuid4
11
+
12
+ import pandas as pd
13
+ from pydantic import BaseModel
14
+
15
+ from ngio.common import Roi
16
+ from ngio.tables._abstract_table import (
17
+ AbstractBaseTable,
18
+ TabularData,
19
+ )
20
+ from ngio.tables.backends import (
21
+ BackendMeta,
22
+ TableBackend,
23
+ convert_to_pandas,
24
+ normalize_pandas_df,
25
+ )
26
+ from ngio.utils import (
27
+ NgioTableValidationError,
28
+ NgioValueError,
29
+ ZarrGroupHandler,
30
+ )
31
+
32
+ REQUIRED_COLUMNS = [
33
+ "x_micrometer",
34
+ "y_micrometer",
35
+ "z_micrometer",
36
+ "len_x_micrometer",
37
+ "len_y_micrometer",
38
+ "len_z_micrometer",
39
+ ]
40
+
41
+ #####################
42
+ # Optional columns are not validated at the moment
43
+ # only a warning is raised if non optional columns are present
44
+ #####################
45
+
46
+ TIME_COLUMNS = [
47
+ "t_second",
48
+ "len_t_second",
49
+ ]
50
+
51
+ ORIGIN_COLUMNS = [
52
+ "x_micrometer_original",
53
+ "y_micrometer_original",
54
+ "z_micrometer_original",
55
+ ]
56
+
57
+ TRANSLATION_COLUMNS = ["translation_x", "translation_y", "translation_z"]
58
+
59
+ PLATE_COLUMNS = [
60
+ "plate_name",
61
+ "row",
62
+ "column",
63
+ "path_in_well",
64
+ "path_in_plate",
65
+ "acquisition_id",
66
+ "acquisition_name",
67
+ ]
68
+
69
+ INDEX_COLUMNS = [
70
+ "FieldIndex",
71
+ "label",
72
+ ]
73
+
74
+ OPTIONAL_COLUMNS = ORIGIN_COLUMNS + TRANSLATION_COLUMNS + PLATE_COLUMNS + INDEX_COLUMNS
75
+
76
+
77
+ def _check_optional_columns(col_name: str) -> None:
78
+ """Check if the column name is in the optional columns."""
79
+ if col_name not in OPTIONAL_COLUMNS + TIME_COLUMNS:
80
+ warnings.warn(
81
+ f"Column {col_name} is not in the optional columns.", stacklevel=2
82
+ )
83
+
84
+
85
+ def _dataframe_to_rois(
86
+ dataframe: pd.DataFrame,
87
+ required_columns: list[str] = REQUIRED_COLUMNS,
88
+ ) -> dict[str, Roi]:
89
+ """Convert a DataFrame to a WorldCooROI object."""
90
+ # Validate the columns of the DataFrame
91
+ _missing_columns = set(required_columns).difference(set(dataframe.columns))
92
+ if len(_missing_columns) != 0:
93
+ raise NgioTableValidationError(
94
+ f"Could not find required columns: {_missing_columns} in the table."
95
+ )
96
+
97
+ extra_columns = set(dataframe.columns).difference(
98
+ set(required_columns + TIME_COLUMNS)
99
+ )
100
+
101
+ for col in extra_columns:
102
+ _check_optional_columns(col)
103
+
104
+ label_is_index = True if dataframe.index.name == "label" else False
105
+
106
+ extras = {}
107
+
108
+ rois = {}
109
+ for row in dataframe.itertuples(index=True):
110
+ # check if optional columns are present
111
+ if len(extra_columns) > 0:
112
+ extras = {col: getattr(row, col, None) for col in extra_columns}
113
+
114
+ z_micrometer = getattr(row, "z_micrometer", None)
115
+ z_length_micrometer = getattr(row, "len_z_micrometer", None)
116
+
117
+ t_second = getattr(row, "t_second", None)
118
+ t_length_second = getattr(row, "len_t_second", None)
119
+
120
+ if label_is_index:
121
+ label = int(row.Index) # type: ignore (type can not be known here, but should be castable to int)
122
+ else:
123
+ label = getattr(row, "label", None)
124
+
125
+ slices = {
126
+ "x": (row.x_micrometer, row.len_x_micrometer),
127
+ "y": (row.y_micrometer, row.len_y_micrometer),
128
+ "z": (z_micrometer, z_length_micrometer),
129
+ }
130
+ if t_second is not None or t_length_second is not None:
131
+ slices["t"] = (t_second, t_length_second)
132
+ roi = Roi.from_values(
133
+ name=str(row.Index),
134
+ slices=slices,
135
+ space="world",
136
+ label=label,
137
+ **extras,
138
+ )
139
+ rois[roi.name] = roi
140
+ return rois
141
+
142
+
143
+ def _rois_to_dataframe(rois: dict[str, Roi], index_key: str | None) -> pd.DataFrame:
144
+ """Convert a list of WorldCooROI objects to a DataFrame."""
145
+ data = []
146
+ for roi in rois.values():
147
+ # This normalization is necessary for backward compatibility
148
+ if roi.space != "world":
149
+ raise NotImplementedError(
150
+ "Only ROIs in world coordinates can be serialized."
151
+ )
152
+
153
+ z_slice = roi.get("z")
154
+ if z_slice is None:
155
+ z_micrometer = 0.0
156
+ len_z_micrometer = 1.0
157
+ else:
158
+ z_micrometer = z_slice.start if z_slice.start is not None else 0.0
159
+ len_z_micrometer = z_slice.length if z_slice.length is not None else 1.0
160
+
161
+ x_slice = roi.get("x")
162
+ if x_slice is None:
163
+ raise NgioValueError("ROI is missing 'x' slice.")
164
+ y_slice = roi.get("y")
165
+ if y_slice is None:
166
+ raise NgioValueError("ROI is missing 'y' slice.")
167
+ row = {
168
+ index_key: roi.get_name(),
169
+ "x_micrometer": x_slice.start if x_slice.start is not None else 0.0,
170
+ "y_micrometer": y_slice.start if y_slice.start is not None else 0.0,
171
+ "z_micrometer": z_micrometer,
172
+ "len_x_micrometer": x_slice.length if x_slice.length is not None else 1.0,
173
+ "len_y_micrometer": y_slice.length if y_slice.length is not None else 1.0,
174
+ "len_z_micrometer": len_z_micrometer,
175
+ }
176
+
177
+ t_slice = roi.get("t")
178
+ if t_slice is not None:
179
+ row["t_second"] = t_slice.start if t_slice.start is not None else 0.0
180
+ row["len_t_second"] = t_slice.length if t_slice.length is not None else 1.0
181
+
182
+ if roi.label is not None and index_key != "label":
183
+ row["label"] = roi.label
184
+
185
+ extra = roi.model_extra or {}
186
+ for col in extra:
187
+ _check_optional_columns(col)
188
+ row[col] = extra[col]
189
+ data.append(row)
190
+
191
+ dataframe = pd.DataFrame(data)
192
+ dataframe = normalize_pandas_df(dataframe, index_key=index_key)
193
+ return dataframe
194
+
195
+
196
+ class RoiDictWrapper:
197
+ """A wrapper for a dictionary of ROIs to provide a consistent interface."""
198
+
199
+ def __init__(self, rois: Iterable[Roi]) -> None:
200
+ self._rois_by_name = {}
201
+ self._rois_by_label = {}
202
+ for roi in rois:
203
+ name = roi.name
204
+ if name in self._rois_by_name:
205
+ name = f"{name}_{uuid4().hex[:8]}"
206
+ self._rois_by_name[name] = roi
207
+ if roi.label is not None:
208
+ self._rois_by_label[roi.label] = roi
209
+
210
+ def get_by_name(self, name: str, default: Roi | None = None) -> Roi | None:
211
+ """Get an ROI by its name."""
212
+ return self._rois_by_name.get(name, default)
213
+
214
+ def get_by_label(self, label: int, default: Roi | None = None) -> Roi | None:
215
+ """Get an ROI by its label."""
216
+ return self._rois_by_label.get(label, default)
217
+
218
+ def _add_roi(self, roi: Roi, overwrite: bool = False) -> None:
219
+ """Add an ROI to the wrapper."""
220
+ if roi.name in self._rois_by_name and not overwrite:
221
+ raise NgioValueError(f"ROI with name {roi.name} already exists.")
222
+
223
+ self._rois_by_name[roi.name] = roi
224
+ if roi.label is not None:
225
+ self._rois_by_label[roi.label] = roi
226
+
227
+ def add_rois(self, rois: Roi | Iterable[Roi], overwrite: bool = False) -> None:
228
+ """Add ROIs to the wrapper."""
229
+ if isinstance(rois, Roi):
230
+ rois = [rois]
231
+
232
+ for roi in rois:
233
+ self._add_roi(roi, overwrite=overwrite)
234
+
235
+ def to_list(self) -> list[Roi]:
236
+ """Return the list of ROIs."""
237
+ return list(self._rois_by_name.values())
238
+
239
+ def to_dataframe(self, index_key: str | None = None) -> pd.DataFrame:
240
+ """Convert the ROIs to a DataFrame."""
241
+ return _rois_to_dataframe(self._rois_by_name, index_key=index_key)
242
+
243
+ @classmethod
244
+ def from_dataframe(
245
+ cls, dataframe: pd.DataFrame, required_columns: list[str] = REQUIRED_COLUMNS
246
+ ) -> "RoiDictWrapper":
247
+ """Create a RoiDictWrapper from a DataFrame."""
248
+ rois = _dataframe_to_rois(dataframe, required_columns=required_columns)
249
+ return cls(rois.values())
250
+
251
+
252
+ def _table_to_rois(
253
+ table: TabularData,
254
+ index_key: str | None = None,
255
+ index_type: Literal["int", "str"] | None = None,
256
+ required_columns: list[str] = REQUIRED_COLUMNS,
257
+ ) -> tuple[pd.DataFrame, RoiDictWrapper]:
258
+ """Convert a table to a dictionary of ROIs.
259
+
260
+ Args:
261
+ table: The table to convert.
262
+ index_key: The column name to use as the index of the DataFrame.
263
+ index_type: The type of the index column in the DataFrame.
264
+ required_columns: The required columns in the DataFrame.
265
+
266
+ Returns:
267
+ A tuple containing the DataFrame and a RoiDictWrapper with the ROIs.
268
+ """
269
+ dataframe = convert_to_pandas(
270
+ table,
271
+ index_key=index_key,
272
+ index_type=index_type,
273
+ )
274
+ roi_dict_wrapper = RoiDictWrapper.from_dataframe(
275
+ dataframe, required_columns=required_columns
276
+ )
277
+ return dataframe, roi_dict_wrapper
278
+
279
+
280
+ class GenericRoiTableV1(AbstractBaseTable):
281
+ def __init__(
282
+ self,
283
+ *,
284
+ rois: Iterable[Roi] | None = None,
285
+ meta: BackendMeta,
286
+ required_columns: list[str] = REQUIRED_COLUMNS,
287
+ ) -> None:
288
+ table = None
289
+
290
+ self._rois: RoiDictWrapper | None = None
291
+ if rois is not None:
292
+ self._rois = RoiDictWrapper(rois)
293
+ table = self._rois.to_dataframe(index_key=meta.index_key)
294
+
295
+ self._required_columns = required_columns
296
+ super().__init__(table_data=table, meta=meta)
297
+
298
+ def __repr__(self) -> str:
299
+ """Return a string representation of the table."""
300
+ rois = self.rois()
301
+ prop = f"num_rois={len(rois)}"
302
+ class_name = self.__class__.__name__
303
+ return f"{class_name}({prop})"
304
+
305
+ @staticmethod
306
+ def table_type() -> str:
307
+ """Return the type of the table."""
308
+ return "generic_roi_table"
309
+
310
+ @staticmethod
311
+ def version() -> Literal["1"]:
312
+ """Return the version of the fractal table."""
313
+ return "1"
314
+
315
+ @property
316
+ def table_data(self) -> TabularData:
317
+ """Return the table."""
318
+ if self._rois is None:
319
+ return super().table_data
320
+
321
+ if len(self.rois()) > 0:
322
+ self._table_data = self._rois.to_dataframe(index_key=self.meta.index_key)
323
+ return super().table_data
324
+
325
+ def set_table_data(
326
+ self, table_data: TabularData | None = None, refresh: bool = False
327
+ ) -> None:
328
+ if table_data is not None:
329
+ if not isinstance(table_data, TabularData):
330
+ raise NgioValueError(
331
+ "The table must be a pandas DataFrame, polars LazyFrame, "
332
+ " or AnnData object."
333
+ )
334
+
335
+ table_data, rois = _table_to_rois(
336
+ table_data,
337
+ index_key=self.index_key,
338
+ index_type=self.index_type,
339
+ required_columns=REQUIRED_COLUMNS,
340
+ )
341
+ self._table_data = table_data
342
+ self._rois = rois
343
+ return None
344
+
345
+ if self._table_data is not None and not refresh:
346
+ return None
347
+
348
+ if self._table_backend is None:
349
+ raise NgioValueError(
350
+ "The table does not have a DataFrame in memory nor a backend."
351
+ )
352
+
353
+ table_data, rois = _table_to_rois(
354
+ self._table_backend.load(),
355
+ index_key=self.index_key,
356
+ index_type=self.index_type,
357
+ required_columns=REQUIRED_COLUMNS,
358
+ )
359
+ self._table_data = table_data
360
+ self._rois = rois
361
+
362
+ def _check_rois(self) -> None:
363
+ """Load the ROIs from the table.
364
+
365
+ If the ROIs are already loaded, do nothing.
366
+ If the ROIs are not loaded, load them from the table.
367
+ """
368
+ if self._rois is None:
369
+ self._rois = RoiDictWrapper.from_dataframe(
370
+ self.dataframe, required_columns=self._required_columns
371
+ )
372
+
373
+ def rois(self) -> list[Roi]:
374
+ """List all ROIs in the table."""
375
+ self._check_rois()
376
+ if self._rois is None:
377
+ return []
378
+ return self._rois.to_list()
379
+
380
+ def add(self, roi: Roi | Iterable[Roi], overwrite: bool = False) -> None:
381
+ """Append ROIs to the current table.
382
+
383
+ Args:
384
+ roi: A single ROI or a list of ROIs to add to the table.
385
+ overwrite: If True, overwrite existing ROIs with the same name.
386
+ """
387
+ if isinstance(roi, Roi):
388
+ roi = [roi]
389
+
390
+ self._check_rois()
391
+ if self._rois is None:
392
+ self._rois = RoiDictWrapper([])
393
+
394
+ self._rois.add_rois(roi, overwrite=overwrite)
395
+
396
+ def get(self, roi_name: str) -> Roi:
397
+ """Get an ROI from the table."""
398
+ self._check_rois()
399
+ if self._rois is None:
400
+ self._rois = RoiDictWrapper([])
401
+
402
+ roi = self._rois.get_by_name(roi_name)
403
+ if roi is None:
404
+ raise NgioValueError(f"ROI with name {roi_name} not found in the table.")
405
+ return roi
406
+
407
+ @classmethod
408
+ def from_table_data(
409
+ cls, table_data: TabularData, meta: BackendMeta
410
+ ) -> "GenericRoiTableV1":
411
+ """Create a new ROI table from a table data."""
412
+ _, rois = _table_to_rois(
413
+ table=table_data,
414
+ index_key=meta.index_key,
415
+ index_type=meta.index_type,
416
+ required_columns=REQUIRED_COLUMNS,
417
+ )
418
+ return cls(rois=rois.to_list(), meta=meta)
419
+
420
+
421
+ class RoiTableV1Meta(BackendMeta):
422
+ """Metadata for the ROI table."""
423
+
424
+ table_version: Literal["1"] = "1"
425
+ type: Literal["roi_table"] = "roi_table"
426
+ index_key: str | None = "FieldIndex"
427
+ index_type: Literal["str", "int"] | None = "str"
428
+
429
+
430
+ class RoiTableV1(GenericRoiTableV1):
431
+ """Class to handle fractal ROI tables.
432
+
433
+ To know more about the ROI table format, please refer to the
434
+ specification at:
435
+ https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
436
+ """
437
+
438
+ def __init__(
439
+ self, rois: Iterable[Roi] | None = None, *, meta: RoiTableV1Meta | None = None
440
+ ) -> None:
441
+ """Create a new ROI table."""
442
+ if meta is None:
443
+ meta = RoiTableV1Meta()
444
+
445
+ if meta.index_key is None:
446
+ meta.index_key = "FieldIndex"
447
+
448
+ if meta.index_type is None:
449
+ meta.index_type = "str"
450
+ super().__init__(meta=meta, rois=rois)
451
+
452
+ @classmethod
453
+ def from_handler(
454
+ cls,
455
+ handler: ZarrGroupHandler,
456
+ backend: TableBackend | None = None,
457
+ ) -> "RoiTableV1":
458
+ table = cls._from_handler(
459
+ handler=handler,
460
+ backend=backend,
461
+ meta_model=RoiTableV1Meta,
462
+ )
463
+ return table
464
+
465
+ @staticmethod
466
+ def table_type() -> Literal["roi_table"]:
467
+ """Return the type of the table."""
468
+ return "roi_table"
469
+
470
+
471
+ class RegionMeta(BaseModel):
472
+ """Metadata for the region."""
473
+
474
+ path: str
475
+
476
+
477
+ class MaskingRoiTableV1Meta(BackendMeta):
478
+ """Metadata for the ROI table."""
479
+
480
+ table_version: Literal["1"] = "1"
481
+ type: Literal["masking_roi_table"] = "masking_roi_table"
482
+ region: RegionMeta | None = None
483
+ instance_key: str = "label"
484
+ index_key: str | None = "label"
485
+ index_type: Literal["int", "str"] | None = "int"
486
+
487
+
488
+ class MaskingRoiTableV1(GenericRoiTableV1):
489
+ """Class to handle fractal ROI tables.
490
+
491
+ To know more about the ROI table format, please refer to the
492
+ specification at:
493
+ https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/
494
+ """
495
+
496
+ def __init__(
497
+ self,
498
+ rois: Iterable[Roi] | None = None,
499
+ *,
500
+ reference_label: str | None = None,
501
+ meta: MaskingRoiTableV1Meta | None = None,
502
+ ) -> None:
503
+ """Create a new ROI table."""
504
+ if meta is None:
505
+ meta = MaskingRoiTableV1Meta()
506
+
507
+ if reference_label is not None:
508
+ path = f"../labels/{reference_label}"
509
+ meta.region = RegionMeta(path=path)
510
+
511
+ if meta.index_key is None:
512
+ meta.index_key = "label"
513
+
514
+ if meta.index_type is None:
515
+ meta.index_type = "int"
516
+ meta.instance_key = meta.index_key
517
+ super().__init__(meta=meta, rois=rois)
518
+
519
+ def __repr__(self) -> str:
520
+ """Return a string representation of the table."""
521
+ rois = self.rois()
522
+ if self.reference_label is not None:
523
+ prop = f"num_rois={len(rois)}, reference_label={self.reference_label}"
524
+ else:
525
+ prop = f"num_rois={len(rois)}"
526
+ return f"MaskingRoiTableV1({prop})"
527
+
528
+ @classmethod
529
+ def from_handler(
530
+ cls,
531
+ handler: ZarrGroupHandler,
532
+ backend: TableBackend | None = None,
533
+ ) -> "MaskingRoiTableV1":
534
+ table = cls._from_handler(
535
+ handler=handler,
536
+ backend=backend,
537
+ meta_model=MaskingRoiTableV1Meta,
538
+ )
539
+ return table
540
+
541
+ @staticmethod
542
+ def table_type() -> Literal["masking_roi_table"]:
543
+ """Return the type of the table."""
544
+ return "masking_roi_table"
545
+
546
+ @property
547
+ def meta(self) -> MaskingRoiTableV1Meta:
548
+ """Return the metadata of the table."""
549
+ if not isinstance(self._meta, MaskingRoiTableV1Meta):
550
+ raise NgioValueError(
551
+ "The metadata of the table is not of type MaskingRoiTableV1Meta."
552
+ )
553
+ return self._meta
554
+
555
+ @property
556
+ def reference_label(self) -> str | None:
557
+ """Return the reference label."""
558
+ path = self.meta.region
559
+ if path is None:
560
+ return None
561
+
562
+ path = path.path
563
+ path = path.split("/")[-1]
564
+ return path
565
+
566
+ def get_label(self, label: int) -> Roi:
567
+ """Get an ROI by label."""
568
+ self._check_rois()
569
+ if self._rois is None:
570
+ self._rois = RoiDictWrapper([])
571
+ roi = self._rois.get_by_label(label)
572
+
573
+ if roi is None:
574
+ raise NgioValueError(f"ROI with label {label} not found.")
575
+ return roi
@@ -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 ADDED
@@ -0,0 +1,45 @@
1
+ """Various utilities for the ngio package."""
2
+
3
+ from ngio.utils._datasets import (
4
+ download_ome_zarr_dataset,
5
+ list_ome_zarr_datasets,
6
+ print_datasets_infos,
7
+ )
8
+ from ngio.utils._errors import (
9
+ NgioError,
10
+ NgioFileExistsError,
11
+ NgioFileNotFoundError,
12
+ NgioTableValidationError,
13
+ NgioValidationError,
14
+ NgioValueError,
15
+ )
16
+ from ngio.utils._fractal_fsspec_store import fractal_fsspec_store
17
+ from ngio.utils._zarr_utils import (
18
+ AccessModeLiteral,
19
+ NgioCache,
20
+ NgioSupportedStore,
21
+ StoreOrGroup,
22
+ ZarrGroupHandler,
23
+ copy_group,
24
+ open_group_wrapper,
25
+ )
26
+
27
+ __all__ = [
28
+ "AccessModeLiteral",
29
+ "NgioCache",
30
+ "NgioError",
31
+ "NgioFileExistsError",
32
+ "NgioFileNotFoundError",
33
+ "NgioSupportedStore",
34
+ "NgioTableValidationError",
35
+ "NgioValidationError",
36
+ "NgioValueError",
37
+ "StoreOrGroup",
38
+ "ZarrGroupHandler",
39
+ "copy_group",
40
+ "download_ome_zarr_dataset",
41
+ "fractal_fsspec_store",
42
+ "list_ome_zarr_datasets",
43
+ "open_group_wrapper",
44
+ "print_datasets_infos",
45
+ ]
ngio/utils/_cache.py ADDED
@@ -0,0 +1,48 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ T = TypeVar("T")
4
+
5
+
6
+ class NgioCache(Generic[T]):
7
+ """A simple cache for NGIO objects."""
8
+
9
+ def __init__(self, use_cache: bool = True):
10
+ self._cache: dict[str, T] = {}
11
+ self._use_cache = use_cache
12
+
13
+ def _cache_sanity_check(self) -> None:
14
+ if len(self._cache) > 0:
15
+ raise RuntimeError(
16
+ "Cache is disabled, but cache contains items. "
17
+ "This indicates a logic error."
18
+ )
19
+
20
+ @property
21
+ def use_cache(self) -> bool:
22
+ return self._use_cache
23
+
24
+ @property
25
+ def cache(self) -> dict[str, T]:
26
+ return self._cache
27
+
28
+ @property
29
+ def is_empty(self) -> bool:
30
+ return len(self._cache) == 0
31
+
32
+ def get(self, key: str, default: T | None = None) -> T | None:
33
+ if not self._use_cache:
34
+ self._cache_sanity_check()
35
+ return default
36
+ return self._cache.get(key, default)
37
+
38
+ def set(self, key: str, value: T, overwrite: bool = True) -> None:
39
+ if not self._use_cache:
40
+ self._cache_sanity_check()
41
+ return
42
+ self._cache[key] = value
43
+
44
+ def clear(self) -> None:
45
+ if not self._use_cache:
46
+ self._cache_sanity_check()
47
+ return
48
+ self._cache.clear()