ngio 0.3.5__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.
- ngio/__init__.py +7 -2
- ngio/common/__init__.py +5 -52
- ngio/common/_dimensions.py +270 -55
- ngio/common/_masking_roi.py +38 -10
- ngio/common/_pyramid.py +51 -30
- ngio/common/_roi.py +269 -82
- ngio/common/_synt_images_utils.py +101 -0
- ngio/common/_zoom.py +49 -19
- ngio/experimental/__init__.py +5 -0
- ngio/experimental/iterators/__init__.py +15 -0
- ngio/experimental/iterators/_abstract_iterator.py +390 -0
- ngio/experimental/iterators/_feature.py +189 -0
- ngio/experimental/iterators/_image_processing.py +130 -0
- ngio/experimental/iterators/_mappers.py +48 -0
- ngio/experimental/iterators/_rois_utils.py +127 -0
- ngio/experimental/iterators/_segmentation.py +235 -0
- ngio/hcs/_plate.py +41 -36
- ngio/images/__init__.py +22 -1
- ngio/images/_abstract_image.py +403 -176
- ngio/images/_create.py +31 -15
- ngio/images/_create_synt_container.py +138 -0
- ngio/images/_image.py +452 -63
- ngio/images/_label.py +56 -30
- ngio/images/_masked_image.py +387 -129
- ngio/images/_ome_zarr_container.py +237 -67
- ngio/{common → images}/_table_ops.py +41 -41
- ngio/io_pipes/__init__.py +75 -0
- ngio/io_pipes/_io_pipes.py +361 -0
- ngio/io_pipes/_io_pipes_masked.py +488 -0
- ngio/io_pipes/_io_pipes_roi.py +152 -0
- ngio/io_pipes/_io_pipes_types.py +56 -0
- ngio/io_pipes/_match_shape.py +376 -0
- ngio/io_pipes/_ops_axes.py +344 -0
- ngio/io_pipes/_ops_slices.py +446 -0
- ngio/io_pipes/_ops_slices_utils.py +196 -0
- ngio/io_pipes/_ops_transforms.py +104 -0
- ngio/io_pipes/_zoom_transform.py +175 -0
- ngio/ome_zarr_meta/__init__.py +4 -2
- ngio/ome_zarr_meta/ngio_specs/__init__.py +4 -10
- ngio/ome_zarr_meta/ngio_specs/_axes.py +186 -175
- ngio/ome_zarr_meta/ngio_specs/_channels.py +55 -18
- ngio/ome_zarr_meta/ngio_specs/_dataset.py +48 -122
- ngio/ome_zarr_meta/ngio_specs/_ngio_hcs.py +3 -3
- ngio/ome_zarr_meta/ngio_specs/_ngio_image.py +38 -87
- ngio/ome_zarr_meta/ngio_specs/_pixel_size.py +17 -1
- ngio/ome_zarr_meta/v04/_v04_spec_utils.py +34 -31
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/mask.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/nuclei.png +0 -0
- ngio/resources/20200812-CardiomyocyteDifferentiation14-Cycle1_B03/raw.jpg +0 -0
- ngio/resources/__init__.py +55 -0
- ngio/resources/resource_model.py +36 -0
- ngio/tables/backends/_abstract_backend.py +5 -6
- ngio/tables/backends/_anndata.py +1 -1
- ngio/tables/backends/_anndata_utils.py +3 -3
- ngio/tables/backends/_non_zarr_backends.py +1 -1
- ngio/tables/backends/_table_backends.py +0 -1
- ngio/tables/backends/_utils.py +3 -3
- ngio/tables/v1/_roi_table.py +165 -70
- ngio/transforms/__init__.py +5 -0
- ngio/transforms/_zoom.py +19 -0
- ngio/utils/__init__.py +2 -3
- ngio/utils/_datasets.py +5 -0
- ngio/utils/_logger.py +19 -0
- ngio/utils/_zarr_utils.py +6 -6
- {ngio-0.3.5.dist-info → ngio-0.4.0.dist-info}/METADATA +16 -14
- ngio-0.4.0.dist-info/RECORD +85 -0
- ngio/common/_array_pipe.py +0 -288
- ngio/common/_axes_transforms.py +0 -64
- ngio/common/_common_types.py +0 -5
- ngio/common/_slicer.py +0 -96
- ngio-0.3.5.dist-info/RECORD +0 -61
- {ngio-0.3.5.dist-info → ngio-0.4.0.dist-info}/WHEEL +0 -0
- {ngio-0.3.5.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
|
|
222
|
-
self.write_from_pandas(table_data)
|
|
223
|
-
elif
|
|
224
|
-
self.write_from_anndata(table_data)
|
|
225
|
-
elif
|
|
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(
|
ngio/tables/backends/_anndata.py
CHANGED
|
@@ -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."""
|
|
@@ -17,11 +17,11 @@ from ngio.utils import (
|
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
20
|
-
from collections.abc import Callable,
|
|
20
|
+
from collections.abc import Callable, Sequence
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def custom_anndata_read_zarr(
|
|
24
|
-
store: StoreOrGroup, elem_to_read:
|
|
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 (
|
|
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
|
|
ngio/tables/backends/_utils.py
CHANGED
|
@@ -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()
|
ngio/tables/v1/_roi_table.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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]
|
|
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(
|
|
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=
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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.
|
|
150
|
+
index_key: roi.get_name(),
|
|
157
151
|
"x_micrometer": roi.x,
|
|
158
152
|
"y_micrometer": roi.y,
|
|
159
|
-
"z_micrometer":
|
|
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":
|
|
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:
|
|
273
|
+
self._rois: RoiDictWrapper | None = None
|
|
185
274
|
if rois is not None:
|
|
186
|
-
self._rois =
|
|
187
|
-
self.
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
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
|
|
461
|
-
"""Get an ROI
|
|
462
|
-
|
|
463
|
-
|
|
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
|
ngio/transforms/_zoom.py
ADDED
|
@@ -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)
|