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,346 @@
|
|
|
1
|
+
"""ROI selection UI helpers for quantification features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from qtpy.QtCore import Qt, QTimer
|
|
8
|
+
from qtpy.QtGui import QGuiApplication
|
|
9
|
+
from qtpy.QtWidgets import (
|
|
10
|
+
QCheckBox,
|
|
11
|
+
QComboBox,
|
|
12
|
+
QFormLayout,
|
|
13
|
+
QGroupBox,
|
|
14
|
+
QLineEdit,
|
|
15
|
+
QPushButton,
|
|
16
|
+
QScrollArea,
|
|
17
|
+
QSizePolicy,
|
|
18
|
+
QVBoxLayout,
|
|
19
|
+
QWidget,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .base import RefreshingComboBox
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ROIConfig:
|
|
27
|
+
"""Configuration for a single ROI entry.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
name : str
|
|
32
|
+
Display name for the ROI.
|
|
33
|
+
layer : str
|
|
34
|
+
Shapes layer name used for the ROI.
|
|
35
|
+
roi_type : str
|
|
36
|
+
Whether the ROI should be included or excluded.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str = ""
|
|
40
|
+
layer: str = ""
|
|
41
|
+
roi_type: str = "Include"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ROISection:
|
|
45
|
+
"""Reusable ROI controls for marker and spots features."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
tab,
|
|
50
|
+
context,
|
|
51
|
+
rois: list[ROIConfig],
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize the ROI helper for a feature.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
tab : QuantificationTab
|
|
58
|
+
Parent quantification tab instance.
|
|
59
|
+
context : FeatureUIContext
|
|
60
|
+
Feature UI context.
|
|
61
|
+
rois : list of ROIConfig
|
|
62
|
+
Feature ROI configuration list.
|
|
63
|
+
"""
|
|
64
|
+
self._tab = tab
|
|
65
|
+
self._context = context
|
|
66
|
+
self._rois = rois
|
|
67
|
+
self._checkbox: QCheckBox | None = None
|
|
68
|
+
self._container: QWidget | None = None
|
|
69
|
+
self._layout: QVBoxLayout | None = None
|
|
70
|
+
self._scroll_area: QScrollArea | None = None
|
|
71
|
+
self._items_container: QWidget | None = None
|
|
72
|
+
self._items: list[tuple[QGroupBox, ROIConfig]] = []
|
|
73
|
+
|
|
74
|
+
def build(self) -> None:
|
|
75
|
+
"""Create the ROI controls and attach to the right column."""
|
|
76
|
+
right_layout = self._context.right_layout
|
|
77
|
+
|
|
78
|
+
checkbox = QCheckBox("ROIs")
|
|
79
|
+
checkbox.toggled.connect(self._toggle)
|
|
80
|
+
|
|
81
|
+
container = QWidget()
|
|
82
|
+
container.setVisible(False)
|
|
83
|
+
container.setMinimumWidth(240)
|
|
84
|
+
container_layout = QVBoxLayout()
|
|
85
|
+
container_layout.setContentsMargins(0, 0, 0, 0)
|
|
86
|
+
container.setLayout(container_layout)
|
|
87
|
+
|
|
88
|
+
scroll_area = QScrollArea()
|
|
89
|
+
scroll_area.setWidgetResizable(True)
|
|
90
|
+
scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
91
|
+
scroll_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
92
|
+
scroll_area.setMinimumWidth(240)
|
|
93
|
+
|
|
94
|
+
items_container = QWidget()
|
|
95
|
+
items_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
96
|
+
layout = QVBoxLayout()
|
|
97
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
98
|
+
layout.setSizeConstraint(QVBoxLayout.SetMinAndMaxSize)
|
|
99
|
+
items_container.setLayout(layout)
|
|
100
|
+
scroll_area.setWidget(items_container)
|
|
101
|
+
|
|
102
|
+
add_button = QPushButton("Add ROI")
|
|
103
|
+
add_button.clicked.connect(self._add_row)
|
|
104
|
+
|
|
105
|
+
container_layout.addWidget(scroll_area)
|
|
106
|
+
container_layout.addWidget(add_button)
|
|
107
|
+
|
|
108
|
+
right_layout.addWidget(checkbox)
|
|
109
|
+
right_layout.addWidget(container)
|
|
110
|
+
|
|
111
|
+
self._checkbox = checkbox
|
|
112
|
+
self._container = container
|
|
113
|
+
self._layout = layout
|
|
114
|
+
self._scroll_area = scroll_area
|
|
115
|
+
self._items_container = items_container
|
|
116
|
+
self._items = []
|
|
117
|
+
|
|
118
|
+
if self._rois:
|
|
119
|
+
checkbox.setChecked(True)
|
|
120
|
+
for roi in self._rois:
|
|
121
|
+
self._add_row(roi)
|
|
122
|
+
self._update_scroll_height()
|
|
123
|
+
|
|
124
|
+
def _toggle(self, enabled: bool) -> None:
|
|
125
|
+
"""Show or hide ROI controls when toggled.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
enabled : bool
|
|
130
|
+
Whether ROI controls should be visible.
|
|
131
|
+
"""
|
|
132
|
+
if self._container is None:
|
|
133
|
+
return
|
|
134
|
+
self._container.setVisible(enabled)
|
|
135
|
+
if enabled:
|
|
136
|
+
if not self._items:
|
|
137
|
+
if self._rois:
|
|
138
|
+
for roi in self._rois:
|
|
139
|
+
self._add_row(roi)
|
|
140
|
+
else:
|
|
141
|
+
self._add_row()
|
|
142
|
+
else:
|
|
143
|
+
self.clear()
|
|
144
|
+
self._tab._features_layout.activate()
|
|
145
|
+
self._tab._apply_features_layout()
|
|
146
|
+
if self._tab._features_scroll_area is not None:
|
|
147
|
+
self._tab._features_scroll_area.updateGeometry()
|
|
148
|
+
QTimer.singleShot(0, self._tab._apply_features_layout)
|
|
149
|
+
QTimer.singleShot(0, self._update_scroll_height)
|
|
150
|
+
|
|
151
|
+
def _add_row(self, roi: ROIConfig | None = None) -> None:
|
|
152
|
+
"""Add a new ROI configuration row.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
roi : ROIConfig or None
|
|
157
|
+
Existing ROI configuration to edit, or ``None`` to create one.
|
|
158
|
+
"""
|
|
159
|
+
if self._layout is None:
|
|
160
|
+
return
|
|
161
|
+
if isinstance(roi, bool):
|
|
162
|
+
roi = None
|
|
163
|
+
roi_index = len(self._items)
|
|
164
|
+
feature_index = self._tab._feature_index(self._context)
|
|
165
|
+
if roi is None:
|
|
166
|
+
roi = ROIConfig()
|
|
167
|
+
self._rois.append(roi)
|
|
168
|
+
|
|
169
|
+
roi_section = QGroupBox(f"Feature {feature_index}: ROI {roi_index}")
|
|
170
|
+
roi_section.setFlat(True)
|
|
171
|
+
roi_section.setStyleSheet(
|
|
172
|
+
"QGroupBox {"
|
|
173
|
+
" margin-top: 6px;"
|
|
174
|
+
"}"
|
|
175
|
+
"QGroupBox::title {"
|
|
176
|
+
" subcontrol-origin: margin;"
|
|
177
|
+
" subcontrol-position: top left;"
|
|
178
|
+
" padding: 0 6px;"
|
|
179
|
+
"}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
form_layout = QFormLayout()
|
|
183
|
+
form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
184
|
+
|
|
185
|
+
roi_name = QLineEdit()
|
|
186
|
+
roi_name.setPlaceholderText("ROI name")
|
|
187
|
+
roi_name.setMinimumWidth(120)
|
|
188
|
+
roi_name.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
189
|
+
roi_name.setText(roi.name)
|
|
190
|
+
roi_name.textChanged.connect(lambda text: setattr(roi, "name", text))
|
|
191
|
+
|
|
192
|
+
shapes_combo = RefreshingComboBox(
|
|
193
|
+
refresh_callback=lambda combo_ref=None: self._refresh_shapes_combo(
|
|
194
|
+
shapes_combo, roi
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
self._tab._configure_combo(shapes_combo)
|
|
198
|
+
shapes_combo.setMinimumWidth(120)
|
|
199
|
+
if roi.layer:
|
|
200
|
+
shapes_combo.setCurrentText(roi.layer)
|
|
201
|
+
shapes_combo.currentTextChanged.connect(
|
|
202
|
+
lambda text: setattr(roi, "layer", text)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
roi_type = QComboBox()
|
|
206
|
+
roi_type.addItems(["Include", "Exclude"])
|
|
207
|
+
self._tab._configure_combo(roi_type)
|
|
208
|
+
roi_type.setMinimumWidth(120)
|
|
209
|
+
if roi.roi_type:
|
|
210
|
+
roi_type.setCurrentText(roi.roi_type)
|
|
211
|
+
roi_type.currentTextChanged.connect(
|
|
212
|
+
lambda text: setattr(roi, "roi_type", text)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
form_layout.addRow("Name", roi_name)
|
|
216
|
+
form_layout.addRow("Layer", shapes_combo)
|
|
217
|
+
form_layout.addRow("Type", roi_type)
|
|
218
|
+
|
|
219
|
+
delete_button = QPushButton("Delete")
|
|
220
|
+
delete_button.clicked.connect(
|
|
221
|
+
lambda _checked=False, section=roi_section: self._remove_row(
|
|
222
|
+
section
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
roi_layout_inner = QVBoxLayout()
|
|
227
|
+
roi_layout_inner.addLayout(form_layout)
|
|
228
|
+
roi_layout_inner.addWidget(delete_button)
|
|
229
|
+
roi_section.setLayout(roi_layout_inner)
|
|
230
|
+
roi_section.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
231
|
+
|
|
232
|
+
self._layout.addWidget(roi_section)
|
|
233
|
+
self._items.append((roi_section, roi))
|
|
234
|
+
self.update_titles()
|
|
235
|
+
self._tab._features_layout.activate()
|
|
236
|
+
QTimer.singleShot(0, self._tab._apply_features_layout)
|
|
237
|
+
QTimer.singleShot(0, self._update_scroll_height)
|
|
238
|
+
|
|
239
|
+
def _remove_row(self, roi_section: QGroupBox) -> None:
|
|
240
|
+
"""Remove an ROI row and update titles.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
roi_section : QGroupBox
|
|
245
|
+
ROI section widget to remove.
|
|
246
|
+
"""
|
|
247
|
+
if self._layout is None:
|
|
248
|
+
return
|
|
249
|
+
item = next(
|
|
250
|
+
(
|
|
251
|
+
(section, roi)
|
|
252
|
+
for section, roi in self._items
|
|
253
|
+
if section is roi_section
|
|
254
|
+
),
|
|
255
|
+
None,
|
|
256
|
+
)
|
|
257
|
+
if item is None:
|
|
258
|
+
return
|
|
259
|
+
self._items.remove(item)
|
|
260
|
+
section, roi = item
|
|
261
|
+
if roi in self._rois:
|
|
262
|
+
self._rois.remove(roi)
|
|
263
|
+
self._layout.removeWidget(roi_section)
|
|
264
|
+
roi_section.deleteLater()
|
|
265
|
+
if not self._items and self._checkbox is not None:
|
|
266
|
+
self._checkbox.setChecked(False)
|
|
267
|
+
self.update_titles()
|
|
268
|
+
self._update_scroll_height()
|
|
269
|
+
self._tab._features_layout.activate()
|
|
270
|
+
QTimer.singleShot(0, self._tab._apply_features_layout)
|
|
271
|
+
|
|
272
|
+
def update_titles(self) -> None:
|
|
273
|
+
"""Refresh ROI section titles based on current feature order."""
|
|
274
|
+
feature_index = self._tab._feature_index(self._context)
|
|
275
|
+
for roi_index, (section, _roi) in enumerate(self._items, start=0):
|
|
276
|
+
section.setTitle(f"Feature {feature_index}: ROI {roi_index}")
|
|
277
|
+
|
|
278
|
+
def clear(self) -> None:
|
|
279
|
+
"""Remove all ROI rows and reset layout state."""
|
|
280
|
+
if self._layout is None:
|
|
281
|
+
return
|
|
282
|
+
for roi_section, roi in list(self._items):
|
|
283
|
+
self._layout.removeWidget(roi_section)
|
|
284
|
+
roi_section.deleteLater()
|
|
285
|
+
if roi in self._rois:
|
|
286
|
+
self._rois.remove(roi)
|
|
287
|
+
self._items.clear()
|
|
288
|
+
self.update_titles()
|
|
289
|
+
self._update_scroll_height()
|
|
290
|
+
|
|
291
|
+
def _update_scroll_height(self) -> None:
|
|
292
|
+
"""Resize the ROI scroll area based on content height.
|
|
293
|
+
|
|
294
|
+
Notes
|
|
295
|
+
-----
|
|
296
|
+
The scroll area grows with content until a maximum height derived
|
|
297
|
+
from the screen size is reached, after which a scrollbar appears.
|
|
298
|
+
"""
|
|
299
|
+
scroll_area = self._scroll_area
|
|
300
|
+
container = self._items_container
|
|
301
|
+
if scroll_area is None or container is None:
|
|
302
|
+
return
|
|
303
|
+
screen = self._tab.window().screen() if self._tab.window() else None
|
|
304
|
+
if screen is None:
|
|
305
|
+
screen = QGuiApplication.primaryScreen()
|
|
306
|
+
screen_height = screen.availableGeometry().height() if screen else 720
|
|
307
|
+
target_height = max(140, int(screen_height * 0.2))
|
|
308
|
+
container.adjustSize()
|
|
309
|
+
content_height = container.sizeHint().height()
|
|
310
|
+
frame = scroll_area.frameWidth() * 2
|
|
311
|
+
height = max(0, min(target_height, content_height + frame))
|
|
312
|
+
scroll_area.setFixedHeight(height)
|
|
313
|
+
if content_height + frame <= target_height:
|
|
314
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
|
315
|
+
else:
|
|
316
|
+
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
|
317
|
+
|
|
318
|
+
def _refresh_shapes_combo(
|
|
319
|
+
self, combo: QComboBox, roi: ROIConfig
|
|
320
|
+
) -> None:
|
|
321
|
+
"""Populate the shapes combo with available ROI layers.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
combo : QComboBox
|
|
326
|
+
Combo box to populate.
|
|
327
|
+
roi : ROIConfig
|
|
328
|
+
ROI configuration to update.
|
|
329
|
+
"""
|
|
330
|
+
current = combo.currentText()
|
|
331
|
+
combo.clear()
|
|
332
|
+
viewer = self._tab._viewer
|
|
333
|
+
if viewer is None:
|
|
334
|
+
combo.addItem("Select shapes")
|
|
335
|
+
return
|
|
336
|
+
for layer in viewer.layers:
|
|
337
|
+
if layer.__class__.__name__ == "Shapes":
|
|
338
|
+
combo.addItem(layer.name)
|
|
339
|
+
if current:
|
|
340
|
+
index = combo.findText(current)
|
|
341
|
+
if index != -1:
|
|
342
|
+
combo.setCurrentIndex(index)
|
|
343
|
+
if roi.layer:
|
|
344
|
+
index = combo.findText(roi.layer)
|
|
345
|
+
if index != -1:
|
|
346
|
+
combo.setCurrentIndex(index)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Spots feature configuration models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from ..base import FeatureData
|
|
8
|
+
from ..roi import ROIConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class SpotsSegmentationConfig:
|
|
13
|
+
"""Configuration for a segmentation filter entry.
|
|
14
|
+
|
|
15
|
+
Attributes
|
|
16
|
+
----------
|
|
17
|
+
label : str
|
|
18
|
+
Name of the labels layer selected for this segmentation entry.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
label: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SpotsChannelConfig:
|
|
26
|
+
"""Configuration for a spots channel entry.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
name : str
|
|
31
|
+
User-friendly label for the channel entry.
|
|
32
|
+
channel : str
|
|
33
|
+
Selected image layer name.
|
|
34
|
+
spots_segmentation : str
|
|
35
|
+
Labels layer containing the spots segmentation for this channel.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str = ""
|
|
39
|
+
channel: str = ""
|
|
40
|
+
spots_segmentation: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class SpotsFeatureData(FeatureData):
|
|
45
|
+
"""Configuration for spots feature inputs.
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
segmentations : list of SpotsSegmentationConfig
|
|
50
|
+
Segmentation filters applied to the full set of spots.
|
|
51
|
+
channels : list of SpotsChannelConfig
|
|
52
|
+
Channel configurations used for spots measurement.
|
|
53
|
+
rois : list of ROIConfig
|
|
54
|
+
ROI entries applied to this feature.
|
|
55
|
+
export_colocalization : bool
|
|
56
|
+
Whether to include colocalization columns in the export.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
segmentations: list[SpotsSegmentationConfig] = field(default_factory=list)
|
|
60
|
+
channels: list[SpotsChannelConfig] = field(default_factory=list)
|
|
61
|
+
rois: list[ROIConfig] = field(default_factory=list)
|
|
62
|
+
export_colocalization: bool = False
|