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.
Files changed (148) hide show
  1. senoquant/__init__.py +6 -0
  2. senoquant/_reader.py +7 -0
  3. senoquant/_widget.py +33 -0
  4. senoquant/napari.yaml +83 -0
  5. senoquant/reader/__init__.py +5 -0
  6. senoquant/reader/core.py +369 -0
  7. senoquant/tabs/__init__.py +15 -0
  8. senoquant/tabs/batch/__init__.py +10 -0
  9. senoquant/tabs/batch/backend.py +641 -0
  10. senoquant/tabs/batch/config.py +270 -0
  11. senoquant/tabs/batch/frontend.py +1283 -0
  12. senoquant/tabs/batch/io.py +326 -0
  13. senoquant/tabs/batch/layers.py +86 -0
  14. senoquant/tabs/quantification/__init__.py +1 -0
  15. senoquant/tabs/quantification/backend.py +228 -0
  16. senoquant/tabs/quantification/features/__init__.py +80 -0
  17. senoquant/tabs/quantification/features/base.py +142 -0
  18. senoquant/tabs/quantification/features/marker/__init__.py +5 -0
  19. senoquant/tabs/quantification/features/marker/config.py +69 -0
  20. senoquant/tabs/quantification/features/marker/dialog.py +437 -0
  21. senoquant/tabs/quantification/features/marker/export.py +879 -0
  22. senoquant/tabs/quantification/features/marker/feature.py +119 -0
  23. senoquant/tabs/quantification/features/marker/morphology.py +285 -0
  24. senoquant/tabs/quantification/features/marker/rows.py +654 -0
  25. senoquant/tabs/quantification/features/marker/thresholding.py +46 -0
  26. senoquant/tabs/quantification/features/roi.py +346 -0
  27. senoquant/tabs/quantification/features/spots/__init__.py +5 -0
  28. senoquant/tabs/quantification/features/spots/config.py +62 -0
  29. senoquant/tabs/quantification/features/spots/dialog.py +477 -0
  30. senoquant/tabs/quantification/features/spots/export.py +1292 -0
  31. senoquant/tabs/quantification/features/spots/feature.py +112 -0
  32. senoquant/tabs/quantification/features/spots/morphology.py +279 -0
  33. senoquant/tabs/quantification/features/spots/rows.py +241 -0
  34. senoquant/tabs/quantification/frontend.py +815 -0
  35. senoquant/tabs/segmentation/__init__.py +1 -0
  36. senoquant/tabs/segmentation/backend.py +131 -0
  37. senoquant/tabs/segmentation/frontend.py +1009 -0
  38. senoquant/tabs/segmentation/models/__init__.py +5 -0
  39. senoquant/tabs/segmentation/models/base.py +146 -0
  40. senoquant/tabs/segmentation/models/cpsam/details.json +65 -0
  41. senoquant/tabs/segmentation/models/cpsam/model.py +150 -0
  42. senoquant/tabs/segmentation/models/default_2d/details.json +69 -0
  43. senoquant/tabs/segmentation/models/default_2d/model.py +664 -0
  44. senoquant/tabs/segmentation/models/default_3d/details.json +69 -0
  45. senoquant/tabs/segmentation/models/default_3d/model.py +682 -0
  46. senoquant/tabs/segmentation/models/hf.py +71 -0
  47. senoquant/tabs/segmentation/models/nuclear_dilation/__init__.py +1 -0
  48. senoquant/tabs/segmentation/models/nuclear_dilation/details.json +26 -0
  49. senoquant/tabs/segmentation/models/nuclear_dilation/model.py +96 -0
  50. senoquant/tabs/segmentation/models/perinuclear_rings/__init__.py +1 -0
  51. senoquant/tabs/segmentation/models/perinuclear_rings/details.json +34 -0
  52. senoquant/tabs/segmentation/models/perinuclear_rings/model.py +132 -0
  53. senoquant/tabs/segmentation/stardist_onnx_utils/__init__.py +2 -0
  54. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/__init__.py +3 -0
  55. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/__init__.py +6 -0
  56. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/generate.py +470 -0
  57. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/prepare.py +273 -0
  58. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/rawdata.py +112 -0
  59. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/data/transform.py +384 -0
  60. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/__init__.py +0 -0
  61. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/blocks.py +184 -0
  62. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/losses.py +79 -0
  63. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/nets.py +165 -0
  64. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/predict.py +467 -0
  65. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/probability.py +67 -0
  66. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/internals/train.py +148 -0
  67. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/io/__init__.py +163 -0
  68. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/__init__.py +52 -0
  69. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/base_model.py +329 -0
  70. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_isotropic.py +160 -0
  71. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_projection.py +178 -0
  72. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_standard.py +446 -0
  73. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/care_upsampling.py +54 -0
  74. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/config.py +254 -0
  75. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/models/pretrained.py +119 -0
  76. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/__init__.py +0 -0
  77. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/scripts/care_predict.py +180 -0
  78. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/__init__.py +5 -0
  79. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/plot_utils.py +159 -0
  80. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/six.py +18 -0
  81. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/tf.py +644 -0
  82. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/utils/utils.py +272 -0
  83. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/csbdeep/version.py +1 -0
  84. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/docs/source/conf.py +368 -0
  85. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/setup.py +68 -0
  86. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_datagen.py +169 -0
  87. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_models.py +462 -0
  88. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tests/test_utils.py +166 -0
  89. senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +34 -0
  90. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/__init__.py +30 -0
  91. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/big.py +624 -0
  92. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/bioimageio_utils.py +494 -0
  93. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/data/__init__.py +39 -0
  94. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/__init__.py +10 -0
  95. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom2d.py +215 -0
  96. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/geometry/geom3d.py +349 -0
  97. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/matching.py +483 -0
  98. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/__init__.py +28 -0
  99. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/base.py +1217 -0
  100. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model2d.py +594 -0
  101. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/models/model3d.py +696 -0
  102. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/nms.py +384 -0
  103. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/__init__.py +2 -0
  104. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/plot.py +74 -0
  105. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/plot/render.py +298 -0
  106. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/rays3d.py +373 -0
  107. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/sample_patches.py +65 -0
  108. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/__init__.py +0 -0
  109. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict2d.py +90 -0
  110. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/scripts/predict3d.py +93 -0
  111. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/utils.py +408 -0
  112. senoquant/tabs/segmentation/stardist_onnx_utils/_stardist/version.py +1 -0
  113. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/__init__.py +45 -0
  114. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/__init__.py +17 -0
  115. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/cli.py +55 -0
  116. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/convert/core.py +285 -0
  117. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/__init__.py +15 -0
  118. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/cli.py +36 -0
  119. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/divisibility.py +193 -0
  120. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +100 -0
  121. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/receptive_field.py +182 -0
  122. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/rf_cli.py +48 -0
  123. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/valid_sizes.py +278 -0
  124. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/__init__.py +8 -0
  125. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/post/core.py +157 -0
  126. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/__init__.py +17 -0
  127. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/pre/core.py +226 -0
  128. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/__init__.py +5 -0
  129. senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/predict/core.py +401 -0
  130. senoquant/tabs/settings/__init__.py +1 -0
  131. senoquant/tabs/settings/backend.py +29 -0
  132. senoquant/tabs/settings/frontend.py +19 -0
  133. senoquant/tabs/spots/__init__.py +1 -0
  134. senoquant/tabs/spots/backend.py +139 -0
  135. senoquant/tabs/spots/frontend.py +800 -0
  136. senoquant/tabs/spots/models/__init__.py +5 -0
  137. senoquant/tabs/spots/models/base.py +94 -0
  138. senoquant/tabs/spots/models/rmp/details.json +61 -0
  139. senoquant/tabs/spots/models/rmp/model.py +499 -0
  140. senoquant/tabs/spots/models/udwt/details.json +103 -0
  141. senoquant/tabs/spots/models/udwt/model.py +482 -0
  142. senoquant/utils.py +25 -0
  143. senoquant-1.0.0b1.dist-info/METADATA +193 -0
  144. senoquant-1.0.0b1.dist-info/RECORD +148 -0
  145. senoquant-1.0.0b1.dist-info/WHEEL +5 -0
  146. senoquant-1.0.0b1.dist-info/entry_points.txt +2 -0
  147. senoquant-1.0.0b1.dist-info/licenses/LICENSE +28 -0
  148. 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)