gfap-segmenter 1.0.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.
- gfap_segmenter/__init__.py +40 -0
- gfap_segmenter/_reader.py +268 -0
- gfap_segmenter/_version.py +34 -0
- gfap_segmenter/_widget.py +1085 -0
- gfap_segmenter/_writer.py +273 -0
- gfap_segmenter/background.py +76 -0
- gfap_segmenter/data_utils.py +141 -0
- gfap_segmenter/model_manager.py +824 -0
- gfap_segmenter/napari.yaml +40 -0
- gfap_segmenter/training.py +241 -0
- gfap_segmenter-1.0.0.dist-info/METADATA +274 -0
- gfap_segmenter-1.0.0.dist-info/RECORD +16 -0
- gfap_segmenter-1.0.0.dist-info/WHEEL +5 -0
- gfap_segmenter-1.0.0.dist-info/entry_points.txt +2 -0
- gfap_segmenter-1.0.0.dist-info/licenses/LICENSE +28 -0
- gfap_segmenter-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from ._version import version as __version__
|
|
3
|
+
except ImportError:
|
|
4
|
+
__version__ = "unknown"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from ._reader import napari_get_reader
|
|
8
|
+
from ._widget import SegmenterWidget
|
|
9
|
+
from ._writer import (
|
|
10
|
+
export_model_bundle,
|
|
11
|
+
import_model_bundle,
|
|
12
|
+
write_multiple,
|
|
13
|
+
write_single_image,
|
|
14
|
+
)
|
|
15
|
+
from .background import BackgroundExecutor
|
|
16
|
+
from .data_utils import (
|
|
17
|
+
CuratedPatch,
|
|
18
|
+
PatchDataset,
|
|
19
|
+
ensure_min_patch_size,
|
|
20
|
+
extract_patch_from_layers,
|
|
21
|
+
generate_augmented_patches,
|
|
22
|
+
)
|
|
23
|
+
from .model_manager import ModelManager, PredictionResult
|
|
24
|
+
|
|
25
|
+
__all__ = (
|
|
26
|
+
"napari_get_reader",
|
|
27
|
+
"write_single_image",
|
|
28
|
+
"write_multiple",
|
|
29
|
+
"export_model_bundle",
|
|
30
|
+
"import_model_bundle",
|
|
31
|
+
"ModelManager",
|
|
32
|
+
"PredictionResult",
|
|
33
|
+
"BackgroundExecutor",
|
|
34
|
+
"CuratedPatch",
|
|
35
|
+
"PatchDataset",
|
|
36
|
+
"ensure_min_patch_size",
|
|
37
|
+
"extract_patch_from_layers",
|
|
38
|
+
"generate_augmented_patches",
|
|
39
|
+
"SegmenterWidget",
|
|
40
|
+
)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Reader utilities for the GFAP napari plugin.
|
|
2
|
+
|
|
3
|
+
This reader wraps :mod:`bioio` (when available) to support common microscopy
|
|
4
|
+
formats used in the GFAP workflow. We currently handle TIFF/TIF stacks and CZI
|
|
5
|
+
files and expose them to napari as ``image`` layers with sensible metadata.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Tuple, Union
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
try: # pragma: no cover - optional dependency
|
|
17
|
+
from bioio import BioImage
|
|
18
|
+
|
|
19
|
+
# Explicitly import CZI backend to ensure it registers
|
|
20
|
+
try:
|
|
21
|
+
import bioio_czi # noqa: F401 - import for side effect (registration)
|
|
22
|
+
except ImportError:
|
|
23
|
+
pass # Backend not available, will fail later with better error message
|
|
24
|
+
BIOIO_AVAILABLE = True
|
|
25
|
+
except Exception: # pragma: no cover - fallback when bioio unavailable
|
|
26
|
+
BioImage = None # type: ignore[assignment]
|
|
27
|
+
BIOIO_AVAILABLE = False
|
|
28
|
+
|
|
29
|
+
try: # pragma: no cover - optional dependency
|
|
30
|
+
from skimage import io as skio
|
|
31
|
+
except Exception as exc: # pragma: no cover
|
|
32
|
+
raise ImportError("scikit-image is required to load TIFF files") from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PathLike = Union[str, Path]
|
|
36
|
+
LayerData = Tuple[np.ndarray, dict, str]
|
|
37
|
+
|
|
38
|
+
SUPPORTED_EXTENSIONS = {".tif", ".tiff", ".czi"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def napari_get_reader(path: Union[PathLike, Sequence[PathLike]]):
|
|
42
|
+
"""Return a callable napari reader when the path is supported.
|
|
43
|
+
|
|
44
|
+
napari may pass either a single path or a list of paths. We check the first
|
|
45
|
+
entry to decide whether we can read the file(s).
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
paths = _ensure_sequence(path)
|
|
49
|
+
first_suffix = Path(paths[0]).suffix.lower()
|
|
50
|
+
if first_suffix not in SUPPORTED_EXTENSIONS:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
return lambda p: _read_as_layers(_ensure_sequence(p))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_sequence(path: Union[PathLike, Sequence[PathLike]]) -> List[str]:
|
|
57
|
+
if isinstance(path, (str, Path)):
|
|
58
|
+
return [str(path)]
|
|
59
|
+
return [str(p) for p in path]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_as_layers(paths: Iterable[PathLike]) -> List[LayerData]:
|
|
63
|
+
layer_data: List[LayerData] = []
|
|
64
|
+
for path in paths:
|
|
65
|
+
path_obj = Path(path)
|
|
66
|
+
array, meta = _load_image(path_obj)
|
|
67
|
+
meta.setdefault("name", path_obj.stem)
|
|
68
|
+
layer_data.append((array, meta, "image"))
|
|
69
|
+
return layer_data
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _load_image(path: Path) -> Tuple[np.ndarray, dict]:
|
|
73
|
+
suffix = path.suffix.lower()
|
|
74
|
+
meta: dict = {}
|
|
75
|
+
|
|
76
|
+
if suffix not in SUPPORTED_EXTENSIONS:
|
|
77
|
+
raise ValueError(f"Unsupported file extension: {suffix}")
|
|
78
|
+
|
|
79
|
+
if BIOIO_AVAILABLE:
|
|
80
|
+
try:
|
|
81
|
+
return _load_with_bioio(path)
|
|
82
|
+
except ImportError:
|
|
83
|
+
# Don't catch ImportError - let it propagate with helpful message
|
|
84
|
+
# This happens when backend is missing
|
|
85
|
+
raise
|
|
86
|
+
except (
|
|
87
|
+
Exception
|
|
88
|
+
) as exc: # pragma: no cover - fallback for runtime issues
|
|
89
|
+
# For CZI files, don't fall back to scikit-image (it can't read them)
|
|
90
|
+
if suffix == ".czi":
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
f"Failed to load CZI file with bioio: {exc}\n"
|
|
93
|
+
"This may indicate a problem with the bioio-czi backend."
|
|
94
|
+
) from exc
|
|
95
|
+
# For other formats, we can try scikit-image as fallback
|
|
96
|
+
meta["load_warning"] = (
|
|
97
|
+
"Falling back to scikit-image; bioio failed with: " f"{exc}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Fallback loader for environments without bioio or on failure
|
|
101
|
+
# Only use for non-CZI files
|
|
102
|
+
if suffix == ".czi":
|
|
103
|
+
raise ImportError(
|
|
104
|
+
"CZI file support requires 'bioio' and 'bioio-czi'. "
|
|
105
|
+
"Install with: pip install bioio bioio-czi"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
data = skio.imread(str(path))
|
|
109
|
+
data = np.asarray(data)
|
|
110
|
+
if data.ndim > 2:
|
|
111
|
+
data = np.squeeze(data)
|
|
112
|
+
return data, meta
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _load_with_bioio(path: Path) -> Tuple[np.ndarray, dict]:
|
|
116
|
+
# For CZI files, ensure backend is imported and dependencies are available
|
|
117
|
+
if path.suffix.lower() == ".czi":
|
|
118
|
+
try:
|
|
119
|
+
import bioio_czi # noqa: F401 - ensure backend is registered
|
|
120
|
+
except ImportError:
|
|
121
|
+
raise ImportError(
|
|
122
|
+
"CZI file support requires 'bioio-czi'. Install with:\n"
|
|
123
|
+
" pip install bioio-czi"
|
|
124
|
+
)
|
|
125
|
+
# Check for underlying CZI library dependency
|
|
126
|
+
try:
|
|
127
|
+
import pylibczirw # noqa: F401
|
|
128
|
+
except ImportError:
|
|
129
|
+
try:
|
|
130
|
+
import aicspylibczi # noqa: F401
|
|
131
|
+
except ImportError:
|
|
132
|
+
raise ImportError(
|
|
133
|
+
"bioio-czi requires either 'pylibczirw' or 'aicspylibczi'. "
|
|
134
|
+
"Install with:\n"
|
|
135
|
+
" pip install pylibczirw"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
bio_img = BioImage(path)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
error_msg = str(exc)
|
|
142
|
+
if (
|
|
143
|
+
"Could not find a backend" in error_msg
|
|
144
|
+
or "backend" in error_msg.lower()
|
|
145
|
+
):
|
|
146
|
+
if path.suffix.lower() == ".czi":
|
|
147
|
+
raise ImportError(
|
|
148
|
+
f"Could not load CZI file: {path.name}\n"
|
|
149
|
+
"The bioio-czi backend may not be properly installed or registered.\n"
|
|
150
|
+
"Try reinstalling: pip install --force-reinstall bioio-czi"
|
|
151
|
+
) from exc
|
|
152
|
+
raise ImportError(
|
|
153
|
+
f"Could not load {path.suffix.upper()} file: {path.name}\n"
|
|
154
|
+
f"Required backend not found. Error: {error_msg}"
|
|
155
|
+
) from exc
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
data_array = bio_img.data
|
|
159
|
+
axis_names: Sequence[str] = tuple(getattr(data_array, "dims", ()))
|
|
160
|
+
data = np.asarray(data_array)
|
|
161
|
+
|
|
162
|
+
# We expect images to be 2D+C, with all other axes (T/S/Z) being size 1 (singleton)
|
|
163
|
+
# For non-singleton T/S/Z axes, take first slice only (don't preserve the dimension)
|
|
164
|
+
# For singleton dimensions, keep them as-is (don't squeeze)
|
|
165
|
+
if axis_names:
|
|
166
|
+
axes_to_reduce = ["T", "S", "Z"]
|
|
167
|
+
for axis in axes_to_reduce:
|
|
168
|
+
if axis in axis_names:
|
|
169
|
+
idx = axis_names.index(axis)
|
|
170
|
+
if data.shape[idx] > 1:
|
|
171
|
+
# Take first slice for non-singleton axes (this reduces the dimension)
|
|
172
|
+
data = np.take(data, indices=0, axis=idx)
|
|
173
|
+
axis_names = [ax for ax in axis_names if ax != axis]
|
|
174
|
+
# If size == 1, we keep it as-is (preserve singleton dimensions)
|
|
175
|
+
else:
|
|
176
|
+
# No axis names - if > 3D, take first slice of leading dimensions if > 1
|
|
177
|
+
# This handles cases where we have T/S/Z axes but no axis names
|
|
178
|
+
while data.ndim > 3:
|
|
179
|
+
if data.shape[0] > 1:
|
|
180
|
+
data = np.take(data, indices=0, axis=0)
|
|
181
|
+
else:
|
|
182
|
+
# Already singleton, preserve it
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
# Ensure Y/X are the last two axes following napari conventions
|
|
186
|
+
data, axis_names = _reorder_axes(data, axis_names)
|
|
187
|
+
|
|
188
|
+
# Infer channel axis if not present but data suggests channels
|
|
189
|
+
if not axis_names and data.ndim == 3:
|
|
190
|
+
# No axis info but 3D - if first dimension is small, likely channels
|
|
191
|
+
if data.shape[0] <= 10:
|
|
192
|
+
axis_names = ["C", "Y", "X"]
|
|
193
|
+
else:
|
|
194
|
+
# Otherwise might be Z-stack, but we expect 2D+C, so treat as channels
|
|
195
|
+
axis_names = ["C", "Y", "X"]
|
|
196
|
+
elif not axis_names and data.ndim > 3:
|
|
197
|
+
# > 3D without axis info - infer structure
|
|
198
|
+
# Assume first small dimension is channels
|
|
199
|
+
if data.shape[0] <= 10:
|
|
200
|
+
axis_names = ["C"] + ["?"] * (data.ndim - 3) + ["Y", "X"]
|
|
201
|
+
else:
|
|
202
|
+
# Can't infer reliably, but we expect 2D+C
|
|
203
|
+
axis_names = ["C"] + ["?"] * (data.ndim - 3) + ["Y", "X"]
|
|
204
|
+
|
|
205
|
+
# Set metadata based on detected/inferred structure
|
|
206
|
+
meta: dict = {}
|
|
207
|
+
channel_names_list = None
|
|
208
|
+
if axis_names and "C" in axis_names:
|
|
209
|
+
channel_axis = axis_names.index("C")
|
|
210
|
+
meta["channel_axis"] = channel_axis
|
|
211
|
+
try:
|
|
212
|
+
channel_names_list = list(bio_img.channel_names)
|
|
213
|
+
# Store channel_names in nested metadata dict only
|
|
214
|
+
# (don't store at top-level as napari passes all top-level keys as kwargs)
|
|
215
|
+
meta.setdefault("metadata", {})[
|
|
216
|
+
"channel_names"
|
|
217
|
+
] = channel_names_list
|
|
218
|
+
except Exception: # pragma: no cover - optional metadata
|
|
219
|
+
pass
|
|
220
|
+
elif data.ndim == 3 and data.shape[0] <= 10:
|
|
221
|
+
# Inferred channel axis (when axis_names was missing)
|
|
222
|
+
meta["channel_axis"] = 0
|
|
223
|
+
try:
|
|
224
|
+
channel_names_list = list(bio_img.channel_names)
|
|
225
|
+
# Store channel_names in nested metadata dict only
|
|
226
|
+
# (don't store at top-level as napari passes all top-level keys as kwargs)
|
|
227
|
+
meta.setdefault("metadata", {})[
|
|
228
|
+
"channel_names"
|
|
229
|
+
] = channel_names_list
|
|
230
|
+
except Exception: # pragma: no cover - optional metadata
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
return data.astype(np.float32, copy=False), meta
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _reduce_axes(
|
|
237
|
+
data: np.ndarray,
|
|
238
|
+
axes: List[str],
|
|
239
|
+
axes_to_drop: Sequence[str],
|
|
240
|
+
) -> Tuple[np.ndarray, List[str]]:
|
|
241
|
+
for axis in axes_to_drop:
|
|
242
|
+
if axis in axes and data.shape[axes.index(axis)] > 1:
|
|
243
|
+
idx = axes.index(axis)
|
|
244
|
+
data = np.take(data, indices=0, axis=idx)
|
|
245
|
+
axes.pop(idx)
|
|
246
|
+
elif axis in axes:
|
|
247
|
+
idx = axes.index(axis)
|
|
248
|
+
data = np.squeeze(data, axis=idx)
|
|
249
|
+
axes.pop(idx)
|
|
250
|
+
return data, axes
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _reorder_axes(
|
|
254
|
+
data: np.ndarray, axes: Sequence[str]
|
|
255
|
+
) -> Tuple[np.ndarray, List[str]]:
|
|
256
|
+
axes = list(axes)
|
|
257
|
+
if not axes:
|
|
258
|
+
return data, axes
|
|
259
|
+
|
|
260
|
+
target_order = [ax for ax in axes if ax not in ("Y", "X")]
|
|
261
|
+
target_order.extend(ax for ax in ("Y", "X") if ax in axes)
|
|
262
|
+
|
|
263
|
+
if target_order == axes:
|
|
264
|
+
return data, axes
|
|
265
|
+
|
|
266
|
+
permutation = [axes.index(ax) for ax in target_order]
|
|
267
|
+
data = np.transpose(data, permutation)
|
|
268
|
+
return data, target_order
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.0.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|