senoquant 1.0.0b1__py3-none-any.whl

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