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,1009 @@
|
|
|
1
|
+
"""Frontend widget for the Segmentation tab."""
|
|
2
|
+
|
|
3
|
+
from qtpy.QtCore import QObject, QThread, Signal
|
|
4
|
+
from qtpy.QtWidgets import (
|
|
5
|
+
QCheckBox,
|
|
6
|
+
QComboBox,
|
|
7
|
+
QDoubleSpinBox,
|
|
8
|
+
QFormLayout,
|
|
9
|
+
QGroupBox,
|
|
10
|
+
QLabel,
|
|
11
|
+
QFrame,
|
|
12
|
+
QPushButton,
|
|
13
|
+
QSizePolicy,
|
|
14
|
+
QSpinBox,
|
|
15
|
+
QVBoxLayout,
|
|
16
|
+
QWidget,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from napari.layers import Image, Labels
|
|
21
|
+
from napari.utils.notifications import (
|
|
22
|
+
Notification,
|
|
23
|
+
NotificationSeverity,
|
|
24
|
+
show_console_notification,
|
|
25
|
+
)
|
|
26
|
+
except Exception: # pragma: no cover - optional import for runtime
|
|
27
|
+
Image = None
|
|
28
|
+
Labels = None
|
|
29
|
+
show_console_notification = None
|
|
30
|
+
Notification = None
|
|
31
|
+
NotificationSeverity = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RefreshingComboBox(QComboBox):
|
|
35
|
+
"""Combo box that refreshes its items when opened."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, refresh_callback=None, parent=None) -> None:
|
|
38
|
+
"""Create a combo box that refreshes on popup.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
refresh_callback : callable or None
|
|
43
|
+
Function invoked before showing the popup.
|
|
44
|
+
parent : QWidget or None
|
|
45
|
+
Optional parent widget.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(parent)
|
|
48
|
+
self._refresh_callback = refresh_callback
|
|
49
|
+
|
|
50
|
+
def showPopup(self) -> None:
|
|
51
|
+
"""Refresh items before showing the popup."""
|
|
52
|
+
if self._refresh_callback is not None:
|
|
53
|
+
self._refresh_callback()
|
|
54
|
+
super().showPopup()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Layer dropdowns refresh at click-time so the UI stays in sync with napari.
|
|
58
|
+
# This keeps options limited to Image layers and preserves existing selections.
|
|
59
|
+
|
|
60
|
+
from .backend import SegmentationBackend
|
|
61
|
+
from ..settings.backend import SettingsBackend
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class SegmentationTab(QWidget):
|
|
65
|
+
"""Segmentation tab UI with nuclear and cytoplasmic sections.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
backend : SegmentationBackend or None
|
|
70
|
+
Backend instance used to discover and load models.
|
|
71
|
+
napari_viewer : object or None
|
|
72
|
+
Napari viewer used to populate layer choices.
|
|
73
|
+
settings_backend : SettingsBackend or None
|
|
74
|
+
Settings store used for preload configuration.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
backend: SegmentationBackend | None = None,
|
|
80
|
+
napari_viewer=None,
|
|
81
|
+
settings_backend: SettingsBackend | None = None,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Create the segmentation tab UI.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
backend : SegmentationBackend or None
|
|
88
|
+
Backend instance used to discover and load models.
|
|
89
|
+
napari_viewer : object or None
|
|
90
|
+
Napari viewer used to populate layer choices.
|
|
91
|
+
settings_backend : SettingsBackend or None
|
|
92
|
+
Settings store used for preload configuration.
|
|
93
|
+
"""
|
|
94
|
+
super().__init__()
|
|
95
|
+
self._backend = backend or SegmentationBackend()
|
|
96
|
+
self._viewer = napari_viewer
|
|
97
|
+
self._nuclear_settings_widgets = {}
|
|
98
|
+
self._cyto_settings_widgets = {}
|
|
99
|
+
self._nuclear_settings_meta = {}
|
|
100
|
+
self._cyto_settings_meta = {}
|
|
101
|
+
self._settings = settings_backend or SettingsBackend()
|
|
102
|
+
self._settings.preload_models_changed.connect(
|
|
103
|
+
self._on_preload_models_changed
|
|
104
|
+
)
|
|
105
|
+
self._active_workers: list[tuple[QThread, QObject]] = []
|
|
106
|
+
|
|
107
|
+
layout = QVBoxLayout()
|
|
108
|
+
layout.addWidget(self._make_nuclear_section())
|
|
109
|
+
layout.addWidget(self._make_cytoplasmic_section())
|
|
110
|
+
layout.addStretch(1)
|
|
111
|
+
self.setLayout(layout)
|
|
112
|
+
|
|
113
|
+
self._refresh_layer_choices()
|
|
114
|
+
self._refresh_model_choices()
|
|
115
|
+
self._update_nuclear_model_settings(self._nuclear_model_combo.currentText())
|
|
116
|
+
self._update_cytoplasmic_model_settings(self._cyto_model_combo.currentText())
|
|
117
|
+
|
|
118
|
+
if self._settings.preload_models_enabled():
|
|
119
|
+
if (
|
|
120
|
+
show_console_notification is not None
|
|
121
|
+
and Notification is not None
|
|
122
|
+
and NotificationSeverity is not None
|
|
123
|
+
):
|
|
124
|
+
show_console_notification(
|
|
125
|
+
Notification(
|
|
126
|
+
"Preloading segmentation models...",
|
|
127
|
+
severity=NotificationSeverity.INFO,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
self._backend.preload_models()
|
|
131
|
+
|
|
132
|
+
def _make_nuclear_section(self) -> QGroupBox:
|
|
133
|
+
"""Build the nuclear segmentation UI section.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
QGroupBox
|
|
138
|
+
Group box containing nuclear segmentation controls.
|
|
139
|
+
"""
|
|
140
|
+
section = QGroupBox("Nuclear segmentation")
|
|
141
|
+
section_layout = QVBoxLayout()
|
|
142
|
+
|
|
143
|
+
form_layout = QFormLayout()
|
|
144
|
+
form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
145
|
+
self._nuclear_layer_combo = RefreshingComboBox(
|
|
146
|
+
refresh_callback=self._refresh_layer_choices
|
|
147
|
+
)
|
|
148
|
+
self._configure_combo(self._nuclear_layer_combo)
|
|
149
|
+
self._nuclear_model_combo = QComboBox()
|
|
150
|
+
self._configure_combo(self._nuclear_model_combo)
|
|
151
|
+
self._nuclear_model_combo.currentTextChanged.connect(
|
|
152
|
+
self._update_nuclear_model_settings
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
form_layout.addRow("Nuclear layer", self._nuclear_layer_combo)
|
|
156
|
+
form_layout.addRow("Model", self._nuclear_model_combo)
|
|
157
|
+
|
|
158
|
+
section_layout.addLayout(form_layout)
|
|
159
|
+
section_layout.addWidget(
|
|
160
|
+
self._make_model_settings_section("Model settings", "nuclear")
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self._nuclear_run_button = QPushButton("Run")
|
|
164
|
+
self._nuclear_run_button.clicked.connect(self._run_nuclear)
|
|
165
|
+
section_layout.addWidget(self._nuclear_run_button)
|
|
166
|
+
section.setLayout(section_layout)
|
|
167
|
+
|
|
168
|
+
return section
|
|
169
|
+
|
|
170
|
+
def _make_cytoplasmic_section(self) -> QGroupBox:
|
|
171
|
+
"""Build the cytoplasmic segmentation UI section.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
QGroupBox
|
|
176
|
+
Group box containing cytoplasmic segmentation controls.
|
|
177
|
+
"""
|
|
178
|
+
section = QGroupBox("Cytoplasmic segmentation")
|
|
179
|
+
section_layout = QVBoxLayout()
|
|
180
|
+
|
|
181
|
+
form_layout = QFormLayout()
|
|
182
|
+
form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
183
|
+
self._cyto_layer_combo = RefreshingComboBox(
|
|
184
|
+
refresh_callback=self._refresh_layer_choices
|
|
185
|
+
)
|
|
186
|
+
self._configure_combo(self._cyto_layer_combo)
|
|
187
|
+
self._cyto_nuclear_layer_combo = RefreshingComboBox(
|
|
188
|
+
refresh_callback=self._refresh_layer_choices
|
|
189
|
+
)
|
|
190
|
+
self._configure_combo(self._cyto_nuclear_layer_combo)
|
|
191
|
+
self._cyto_nuclear_layer_combo.currentTextChanged.connect(
|
|
192
|
+
self._on_cyto_nuclear_layer_changed
|
|
193
|
+
)
|
|
194
|
+
self._cyto_model_combo = QComboBox()
|
|
195
|
+
self._configure_combo(self._cyto_model_combo)
|
|
196
|
+
self._cyto_model_combo.currentTextChanged.connect(
|
|
197
|
+
self._update_cytoplasmic_model_settings
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self._cyto_layer_label = QLabel("Cytoplasmic layer")
|
|
201
|
+
form_layout.addRow(self._cyto_layer_label, self._cyto_layer_combo)
|
|
202
|
+
self._cyto_nuclear_label = QLabel("Nuclear layer")
|
|
203
|
+
form_layout.addRow(self._cyto_nuclear_label, self._cyto_nuclear_layer_combo)
|
|
204
|
+
form_layout.addRow("Model", self._cyto_model_combo)
|
|
205
|
+
|
|
206
|
+
section_layout.addLayout(form_layout)
|
|
207
|
+
section_layout.addWidget(
|
|
208
|
+
self._make_model_settings_section("Model settings", "cytoplasmic")
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
self._cyto_run_button = QPushButton("Run")
|
|
212
|
+
self._cyto_run_button.clicked.connect(self._run_cytoplasmic)
|
|
213
|
+
section_layout.addWidget(self._cyto_run_button)
|
|
214
|
+
section.setLayout(section_layout)
|
|
215
|
+
return section
|
|
216
|
+
|
|
217
|
+
def _make_model_settings_section(self, title: str, section_key: str) -> QGroupBox:
|
|
218
|
+
"""Build the model settings section container.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
title : str
|
|
223
|
+
Section title displayed on the ring.
|
|
224
|
+
section_key : str
|
|
225
|
+
Section identifier used to store the settings layout.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
QGroupBox
|
|
230
|
+
Group box containing model-specific settings.
|
|
231
|
+
"""
|
|
232
|
+
return self._make_titled_section(title, section_key)
|
|
233
|
+
|
|
234
|
+
def _make_titled_section(self, title: str, section_key: str) -> QGroupBox:
|
|
235
|
+
"""Create a titled box that mimics a group box ring.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
title : str
|
|
240
|
+
Title displayed on the ring.
|
|
241
|
+
section_key : str
|
|
242
|
+
Section identifier used to store the settings layout.
|
|
243
|
+
|
|
244
|
+
Returns
|
|
245
|
+
-------
|
|
246
|
+
QGroupBox
|
|
247
|
+
Group box containing a framed content area.
|
|
248
|
+
"""
|
|
249
|
+
section = QGroupBox(title)
|
|
250
|
+
section.setFlat(True)
|
|
251
|
+
section.setStyleSheet(
|
|
252
|
+
"QGroupBox {"
|
|
253
|
+
" margin-top: 8px;"
|
|
254
|
+
"}"
|
|
255
|
+
"QGroupBox::title {"
|
|
256
|
+
" subcontrol-origin: margin;"
|
|
257
|
+
" subcontrol-position: top left;"
|
|
258
|
+
" padding: 0 6px;"
|
|
259
|
+
"}"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
frame = QFrame()
|
|
263
|
+
frame.setFrameShape(QFrame.StyledPanel)
|
|
264
|
+
frame.setFrameShadow(QFrame.Plain)
|
|
265
|
+
frame.setObjectName("titled-section-frame")
|
|
266
|
+
frame.setStyleSheet(
|
|
267
|
+
"QFrame#titled-section-frame {"
|
|
268
|
+
" border: 1px solid palette(mid);"
|
|
269
|
+
" border-radius: 4px;"
|
|
270
|
+
"}"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
settings_layout = QVBoxLayout()
|
|
274
|
+
settings_layout.setContentsMargins(10, 12, 10, 10)
|
|
275
|
+
frame.setLayout(settings_layout)
|
|
276
|
+
|
|
277
|
+
section_layout = QVBoxLayout()
|
|
278
|
+
section_layout.setContentsMargins(8, 12, 8, 4)
|
|
279
|
+
section_layout.addWidget(frame)
|
|
280
|
+
section.setLayout(section_layout)
|
|
281
|
+
|
|
282
|
+
if section_key == "nuclear":
|
|
283
|
+
self._nuclear_model_settings_layout = settings_layout
|
|
284
|
+
else:
|
|
285
|
+
self._cyto_model_settings_layout = settings_layout
|
|
286
|
+
|
|
287
|
+
return section
|
|
288
|
+
|
|
289
|
+
def _refresh_layer_choices(self) -> None:
|
|
290
|
+
"""Populate layer dropdowns from the napari viewer."""
|
|
291
|
+
nuclear_current = self._nuclear_layer_combo.currentText()
|
|
292
|
+
cyto_current = self._cyto_layer_combo.currentText()
|
|
293
|
+
cyto_nuclear_current = self._cyto_nuclear_layer_combo.currentText()
|
|
294
|
+
|
|
295
|
+
self._nuclear_layer_combo.clear()
|
|
296
|
+
self._cyto_layer_combo.clear()
|
|
297
|
+
self._cyto_nuclear_layer_combo.clear()
|
|
298
|
+
if self._viewer is None:
|
|
299
|
+
self._nuclear_layer_combo.addItem("Select a layer")
|
|
300
|
+
self._cyto_layer_combo.addItem("Select a layer")
|
|
301
|
+
self._cyto_nuclear_layer_combo.addItem("Select a layer")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
# For nuclear and cytoplasmic layers, use Image layers
|
|
305
|
+
names = [layer.name for layer in self._iter_image_layers()]
|
|
306
|
+
for name in names:
|
|
307
|
+
self._nuclear_layer_combo.addItem(name)
|
|
308
|
+
self._cyto_layer_combo.addItem(name)
|
|
309
|
+
|
|
310
|
+
# For cytoplasmic nuclear layer, check if model uses nuclear-only mode
|
|
311
|
+
cyto_model_name = self._cyto_model_combo.currentText()
|
|
312
|
+
if cyto_model_name and cyto_model_name != "No models found":
|
|
313
|
+
try:
|
|
314
|
+
model = self._backend.get_model(cyto_model_name)
|
|
315
|
+
modes = model.cytoplasmic_input_modes()
|
|
316
|
+
if modes == ["nuclear"]:
|
|
317
|
+
# Nuclear-only mode: populate with Labels layers
|
|
318
|
+
label_names = [layer.name for layer in self._iter_label_layers()]
|
|
319
|
+
for name in label_names:
|
|
320
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
321
|
+
else:
|
|
322
|
+
# Standard mode: populate with Image layers
|
|
323
|
+
for name in names:
|
|
324
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
325
|
+
except Exception:
|
|
326
|
+
# Fallback to Image layers if model can't be loaded
|
|
327
|
+
for name in names:
|
|
328
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
329
|
+
else:
|
|
330
|
+
# No model selected: populate with Image layers
|
|
331
|
+
for name in names:
|
|
332
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
333
|
+
|
|
334
|
+
self._cyto_nuclear_layer_combo.insertItem(0, "Select a layer")
|
|
335
|
+
|
|
336
|
+
self._restore_combo_selection(self._nuclear_layer_combo, nuclear_current)
|
|
337
|
+
self._restore_combo_selection(self._cyto_layer_combo, cyto_current)
|
|
338
|
+
self._restore_combo_selection(
|
|
339
|
+
self._cyto_nuclear_layer_combo, cyto_nuclear_current
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _refresh_model_choices(self) -> None:
|
|
343
|
+
"""Populate the model dropdowns from available model folders."""
|
|
344
|
+
self._nuclear_model_combo.clear()
|
|
345
|
+
self._cyto_model_combo.clear()
|
|
346
|
+
|
|
347
|
+
nuclear_names = self._backend.list_model_names(task="nuclear")
|
|
348
|
+
if not nuclear_names:
|
|
349
|
+
self._nuclear_model_combo.addItem("No models found")
|
|
350
|
+
else:
|
|
351
|
+
self._nuclear_model_combo.addItems(nuclear_names)
|
|
352
|
+
|
|
353
|
+
cyto_names = self._backend.list_model_names(task="cytoplasmic")
|
|
354
|
+
if not cyto_names:
|
|
355
|
+
self._cyto_model_combo.addItem("No models found")
|
|
356
|
+
else:
|
|
357
|
+
self._cyto_model_combo.addItems(cyto_names)
|
|
358
|
+
|
|
359
|
+
# Trigger initial model settings update to configure layer filters
|
|
360
|
+
if cyto_names:
|
|
361
|
+
self._update_cytoplasmic_model_settings(self._cyto_model_combo.currentText())
|
|
362
|
+
|
|
363
|
+
def _update_nuclear_model_settings(self, model_name: str) -> None:
|
|
364
|
+
"""Rebuild the nuclear model settings area for the selected model.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
model_name : str
|
|
369
|
+
Selected model name from the dropdown.
|
|
370
|
+
"""
|
|
371
|
+
self._refresh_model_settings_layout(
|
|
372
|
+
self._nuclear_model_settings_layout, model_name
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
def _update_cytoplasmic_model_settings(self, model_name: str) -> None:
|
|
376
|
+
"""Rebuild the cytoplasmic model settings area for the selected model.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
model_name : str
|
|
381
|
+
Selected model name from the dropdown.
|
|
382
|
+
"""
|
|
383
|
+
self._refresh_model_settings_layout(
|
|
384
|
+
self._cyto_model_settings_layout, model_name
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
if not model_name or model_name == "No models found":
|
|
388
|
+
self._cyto_layer_combo.setVisible(True)
|
|
389
|
+
self._cyto_layer_combo.setEnabled(False)
|
|
390
|
+
self._cyto_nuclear_layer_combo.setEnabled(False)
|
|
391
|
+
self._cyto_nuclear_label.setText("Nuclear layer")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
model = self._backend.get_model(model_name)
|
|
395
|
+
modes = model.cytoplasmic_input_modes()
|
|
396
|
+
|
|
397
|
+
# Check if model only uses nuclear input (nuclear-only mode)
|
|
398
|
+
if modes == ["nuclear"]:
|
|
399
|
+
# Hide cytoplasmic layer and label, show only nuclear
|
|
400
|
+
self._cyto_layer_combo.setVisible(False)
|
|
401
|
+
self._cyto_layer_label.setVisible(False)
|
|
402
|
+
self._cyto_nuclear_layer_combo.setEnabled(True)
|
|
403
|
+
self._cyto_nuclear_label.setText("Nuclear layer")
|
|
404
|
+
# For nuclear-only models, populate with Labels layers
|
|
405
|
+
self._refresh_nuclear_labels_for_cyto()
|
|
406
|
+
elif "nuclear+cytoplasmic" in modes:
|
|
407
|
+
self._cyto_layer_combo.setVisible(True)
|
|
408
|
+
self._cyto_layer_label.setVisible(True)
|
|
409
|
+
self._cyto_layer_combo.setEnabled(True)
|
|
410
|
+
optional = model.cytoplasmic_nuclear_optional()
|
|
411
|
+
suffix = "optional" if optional else "mandatory"
|
|
412
|
+
self._cyto_nuclear_label.setText(f"Nuclear layer ({suffix})")
|
|
413
|
+
self._cyto_nuclear_layer_combo.setEnabled(True)
|
|
414
|
+
# For standard models, populate with Image layers
|
|
415
|
+
self._refresh_nuclear_images_for_cyto()
|
|
416
|
+
else:
|
|
417
|
+
# Only cytoplasmic
|
|
418
|
+
self._cyto_layer_combo.setVisible(True)
|
|
419
|
+
self._cyto_layer_label.setVisible(True)
|
|
420
|
+
self._cyto_layer_combo.setEnabled(True)
|
|
421
|
+
self._cyto_nuclear_label.setText("Nuclear layer")
|
|
422
|
+
self._cyto_nuclear_layer_combo.setEnabled(False)
|
|
423
|
+
# For standard models, populate with Image layers
|
|
424
|
+
self._refresh_nuclear_images_for_cyto()
|
|
425
|
+
|
|
426
|
+
self._update_cytoplasmic_run_state(model)
|
|
427
|
+
|
|
428
|
+
def _refresh_nuclear_labels_for_cyto(self) -> None:
|
|
429
|
+
"""Refresh cytoplasmic nuclear layer combo with Labels layers."""
|
|
430
|
+
current = self._cyto_nuclear_layer_combo.currentText()
|
|
431
|
+
self._cyto_nuclear_layer_combo.clear()
|
|
432
|
+
|
|
433
|
+
if self._viewer is None:
|
|
434
|
+
self._cyto_nuclear_layer_combo.addItem("Select a layer")
|
|
435
|
+
return
|
|
436
|
+
|
|
437
|
+
label_names = [layer.name for layer in self._iter_label_layers()]
|
|
438
|
+
for name in label_names:
|
|
439
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
440
|
+
self._cyto_nuclear_layer_combo.insertItem(0, "Select a layer")
|
|
441
|
+
self._restore_combo_selection(self._cyto_nuclear_layer_combo, current)
|
|
442
|
+
|
|
443
|
+
def _refresh_nuclear_images_for_cyto(self) -> None:
|
|
444
|
+
"""Refresh cytoplasmic nuclear layer combo with Image layers."""
|
|
445
|
+
current = self._cyto_nuclear_layer_combo.currentText()
|
|
446
|
+
self._cyto_nuclear_layer_combo.clear()
|
|
447
|
+
|
|
448
|
+
if self._viewer is None:
|
|
449
|
+
self._cyto_nuclear_layer_combo.addItem("Select a layer")
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
image_names = [layer.name for layer in self._iter_image_layers()]
|
|
453
|
+
for name in image_names:
|
|
454
|
+
self._cyto_nuclear_layer_combo.addItem(name)
|
|
455
|
+
self._cyto_nuclear_layer_combo.insertItem(0, "Select a layer")
|
|
456
|
+
self._restore_combo_selection(self._cyto_nuclear_layer_combo, current)
|
|
457
|
+
|
|
458
|
+
def _iter_label_layers(self) -> list:
|
|
459
|
+
"""Iterate over Labels layers in the viewer."""
|
|
460
|
+
if self._viewer is None:
|
|
461
|
+
return []
|
|
462
|
+
|
|
463
|
+
label_layers = []
|
|
464
|
+
for layer in self._viewer.layers:
|
|
465
|
+
if Labels is not None:
|
|
466
|
+
if isinstance(layer, Labels):
|
|
467
|
+
label_layers.append(layer)
|
|
468
|
+
else:
|
|
469
|
+
if layer.__class__.__name__ == "Labels":
|
|
470
|
+
label_layers.append(layer)
|
|
471
|
+
return label_layers
|
|
472
|
+
|
|
473
|
+
def _iter_image_layers(self) -> list:
|
|
474
|
+
if self._viewer is None:
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
image_layers = []
|
|
478
|
+
for layer in self._viewer.layers:
|
|
479
|
+
if Image is not None:
|
|
480
|
+
if isinstance(layer, Image):
|
|
481
|
+
image_layers.append(layer)
|
|
482
|
+
else:
|
|
483
|
+
if layer.__class__.__name__ == "Image":
|
|
484
|
+
image_layers.append(layer)
|
|
485
|
+
return image_layers
|
|
486
|
+
|
|
487
|
+
def _restore_combo_selection(self, combo: QComboBox, name: str) -> None:
|
|
488
|
+
if not name:
|
|
489
|
+
return
|
|
490
|
+
index = combo.findText(name)
|
|
491
|
+
if index != -1:
|
|
492
|
+
combo.setCurrentIndex(index)
|
|
493
|
+
|
|
494
|
+
def _refresh_model_settings_layout(
|
|
495
|
+
self,
|
|
496
|
+
settings_layout: QVBoxLayout,
|
|
497
|
+
model_name: str,
|
|
498
|
+
) -> None:
|
|
499
|
+
"""Rebuild the provided model settings area for the selected model.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
settings_layout : QVBoxLayout
|
|
504
|
+
Layout to update with model settings controls.
|
|
505
|
+
model_name : str
|
|
506
|
+
Selected model name from the dropdown.
|
|
507
|
+
"""
|
|
508
|
+
self._clear_layout(settings_layout)
|
|
509
|
+
|
|
510
|
+
if not model_name or model_name == "No models found":
|
|
511
|
+
settings_layout.addWidget(
|
|
512
|
+
QLabel("Select a model to configure its settings.")
|
|
513
|
+
)
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
model = self._backend.get_model(model_name)
|
|
517
|
+
settings_map = (
|
|
518
|
+
self._nuclear_settings_widgets
|
|
519
|
+
if settings_layout is self._nuclear_model_settings_layout
|
|
520
|
+
else self._cyto_settings_widgets
|
|
521
|
+
)
|
|
522
|
+
settings_meta = (
|
|
523
|
+
self._nuclear_settings_meta
|
|
524
|
+
if settings_layout is self._nuclear_model_settings_layout
|
|
525
|
+
else self._cyto_settings_meta
|
|
526
|
+
)
|
|
527
|
+
settings_map.clear()
|
|
528
|
+
settings_meta.clear()
|
|
529
|
+
form_layout = self._build_model_settings(
|
|
530
|
+
model, settings_map, settings_meta
|
|
531
|
+
)
|
|
532
|
+
if form_layout is None:
|
|
533
|
+
settings_layout.addWidget(
|
|
534
|
+
QLabel(f"No settings defined for '{model_name}'.")
|
|
535
|
+
)
|
|
536
|
+
else:
|
|
537
|
+
settings_layout.addLayout(form_layout)
|
|
538
|
+
|
|
539
|
+
def _update_cytoplasmic_run_state(self, model) -> None:
|
|
540
|
+
"""Enable/disable cytoplasmic run button based on required inputs."""
|
|
541
|
+
modes = model.cytoplasmic_input_modes()
|
|
542
|
+
|
|
543
|
+
# Nuclear-only model: only needs nuclear layer
|
|
544
|
+
if modes == ["nuclear"]:
|
|
545
|
+
nuclear_layer = self._get_layer_by_name(
|
|
546
|
+
self._cyto_nuclear_layer_combo.currentText()
|
|
547
|
+
)
|
|
548
|
+
self._cyto_run_button.setEnabled(nuclear_layer is not None)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
# Check if nuclear is required
|
|
552
|
+
if self._cyto_requires_nuclear(model):
|
|
553
|
+
nuclear_layer = self._get_layer_by_name(
|
|
554
|
+
self._cyto_nuclear_layer_combo.currentText()
|
|
555
|
+
)
|
|
556
|
+
self._cyto_run_button.setEnabled(nuclear_layer is not None)
|
|
557
|
+
else:
|
|
558
|
+
self._cyto_run_button.setEnabled(True)
|
|
559
|
+
|
|
560
|
+
def _clear_layout(self, layout: QVBoxLayout) -> None:
|
|
561
|
+
"""Remove widgets and nested layouts from the provided layout.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
layout : QVBoxLayout
|
|
566
|
+
Layout to clear.
|
|
567
|
+
"""
|
|
568
|
+
while layout.count():
|
|
569
|
+
item = layout.takeAt(0)
|
|
570
|
+
child_layout = item.layout()
|
|
571
|
+
if child_layout is not None:
|
|
572
|
+
self._clear_layout(child_layout)
|
|
573
|
+
continue
|
|
574
|
+
widget = item.widget()
|
|
575
|
+
if widget is not None:
|
|
576
|
+
widget.deleteLater()
|
|
577
|
+
|
|
578
|
+
def _build_model_settings(
|
|
579
|
+
self, model, settings_map: dict, settings_meta: dict
|
|
580
|
+
) -> QFormLayout | None:
|
|
581
|
+
"""Build model settings controls from model metadata.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
model : SenoQuantSegmentationModel
|
|
586
|
+
Model wrapper providing settings metadata.
|
|
587
|
+
settings_map : dict
|
|
588
|
+
Mapping of setting keys to their widgets.
|
|
589
|
+
settings_meta : dict
|
|
590
|
+
Mapping of setting keys to their metadata dictionaries.
|
|
591
|
+
|
|
592
|
+
Returns
|
|
593
|
+
-------
|
|
594
|
+
QFormLayout or None
|
|
595
|
+
Form layout containing controls or None if no settings exist.
|
|
596
|
+
"""
|
|
597
|
+
settings = model.list_settings()
|
|
598
|
+
if not settings:
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
form_layout = QFormLayout()
|
|
602
|
+
for setting in settings:
|
|
603
|
+
setting_type = setting.get("type")
|
|
604
|
+
label = setting.get("label", setting.get("key", "Setting"))
|
|
605
|
+
key = setting.get("key", label)
|
|
606
|
+
settings_meta[key] = setting
|
|
607
|
+
|
|
608
|
+
if setting_type == "float":
|
|
609
|
+
widget = QDoubleSpinBox()
|
|
610
|
+
decimals = int(setting.get("decimals", 1))
|
|
611
|
+
widget.setDecimals(decimals)
|
|
612
|
+
widget.setRange(
|
|
613
|
+
float(setting.get("min", 0.0)),
|
|
614
|
+
float(setting.get("max", 1.0)),
|
|
615
|
+
)
|
|
616
|
+
widget.setSingleStep(0.1)
|
|
617
|
+
widget.setValue(float(setting.get("default", 0.0)))
|
|
618
|
+
settings_map[key] = widget
|
|
619
|
+
form_layout.addRow(label, widget)
|
|
620
|
+
elif setting_type == "int":
|
|
621
|
+
widget = QSpinBox()
|
|
622
|
+
widget.setRange(
|
|
623
|
+
int(setting.get("min", 0)),
|
|
624
|
+
int(setting.get("max", 100)),
|
|
625
|
+
)
|
|
626
|
+
widget.setSingleStep(1)
|
|
627
|
+
widget.setValue(int(setting.get("default", 0)))
|
|
628
|
+
settings_map[key] = widget
|
|
629
|
+
form_layout.addRow(label, widget)
|
|
630
|
+
elif setting_type == "bool":
|
|
631
|
+
widget = QCheckBox()
|
|
632
|
+
widget.setChecked(bool(setting.get("default", False)))
|
|
633
|
+
widget.toggled.connect(
|
|
634
|
+
lambda _checked, m=settings_map, meta=settings_meta: self._apply_setting_dependencies(m, meta)
|
|
635
|
+
)
|
|
636
|
+
settings_map[key] = widget
|
|
637
|
+
form_layout.addRow(label, widget)
|
|
638
|
+
else:
|
|
639
|
+
form_layout.addRow(label, QLabel("Unsupported setting type"))
|
|
640
|
+
|
|
641
|
+
self._apply_setting_dependencies(settings_map, settings_meta)
|
|
642
|
+
|
|
643
|
+
return form_layout
|
|
644
|
+
|
|
645
|
+
def _apply_setting_dependencies(
|
|
646
|
+
self, settings_map: dict, settings_meta: dict
|
|
647
|
+
) -> None:
|
|
648
|
+
"""Apply enabled/disabled relationships between settings."""
|
|
649
|
+
for key, setting in settings_meta.items():
|
|
650
|
+
widget = settings_map.get(key)
|
|
651
|
+
if widget is None:
|
|
652
|
+
continue
|
|
653
|
+
|
|
654
|
+
enabled_by = setting.get("enabled_by")
|
|
655
|
+
disabled_by = setting.get("disabled_by")
|
|
656
|
+
|
|
657
|
+
if enabled_by:
|
|
658
|
+
controller = settings_map.get(enabled_by)
|
|
659
|
+
if isinstance(controller, QCheckBox):
|
|
660
|
+
widget.setEnabled(controller.isChecked())
|
|
661
|
+
if disabled_by:
|
|
662
|
+
controller = settings_map.get(disabled_by)
|
|
663
|
+
if isinstance(controller, QCheckBox):
|
|
664
|
+
widget.setEnabled(not controller.isChecked())
|
|
665
|
+
|
|
666
|
+
def _collect_settings(self, settings_map: dict) -> dict:
|
|
667
|
+
"""Collect current values from the settings widgets.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
settings_map : dict
|
|
672
|
+
Mapping of setting keys to their widgets.
|
|
673
|
+
|
|
674
|
+
Returns
|
|
675
|
+
-------
|
|
676
|
+
dict
|
|
677
|
+
Setting values keyed by setting name.
|
|
678
|
+
"""
|
|
679
|
+
values = {}
|
|
680
|
+
for key, widget in settings_map.items():
|
|
681
|
+
if hasattr(widget, "value"):
|
|
682
|
+
values[key] = widget.value()
|
|
683
|
+
elif isinstance(widget, QCheckBox):
|
|
684
|
+
values[key] = widget.isChecked()
|
|
685
|
+
return values
|
|
686
|
+
|
|
687
|
+
def _configure_combo(self, combo: QComboBox) -> None:
|
|
688
|
+
"""Apply sizing defaults to combo boxes."""
|
|
689
|
+
combo.setSizeAdjustPolicy(
|
|
690
|
+
QComboBox.AdjustToMinimumContentsLengthWithIcon
|
|
691
|
+
)
|
|
692
|
+
combo.setMinimumContentsLength(20)
|
|
693
|
+
combo.setMinimumWidth(180)
|
|
694
|
+
combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
695
|
+
|
|
696
|
+
def _run_nuclear(self) -> None:
|
|
697
|
+
"""Run nuclear segmentation for the selected model."""
|
|
698
|
+
model_name = self._nuclear_model_combo.currentText()
|
|
699
|
+
if not model_name or model_name == "No models found":
|
|
700
|
+
return
|
|
701
|
+
model = self._backend.get_preloaded_model(model_name)
|
|
702
|
+
settings = self._collect_settings(self._nuclear_settings_widgets)
|
|
703
|
+
layer_name = self._nuclear_layer_combo.currentText()
|
|
704
|
+
layer = self._get_layer_by_name(layer_name)
|
|
705
|
+
if not self._validate_single_channel_layer(layer, "Nuclear layer"):
|
|
706
|
+
return
|
|
707
|
+
self._start_background_run(
|
|
708
|
+
run_button=self._nuclear_run_button,
|
|
709
|
+
run_text="Run",
|
|
710
|
+
task="nuclear",
|
|
711
|
+
run_callable=lambda: model.run(
|
|
712
|
+
task="nuclear",
|
|
713
|
+
layer=layer,
|
|
714
|
+
settings=settings,
|
|
715
|
+
),
|
|
716
|
+
on_success=lambda result: self._add_labels_layer(
|
|
717
|
+
layer,
|
|
718
|
+
result.get("masks"),
|
|
719
|
+
model_name=model_name,
|
|
720
|
+
label_type="nuc",
|
|
721
|
+
),
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def _run_cytoplasmic(self) -> None:
|
|
725
|
+
"""Run cytoplasmic segmentation for the selected model."""
|
|
726
|
+
model_name = self._cyto_model_combo.currentText()
|
|
727
|
+
if not model_name or model_name == "No models found":
|
|
728
|
+
return
|
|
729
|
+
model = self._backend.get_preloaded_model(model_name)
|
|
730
|
+
settings = self._collect_settings(self._cyto_settings_widgets)
|
|
731
|
+
modes = model.cytoplasmic_input_modes()
|
|
732
|
+
|
|
733
|
+
# Handle nuclear-only models
|
|
734
|
+
if modes == ["nuclear"]:
|
|
735
|
+
nuclear_layer = self._get_layer_by_name(
|
|
736
|
+
self._cyto_nuclear_layer_combo.currentText()
|
|
737
|
+
)
|
|
738
|
+
if not self._validate_single_channel_layer(nuclear_layer, "Nuclear layer"):
|
|
739
|
+
return
|
|
740
|
+
self._start_background_run(
|
|
741
|
+
run_button=self._cyto_run_button,
|
|
742
|
+
run_text="Run",
|
|
743
|
+
task="cytoplasmic",
|
|
744
|
+
run_callable=lambda: model.run(
|
|
745
|
+
task="cytoplasmic",
|
|
746
|
+
nuclear_layer=nuclear_layer,
|
|
747
|
+
settings=settings,
|
|
748
|
+
),
|
|
749
|
+
on_success=lambda result: self._add_labels_layer(
|
|
750
|
+
nuclear_layer,
|
|
751
|
+
result.get("masks"),
|
|
752
|
+
model_name=model_name,
|
|
753
|
+
label_type="cyto",
|
|
754
|
+
),
|
|
755
|
+
)
|
|
756
|
+
return
|
|
757
|
+
|
|
758
|
+
# Standard models: require cytoplasmic layer
|
|
759
|
+
cyto_layer = self._get_layer_by_name(self._cyto_layer_combo.currentText())
|
|
760
|
+
nuclear_layer = self._get_layer_by_name(
|
|
761
|
+
self._cyto_nuclear_layer_combo.currentText()
|
|
762
|
+
)
|
|
763
|
+
if not self._validate_single_channel_layer(cyto_layer, "Cytoplasmic layer"):
|
|
764
|
+
return
|
|
765
|
+
if nuclear_layer is not None and not self._validate_single_channel_layer(
|
|
766
|
+
nuclear_layer, "Nuclear layer"
|
|
767
|
+
):
|
|
768
|
+
return
|
|
769
|
+
if self._cyto_requires_nuclear(model) and nuclear_layer is None:
|
|
770
|
+
return
|
|
771
|
+
self._start_background_run(
|
|
772
|
+
run_button=self._cyto_run_button,
|
|
773
|
+
run_text="Run",
|
|
774
|
+
task="cytoplasmic",
|
|
775
|
+
run_callable=lambda: model.run(
|
|
776
|
+
task="cytoplasmic",
|
|
777
|
+
cytoplasmic_layer=cyto_layer,
|
|
778
|
+
nuclear_layer=nuclear_layer,
|
|
779
|
+
settings=settings,
|
|
780
|
+
),
|
|
781
|
+
on_success=lambda result: self._add_labels_layer(
|
|
782
|
+
cyto_layer,
|
|
783
|
+
result.get("masks"),
|
|
784
|
+
model_name=model_name,
|
|
785
|
+
label_type="cyto",
|
|
786
|
+
),
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
def _start_background_run(
|
|
790
|
+
self,
|
|
791
|
+
run_button: QPushButton,
|
|
792
|
+
run_text: str,
|
|
793
|
+
task: str,
|
|
794
|
+
run_callable,
|
|
795
|
+
on_success,
|
|
796
|
+
) -> None:
|
|
797
|
+
"""Run a model in a background thread and manage UI state.
|
|
798
|
+
|
|
799
|
+
Parameters
|
|
800
|
+
----------
|
|
801
|
+
run_button : QPushButton
|
|
802
|
+
Button to disable while the background task runs.
|
|
803
|
+
run_text : str
|
|
804
|
+
Label text to restore after completion.
|
|
805
|
+
task : str
|
|
806
|
+
Task name used for error messaging.
|
|
807
|
+
run_callable : callable
|
|
808
|
+
Callable that executes the model run.
|
|
809
|
+
on_success : callable
|
|
810
|
+
Callback invoked with the run result dictionary.
|
|
811
|
+
"""
|
|
812
|
+
run_button.setEnabled(False)
|
|
813
|
+
run_button.setText("Running...")
|
|
814
|
+
|
|
815
|
+
thread = QThread(self)
|
|
816
|
+
worker = _RunWorker(run_callable)
|
|
817
|
+
worker.moveToThread(thread)
|
|
818
|
+
|
|
819
|
+
def handle_success(result: dict) -> None:
|
|
820
|
+
on_success(result)
|
|
821
|
+
self._finish_background_run(run_button, run_text, thread, worker)
|
|
822
|
+
|
|
823
|
+
def handle_error(message: str) -> None:
|
|
824
|
+
self._notify(f"{task.capitalize()} run failed: {message}")
|
|
825
|
+
self._finish_background_run(run_button, run_text, thread, worker)
|
|
826
|
+
|
|
827
|
+
thread.started.connect(worker.run)
|
|
828
|
+
worker.finished.connect(handle_success)
|
|
829
|
+
worker.error.connect(handle_error)
|
|
830
|
+
worker.finished.connect(thread.quit)
|
|
831
|
+
worker.error.connect(thread.quit)
|
|
832
|
+
thread.finished.connect(thread.deleteLater)
|
|
833
|
+
thread.finished.connect(worker.deleteLater)
|
|
834
|
+
|
|
835
|
+
self._active_workers.append((thread, worker))
|
|
836
|
+
thread.start()
|
|
837
|
+
|
|
838
|
+
def _finish_background_run(
|
|
839
|
+
self,
|
|
840
|
+
run_button: QPushButton,
|
|
841
|
+
run_text: str,
|
|
842
|
+
thread: QThread,
|
|
843
|
+
worker: QObject,
|
|
844
|
+
) -> None:
|
|
845
|
+
"""Restore UI state after a background run completes.
|
|
846
|
+
|
|
847
|
+
Parameters
|
|
848
|
+
----------
|
|
849
|
+
run_button : QPushButton
|
|
850
|
+
Button to restore after completion.
|
|
851
|
+
run_text : str
|
|
852
|
+
Label text to restore on the button.
|
|
853
|
+
thread : QThread
|
|
854
|
+
Background thread being torn down.
|
|
855
|
+
worker : QObject
|
|
856
|
+
Worker object associated with the thread.
|
|
857
|
+
"""
|
|
858
|
+
run_button.setEnabled(True)
|
|
859
|
+
run_button.setText(run_text)
|
|
860
|
+
try:
|
|
861
|
+
self._active_workers.remove((thread, worker))
|
|
862
|
+
except ValueError:
|
|
863
|
+
pass
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _get_layer_by_name(self, name: str):
|
|
867
|
+
"""Return a viewer layer with the given name, if it exists.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
name : str
|
|
872
|
+
Layer name to locate.
|
|
873
|
+
|
|
874
|
+
Returns
|
|
875
|
+
-------
|
|
876
|
+
object or None
|
|
877
|
+
Matching layer object or None if not found.
|
|
878
|
+
"""
|
|
879
|
+
if self._viewer is None:
|
|
880
|
+
return None
|
|
881
|
+
for layer in self._viewer.layers:
|
|
882
|
+
if layer.name == name:
|
|
883
|
+
return layer
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
def _validate_single_channel_layer(self, layer, label: str) -> bool:
|
|
887
|
+
"""Validate that a layer is single-channel 2D/3D image data.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
layer : object or None
|
|
892
|
+
Napari layer to validate.
|
|
893
|
+
label : str
|
|
894
|
+
User-facing label for notifications.
|
|
895
|
+
|
|
896
|
+
Returns
|
|
897
|
+
-------
|
|
898
|
+
bool
|
|
899
|
+
True if the layer is valid for single-channel processing.
|
|
900
|
+
"""
|
|
901
|
+
if layer is None:
|
|
902
|
+
return False
|
|
903
|
+
if getattr(layer, "rgb", False):
|
|
904
|
+
self._notify(f"{label} must be single-channel (not RGB).")
|
|
905
|
+
return False
|
|
906
|
+
shape = getattr(getattr(layer, "data", None), "shape", None)
|
|
907
|
+
if shape is None:
|
|
908
|
+
return False
|
|
909
|
+
squeezed_ndim = sum(dim != 1 for dim in shape)
|
|
910
|
+
if squeezed_ndim not in (2, 3):
|
|
911
|
+
self._notify(f"{label} must be 2D or 3D single-channel.")
|
|
912
|
+
return False
|
|
913
|
+
return True
|
|
914
|
+
|
|
915
|
+
def _notify(self, message: str) -> None:
|
|
916
|
+
"""Send a warning notification to the napari console.
|
|
917
|
+
|
|
918
|
+
Parameters
|
|
919
|
+
----------
|
|
920
|
+
message : str
|
|
921
|
+
Notification message to display.
|
|
922
|
+
"""
|
|
923
|
+
if (
|
|
924
|
+
show_console_notification is not None
|
|
925
|
+
and Notification is not None
|
|
926
|
+
and NotificationSeverity is not None
|
|
927
|
+
):
|
|
928
|
+
show_console_notification(
|
|
929
|
+
Notification(message, severity=NotificationSeverity.WARNING)
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
def _on_preload_models_changed(self, enabled: bool) -> None:
|
|
933
|
+
"""Handle preload setting changes.
|
|
934
|
+
|
|
935
|
+
Parameters
|
|
936
|
+
----------
|
|
937
|
+
enabled : bool
|
|
938
|
+
Whether preloading is enabled.
|
|
939
|
+
"""
|
|
940
|
+
if enabled:
|
|
941
|
+
if (
|
|
942
|
+
show_console_notification is not None
|
|
943
|
+
and Notification is not None
|
|
944
|
+
and NotificationSeverity is not None
|
|
945
|
+
):
|
|
946
|
+
show_console_notification(
|
|
947
|
+
Notification(
|
|
948
|
+
"Preloading segmentation models...",
|
|
949
|
+
severity=NotificationSeverity.INFO,
|
|
950
|
+
)
|
|
951
|
+
)
|
|
952
|
+
self._backend.preload_models()
|
|
953
|
+
|
|
954
|
+
def _cyto_requires_nuclear(self, model) -> bool:
|
|
955
|
+
"""Return True when cytoplasmic mode requires a nuclear channel."""
|
|
956
|
+
modes = model.cytoplasmic_input_modes()
|
|
957
|
+
if modes == ["nuclear"]:
|
|
958
|
+
return True
|
|
959
|
+
if "nuclear+cytoplasmic" not in modes:
|
|
960
|
+
return False
|
|
961
|
+
return not model.cytoplasmic_nuclear_optional()
|
|
962
|
+
|
|
963
|
+
def _on_cyto_nuclear_layer_changed(self) -> None:
|
|
964
|
+
model_name = self._cyto_model_combo.currentText()
|
|
965
|
+
if not model_name or model_name == "No models found":
|
|
966
|
+
self._cyto_run_button.setEnabled(False)
|
|
967
|
+
return
|
|
968
|
+
model = self._backend.get_model(model_name)
|
|
969
|
+
self._update_cytoplasmic_run_state(model)
|
|
970
|
+
|
|
971
|
+
def _add_labels_layer(self, source_layer, masks, model_name: str, label_type: str) -> None:
|
|
972
|
+
if self._viewer is None or source_layer is None or masks is None:
|
|
973
|
+
return
|
|
974
|
+
label_name = f"{source_layer.name}_{model_name}_{label_type}_labels"
|
|
975
|
+
self._viewer.add_labels(
|
|
976
|
+
masks,
|
|
977
|
+
name=label_name,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# Get the labels layer and set contour = 2
|
|
981
|
+
labels_layer = self._viewer.layers[label_name]
|
|
982
|
+
labels_layer.contour = 2
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
class _RunWorker(QObject):
|
|
986
|
+
"""Worker that executes a callable in a background thread."""
|
|
987
|
+
|
|
988
|
+
finished = Signal(dict)
|
|
989
|
+
error = Signal(str)
|
|
990
|
+
|
|
991
|
+
def __init__(self, run_callable) -> None:
|
|
992
|
+
"""Initialize the worker with a callable.
|
|
993
|
+
|
|
994
|
+
Parameters
|
|
995
|
+
----------
|
|
996
|
+
run_callable : callable
|
|
997
|
+
Callable to execute on the worker thread.
|
|
998
|
+
"""
|
|
999
|
+
super().__init__()
|
|
1000
|
+
self._run_callable = run_callable
|
|
1001
|
+
|
|
1002
|
+
def run(self) -> None:
|
|
1003
|
+
"""Execute the callable and emit results."""
|
|
1004
|
+
try:
|
|
1005
|
+
result = self._run_callable()
|
|
1006
|
+
except Exception as exc: # pragma: no cover - runtime error path
|
|
1007
|
+
self.error.emit(str(exc))
|
|
1008
|
+
return
|
|
1009
|
+
self.finished.emit(result)
|