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,641 @@
|
|
|
1
|
+
"""Batch processing backend.
|
|
2
|
+
|
|
3
|
+
This module coordinates per-image batch processing for segmentation,
|
|
4
|
+
spot detection, and quantification. It provides a single entry point
|
|
5
|
+
(`BatchBackend.run_job`) that consumes a :class:`BatchJobConfig` and
|
|
6
|
+
produces a :class:`BatchSummary` describing outputs and errors.
|
|
7
|
+
|
|
8
|
+
The batch run flow is:
|
|
9
|
+
|
|
10
|
+
1. Normalize input extensions and discover files.
|
|
11
|
+
2. Resolve channel mapping for named channels.
|
|
12
|
+
3. For each file (and each scene, if enabled):
|
|
13
|
+
a. Optionally run nuclear segmentation.
|
|
14
|
+
b. Optionally run cytoplasmic segmentation.
|
|
15
|
+
c. Optionally run spot detection for selected channels.
|
|
16
|
+
d. Optionally run quantification using a temporary viewer shim.
|
|
17
|
+
4. Persist mask outputs and quantification results.
|
|
18
|
+
|
|
19
|
+
Notes
|
|
20
|
+
-----
|
|
21
|
+
This backend is intentionally UI-agnostic. UI widgets build a
|
|
22
|
+
``BatchJobConfig`` and pass it here for execution.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Iterable
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from senoquant.tabs.quantification.backend import QuantificationBackend
|
|
34
|
+
from senoquant.tabs.segmentation.backend import SegmentationBackend
|
|
35
|
+
from senoquant.tabs.spots.backend import SpotsBackend
|
|
36
|
+
from senoquant.tabs.spots.frontend import _filter_labels_by_size
|
|
37
|
+
|
|
38
|
+
from .config import BatchChannelConfig, BatchJobConfig
|
|
39
|
+
from .layers import BatchViewer, Image, Labels
|
|
40
|
+
from .io import (
|
|
41
|
+
basename_for_path,
|
|
42
|
+
iter_input_files,
|
|
43
|
+
load_channel_data,
|
|
44
|
+
list_scenes,
|
|
45
|
+
normalize_extensions,
|
|
46
|
+
resolve_channel_index,
|
|
47
|
+
safe_scene_dir,
|
|
48
|
+
write_array,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class BatchItemResult:
|
|
54
|
+
"""Result metadata for a single processed image.
|
|
55
|
+
|
|
56
|
+
Attributes
|
|
57
|
+
----------
|
|
58
|
+
path : Path
|
|
59
|
+
Input file path.
|
|
60
|
+
scene_id : str or None
|
|
61
|
+
Scene identifier for multi-scene files.
|
|
62
|
+
outputs : dict of str to Path
|
|
63
|
+
Mapping of output labels to written files.
|
|
64
|
+
errors : list of str
|
|
65
|
+
Collected error messages for this item.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
path: Path
|
|
69
|
+
scene_id: str | None
|
|
70
|
+
outputs: dict[str, Path] = field(default_factory=dict)
|
|
71
|
+
errors: list[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(slots=True)
|
|
75
|
+
class BatchSummary:
|
|
76
|
+
"""Aggregated results for a batch run.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
input_root : Path
|
|
81
|
+
Root input directory.
|
|
82
|
+
output_root : Path
|
|
83
|
+
Root output directory.
|
|
84
|
+
processed : int
|
|
85
|
+
Number of successfully processed items.
|
|
86
|
+
skipped : int
|
|
87
|
+
Number of skipped items.
|
|
88
|
+
failed : int
|
|
89
|
+
Number of failed items.
|
|
90
|
+
results : list of BatchItemResult
|
|
91
|
+
Per-item metadata for the run.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
input_root: Path
|
|
95
|
+
output_root: Path
|
|
96
|
+
processed: int
|
|
97
|
+
skipped: int
|
|
98
|
+
failed: int
|
|
99
|
+
results: list[BatchItemResult]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class BatchBackend:
|
|
103
|
+
"""Backend for batch segmentation and spot detection workflows."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
segmentation_backend: SegmentationBackend | None = None,
|
|
108
|
+
spots_backend: SpotsBackend | None = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Initialize the backend.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
segmentation_backend : SegmentationBackend or None, optional
|
|
115
|
+
Backend used to resolve segmentation models. A default
|
|
116
|
+
instance is created when omitted.
|
|
117
|
+
spots_backend : SpotsBackend or None, optional
|
|
118
|
+
Backend used to resolve spot detection models. A default
|
|
119
|
+
instance is created when omitted.
|
|
120
|
+
"""
|
|
121
|
+
self._segmentation_backend = segmentation_backend or SegmentationBackend()
|
|
122
|
+
self._spots_backend = spots_backend or SpotsBackend()
|
|
123
|
+
|
|
124
|
+
def run_job(self, job: BatchJobConfig) -> BatchSummary:
|
|
125
|
+
"""Run a batch job using a configuration object.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
job : BatchJobConfig
|
|
130
|
+
Fully-populated batch configuration.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
BatchSummary
|
|
135
|
+
Summary of the batch run (counts + per-item metadata).
|
|
136
|
+
"""
|
|
137
|
+
return self.process_folder(
|
|
138
|
+
job.input_path,
|
|
139
|
+
job.output_path,
|
|
140
|
+
channel_map=job.channel_map,
|
|
141
|
+
nuclear_model=job.nuclear.model if job.nuclear.enabled else None,
|
|
142
|
+
nuclear_channel=job.nuclear.channel or None,
|
|
143
|
+
nuclear_settings=job.nuclear.settings,
|
|
144
|
+
cyto_model=job.cytoplasmic.model if job.cytoplasmic.enabled else None,
|
|
145
|
+
cyto_channel=job.cytoplasmic.channel or None,
|
|
146
|
+
cyto_nuclear_channel=job.cytoplasmic.nuclear_channel or None,
|
|
147
|
+
cyto_settings=job.cytoplasmic.settings,
|
|
148
|
+
spot_detector=job.spots.detector if job.spots.enabled else None,
|
|
149
|
+
spot_channels=job.spots.channels,
|
|
150
|
+
spot_settings=job.spots.settings,
|
|
151
|
+
spot_min_size=job.spots.min_size,
|
|
152
|
+
spot_max_size=job.spots.max_size,
|
|
153
|
+
quantification_features=job.quantification.features,
|
|
154
|
+
quantification_format=job.quantification.format,
|
|
155
|
+
extensions=job.extensions,
|
|
156
|
+
include_subfolders=job.include_subfolders,
|
|
157
|
+
output_format=job.output_format,
|
|
158
|
+
overwrite=job.overwrite,
|
|
159
|
+
process_all_scenes=job.process_all_scenes,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def process_folder(
|
|
163
|
+
self,
|
|
164
|
+
input_path: str,
|
|
165
|
+
output_path: str,
|
|
166
|
+
*,
|
|
167
|
+
channel_map: Iterable[BatchChannelConfig | dict] | None = None,
|
|
168
|
+
nuclear_model: str | None = None,
|
|
169
|
+
nuclear_channel: str | int | None = None,
|
|
170
|
+
nuclear_settings: dict | None = None,
|
|
171
|
+
cyto_model: str | None = None,
|
|
172
|
+
cyto_channel: str | int | None = None,
|
|
173
|
+
cyto_nuclear_channel: str | int | None = None,
|
|
174
|
+
cyto_settings: dict | None = None,
|
|
175
|
+
spot_detector: str | None = None,
|
|
176
|
+
spot_channels: Iterable[str | int] | None = None,
|
|
177
|
+
spot_settings: dict | None = None,
|
|
178
|
+
spot_min_size: int = 0,
|
|
179
|
+
spot_max_size: int = 0,
|
|
180
|
+
quantification_features: Iterable[object] | None = None,
|
|
181
|
+
quantification_format: str = "xlsx",
|
|
182
|
+
quantification_tab: object | None = None,
|
|
183
|
+
extensions: Iterable[str] | None = None,
|
|
184
|
+
include_subfolders: bool = False,
|
|
185
|
+
output_format: str = "tif",
|
|
186
|
+
overwrite: bool = False,
|
|
187
|
+
process_all_scenes: bool = False,
|
|
188
|
+
progress_callback: callable | None = None,
|
|
189
|
+
) -> BatchSummary:
|
|
190
|
+
"""Run batch processing on a folder of images.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
input_path : str
|
|
195
|
+
Folder containing input images.
|
|
196
|
+
output_path : str
|
|
197
|
+
Folder where outputs should be written.
|
|
198
|
+
channel_map : iterable of BatchChannelConfig or dict, optional
|
|
199
|
+
Mapping from channel names to indices.
|
|
200
|
+
nuclear_model : str or None, optional
|
|
201
|
+
Segmentation model name for nuclei.
|
|
202
|
+
nuclear_channel : str or int or None, optional
|
|
203
|
+
Channel selection for nuclei.
|
|
204
|
+
nuclear_settings : dict or None, optional
|
|
205
|
+
Model settings for nuclear segmentation.
|
|
206
|
+
cyto_model : str or None, optional
|
|
207
|
+
Segmentation model name for cytoplasm.
|
|
208
|
+
cyto_channel : str or int or None, optional
|
|
209
|
+
Channel selection for cytoplasm.
|
|
210
|
+
cyto_nuclear_channel : str or int or None, optional
|
|
211
|
+
Optional nuclear channel used by cytoplasmic models.
|
|
212
|
+
cyto_settings : dict or None, optional
|
|
213
|
+
Model settings for cytoplasmic segmentation.
|
|
214
|
+
spot_detector : str or None, optional
|
|
215
|
+
Spot detection model name.
|
|
216
|
+
spot_channels : iterable of str or int or None, optional
|
|
217
|
+
Channels used for spot detection.
|
|
218
|
+
spot_settings : dict or None, optional
|
|
219
|
+
Detector settings.
|
|
220
|
+
spot_min_size : int, optional
|
|
221
|
+
Minimum spot size in pixels (0 = no minimum).
|
|
222
|
+
spot_max_size : int, optional
|
|
223
|
+
Maximum spot size in pixels (0 = no maximum).
|
|
224
|
+
quantification_features : iterable of object or None, optional
|
|
225
|
+
Quantification feature contexts (UI-generated).
|
|
226
|
+
quantification_format : str, optional
|
|
227
|
+
Output format for quantification (``"csv"`` or ``"xlsx"``).
|
|
228
|
+
quantification_tab : object or None, optional
|
|
229
|
+
Quantification tab instance for viewer wiring.
|
|
230
|
+
extensions : iterable of str or None, optional
|
|
231
|
+
File extensions to include.
|
|
232
|
+
include_subfolders : bool, optional
|
|
233
|
+
Whether to recurse into subfolders.
|
|
234
|
+
output_format : str, optional
|
|
235
|
+
Mask output format (``"tif"`` or ``"npy"``).
|
|
236
|
+
overwrite : bool, optional
|
|
237
|
+
Whether to overwrite existing output folders.
|
|
238
|
+
process_all_scenes : bool, optional
|
|
239
|
+
Whether to process all scenes in multi-scene files.
|
|
240
|
+
progress_callback : callable or None, optional
|
|
241
|
+
Optional callback invoked with (current, total, message) to
|
|
242
|
+
report progress during batch processing.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
BatchSummary
|
|
247
|
+
Summary of the batch run.
|
|
248
|
+
"""
|
|
249
|
+
input_root = Path(input_path).expanduser()
|
|
250
|
+
output_root = Path(output_path).expanduser()
|
|
251
|
+
output_root.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
|
|
253
|
+
normalized_exts = normalize_extensions(extensions)
|
|
254
|
+
files = list(iter_input_files(input_root, normalized_exts, include_subfolders))
|
|
255
|
+
|
|
256
|
+
results: list[BatchItemResult] = []
|
|
257
|
+
processed = skipped = failed = 0
|
|
258
|
+
normalized_channels = _normalize_channel_map(channel_map)
|
|
259
|
+
nuclear_settings = nuclear_settings or {}
|
|
260
|
+
cyto_settings = cyto_settings or {}
|
|
261
|
+
spot_settings = spot_settings or {}
|
|
262
|
+
quant_backend = QuantificationBackend()
|
|
263
|
+
|
|
264
|
+
# Count total items to process
|
|
265
|
+
total_items = 0
|
|
266
|
+
for path in files:
|
|
267
|
+
scenes = self._iter_scenes(path, process_all_scenes)
|
|
268
|
+
total_items += len(scenes)
|
|
269
|
+
|
|
270
|
+
if progress_callback is not None:
|
|
271
|
+
progress_callback(0, total_items, "Starting batch processing...")
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
not nuclear_model
|
|
275
|
+
and not cyto_model
|
|
276
|
+
and not spot_detector
|
|
277
|
+
and not quantification_features
|
|
278
|
+
):
|
|
279
|
+
return BatchSummary(
|
|
280
|
+
input_root=input_root,
|
|
281
|
+
output_root=output_root,
|
|
282
|
+
processed=0,
|
|
283
|
+
skipped=0,
|
|
284
|
+
failed=0,
|
|
285
|
+
results=[],
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Iterate over files and (optionally) scene variants.
|
|
289
|
+
current_item = 0
|
|
290
|
+
for path in files:
|
|
291
|
+
scenes = self._iter_scenes(path, process_all_scenes)
|
|
292
|
+
for scene_id in scenes:
|
|
293
|
+
current_item += 1
|
|
294
|
+
item_result = BatchItemResult(path=path, scene_id=scene_id)
|
|
295
|
+
|
|
296
|
+
if progress_callback is not None:
|
|
297
|
+
scene_label = f" (Scene: {scene_id})" if scene_id else ""
|
|
298
|
+
progress_callback(
|
|
299
|
+
current_item,
|
|
300
|
+
total_items,
|
|
301
|
+
f"Processing {path.name}{scene_label}..."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
output_dir = _resolve_output_dir(
|
|
306
|
+
output_root, path, scene_id, overwrite
|
|
307
|
+
)
|
|
308
|
+
if output_dir is None:
|
|
309
|
+
skipped += 1
|
|
310
|
+
results.append(item_result)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Collect labels for later quantification.
|
|
314
|
+
labels_data: dict[str, np.ndarray] = {}
|
|
315
|
+
labels_meta: dict[str, dict] = {}
|
|
316
|
+
|
|
317
|
+
if nuclear_model:
|
|
318
|
+
channel_idx = resolve_channel_index(
|
|
319
|
+
nuclear_channel, normalized_channels
|
|
320
|
+
)
|
|
321
|
+
image, metadata = load_channel_data(
|
|
322
|
+
path, channel_idx, scene_id
|
|
323
|
+
)
|
|
324
|
+
if image is None:
|
|
325
|
+
raise RuntimeError("Failed to read nuclear image data.")
|
|
326
|
+
seg_layer = Image(image, "nuclear", metadata)
|
|
327
|
+
model = self._segmentation_backend.get_model(nuclear_model)
|
|
328
|
+
seg_result = model.run(
|
|
329
|
+
task="nuclear",
|
|
330
|
+
layer=seg_layer,
|
|
331
|
+
settings=nuclear_settings,
|
|
332
|
+
)
|
|
333
|
+
masks = seg_result.get("masks")
|
|
334
|
+
if masks is not None:
|
|
335
|
+
channel_name = _resolve_channel_name(
|
|
336
|
+
nuclear_channel, normalized_channels
|
|
337
|
+
)
|
|
338
|
+
label_name = f"{channel_name}_{nuclear_model}_nuc_labels"
|
|
339
|
+
out_path = write_array(
|
|
340
|
+
output_dir,
|
|
341
|
+
label_name,
|
|
342
|
+
masks,
|
|
343
|
+
output_format,
|
|
344
|
+
)
|
|
345
|
+
labels_data[label_name] = masks
|
|
346
|
+
labels_meta[label_name] = metadata
|
|
347
|
+
item_result.outputs[label_name] = out_path
|
|
348
|
+
|
|
349
|
+
if cyto_model:
|
|
350
|
+
channel_idx = resolve_channel_index(
|
|
351
|
+
cyto_channel, normalized_channels
|
|
352
|
+
)
|
|
353
|
+
cyto_image, cyto_meta = load_channel_data(
|
|
354
|
+
path, channel_idx, scene_id
|
|
355
|
+
)
|
|
356
|
+
if cyto_image is None:
|
|
357
|
+
raise RuntimeError(
|
|
358
|
+
"Failed to read cytoplasmic image data."
|
|
359
|
+
)
|
|
360
|
+
cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
|
|
361
|
+
cyto_nuclear_layer = None
|
|
362
|
+
if cyto_nuclear_channel is not None:
|
|
363
|
+
nuclear_idx = resolve_channel_index(
|
|
364
|
+
cyto_nuclear_channel, normalized_channels
|
|
365
|
+
)
|
|
366
|
+
nuclear_image, nuclear_meta = load_channel_data(
|
|
367
|
+
path, nuclear_idx, scene_id
|
|
368
|
+
)
|
|
369
|
+
if nuclear_image is None:
|
|
370
|
+
raise RuntimeError(
|
|
371
|
+
"Failed to read cytoplasmic nuclear data."
|
|
372
|
+
)
|
|
373
|
+
cyto_nuclear_layer = Image(
|
|
374
|
+
nuclear_image, "nuclear", nuclear_meta
|
|
375
|
+
)
|
|
376
|
+
model = self._segmentation_backend.get_model(cyto_model)
|
|
377
|
+
seg_result = model.run(
|
|
378
|
+
task="cytoplasmic",
|
|
379
|
+
layer=cyto_layer,
|
|
380
|
+
nuclear_layer=cyto_nuclear_layer,
|
|
381
|
+
settings=cyto_settings,
|
|
382
|
+
)
|
|
383
|
+
masks = seg_result.get("masks")
|
|
384
|
+
if masks is not None:
|
|
385
|
+
channel_name = _resolve_channel_name(
|
|
386
|
+
cyto_channel, normalized_channels
|
|
387
|
+
)
|
|
388
|
+
label_name = f"{channel_name}_{cyto_model}_cyto_labels"
|
|
389
|
+
out_path = write_array(
|
|
390
|
+
output_dir,
|
|
391
|
+
label_name,
|
|
392
|
+
masks,
|
|
393
|
+
output_format,
|
|
394
|
+
)
|
|
395
|
+
labels_data[label_name] = masks
|
|
396
|
+
labels_meta[label_name] = cyto_meta
|
|
397
|
+
item_result.outputs[label_name] = out_path
|
|
398
|
+
|
|
399
|
+
if spot_detector:
|
|
400
|
+
resolved_spot_channels = list(spot_channels or [])
|
|
401
|
+
for channel_choice in resolved_spot_channels:
|
|
402
|
+
channel_idx = resolve_channel_index(
|
|
403
|
+
channel_choice, normalized_channels
|
|
404
|
+
)
|
|
405
|
+
spot_image, spot_meta = load_channel_data(
|
|
406
|
+
path, channel_idx, scene_id
|
|
407
|
+
)
|
|
408
|
+
if spot_image is None:
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
"Failed to read spot image data."
|
|
411
|
+
)
|
|
412
|
+
spot_layer = Image(spot_image, "spots", spot_meta)
|
|
413
|
+
detector = self._spots_backend.get_detector(
|
|
414
|
+
spot_detector
|
|
415
|
+
)
|
|
416
|
+
spot_result = detector.run(
|
|
417
|
+
layer=spot_layer,
|
|
418
|
+
settings=spot_settings,
|
|
419
|
+
)
|
|
420
|
+
mask = spot_result.get("mask")
|
|
421
|
+
if mask is None:
|
|
422
|
+
continue
|
|
423
|
+
# Apply size filtering if enabled
|
|
424
|
+
if spot_min_size > 0 or spot_max_size > 0:
|
|
425
|
+
mask = _filter_labels_by_size(mask, spot_min_size, spot_max_size)
|
|
426
|
+
channel_name = _resolve_channel_name(
|
|
427
|
+
channel_choice, normalized_channels
|
|
428
|
+
)
|
|
429
|
+
label_name = f"{channel_name}_{spot_detector}_spot_labels"
|
|
430
|
+
out_path = write_array(
|
|
431
|
+
output_dir,
|
|
432
|
+
label_name,
|
|
433
|
+
mask,
|
|
434
|
+
output_format,
|
|
435
|
+
)
|
|
436
|
+
labels_data[label_name] = mask
|
|
437
|
+
labels_meta[label_name] = spot_meta
|
|
438
|
+
item_result.outputs[label_name] = out_path
|
|
439
|
+
|
|
440
|
+
if quantification_features:
|
|
441
|
+
viewer = _build_viewer_for_quantification(
|
|
442
|
+
path,
|
|
443
|
+
scene_id,
|
|
444
|
+
normalized_channels,
|
|
445
|
+
labels_data,
|
|
446
|
+
labels_meta,
|
|
447
|
+
)
|
|
448
|
+
_apply_quantification_viewer(
|
|
449
|
+
quantification_features, quantification_tab, viewer
|
|
450
|
+
)
|
|
451
|
+
result = quant_backend.process(
|
|
452
|
+
quantification_features,
|
|
453
|
+
str(output_dir),
|
|
454
|
+
"",
|
|
455
|
+
quantification_format,
|
|
456
|
+
)
|
|
457
|
+
item_result.outputs["quantification_root"] = result.output_root
|
|
458
|
+
|
|
459
|
+
processed += 1
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
failed += 1
|
|
462
|
+
item_result.errors.append(str(exc))
|
|
463
|
+
results.append(item_result)
|
|
464
|
+
|
|
465
|
+
return BatchSummary(
|
|
466
|
+
input_root=input_root,
|
|
467
|
+
output_root=output_root,
|
|
468
|
+
processed=processed,
|
|
469
|
+
skipped=skipped,
|
|
470
|
+
failed=failed,
|
|
471
|
+
results=results,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def _iter_scenes(self, path: Path, process_all: bool) -> list[str | None]:
|
|
475
|
+
"""Return a list of scene identifiers to process."""
|
|
476
|
+
if not process_all:
|
|
477
|
+
return [None]
|
|
478
|
+
scenes = list_scenes(path)
|
|
479
|
+
return scenes or [None]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _normalize_channel_map(
|
|
483
|
+
channel_map: Iterable[BatchChannelConfig | dict] | None,
|
|
484
|
+
) -> list[BatchChannelConfig]:
|
|
485
|
+
"""Normalize channel mapping payloads into config objects.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
channel_map : iterable of BatchChannelConfig or dict or None
|
|
490
|
+
Channel mapping definitions from the UI or JSON payload.
|
|
491
|
+
|
|
492
|
+
Returns
|
|
493
|
+
-------
|
|
494
|
+
list of BatchChannelConfig
|
|
495
|
+
Normalized channel mapping list.
|
|
496
|
+
"""
|
|
497
|
+
if channel_map is None:
|
|
498
|
+
return []
|
|
499
|
+
normalized: list[BatchChannelConfig] = []
|
|
500
|
+
for entry in channel_map:
|
|
501
|
+
if isinstance(entry, BatchChannelConfig):
|
|
502
|
+
name = entry.name.strip()
|
|
503
|
+
index = entry.index
|
|
504
|
+
elif isinstance(entry, dict):
|
|
505
|
+
name = str(entry.get("name", "")).strip()
|
|
506
|
+
index = int(entry.get("index", 0))
|
|
507
|
+
else:
|
|
508
|
+
continue
|
|
509
|
+
if not name:
|
|
510
|
+
name = f"Channel {index}"
|
|
511
|
+
normalized.append(BatchChannelConfig(name=name, index=index))
|
|
512
|
+
return normalized
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _resolve_channel_name(
|
|
516
|
+
choice: str | int,
|
|
517
|
+
channel_map: list[BatchChannelConfig],
|
|
518
|
+
) -> str:
|
|
519
|
+
"""Resolve a user-friendly channel name from a choice.
|
|
520
|
+
|
|
521
|
+
Parameters
|
|
522
|
+
----------
|
|
523
|
+
choice : str or int
|
|
524
|
+
Channel selection (name or index).
|
|
525
|
+
channel_map : list of BatchChannelConfig
|
|
526
|
+
Channel mapping list for name lookup.
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
str
|
|
531
|
+
Channel name for use in output labels.
|
|
532
|
+
"""
|
|
533
|
+
if isinstance(choice, int):
|
|
534
|
+
return str(choice)
|
|
535
|
+
text = str(choice).strip()
|
|
536
|
+
if text.isdigit():
|
|
537
|
+
return text
|
|
538
|
+
for channel in channel_map:
|
|
539
|
+
if channel.name == text:
|
|
540
|
+
return channel.name
|
|
541
|
+
return text
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _resolve_output_dir(
|
|
545
|
+
output_root: Path,
|
|
546
|
+
path: Path,
|
|
547
|
+
scene_id: str | None,
|
|
548
|
+
overwrite: bool,
|
|
549
|
+
) -> Path | None:
|
|
550
|
+
"""Resolve (and optionally create) the output directory for a run.
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
output_root : Path
|
|
555
|
+
Root output folder.
|
|
556
|
+
path : Path
|
|
557
|
+
Input file path.
|
|
558
|
+
scene_id : str or None
|
|
559
|
+
Optional scene identifier.
|
|
560
|
+
overwrite : bool
|
|
561
|
+
Whether to overwrite existing folders.
|
|
562
|
+
|
|
563
|
+
Returns
|
|
564
|
+
-------
|
|
565
|
+
Path or None
|
|
566
|
+
Output directory path, or None when skipped.
|
|
567
|
+
"""
|
|
568
|
+
base_name = basename_for_path(path)
|
|
569
|
+
output_dir = output_root / base_name
|
|
570
|
+
if scene_id:
|
|
571
|
+
output_dir = output_dir / safe_scene_dir(scene_id)
|
|
572
|
+
if output_dir.exists() and not overwrite:
|
|
573
|
+
return None
|
|
574
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
575
|
+
return output_dir
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _build_viewer_for_quantification(
|
|
579
|
+
path: Path,
|
|
580
|
+
scene_id: str | None,
|
|
581
|
+
channel_map: list[BatchChannelConfig],
|
|
582
|
+
labels_data: dict[str, np.ndarray],
|
|
583
|
+
labels_meta: dict[str, dict],
|
|
584
|
+
) -> BatchViewer:
|
|
585
|
+
"""Build a minimal viewer shim for quantification exports.
|
|
586
|
+
|
|
587
|
+
Parameters
|
|
588
|
+
----------
|
|
589
|
+
path : Path
|
|
590
|
+
Input file path.
|
|
591
|
+
scene_id : str or None
|
|
592
|
+
Optional scene identifier.
|
|
593
|
+
channel_map : list of BatchChannelConfig
|
|
594
|
+
Channel mapping definitions used to load images.
|
|
595
|
+
labels_data : dict of str to numpy.ndarray
|
|
596
|
+
Generated label masks keyed by label name.
|
|
597
|
+
labels_meta : dict of str to dict
|
|
598
|
+
Metadata associated with each labels layer.
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
BatchViewer
|
|
603
|
+
Viewer shim with Image/Labels layers.
|
|
604
|
+
"""
|
|
605
|
+
layers: list[object] = []
|
|
606
|
+
for channel in channel_map:
|
|
607
|
+
image, metadata = load_channel_data(path, channel.index, scene_id)
|
|
608
|
+
if image is None:
|
|
609
|
+
continue
|
|
610
|
+
layers.append(Image(image, channel.name, metadata))
|
|
611
|
+
for name, data in labels_data.items():
|
|
612
|
+
metadata = labels_meta.get(name, {})
|
|
613
|
+
layers.append(Labels(data, name, metadata))
|
|
614
|
+
return BatchViewer(layers)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _apply_quantification_viewer(
|
|
618
|
+
features: Iterable[object],
|
|
619
|
+
quantification_tab: object | None,
|
|
620
|
+
viewer: BatchViewer,
|
|
621
|
+
) -> None:
|
|
622
|
+
"""Attach a batch viewer to quantification handlers.
|
|
623
|
+
|
|
624
|
+
Parameters
|
|
625
|
+
----------
|
|
626
|
+
features : iterable of object
|
|
627
|
+
Feature UI contexts (from QuantificationTab).
|
|
628
|
+
quantification_tab : object or None
|
|
629
|
+
Quantification tab instance (optional).
|
|
630
|
+
viewer : BatchViewer
|
|
631
|
+
Viewer shim with layers to expose to feature handlers.
|
|
632
|
+
"""
|
|
633
|
+
if quantification_tab is not None:
|
|
634
|
+
setattr(quantification_tab, "_viewer", viewer)
|
|
635
|
+
for context in features:
|
|
636
|
+
handler = getattr(context, "feature_handler", None)
|
|
637
|
+
if handler is None:
|
|
638
|
+
continue
|
|
639
|
+
tab = getattr(handler, "_tab", None)
|
|
640
|
+
if tab is not None:
|
|
641
|
+
setattr(tab, "_viewer", viewer)
|