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.
@@ -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