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,477 @@
|
|
|
1
|
+
"""Spots channels dialog."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from qtpy.QtCore import Qt, QTimer
|
|
8
|
+
from qtpy.QtWidgets import (
|
|
9
|
+
QComboBox,
|
|
10
|
+
QDialog,
|
|
11
|
+
QGroupBox,
|
|
12
|
+
QPushButton,
|
|
13
|
+
QScrollArea,
|
|
14
|
+
QSplitter,
|
|
15
|
+
QSizePolicy,
|
|
16
|
+
QVBoxLayout,
|
|
17
|
+
QWidget,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .config import (
|
|
21
|
+
SpotsChannelConfig,
|
|
22
|
+
SpotsFeatureData,
|
|
23
|
+
SpotsSegmentationConfig,
|
|
24
|
+
)
|
|
25
|
+
from .rows import SpotsChannelRow, SpotsSegmentationRow
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .feature import SpotsFeature
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SpotsChannelsDialog(QDialog):
|
|
32
|
+
"""Dialog for configuring spots channels."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, feature: "SpotsFeature") -> None:
|
|
35
|
+
"""Initialize the spots channels dialog.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
feature : SpotsFeature
|
|
40
|
+
Spots feature instance owning the dialog.
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(feature._tab)
|
|
43
|
+
self._feature = feature
|
|
44
|
+
self._tab = feature._tab
|
|
45
|
+
data = feature._state.data
|
|
46
|
+
if not isinstance(data, SpotsFeatureData):
|
|
47
|
+
data = SpotsFeatureData()
|
|
48
|
+
feature._state.data = data
|
|
49
|
+
self._data = data
|
|
50
|
+
self._segmentations = data.segmentations
|
|
51
|
+
self._channels = data.channels
|
|
52
|
+
self._rows: list[SpotsChannelRow] = []
|
|
53
|
+
self._segmentation_rows: list[SpotsSegmentationRow] = []
|
|
54
|
+
self._layout_watch_timer: QTimer | None = None
|
|
55
|
+
self._layout_last_sizes: dict[str, tuple[int, int]] = {}
|
|
56
|
+
|
|
57
|
+
self.setWindowTitle("Spots channels")
|
|
58
|
+
self.setMinimumSize(600, 800)
|
|
59
|
+
layout = QVBoxLayout()
|
|
60
|
+
|
|
61
|
+
segmentations_section = self._build_segmentations_section()
|
|
62
|
+
channels_section = self._build_channels_section()
|
|
63
|
+
splitter = QSplitter(Qt.Vertical)
|
|
64
|
+
splitter.setChildrenCollapsible(False)
|
|
65
|
+
splitter.addWidget(segmentations_section)
|
|
66
|
+
splitter.addWidget(channels_section)
|
|
67
|
+
splitter.setStretchFactor(0, 2)
|
|
68
|
+
splitter.setStretchFactor(1, 3)
|
|
69
|
+
layout.addWidget(splitter, 1)
|
|
70
|
+
|
|
71
|
+
close_button = QPushButton("Save")
|
|
72
|
+
close_button.clicked.connect(self.accept)
|
|
73
|
+
layout.addWidget(close_button)
|
|
74
|
+
|
|
75
|
+
self.setLayout(layout)
|
|
76
|
+
self._load_segmentations()
|
|
77
|
+
self._load_channels()
|
|
78
|
+
self._start_layout_watch()
|
|
79
|
+
|
|
80
|
+
def closeEvent(self, event) -> None:
|
|
81
|
+
"""Handle window close as a save action.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
event : QCloseEvent
|
|
86
|
+
Close event from Qt.
|
|
87
|
+
"""
|
|
88
|
+
self.accept()
|
|
89
|
+
event.accept()
|
|
90
|
+
|
|
91
|
+
def _build_segmentations_section(self) -> QGroupBox:
|
|
92
|
+
"""Create the segmentations section with add/remove controls.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
QGroupBox
|
|
97
|
+
Group box containing segmentation rows and the add button.
|
|
98
|
+
"""
|
|
99
|
+
section = QGroupBox(
|
|
100
|
+
"Nuclear/cytoplasmic segmentations to exclude background spots"
|
|
101
|
+
)
|
|
102
|
+
section.setFlat(True)
|
|
103
|
+
section.setStyleSheet(self._section_stylesheet())
|
|
104
|
+
|
|
105
|
+
self._segmentations_container = QWidget()
|
|
106
|
+
self._segmentations_container.setSizePolicy(
|
|
107
|
+
QSizePolicy.Expanding, QSizePolicy.Fixed
|
|
108
|
+
)
|
|
109
|
+
self._segmentations_layout = QVBoxLayout()
|
|
110
|
+
self._segmentations_layout.setContentsMargins(0, 0, 0, 0)
|
|
111
|
+
self._segmentations_layout.setSpacing(8)
|
|
112
|
+
self._segmentations_container.setLayout(self._segmentations_layout)
|
|
113
|
+
|
|
114
|
+
self._segmentations_scroll_area = QScrollArea()
|
|
115
|
+
self._segmentations_scroll_area.setWidgetResizable(True)
|
|
116
|
+
self._segmentations_scroll_area.setHorizontalScrollBarPolicy(
|
|
117
|
+
Qt.ScrollBarAlwaysOff
|
|
118
|
+
)
|
|
119
|
+
self._segmentations_scroll_area.setSizePolicy(
|
|
120
|
+
QSizePolicy.Expanding, QSizePolicy.Expanding
|
|
121
|
+
)
|
|
122
|
+
self._segmentations_scroll_area.setWidget(
|
|
123
|
+
self._segmentations_container
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
add_button = QPushButton("Add segmentation")
|
|
127
|
+
add_button.clicked.connect(self._add_segmentation)
|
|
128
|
+
|
|
129
|
+
section_layout = QVBoxLayout()
|
|
130
|
+
section_layout.setContentsMargins(10, 12, 10, 10)
|
|
131
|
+
section_layout.addWidget(self._segmentations_scroll_area)
|
|
132
|
+
section_layout.addWidget(add_button)
|
|
133
|
+
section.setLayout(section_layout)
|
|
134
|
+
self._segmentations_section = section
|
|
135
|
+
return section
|
|
136
|
+
|
|
137
|
+
def _build_channels_section(self) -> QGroupBox:
|
|
138
|
+
"""Create the channels section with add/remove controls.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
QGroupBox
|
|
143
|
+
Group box containing channel rows and the add button.
|
|
144
|
+
"""
|
|
145
|
+
self._channels_container = QWidget()
|
|
146
|
+
self._channels_container.setSizePolicy(
|
|
147
|
+
QSizePolicy.Expanding, QSizePolicy.Fixed
|
|
148
|
+
)
|
|
149
|
+
self._channels_layout = QVBoxLayout()
|
|
150
|
+
self._channels_layout.setContentsMargins(0, 0, 0, 0)
|
|
151
|
+
self._channels_layout.setSpacing(8)
|
|
152
|
+
self._channels_container.setLayout(self._channels_layout)
|
|
153
|
+
|
|
154
|
+
section = QGroupBox("Channels")
|
|
155
|
+
section.setFlat(True)
|
|
156
|
+
section.setStyleSheet(self._section_stylesheet())
|
|
157
|
+
|
|
158
|
+
self._channels_scroll_area = QScrollArea()
|
|
159
|
+
self._channels_scroll_area.setWidgetResizable(True)
|
|
160
|
+
self._channels_scroll_area.setHorizontalScrollBarPolicy(
|
|
161
|
+
Qt.ScrollBarAlwaysOff
|
|
162
|
+
)
|
|
163
|
+
self._channels_scroll_area.setSizePolicy(
|
|
164
|
+
QSizePolicy.Expanding, QSizePolicy.Expanding
|
|
165
|
+
)
|
|
166
|
+
self._channels_scroll_area.setWidget(self._channels_container)
|
|
167
|
+
|
|
168
|
+
add_button = QPushButton("Add channel")
|
|
169
|
+
add_button.clicked.connect(self._add_channel)
|
|
170
|
+
|
|
171
|
+
section_layout = QVBoxLayout()
|
|
172
|
+
section_layout.setContentsMargins(10, 12, 10, 10)
|
|
173
|
+
section_layout.addWidget(self._channels_scroll_area)
|
|
174
|
+
section_layout.addWidget(add_button)
|
|
175
|
+
section.setLayout(section_layout)
|
|
176
|
+
|
|
177
|
+
self._channels_section = section
|
|
178
|
+
return section
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _section_stylesheet() -> str:
|
|
182
|
+
"""Return the stylesheet used for dialog sections.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
str
|
|
187
|
+
Qt stylesheet string for section group boxes.
|
|
188
|
+
"""
|
|
189
|
+
return (
|
|
190
|
+
"QGroupBox {"
|
|
191
|
+
" margin-top: 8px;"
|
|
192
|
+
"}"
|
|
193
|
+
"QGroupBox::title {"
|
|
194
|
+
" subcontrol-origin: margin;"
|
|
195
|
+
" subcontrol-position: top left;"
|
|
196
|
+
" padding: 0 6px;"
|
|
197
|
+
"}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _refresh_labels_combo(self, combo: QComboBox, filter_type: str = "cellular") -> None:
|
|
201
|
+
"""Refresh labels layer options for the dialog.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
combo : QComboBox
|
|
206
|
+
Labels combo box to refresh.
|
|
207
|
+
filter_type : str, optional
|
|
208
|
+
Type of labels to show: "cellular" for nuc/cyto labels,
|
|
209
|
+
"spots" for spot labels. Defaults to "cellular".
|
|
210
|
+
"""
|
|
211
|
+
current = combo.currentText()
|
|
212
|
+
combo.clear()
|
|
213
|
+
viewer = self._tab._viewer
|
|
214
|
+
if viewer is None:
|
|
215
|
+
combo.addItem("Select labels")
|
|
216
|
+
return
|
|
217
|
+
for layer in viewer.layers:
|
|
218
|
+
if layer.__class__.__name__ == "Labels":
|
|
219
|
+
layer_name = layer.name
|
|
220
|
+
# Filter based on label type
|
|
221
|
+
if filter_type == "cellular" and self._is_cellular_label(layer_name):
|
|
222
|
+
combo.addItem(layer_name)
|
|
223
|
+
elif filter_type == "spots" and self._is_spot_label(layer_name):
|
|
224
|
+
combo.addItem(layer_name)
|
|
225
|
+
if current:
|
|
226
|
+
index = combo.findText(current)
|
|
227
|
+
if index != -1:
|
|
228
|
+
combo.setCurrentIndex(index)
|
|
229
|
+
|
|
230
|
+
def _is_cellular_label(self, layer_name: str) -> bool:
|
|
231
|
+
"""Check if a label layer is a cellular segmentation.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
layer_name : str
|
|
236
|
+
Name of the labels layer.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
bool
|
|
241
|
+
True if the layer is a cellular label (nuclear or cytoplasmic).
|
|
242
|
+
"""
|
|
243
|
+
return layer_name.endswith("_nuc_labels") or layer_name.endswith("_cyto_labels")
|
|
244
|
+
|
|
245
|
+
def _is_spot_label(self, layer_name: str) -> bool:
|
|
246
|
+
"""Check if a label layer is a spot segmentation.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
layer_name : str
|
|
251
|
+
Name of the labels layer.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
bool
|
|
256
|
+
True if the layer is a spot label.
|
|
257
|
+
"""
|
|
258
|
+
return layer_name.endswith("_spot_labels")
|
|
259
|
+
|
|
260
|
+
def _refresh_image_combo(self, combo: QComboBox) -> None:
|
|
261
|
+
"""Refresh image layer options for the dialog.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
combo : QComboBox
|
|
266
|
+
Image combo box to refresh.
|
|
267
|
+
"""
|
|
268
|
+
current = combo.currentText()
|
|
269
|
+
combo.clear()
|
|
270
|
+
viewer = self._tab._viewer
|
|
271
|
+
if viewer is None:
|
|
272
|
+
combo.addItem("Select image")
|
|
273
|
+
return
|
|
274
|
+
for layer in viewer.layers:
|
|
275
|
+
if layer.__class__.__name__ == "Image":
|
|
276
|
+
combo.addItem(layer.name)
|
|
277
|
+
if current:
|
|
278
|
+
index = combo.findText(current)
|
|
279
|
+
if index != -1:
|
|
280
|
+
combo.setCurrentIndex(index)
|
|
281
|
+
|
|
282
|
+
def _load_segmentations(self) -> None:
|
|
283
|
+
"""Build segmentation rows from stored data."""
|
|
284
|
+
if not self._segmentations:
|
|
285
|
+
return
|
|
286
|
+
for segmentation_data in self._segmentations:
|
|
287
|
+
if not isinstance(segmentation_data, SpotsSegmentationConfig):
|
|
288
|
+
continue
|
|
289
|
+
self._add_segmentation(segmentation_data)
|
|
290
|
+
|
|
291
|
+
def _load_channels(self) -> None:
|
|
292
|
+
"""Build channel rows from stored data."""
|
|
293
|
+
if not self._channels:
|
|
294
|
+
return
|
|
295
|
+
for channel_data in self._channels:
|
|
296
|
+
if not isinstance(channel_data, SpotsChannelConfig):
|
|
297
|
+
continue
|
|
298
|
+
self._add_channel(channel_data)
|
|
299
|
+
|
|
300
|
+
def _add_channel(self, channel_data: SpotsChannelConfig | None = None) -> None:
|
|
301
|
+
"""Add a channel row to the dialog.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
channel_data : SpotsChannelConfig or None
|
|
306
|
+
Channel configuration data.
|
|
307
|
+
"""
|
|
308
|
+
if isinstance(channel_data, bool):
|
|
309
|
+
channel_data = None
|
|
310
|
+
if not isinstance(channel_data, SpotsChannelConfig):
|
|
311
|
+
channel_data = SpotsChannelConfig()
|
|
312
|
+
self._channels.append(channel_data)
|
|
313
|
+
row = SpotsChannelRow(self, channel_data)
|
|
314
|
+
self._rows.append(row)
|
|
315
|
+
self._channels_layout.addWidget(row)
|
|
316
|
+
self._renumber_rows()
|
|
317
|
+
self._schedule_layout_update()
|
|
318
|
+
|
|
319
|
+
def _remove_channel(self, row: SpotsChannelRow) -> None:
|
|
320
|
+
"""Remove a channel row and its stored data.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
row : SpotsChannelRow
|
|
325
|
+
Row instance to remove.
|
|
326
|
+
"""
|
|
327
|
+
if row not in self._rows:
|
|
328
|
+
return
|
|
329
|
+
self._rows.remove(row)
|
|
330
|
+
if row.data in self._channels:
|
|
331
|
+
self._channels.remove(row.data)
|
|
332
|
+
self._channels_layout.removeWidget(row)
|
|
333
|
+
row.deleteLater()
|
|
334
|
+
self._renumber_rows()
|
|
335
|
+
self._schedule_layout_update()
|
|
336
|
+
|
|
337
|
+
def _renumber_rows(self) -> None:
|
|
338
|
+
"""Update channel row titles after changes."""
|
|
339
|
+
for index, row in enumerate(self._rows, start=0):
|
|
340
|
+
row.update_title(index)
|
|
341
|
+
|
|
342
|
+
def _add_segmentation(
|
|
343
|
+
self, segmentation_data: SpotsSegmentationConfig | None = None
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Add a segmentation row to the dialog.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
segmentation_data : SpotsSegmentationConfig or None
|
|
350
|
+
Segmentation configuration data.
|
|
351
|
+
"""
|
|
352
|
+
if isinstance(segmentation_data, bool):
|
|
353
|
+
segmentation_data = None
|
|
354
|
+
if not isinstance(segmentation_data, SpotsSegmentationConfig):
|
|
355
|
+
segmentation_data = SpotsSegmentationConfig()
|
|
356
|
+
self._segmentations.append(segmentation_data)
|
|
357
|
+
row = SpotsSegmentationRow(self, segmentation_data)
|
|
358
|
+
self._segmentation_rows.append(row)
|
|
359
|
+
self._segmentations_layout.addWidget(row)
|
|
360
|
+
self._renumber_segmentations()
|
|
361
|
+
self._schedule_layout_update()
|
|
362
|
+
|
|
363
|
+
def _remove_segmentation(self, row: SpotsSegmentationRow) -> None:
|
|
364
|
+
"""Remove a segmentation row and its stored data.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
row : SpotsSegmentationRow
|
|
369
|
+
Row instance to remove.
|
|
370
|
+
"""
|
|
371
|
+
if row not in self._segmentation_rows:
|
|
372
|
+
return
|
|
373
|
+
self._segmentation_rows.remove(row)
|
|
374
|
+
if row.data in self._segmentations:
|
|
375
|
+
self._segmentations.remove(row.data)
|
|
376
|
+
self._segmentations_layout.removeWidget(row)
|
|
377
|
+
row.deleteLater()
|
|
378
|
+
self._renumber_segmentations()
|
|
379
|
+
self._schedule_layout_update()
|
|
380
|
+
|
|
381
|
+
def _renumber_segmentations(self) -> None:
|
|
382
|
+
"""Update segmentation row titles after changes."""
|
|
383
|
+
for index, row in enumerate(self._segmentation_rows, start=0):
|
|
384
|
+
row.update_title(index)
|
|
385
|
+
|
|
386
|
+
def _start_layout_watch(self) -> None:
|
|
387
|
+
"""Start a timer to monitor layout changes in the dialog."""
|
|
388
|
+
if self._layout_watch_timer is not None:
|
|
389
|
+
return
|
|
390
|
+
self._layout_watch_timer = QTimer(self)
|
|
391
|
+
self._layout_watch_timer.setInterval(150)
|
|
392
|
+
self._layout_watch_timer.timeout.connect(self._poll_layout)
|
|
393
|
+
self._layout_watch_timer.start()
|
|
394
|
+
|
|
395
|
+
def _schedule_layout_update(self) -> None:
|
|
396
|
+
"""Schedule a layout update on the next timer tick."""
|
|
397
|
+
self._layout_last_sizes.clear()
|
|
398
|
+
|
|
399
|
+
def _poll_layout(self) -> None:
|
|
400
|
+
"""Recompute layout sizing when content changes."""
|
|
401
|
+
self._apply_scroll_area_layout(
|
|
402
|
+
"segmentations",
|
|
403
|
+
self._segmentations_scroll_area,
|
|
404
|
+
self._segmentations_layout,
|
|
405
|
+
max_ratio=0.2,
|
|
406
|
+
)
|
|
407
|
+
self._apply_scroll_area_layout(
|
|
408
|
+
"channels",
|
|
409
|
+
self._channels_scroll_area,
|
|
410
|
+
self._channels_layout,
|
|
411
|
+
max_ratio=0.8,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def _apply_scroll_area_layout(
|
|
415
|
+
self,
|
|
416
|
+
key: str,
|
|
417
|
+
scroll_area: QScrollArea,
|
|
418
|
+
layout: QVBoxLayout,
|
|
419
|
+
max_ratio: float,
|
|
420
|
+
) -> None:
|
|
421
|
+
"""Apply sizing rules for a scroll area section.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
key : str
|
|
426
|
+
Cache key for the section size.
|
|
427
|
+
scroll_area : QScrollArea
|
|
428
|
+
Scroll area to resize.
|
|
429
|
+
layout : QVBoxLayout
|
|
430
|
+
Layout containing section rows.
|
|
431
|
+
max_ratio : float
|
|
432
|
+
Maximum height ratio relative to the screen.
|
|
433
|
+
"""
|
|
434
|
+
size = self._layout_content_size(layout)
|
|
435
|
+
if self._layout_last_sizes.get(key) == size:
|
|
436
|
+
return
|
|
437
|
+
self._layout_last_sizes[key] = size
|
|
438
|
+
content = scroll_area.widget()
|
|
439
|
+
if content is not None:
|
|
440
|
+
content.setMinimumWidth(scroll_area.viewport().width())
|
|
441
|
+
scroll_area.updateGeometry()
|
|
442
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
443
|
+
|
|
444
|
+
def _layout_content_size(self, layout: QVBoxLayout) -> tuple[int, int]:
|
|
445
|
+
"""Return content size for a vertical layout.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
layout : QVBoxLayout
|
|
450
|
+
Layout to measure.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
tuple of int
|
|
455
|
+
(width, height) of the layout contents.
|
|
456
|
+
"""
|
|
457
|
+
layout.activate()
|
|
458
|
+
margins = layout.contentsMargins()
|
|
459
|
+
spacing = layout.spacing()
|
|
460
|
+
count = layout.count()
|
|
461
|
+
total_height = margins.top() + margins.bottom()
|
|
462
|
+
max_width = 0
|
|
463
|
+
for index in range(count):
|
|
464
|
+
item = layout.itemAt(index)
|
|
465
|
+
widget = item.widget()
|
|
466
|
+
if widget is None:
|
|
467
|
+
item_size = item.sizeHint()
|
|
468
|
+
else:
|
|
469
|
+
item_size = widget.sizeHint().expandedTo(
|
|
470
|
+
widget.minimumSizeHint()
|
|
471
|
+
)
|
|
472
|
+
max_width = max(max_width, item_size.width())
|
|
473
|
+
total_height += item_size.height()
|
|
474
|
+
if count > 1:
|
|
475
|
+
total_height += spacing * (count - 1)
|
|
476
|
+
total_width = margins.left() + margins.right() + max_width
|
|
477
|
+
return (total_width, total_height)
|