senoquant 1.0.0b2__py3-none-any.whl → 1.0.0b4__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 -2
- senoquant/_reader.py +1 -1
- senoquant/_widget.py +9 -1
- senoquant/reader/core.py +201 -18
- senoquant/tabs/__init__.py +2 -0
- senoquant/tabs/batch/backend.py +76 -27
- senoquant/tabs/batch/frontend.py +127 -25
- senoquant/tabs/quantification/features/marker/dialog.py +26 -6
- senoquant/tabs/quantification/features/marker/export.py +97 -24
- senoquant/tabs/quantification/features/marker/rows.py +2 -2
- senoquant/tabs/quantification/features/spots/dialog.py +41 -11
- senoquant/tabs/quantification/features/spots/export.py +163 -10
- senoquant/tabs/quantification/frontend.py +2 -2
- senoquant/tabs/segmentation/frontend.py +46 -9
- senoquant/tabs/segmentation/models/cpsam/model.py +1 -1
- senoquant/tabs/segmentation/models/default_2d/model.py +22 -77
- senoquant/tabs/segmentation/models/default_3d/model.py +8 -74
- senoquant/tabs/segmentation/stardist_onnx_utils/_csbdeep/tools/create_zip_contents.py +0 -0
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/inspect/probe.py +13 -13
- senoquant/tabs/segmentation/stardist_onnx_utils/onnx_framework/stardist_libs.py +171 -0
- senoquant/tabs/spots/frontend.py +96 -5
- senoquant/tabs/spots/models/rmp/details.json +3 -9
- senoquant/tabs/spots/models/rmp/model.py +341 -266
- senoquant/tabs/spots/models/ufish/details.json +32 -0
- senoquant/tabs/spots/models/ufish/model.py +327 -0
- senoquant/tabs/spots/ufish_utils/__init__.py +13 -0
- senoquant/tabs/spots/ufish_utils/core.py +387 -0
- senoquant/tabs/visualization/__init__.py +1 -0
- senoquant/tabs/visualization/backend.py +306 -0
- senoquant/tabs/visualization/frontend.py +1113 -0
- senoquant/tabs/visualization/plots/__init__.py +80 -0
- senoquant/tabs/visualization/plots/base.py +152 -0
- senoquant/tabs/visualization/plots/double_expression.py +187 -0
- senoquant/tabs/visualization/plots/spatialplot.py +156 -0
- senoquant/tabs/visualization/plots/umap.py +140 -0
- senoquant/utils.py +1 -1
- senoquant-1.0.0b4.dist-info/METADATA +162 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/RECORD +53 -30
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/top_level.txt +1 -0
- ufish/__init__.py +1 -0
- ufish/api.py +778 -0
- ufish/model/__init__.py +0 -0
- ufish/model/loss.py +62 -0
- ufish/model/network/__init__.py +0 -0
- ufish/model/network/spot_learn.py +50 -0
- ufish/model/network/ufish_net.py +204 -0
- ufish/model/train.py +175 -0
- ufish/utils/__init__.py +0 -0
- ufish/utils/img.py +418 -0
- ufish/utils/log.py +8 -0
- ufish/utils/spot_calling.py +115 -0
- senoquant/tabs/spots/models/udwt/details.json +0 -103
- senoquant/tabs/spots/models/udwt/model.py +0 -482
- senoquant-1.0.0b2.dist-info/METADATA +0 -193
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/WHEEL +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/entry_points.txt +0 -0
- {senoquant-1.0.0b2.dist-info → senoquant-1.0.0b4.dist-info}/licenses/LICENSE +0 -0
senoquant/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"""SenoQuant napari plugin package."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
try:
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
__version__ = version("senoquant")
|
|
6
|
+
except Exception:
|
|
7
|
+
__version__ = "1.0.0b4" # Fallback for development
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
from ._widget import SenoQuantWidget
|
|
6
10
|
__all__ = ["SenoQuantWidget"]
|
senoquant/_reader.py
CHANGED
senoquant/_widget.py
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from qtpy.QtWidgets import QTabWidget, QVBoxLayout, QWidget
|
|
4
4
|
|
|
5
|
-
from .tabs import
|
|
5
|
+
from .tabs import (
|
|
6
|
+
BatchTab,
|
|
7
|
+
QuantificationTab,
|
|
8
|
+
SegmentationTab,
|
|
9
|
+
SettingsTab,
|
|
10
|
+
SpotsTab,
|
|
11
|
+
VisualizationTab,
|
|
12
|
+
)
|
|
6
13
|
from .tabs.settings.backend import SettingsBackend
|
|
7
14
|
|
|
8
15
|
|
|
@@ -26,6 +33,7 @@ class SenoQuantWidget(QWidget):
|
|
|
26
33
|
)
|
|
27
34
|
tabs.addTab(SpotsTab(napari_viewer=napari_viewer), "Spots")
|
|
28
35
|
tabs.addTab(QuantificationTab(napari_viewer=napari_viewer), "Quantification")
|
|
36
|
+
tabs.addTab(VisualizationTab(napari_viewer=napari_viewer), "Visualization")
|
|
29
37
|
tabs.addTab(BatchTab(napari_viewer=napari_viewer), "Batch")
|
|
30
38
|
tabs.addTab(SettingsTab(backend=self._settings_backend), "Settings")
|
|
31
39
|
|
senoquant/reader/core.py
CHANGED
|
@@ -75,7 +75,7 @@ def _read_senoquant(path: str) -> Iterable[tuple]:
|
|
|
75
75
|
Returns
|
|
76
76
|
-------
|
|
77
77
|
iterable of tuple
|
|
78
|
-
|
|
78
|
+
napari layer tuples of the form ``(data, metadata, layer_type)``.
|
|
79
79
|
|
|
80
80
|
Notes
|
|
81
81
|
-----
|
|
@@ -91,25 +91,208 @@ def _read_senoquant(path: str) -> Iterable[tuple]:
|
|
|
91
91
|
|
|
92
92
|
base_name = Path(path).name
|
|
93
93
|
image = _open_bioimage(path)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
try:
|
|
95
|
+
layers: list[tuple] = []
|
|
96
|
+
colormap_cycle = _colormap_cycle()
|
|
97
|
+
scenes = list(getattr(image, "scenes", []) or [])
|
|
98
|
+
selected_scene_indices = _select_scene_indices(path, scenes)
|
|
99
|
+
if not selected_scene_indices:
|
|
100
|
+
return layers
|
|
101
|
+
|
|
102
|
+
for scene_idx in selected_scene_indices:
|
|
103
|
+
scene_id = scenes[scene_idx]
|
|
104
|
+
image.set_scene(scene_id)
|
|
105
|
+
layers.extend(
|
|
106
|
+
_iter_channel_layers(
|
|
107
|
+
image,
|
|
108
|
+
base_name=base_name,
|
|
109
|
+
scene_id=scene_id,
|
|
110
|
+
scene_idx=scene_idx,
|
|
111
|
+
total_scenes=len(scenes),
|
|
112
|
+
path=path,
|
|
113
|
+
colormap_cycle=colormap_cycle,
|
|
114
|
+
)
|
|
109
115
|
)
|
|
116
|
+
|
|
117
|
+
return layers
|
|
118
|
+
finally:
|
|
119
|
+
if hasattr(image, "close"):
|
|
120
|
+
try:
|
|
121
|
+
image.close()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _select_scene_indices(path: str, scenes: list[str]) -> list[int]:
|
|
127
|
+
"""Return scene indices selected by the user for loading."""
|
|
128
|
+
if not scenes:
|
|
129
|
+
return []
|
|
130
|
+
if len(scenes) == 1:
|
|
131
|
+
return [0]
|
|
132
|
+
|
|
133
|
+
selected = _prompt_scene_selection(path, scenes)
|
|
134
|
+
if selected is None:
|
|
135
|
+
return []
|
|
136
|
+
return selected
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _prompt_scene_selection(path: str, scenes: list[str]) -> list[int] | None:
|
|
140
|
+
"""Show a scene-selection dialog and return selected indices.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
list[int] or None
|
|
145
|
+
Selected scene indices. Returns ``None`` when the dialog is cancelled.
|
|
146
|
+
If Qt is not available, all scenes are selected.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
from qtpy.QtCore import Qt
|
|
150
|
+
from qtpy.QtWidgets import (
|
|
151
|
+
QApplication,
|
|
152
|
+
QDialog,
|
|
153
|
+
QDialogButtonBox,
|
|
154
|
+
QHBoxLayout,
|
|
155
|
+
QLabel,
|
|
156
|
+
QListWidget,
|
|
157
|
+
QListWidgetItem,
|
|
158
|
+
QMessageBox,
|
|
159
|
+
QPushButton,
|
|
160
|
+
QVBoxLayout,
|
|
161
|
+
)
|
|
162
|
+
except Exception:
|
|
163
|
+
return list(range(len(scenes)))
|
|
164
|
+
|
|
165
|
+
app = QApplication.instance()
|
|
166
|
+
if app is None:
|
|
167
|
+
return list(range(len(scenes)))
|
|
168
|
+
|
|
169
|
+
dialog = QDialog(_napari_dialog_parent(app))
|
|
170
|
+
dialog.setWindowTitle("Select scenes to load")
|
|
171
|
+
dialog.setMinimumWidth(520)
|
|
172
|
+
_apply_napari_dialog_theme(dialog, app)
|
|
173
|
+
|
|
174
|
+
layout = QVBoxLayout(dialog)
|
|
175
|
+
layout.addWidget(QLabel(f"File: {Path(path).name}"))
|
|
176
|
+
layout.addWidget(QLabel(f"Select scenes to load ({len(scenes)} total):"))
|
|
177
|
+
|
|
178
|
+
scene_list = QListWidget(dialog)
|
|
179
|
+
for index, scene_id in enumerate(scenes):
|
|
180
|
+
scene_name = str(scene_id).strip() or f"Scene {index}"
|
|
181
|
+
item = QListWidgetItem(f"{index}: {scene_name}")
|
|
182
|
+
item.setData(Qt.UserRole, index)
|
|
183
|
+
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
|
184
|
+
item.setCheckState(Qt.Checked)
|
|
185
|
+
scene_list.addItem(item)
|
|
186
|
+
scene_list.setMinimumHeight(300)
|
|
187
|
+
layout.addWidget(scene_list)
|
|
188
|
+
|
|
189
|
+
controls = QHBoxLayout()
|
|
190
|
+
select_all_button = QPushButton("Select all")
|
|
191
|
+
clear_all_button = QPushButton("Clear all")
|
|
192
|
+
controls.addWidget(select_all_button)
|
|
193
|
+
controls.addWidget(clear_all_button)
|
|
194
|
+
controls.addStretch(1)
|
|
195
|
+
layout.addLayout(controls)
|
|
196
|
+
|
|
197
|
+
select_all_button.clicked.connect(
|
|
198
|
+
lambda: _set_scene_checks(scene_list, Qt.Checked)
|
|
199
|
+
)
|
|
200
|
+
clear_all_button.clicked.connect(lambda: _set_scene_checks(scene_list, Qt.Unchecked))
|
|
201
|
+
|
|
202
|
+
buttons = QDialogButtonBox(
|
|
203
|
+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog
|
|
204
|
+
)
|
|
205
|
+
layout.addWidget(buttons)
|
|
206
|
+
|
|
207
|
+
def _accept_if_valid() -> None:
|
|
208
|
+
checked = _checked_scene_indices(scene_list)
|
|
209
|
+
if checked:
|
|
210
|
+
dialog.accept()
|
|
211
|
+
return
|
|
212
|
+
QMessageBox.warning(
|
|
213
|
+
dialog,
|
|
214
|
+
"No scenes selected",
|
|
215
|
+
"Select at least one scene to load.",
|
|
110
216
|
)
|
|
111
217
|
|
|
112
|
-
|
|
218
|
+
buttons.accepted.connect(_accept_if_valid)
|
|
219
|
+
buttons.rejected.connect(dialog.reject)
|
|
220
|
+
|
|
221
|
+
if dialog.exec() != QDialog.Accepted:
|
|
222
|
+
return None
|
|
223
|
+
return _checked_scene_indices(scene_list)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _napari_dialog_parent(app):
|
|
227
|
+
"""Return a good parent window for napari-linked dialogs."""
|
|
228
|
+
try:
|
|
229
|
+
import napari
|
|
230
|
+
|
|
231
|
+
viewer = napari.current_viewer()
|
|
232
|
+
except Exception:
|
|
233
|
+
viewer = None
|
|
234
|
+
if viewer is not None:
|
|
235
|
+
window = getattr(viewer, "window", None)
|
|
236
|
+
qt_window = getattr(window, "_qt_window", None)
|
|
237
|
+
if qt_window is not None:
|
|
238
|
+
return qt_window
|
|
239
|
+
return app.activeWindow()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _apply_napari_dialog_theme(dialog, app) -> None:
|
|
243
|
+
"""Apply napari/app stylesheet so popup theming is consistent."""
|
|
244
|
+
stylesheet = ""
|
|
245
|
+
try:
|
|
246
|
+
import napari
|
|
247
|
+
|
|
248
|
+
viewer = napari.current_viewer()
|
|
249
|
+
theme_id = getattr(viewer, "theme", None) if viewer is not None else None
|
|
250
|
+
if theme_id:
|
|
251
|
+
try:
|
|
252
|
+
from napari.qt import get_stylesheet
|
|
253
|
+
|
|
254
|
+
stylesheet = str(get_stylesheet(theme_id) or "")
|
|
255
|
+
except Exception:
|
|
256
|
+
stylesheet = ""
|
|
257
|
+
except Exception:
|
|
258
|
+
stylesheet = ""
|
|
259
|
+
|
|
260
|
+
if not stylesheet:
|
|
261
|
+
try:
|
|
262
|
+
stylesheet = str(app.styleSheet() or "")
|
|
263
|
+
except Exception:
|
|
264
|
+
stylesheet = ""
|
|
265
|
+
if not stylesheet:
|
|
266
|
+
try:
|
|
267
|
+
parent = dialog.parentWidget()
|
|
268
|
+
stylesheet = str(parent.styleSheet() if parent is not None else "")
|
|
269
|
+
except Exception:
|
|
270
|
+
stylesheet = ""
|
|
271
|
+
|
|
272
|
+
if stylesheet:
|
|
273
|
+
dialog.setStyleSheet(stylesheet)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _set_scene_checks(scene_list, state) -> None:
|
|
277
|
+
"""Set check state for all scene list items."""
|
|
278
|
+
for row in range(scene_list.count()):
|
|
279
|
+
item = scene_list.item(row)
|
|
280
|
+
if item is not None:
|
|
281
|
+
item.setCheckState(state)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _checked_scene_indices(scene_list) -> list[int]:
|
|
285
|
+
"""Return checked scene indices from a QListWidget."""
|
|
286
|
+
from qtpy.QtCore import Qt
|
|
287
|
+
|
|
288
|
+
selected: list[int] = []
|
|
289
|
+
for row in range(scene_list.count()):
|
|
290
|
+
item = scene_list.item(row)
|
|
291
|
+
if item is None:
|
|
292
|
+
continue
|
|
293
|
+
if item.checkState() == Qt.Checked:
|
|
294
|
+
selected.append(int(item.data(Qt.UserRole)))
|
|
295
|
+
return selected
|
|
113
296
|
|
|
114
297
|
|
|
115
298
|
def _open_bioimage(path: str):
|
|
@@ -304,7 +487,7 @@ def _iter_channel_layers(
|
|
|
304
487
|
Returns
|
|
305
488
|
-------
|
|
306
489
|
list of tuple
|
|
307
|
-
|
|
490
|
+
napari layer tuples for each channel.
|
|
308
491
|
"""
|
|
309
492
|
dims = getattr(image, "dims", None)
|
|
310
493
|
axes_present = _axes_present(image)
|
senoquant/tabs/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .segmentation.frontend import SegmentationTab
|
|
4
4
|
from .spots.frontend import SpotsTab
|
|
5
5
|
from .quantification.frontend import QuantificationTab
|
|
6
|
+
from .visualization.frontend import VisualizationTab
|
|
6
7
|
from .settings.frontend import SettingsTab
|
|
7
8
|
from .batch.frontend import BatchTab
|
|
8
9
|
|
|
@@ -10,6 +11,7 @@ __all__ = [
|
|
|
10
11
|
"SegmentationTab",
|
|
11
12
|
"SpotsTab",
|
|
12
13
|
"QuantificationTab",
|
|
14
|
+
"VisualizationTab",
|
|
13
15
|
"SettingsTab",
|
|
14
16
|
"BatchTab",
|
|
15
17
|
]
|
senoquant/tabs/batch/backend.py
CHANGED
|
@@ -208,7 +208,8 @@ class BatchBackend:
|
|
|
208
208
|
cyto_channel : str or int or None, optional
|
|
209
209
|
Channel selection for cytoplasm.
|
|
210
210
|
cyto_nuclear_channel : str or int or None, optional
|
|
211
|
-
Optional nuclear
|
|
211
|
+
Optional nuclear input for cytoplasmic models. This may be a
|
|
212
|
+
channel selection or a generated nuclear label name.
|
|
212
213
|
cyto_settings : dict or None, optional
|
|
213
214
|
Model settings for cytoplasmic segmentation.
|
|
214
215
|
spot_detector : str or None, optional
|
|
@@ -260,6 +261,14 @@ class BatchBackend:
|
|
|
260
261
|
cyto_settings = cyto_settings or {}
|
|
261
262
|
spot_settings = spot_settings or {}
|
|
262
263
|
quant_backend = QuantificationBackend()
|
|
264
|
+
cyto_model_instance = None
|
|
265
|
+
cyto_nuclear_only = False
|
|
266
|
+
if cyto_model:
|
|
267
|
+
cyto_model_instance = self._segmentation_backend.get_model(cyto_model)
|
|
268
|
+
modes: list[str] = []
|
|
269
|
+
if hasattr(cyto_model_instance, "cytoplasmic_input_modes"):
|
|
270
|
+
modes = cyto_model_instance.cytoplasmic_input_modes()
|
|
271
|
+
cyto_nuclear_only = modes == ["nuclear"]
|
|
263
272
|
|
|
264
273
|
# Count total items to process
|
|
265
274
|
total_items = 0
|
|
@@ -343,40 +352,67 @@ class BatchBackend:
|
|
|
343
352
|
output_format,
|
|
344
353
|
)
|
|
345
354
|
labels_data[label_name] = masks
|
|
346
|
-
labels_meta[label_name] =
|
|
355
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
356
|
+
metadata, "nuclear"
|
|
357
|
+
)
|
|
347
358
|
item_result.outputs[label_name] = out_path
|
|
348
359
|
|
|
349
360
|
if cyto_model:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
361
|
+
cyto_layer = None
|
|
362
|
+
cyto_meta: dict = {}
|
|
363
|
+
if not cyto_nuclear_only:
|
|
364
|
+
channel_idx = resolve_channel_index(
|
|
365
|
+
cyto_channel, normalized_channels
|
|
365
366
|
)
|
|
366
|
-
|
|
367
|
-
path,
|
|
367
|
+
cyto_image, cyto_meta = load_channel_data(
|
|
368
|
+
path, channel_idx, scene_id
|
|
368
369
|
)
|
|
369
|
-
if
|
|
370
|
+
if cyto_image is None:
|
|
370
371
|
raise RuntimeError(
|
|
371
|
-
"Failed to read cytoplasmic
|
|
372
|
+
"Failed to read cytoplasmic image data."
|
|
372
373
|
)
|
|
373
|
-
|
|
374
|
-
|
|
374
|
+
cyto_layer = Image(cyto_image, "cytoplasmic", cyto_meta)
|
|
375
|
+
cyto_nuclear_layer = None
|
|
376
|
+
if cyto_nuclear_channel is not None:
|
|
377
|
+
cyto_nuclear_key = str(cyto_nuclear_channel)
|
|
378
|
+
if (
|
|
379
|
+
cyto_nuclear_only
|
|
380
|
+
and cyto_nuclear_key in labels_data
|
|
381
|
+
):
|
|
382
|
+
nuclear_meta = labels_meta.get(cyto_nuclear_key, {})
|
|
383
|
+
cyto_nuclear_layer = Labels(
|
|
384
|
+
labels_data[cyto_nuclear_key],
|
|
385
|
+
cyto_nuclear_key,
|
|
386
|
+
nuclear_meta,
|
|
387
|
+
)
|
|
388
|
+
if not cyto_meta:
|
|
389
|
+
cyto_meta = dict(nuclear_meta)
|
|
390
|
+
else:
|
|
391
|
+
nuclear_idx = resolve_channel_index(
|
|
392
|
+
cyto_nuclear_channel, normalized_channels
|
|
393
|
+
)
|
|
394
|
+
nuclear_image, nuclear_meta = load_channel_data(
|
|
395
|
+
path, nuclear_idx, scene_id
|
|
396
|
+
)
|
|
397
|
+
if nuclear_image is None:
|
|
398
|
+
raise RuntimeError(
|
|
399
|
+
"Failed to read cytoplasmic nuclear data."
|
|
400
|
+
)
|
|
401
|
+
cyto_nuclear_layer = Image(
|
|
402
|
+
nuclear_image, "nuclear", nuclear_meta
|
|
403
|
+
)
|
|
404
|
+
if not cyto_meta:
|
|
405
|
+
cyto_meta = dict(nuclear_meta)
|
|
406
|
+
if cyto_nuclear_only and cyto_nuclear_layer is None:
|
|
407
|
+
raise RuntimeError(
|
|
408
|
+
"Selected cytoplasmic model requires nuclear labels."
|
|
375
409
|
)
|
|
376
|
-
|
|
377
|
-
|
|
410
|
+
if cyto_model_instance is None:
|
|
411
|
+
raise RuntimeError("Failed to load cytoplasmic model.")
|
|
412
|
+
seg_result = cyto_model_instance.run(
|
|
378
413
|
task="cytoplasmic",
|
|
379
414
|
layer=cyto_layer,
|
|
415
|
+
cytoplasmic_layer=cyto_layer,
|
|
380
416
|
nuclear_layer=cyto_nuclear_layer,
|
|
381
417
|
settings=cyto_settings,
|
|
382
418
|
)
|
|
@@ -393,7 +429,9 @@ class BatchBackend:
|
|
|
393
429
|
output_format,
|
|
394
430
|
)
|
|
395
431
|
labels_data[label_name] = masks
|
|
396
|
-
labels_meta[label_name] =
|
|
432
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
433
|
+
cyto_meta, "cytoplasmic"
|
|
434
|
+
)
|
|
397
435
|
item_result.outputs[label_name] = out_path
|
|
398
436
|
|
|
399
437
|
if spot_detector:
|
|
@@ -434,7 +472,9 @@ class BatchBackend:
|
|
|
434
472
|
output_format,
|
|
435
473
|
)
|
|
436
474
|
labels_data[label_name] = mask
|
|
437
|
-
labels_meta[label_name] =
|
|
475
|
+
labels_meta[label_name] = _with_task_metadata(
|
|
476
|
+
spot_meta, "spots"
|
|
477
|
+
)
|
|
438
478
|
item_result.outputs[label_name] = out_path
|
|
439
479
|
|
|
440
480
|
if quantification_features:
|
|
@@ -575,6 +615,15 @@ def _resolve_output_dir(
|
|
|
575
615
|
return output_dir
|
|
576
616
|
|
|
577
617
|
|
|
618
|
+
def _with_task_metadata(metadata: dict | None, task: str) -> dict:
|
|
619
|
+
"""Return a metadata copy with task type attached."""
|
|
620
|
+
payload: dict[str, object] = {}
|
|
621
|
+
if isinstance(metadata, dict):
|
|
622
|
+
payload.update(metadata)
|
|
623
|
+
payload["task"] = task
|
|
624
|
+
return payload
|
|
625
|
+
|
|
626
|
+
|
|
578
627
|
def _build_viewer_for_quantification(
|
|
579
628
|
path: Path,
|
|
580
629
|
scene_id: str | None,
|