senoquant 1.0.0b1__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.
- senoquant/__init__.py +6 -0
- senoquant/_reader.py +7 -0
- senoquant/_widget.py +33 -0
- senoquant/napari.yaml +83 -0
- senoquant/reader/__init__.py +5 -0
- senoquant/reader/core.py +369 -0
- senoquant/tabs/__init__.py +15 -0
- senoquant/tabs/batch/__init__.py +10 -0
- senoquant/tabs/batch/backend.py +641 -0
- senoquant/tabs/batch/config.py +270 -0
- senoquant/tabs/batch/frontend.py +1283 -0
- senoquant/tabs/batch/io.py +326 -0
- senoquant/tabs/batch/layers.py +86 -0
- senoquant/tabs/quantification/__init__.py +1 -0
- senoquant/tabs/quantification/backend.py +228 -0
- senoquant/tabs/quantification/features/__init__.py +80 -0
- senoquant/tabs/quantification/features/base.py +142 -0
- senoquant/tabs/quantification/features/marker/__init__.py +5 -0
- senoquant/tabs/quantification/features/marker/config.py +69 -0
- senoquant/tabs/quantification/features/marker/dialog.py +437 -0
- senoquant/tabs/quantification/features/marker/export.py +879 -0
- senoquant/tabs/quantification/features/marker/feature.py +119 -0
- senoquant/tabs/quantification/features/marker/morphology.py +285 -0
- senoquant/tabs/quantification/features/marker/rows.py +654 -0
- senoquant/tabs/quantification/features/marker/thresholding.py +46 -0
- senoquant/tabs/quantification/features/roi.py +346 -0
- senoquant/tabs/quantification/features/spots/__init__.py +5 -0
- senoquant/tabs/quantification/features/spots/config.py +62 -0
- senoquant/tabs/quantification/features/spots/dialog.py +477 -0
- senoquant/tabs/quantification/features/spots/export.py +1292 -0
- senoquant/tabs/quantification/features/spots/feature.py +112 -0
- senoquant/tabs/quantification/features/spots/morphology.py +279 -0
- senoquant/tabs/quantification/features/spots/rows.py +241 -0
- senoquant/tabs/quantification/frontend.py +815 -0
- senoquant/tabs/segmentation/__init__.py +1 -0
- senoquant/tabs/segmentation/backend.py +131 -0
- senoquant/tabs/segmentation/frontend.py +1009 -0
- senoquant/tabs/segmentation/models/__init__.py +5 -0
- senoquant/tabs/segmentation/models/base.py +146 -0
- senoquant/tabs/segmentation/models/cpsam/details.json +65 -0
- senoquant/tabs/segmentation/models/cpsam/model.py +150 -0
- senoquant/tabs/segmentation/models/default_2d/details.json +69 -0
- senoquant/tabs/segmentation/models/default_2d/model.py +664 -0
- senoquant/tabs/segmentation/models/default_3d/details.json +69 -0
- senoquant/tabs/segmentation/models/default_3d/model.py +682 -0
- senoquant/tabs/segmentation/models/hf.py +71 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/__init__.py +1 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/details.json +26 -0
- senoquant/tabs/segmentation/models/nuclear_dilation/model.py +96 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/__init__.py +1 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/details.json +34 -0
- senoquant/tabs/segmentation/models/perinuclear_rings/model.py +132 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/__init__.py +2 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/__init__.py +3 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/__init__.py +6 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/generate.py +470 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/prepare.py +273 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/rawdata.py +112 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/transform.py +384 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/blocks.py +184 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/losses.py +79 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/nets.py +165 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/predict.py +467 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/probability.py +67 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/train.py +148 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/io/__init__.py +163 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/__init__.py +52 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/base_model.py +329 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_isotropic.py +160 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_projection.py +178 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_standard.py +446 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_upsampling.py +54 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/config.py +254 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/pretrained.py +119 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/care_predict.py +180 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/__init__.py +5 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/plot_utils.py +159 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/six.py +18 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/tf.py +644 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/utils.py +272 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/version.py +1 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/docs/source/conf.py +368 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/setup.py +68 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_datagen.py +169 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_models.py +462 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_utils.py +166 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +34 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/__init__.py +30 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/big.py +624 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/bioimageio_utils.py +494 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/data/__init__.py +39 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/__init__.py +10 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom2d.py +215 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom3d.py +349 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/matching.py +483 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/__init__.py +28 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/base.py +1217 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model2d.py +594 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model3d.py +696 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/nms.py +384 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/__init__.py +2 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/plot.py +74 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/render.py +298 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/rays3d.py +373 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/sample_patches.py +65 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/__init__.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict2d.py +90 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict3d.py +93 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/utils.py +408 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/version.py +1 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/__init__.py +45 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/__init__.py +17 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/cli.py +55 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/core.py +285 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/__init__.py +15 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/cli.py +36 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/divisibility.py +193 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +100 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/receptive_field.py +182 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/rf_cli.py +48 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/valid_sizes.py +278 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/__init__.py +8 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/core.py +157 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/__init__.py +17 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/core.py +226 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/__init__.py +5 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/core.py +401 -0
- senoquant/tabs/settings/__init__.py +1 -0
- senoquant/tabs/settings/backend.py +29 -0
- senoquant/tabs/settings/frontend.py +19 -0
- senoquant/tabs/spots/__init__.py +1 -0
- senoquant/tabs/spots/backend.py +139 -0
- senoquant/tabs/spots/frontend.py +800 -0
- senoquant/tabs/spots/models/__init__.py +5 -0
- senoquant/tabs/spots/models/base.py +94 -0
- senoquant/tabs/spots/models/rmp/details.json +61 -0
- senoquant/tabs/spots/models/rmp/model.py +499 -0
- senoquant/tabs/spots/models/udwt/details.json +103 -0
- senoquant/tabs/spots/models/udwt/model.py +482 -0
- senoquant/utils.py +25 -0
- senoquant-1.0.0b1.dist-info/METADATA +193 -0
- senoquant-1.0.0b1.dist-info/RECORD +148 -0
- senoquant-1.0.0b1.dist-info/WHEEL +5 -0
- senoquant-1.0.0b1.dist-info/entry_points.txt +2 -0
- senoquant-1.0.0b1.dist-info/licenses/LICENSE +28 -0
- senoquant-1.0.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""I/O helpers for batch processing.
|
|
2
|
+
|
|
3
|
+
This module provides filesystem and image-loading utilities used by the
|
|
4
|
+
batch backend. Functions are intentionally stateless and easy to mock in
|
|
5
|
+
tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Iterable
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from senoquant.reader import core as reader_core
|
|
16
|
+
from .config import BatchChannelConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_extensions(extensions: Iterable[str] | None) -> set[str] | None:
|
|
20
|
+
"""Normalize extension list to lowercase with leading dots.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
extensions : iterable of str or None
|
|
25
|
+
Raw extension strings (with or without dots).
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
set of str or None
|
|
30
|
+
Normalized extensions or None when no filtering is requested.
|
|
31
|
+
"""
|
|
32
|
+
if extensions is None:
|
|
33
|
+
return None
|
|
34
|
+
normalized = set()
|
|
35
|
+
for ext in extensions:
|
|
36
|
+
if not ext:
|
|
37
|
+
continue
|
|
38
|
+
cleaned = ext.strip().lower()
|
|
39
|
+
if not cleaned:
|
|
40
|
+
continue
|
|
41
|
+
if not cleaned.startswith("."):
|
|
42
|
+
cleaned = f".{cleaned}"
|
|
43
|
+
normalized.add(cleaned)
|
|
44
|
+
return normalized or None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def iter_input_files(
|
|
48
|
+
root: Path, extensions: set[str] | None, include_subfolders: bool
|
|
49
|
+
) -> Iterable[Path]:
|
|
50
|
+
"""Yield input files from a root folder.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
root : Path
|
|
55
|
+
Directory to scan.
|
|
56
|
+
extensions : set of str or None
|
|
57
|
+
Allowed file extensions. None disables filtering.
|
|
58
|
+
include_subfolders : bool
|
|
59
|
+
Whether to scan subfolders recursively.
|
|
60
|
+
|
|
61
|
+
Yields
|
|
62
|
+
------
|
|
63
|
+
Path
|
|
64
|
+
File paths that match the extension criteria.
|
|
65
|
+
"""
|
|
66
|
+
if not root.exists():
|
|
67
|
+
return
|
|
68
|
+
iterator = root.rglob("*") if include_subfolders else root.iterdir()
|
|
69
|
+
for path in iterator:
|
|
70
|
+
if not path.is_file():
|
|
71
|
+
continue
|
|
72
|
+
if extensions is None:
|
|
73
|
+
yield path
|
|
74
|
+
continue
|
|
75
|
+
name = path.name.lower()
|
|
76
|
+
if any(name.endswith(ext) for ext in extensions):
|
|
77
|
+
yield path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def basename_for_path(path: Path) -> str:
|
|
81
|
+
"""Return a filesystem-friendly base name for a file path.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
path : Path
|
|
86
|
+
Input file path.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
str
|
|
91
|
+
Base name with common microscopy extensions removed.
|
|
92
|
+
"""
|
|
93
|
+
name = path.name
|
|
94
|
+
lowered = name.lower()
|
|
95
|
+
for ext in (".ome.tiff", ".ome.tif", ".tiff", ".tif"):
|
|
96
|
+
if lowered.endswith(ext):
|
|
97
|
+
return name[: -len(ext)]
|
|
98
|
+
if "." in name:
|
|
99
|
+
return name.rsplit(".", 1)[0]
|
|
100
|
+
return name
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def safe_scene_dir(scene_id: str) -> str:
|
|
104
|
+
"""Return a sanitized scene identifier for folder naming.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
scene_id : str
|
|
109
|
+
Scene identifier from BioIO.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
str
|
|
114
|
+
Filesystem-safe scene folder name.
|
|
115
|
+
"""
|
|
116
|
+
safe = scene_id.strip().replace("/", "_").replace("\\", "_")
|
|
117
|
+
return safe or "scene"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_array(
|
|
121
|
+
output_dir: Path, name: str, data: np.ndarray, output_format: str
|
|
122
|
+
) -> Path:
|
|
123
|
+
"""Write an array to disk in the requested format.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
output_dir : Path
|
|
128
|
+
Destination folder.
|
|
129
|
+
name : str
|
|
130
|
+
Base name for the output file.
|
|
131
|
+
data : numpy.ndarray
|
|
132
|
+
Array data to serialize.
|
|
133
|
+
output_format : str
|
|
134
|
+
Output format (``"tif"`` or ``"npy"``).
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
Path
|
|
139
|
+
Path to the written file.
|
|
140
|
+
"""
|
|
141
|
+
output_format = output_format.lower().strip()
|
|
142
|
+
if output_format == "npy":
|
|
143
|
+
path = output_dir / f"{name}.npy"
|
|
144
|
+
np.save(path, data)
|
|
145
|
+
return path
|
|
146
|
+
|
|
147
|
+
path = output_dir / f"{name}.tif"
|
|
148
|
+
try:
|
|
149
|
+
import tifffile
|
|
150
|
+
|
|
151
|
+
tifffile.imwrite(str(path), data)
|
|
152
|
+
return path
|
|
153
|
+
except Exception:
|
|
154
|
+
fallback = output_dir / f"{name}.npy"
|
|
155
|
+
np.save(fallback, data)
|
|
156
|
+
return fallback
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_channel_index(
|
|
160
|
+
choice: str | int | None,
|
|
161
|
+
channel_map: list[BatchChannelConfig],
|
|
162
|
+
) -> int:
|
|
163
|
+
"""Resolve a channel selection into a numeric index.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
choice : str or int or None
|
|
168
|
+
Channel selection from the UI (name or index).
|
|
169
|
+
channel_map : list of BatchChannelConfig
|
|
170
|
+
Mapping from names to indices.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
int
|
|
175
|
+
Resolved channel index.
|
|
176
|
+
|
|
177
|
+
Raises
|
|
178
|
+
------
|
|
179
|
+
ValueError
|
|
180
|
+
If the selection is missing or unknown.
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(choice, int):
|
|
183
|
+
return choice
|
|
184
|
+
if choice is None:
|
|
185
|
+
raise ValueError("Channel selection is required.")
|
|
186
|
+
text = str(choice).strip()
|
|
187
|
+
if not text:
|
|
188
|
+
raise ValueError("Channel selection is required.")
|
|
189
|
+
if text.isdigit():
|
|
190
|
+
return int(text)
|
|
191
|
+
for channel in channel_map:
|
|
192
|
+
if channel.name == text:
|
|
193
|
+
return channel.index
|
|
194
|
+
raise ValueError(f"Unknown channel selection: {text}.")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def spot_label_name(
|
|
198
|
+
choice: str | int,
|
|
199
|
+
channel_map: list[BatchChannelConfig],
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Build the output label name for a spot channel.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
choice : str or int
|
|
206
|
+
Channel selection.
|
|
207
|
+
channel_map : list of BatchChannelConfig
|
|
208
|
+
Channel mapping list for name lookup.
|
|
209
|
+
|
|
210
|
+
Returns
|
|
211
|
+
-------
|
|
212
|
+
str
|
|
213
|
+
Standardized spot label name.
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(choice, int):
|
|
216
|
+
name = str(choice)
|
|
217
|
+
else:
|
|
218
|
+
name = str(choice).strip()
|
|
219
|
+
if name.isdigit():
|
|
220
|
+
return f"spot_labels_{name}"
|
|
221
|
+
for channel in channel_map:
|
|
222
|
+
if channel.name == name:
|
|
223
|
+
name = channel.name
|
|
224
|
+
break
|
|
225
|
+
return f"spot_labels_{sanitize_label(name)}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sanitize_label(name: str) -> str:
|
|
229
|
+
"""Sanitize a label name for filesystem use."""
|
|
230
|
+
safe = []
|
|
231
|
+
for char in name.strip():
|
|
232
|
+
if char.isalnum():
|
|
233
|
+
safe.append(char)
|
|
234
|
+
else:
|
|
235
|
+
safe.append("_")
|
|
236
|
+
result = "".join(safe).strip("_")
|
|
237
|
+
return result or "spots"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def load_channel_data(
|
|
241
|
+
path: Path,
|
|
242
|
+
channel_index: int,
|
|
243
|
+
scene_id: str | None,
|
|
244
|
+
) -> tuple[np.ndarray | None, dict]:
|
|
245
|
+
"""Load a single-channel image array for the given path.
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
path : Path
|
|
250
|
+
Input file path.
|
|
251
|
+
channel_index : int
|
|
252
|
+
Channel index to extract.
|
|
253
|
+
scene_id : str or None
|
|
254
|
+
Optional scene identifier.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
tuple of (numpy.ndarray or None, dict)
|
|
259
|
+
The extracted image data and metadata.
|
|
260
|
+
"""
|
|
261
|
+
image = reader_core._open_bioimage(str(path))
|
|
262
|
+
try:
|
|
263
|
+
if scene_id:
|
|
264
|
+
image.set_scene(scene_id)
|
|
265
|
+
metadata = {"physical_pixel_sizes": reader_core._physical_pixel_sizes(image)}
|
|
266
|
+
axes_present = reader_core._axes_present(image)
|
|
267
|
+
dims = getattr(image, "dims", None)
|
|
268
|
+
c_size = getattr(dims, "C", 1) if "C" in axes_present else 1
|
|
269
|
+
z_size = getattr(dims, "Z", 1) if "Z" in axes_present else 1
|
|
270
|
+
|
|
271
|
+
if c_size > 1:
|
|
272
|
+
order = "CZYX" if z_size > 1 else "CYX"
|
|
273
|
+
else:
|
|
274
|
+
order = "ZYX" if z_size > 1 else "YX"
|
|
275
|
+
|
|
276
|
+
kwargs: dict[str, int] = {}
|
|
277
|
+
if "T" in axes_present and "T" not in order:
|
|
278
|
+
kwargs["T"] = 0
|
|
279
|
+
if "C" in axes_present and "C" not in order:
|
|
280
|
+
kwargs["C"] = 0
|
|
281
|
+
if "Z" in axes_present and "Z" not in order:
|
|
282
|
+
kwargs["Z"] = 0
|
|
283
|
+
|
|
284
|
+
data = image.get_image_data(order, **kwargs)
|
|
285
|
+
if c_size > 1:
|
|
286
|
+
if channel_index >= c_size or channel_index < 0:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Channel index {channel_index} out of range for {path.name}."
|
|
289
|
+
)
|
|
290
|
+
data = data[channel_index]
|
|
291
|
+
return np.asarray(data), metadata
|
|
292
|
+
finally:
|
|
293
|
+
if hasattr(image, "close"):
|
|
294
|
+
try:
|
|
295
|
+
image.close()
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def list_scenes(path: Path) -> list[str]:
|
|
302
|
+
"""Return scene identifiers for a BioIO image path.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
path : Path
|
|
307
|
+
Input file path.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
list of str
|
|
312
|
+
Scene identifiers, or an empty list if unavailable.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
image = reader_core._open_bioimage(str(path))
|
|
316
|
+
except Exception:
|
|
317
|
+
return []
|
|
318
|
+
try:
|
|
319
|
+
scenes = list(getattr(image, "scenes", []) or [])
|
|
320
|
+
finally:
|
|
321
|
+
if hasattr(image, "close"):
|
|
322
|
+
try:
|
|
323
|
+
image.close()
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
return scenes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Lightweight layer shims used for batch processing.
|
|
2
|
+
|
|
3
|
+
These classes emulate the minimal attributes used by feature exporters
|
|
4
|
+
and quantification routines, without requiring a live napari viewer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Iterable
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
class Image:
|
|
14
|
+
"""Lightweight image layer placeholder.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
data : numpy.ndarray or None
|
|
19
|
+
Image data array.
|
|
20
|
+
name : str
|
|
21
|
+
Layer name.
|
|
22
|
+
metadata : dict or None, optional
|
|
23
|
+
Metadata dictionary (e.g., pixel sizes).
|
|
24
|
+
rgb : bool, optional
|
|
25
|
+
Whether the layer should be treated as RGB.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
data: np.ndarray | None,
|
|
31
|
+
name: str,
|
|
32
|
+
metadata: dict | None = None,
|
|
33
|
+
rgb: bool = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
self.data = data
|
|
36
|
+
self.name = name
|
|
37
|
+
self.metadata = metadata or {}
|
|
38
|
+
self.rgb = rgb
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Labels:
|
|
42
|
+
"""Lightweight labels layer placeholder.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
data : numpy.ndarray or None
|
|
47
|
+
Label image data.
|
|
48
|
+
name : str
|
|
49
|
+
Layer name.
|
|
50
|
+
metadata : dict or None, optional
|
|
51
|
+
Metadata dictionary (e.g., pixel sizes).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
data: np.ndarray | None,
|
|
57
|
+
name: str,
|
|
58
|
+
metadata: dict | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.data = data
|
|
61
|
+
self.name = name
|
|
62
|
+
self.metadata = metadata or {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BatchViewer:
|
|
66
|
+
"""Minimal viewer shim exposing layers for export routines.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
layers : iterable of object or None, optional
|
|
71
|
+
Initial layer collection.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, layers: Iterable[object] | None = None) -> None:
|
|
75
|
+
"""Initialize the viewer shim."""
|
|
76
|
+
self.layers = list(layers) if layers is not None else []
|
|
77
|
+
|
|
78
|
+
def set_layers(self, layers: Iterable[object]) -> None:
|
|
79
|
+
"""Replace the current layer collection.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
layers : iterable of object
|
|
84
|
+
New layer collection.
|
|
85
|
+
"""
|
|
86
|
+
self.layers = list(layers)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Quantification tab modules."""
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Backend logic for the Quantification tab."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
from .features import FeatureConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FeatureExportResult:
|
|
16
|
+
"""Output metadata for a single feature export.
|
|
17
|
+
|
|
18
|
+
Attributes
|
|
19
|
+
----------
|
|
20
|
+
feature_id : str
|
|
21
|
+
Stable identifier for the exported feature instance.
|
|
22
|
+
feature_type : str
|
|
23
|
+
Feature type name used for routing (e.g., ``"Markers"``).
|
|
24
|
+
feature_name : str
|
|
25
|
+
Display name provided by the user.
|
|
26
|
+
temp_dir : Path
|
|
27
|
+
Temporary directory where the feature wrote its outputs.
|
|
28
|
+
outputs : list of Path
|
|
29
|
+
Explicit file paths returned by the feature processor.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
feature_id: str
|
|
33
|
+
feature_type: str
|
|
34
|
+
feature_name: str
|
|
35
|
+
temp_dir: Path
|
|
36
|
+
outputs: list[Path] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class QuantificationResult:
|
|
41
|
+
"""Aggregated output information for a quantification run.
|
|
42
|
+
|
|
43
|
+
Attributes
|
|
44
|
+
----------
|
|
45
|
+
output_root : Path
|
|
46
|
+
Root output directory for the run.
|
|
47
|
+
temp_root : Path
|
|
48
|
+
Temporary root directory used during processing.
|
|
49
|
+
feature_outputs : list of FeatureExportResult
|
|
50
|
+
Per-feature export metadata for the run.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
output_root: Path
|
|
54
|
+
temp_root: Path
|
|
55
|
+
feature_outputs: list[FeatureExportResult]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class QuantificationBackend:
|
|
59
|
+
"""Backend orchestrator for quantification exports.
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
Feature export routines live with their feature implementations. The
|
|
64
|
+
backend iterates through configured feature contexts, asks each feature
|
|
65
|
+
handler to export into a temporary directory, and then routes those
|
|
66
|
+
outputs into a final output structure.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self) -> None:
|
|
70
|
+
"""Initialize the backend state.
|
|
71
|
+
|
|
72
|
+
Attributes
|
|
73
|
+
----------
|
|
74
|
+
metrics : list
|
|
75
|
+
Placeholder container for computed metrics.
|
|
76
|
+
"""
|
|
77
|
+
self.metrics: list[object] = []
|
|
78
|
+
|
|
79
|
+
def process(
|
|
80
|
+
self,
|
|
81
|
+
features: Iterable[object],
|
|
82
|
+
output_path: str,
|
|
83
|
+
output_name: str,
|
|
84
|
+
export_format: str,
|
|
85
|
+
cleanup: bool = True,
|
|
86
|
+
) -> QuantificationResult:
|
|
87
|
+
"""Run feature exports and route their outputs.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
features : iterable of object
|
|
92
|
+
Feature UI contexts with ``state`` and ``feature_handler``.
|
|
93
|
+
Each handler should implement ``export(temp_dir, export_format)``.
|
|
94
|
+
output_path : str
|
|
95
|
+
Base output folder path.
|
|
96
|
+
output_name : str
|
|
97
|
+
Folder name used to group exported outputs.
|
|
98
|
+
export_format : str
|
|
99
|
+
File format requested by the user (``"csv"`` or ``"xlsx"``).
|
|
100
|
+
cleanup : bool, optional
|
|
101
|
+
Whether to delete temporary export folders after routing.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
QuantificationResult
|
|
106
|
+
Output metadata for the completed run.
|
|
107
|
+
|
|
108
|
+
Notes
|
|
109
|
+
-----
|
|
110
|
+
If a feature export does not return explicit output paths, the backend
|
|
111
|
+
will move all files found in the feature's temp directory. This allows
|
|
112
|
+
feature implementations to either return specific files or simply write
|
|
113
|
+
into the provided temporary directory.
|
|
114
|
+
"""
|
|
115
|
+
output_root = self._resolve_output_root(output_path, output_name)
|
|
116
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
temp_root = Path(tempfile.mkdtemp(prefix="senoquant-quant-"))
|
|
118
|
+
|
|
119
|
+
feature_outputs: list[FeatureExportResult] = []
|
|
120
|
+
for context in features:
|
|
121
|
+
feature = getattr(context, "state", None)
|
|
122
|
+
handler = getattr(context, "feature_handler", None)
|
|
123
|
+
if not isinstance(feature, FeatureConfig):
|
|
124
|
+
continue
|
|
125
|
+
temp_dir = temp_root / feature.feature_id
|
|
126
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
outputs: list[Path] = []
|
|
128
|
+
if handler is not None and hasattr(handler, "export"):
|
|
129
|
+
outputs = [
|
|
130
|
+
Path(path)
|
|
131
|
+
for path in handler.export(temp_dir, export_format)
|
|
132
|
+
]
|
|
133
|
+
feature_outputs.append(
|
|
134
|
+
FeatureExportResult(
|
|
135
|
+
feature_id=feature.feature_id,
|
|
136
|
+
feature_type=feature.type_name,
|
|
137
|
+
feature_name=feature.name,
|
|
138
|
+
temp_dir=temp_dir,
|
|
139
|
+
outputs=outputs,
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self._route_feature_outputs(output_root, feature_outputs)
|
|
144
|
+
if cleanup:
|
|
145
|
+
shutil.rmtree(temp_root, ignore_errors=True)
|
|
146
|
+
return QuantificationResult(
|
|
147
|
+
output_root=output_root,
|
|
148
|
+
temp_root=temp_root,
|
|
149
|
+
feature_outputs=feature_outputs,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def _resolve_output_root(self, output_path: str, output_name: str) -> Path:
|
|
153
|
+
"""Resolve the final output root directory.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
output_path : str
|
|
158
|
+
Base output folder path.
|
|
159
|
+
output_name : str
|
|
160
|
+
Folder name used to group exported outputs.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
Path
|
|
165
|
+
Resolved output directory path.
|
|
166
|
+
"""
|
|
167
|
+
base = Path(output_path) if output_path else Path.cwd()
|
|
168
|
+
if output_name:
|
|
169
|
+
return base / output_name
|
|
170
|
+
return base
|
|
171
|
+
|
|
172
|
+
def _route_feature_outputs(
|
|
173
|
+
self,
|
|
174
|
+
output_root: Path,
|
|
175
|
+
feature_outputs: Iterable[FeatureExportResult],
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Move feature outputs from temp folders to the final location.
|
|
178
|
+
|
|
179
|
+
Parameters
|
|
180
|
+
----------
|
|
181
|
+
output_root : Path
|
|
182
|
+
Destination root folder.
|
|
183
|
+
feature_outputs : iterable of FeatureExportResult
|
|
184
|
+
Export results to route.
|
|
185
|
+
|
|
186
|
+
Notes
|
|
187
|
+
-----
|
|
188
|
+
When a feature returns no explicit output list, all files present
|
|
189
|
+
in the temporary directory are routed instead. Subdirectories are
|
|
190
|
+
not traversed.
|
|
191
|
+
"""
|
|
192
|
+
for feature_output in feature_outputs:
|
|
193
|
+
feature_dir = output_root / self._feature_dir_name(feature_output)
|
|
194
|
+
feature_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
outputs = feature_output.outputs
|
|
196
|
+
if outputs:
|
|
197
|
+
for path in outputs:
|
|
198
|
+
if path.exists():
|
|
199
|
+
shutil.move(str(path), feature_dir / path.name)
|
|
200
|
+
else:
|
|
201
|
+
for path in feature_output.temp_dir.glob("*"):
|
|
202
|
+
if path.is_file():
|
|
203
|
+
shutil.move(str(path), feature_dir / path.name)
|
|
204
|
+
|
|
205
|
+
def _feature_dir_name(self, feature_output: FeatureExportResult) -> str:
|
|
206
|
+
"""Build a filesystem-friendly folder name for a feature.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
feature_output : FeatureExportResult
|
|
211
|
+
Export result metadata.
|
|
212
|
+
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
str
|
|
216
|
+
Directory name for the feature outputs.
|
|
217
|
+
|
|
218
|
+
Notes
|
|
219
|
+
-----
|
|
220
|
+
Non-alphanumeric characters are replaced to avoid filesystem issues.
|
|
221
|
+
"""
|
|
222
|
+
name = feature_output.feature_name.strip()
|
|
223
|
+
if not name:
|
|
224
|
+
name = feature_output.feature_type
|
|
225
|
+
safe = "".join(
|
|
226
|
+
char if char.isalnum() or char in "-_ " else "_" for char in name
|
|
227
|
+
)
|
|
228
|
+
return safe.replace(" ", "_").lower()
|