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,654 @@
|
|
|
1
|
+
"""Marker channels dialog rows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from qtpy.QtCore import Qt
|
|
10
|
+
from qtpy.QtWidgets import (
|
|
11
|
+
QCheckBox,
|
|
12
|
+
QComboBox,
|
|
13
|
+
QDoubleSpinBox,
|
|
14
|
+
QFormLayout,
|
|
15
|
+
QGroupBox,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QLineEdit,
|
|
18
|
+
QPushButton,
|
|
19
|
+
QSizePolicy,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
QWidget,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from senoquant.utils import layer_data_asarray
|
|
25
|
+
from ..base import RefreshingComboBox
|
|
26
|
+
from .thresholding import THRESHOLD_METHODS, compute_threshold
|
|
27
|
+
from .config import MarkerChannelConfig, MarkerSegmentationConfig
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .dialog import MarkerChannelsDialog
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from superqt import QDoubleRangeSlider as RangeSlider
|
|
34
|
+
except ImportError: # pragma: no cover - fallback when superqt is unavailable
|
|
35
|
+
try:
|
|
36
|
+
from superqt import QRangeSlider as RangeSlider
|
|
37
|
+
except ImportError: # pragma: no cover
|
|
38
|
+
RangeSlider = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MarkerSegmentationRow(QGroupBox):
|
|
42
|
+
"""Segmentation row widget for marker segmentations."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self, dialog: MarkerChannelsDialog, data: MarkerSegmentationConfig
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize a segmentation row widget.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
dialog : MarkerChannelsDialog
|
|
52
|
+
Parent dialog instance.
|
|
53
|
+
data : MarkerSegmentationConfig
|
|
54
|
+
Segmentation configuration data.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__()
|
|
57
|
+
self._dialog = dialog
|
|
58
|
+
self._tab = dialog._tab
|
|
59
|
+
self.data = data
|
|
60
|
+
|
|
61
|
+
self.setFlat(True)
|
|
62
|
+
self.setStyleSheet(
|
|
63
|
+
"QGroupBox {"
|
|
64
|
+
" margin-top: 6px;"
|
|
65
|
+
"}"
|
|
66
|
+
"QGroupBox::title {"
|
|
67
|
+
" subcontrol-origin: margin;"
|
|
68
|
+
" subcontrol-position: top left;"
|
|
69
|
+
" padding: 0 6px;"
|
|
70
|
+
"}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
layout = QVBoxLayout()
|
|
74
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
75
|
+
layout.setSpacing(6)
|
|
76
|
+
|
|
77
|
+
form_layout = QFormLayout()
|
|
78
|
+
form_layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
79
|
+
labels_combo = RefreshingComboBox(
|
|
80
|
+
refresh_callback=lambda combo_ref=None: self._dialog._refresh_labels_combo(
|
|
81
|
+
labels_combo
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
self._tab._configure_combo(labels_combo)
|
|
85
|
+
labels_combo.currentTextChanged.connect(
|
|
86
|
+
lambda text: self._set_data("label", text)
|
|
87
|
+
)
|
|
88
|
+
form_layout.addRow("Labels", labels_combo)
|
|
89
|
+
layout.addLayout(form_layout)
|
|
90
|
+
|
|
91
|
+
delete_button = QPushButton("Delete")
|
|
92
|
+
delete_button.clicked.connect(
|
|
93
|
+
lambda: self._dialog._remove_segmentation(self)
|
|
94
|
+
)
|
|
95
|
+
layout.addWidget(delete_button)
|
|
96
|
+
|
|
97
|
+
self._labels_combo = labels_combo
|
|
98
|
+
self.setLayout(layout)
|
|
99
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
100
|
+
self._restore_state()
|
|
101
|
+
|
|
102
|
+
def update_title(self, index: int) -> None:
|
|
103
|
+
"""Update the title label for the segmentation row.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
index : int
|
|
108
|
+
0-based index used in the title.
|
|
109
|
+
"""
|
|
110
|
+
self.setTitle(f"Segmentation {index}")
|
|
111
|
+
|
|
112
|
+
def _set_data(self, key: str, value) -> None:
|
|
113
|
+
"""Update the segmentation data model."""
|
|
114
|
+
setattr(self.data, key, value)
|
|
115
|
+
|
|
116
|
+
def _restore_state(self) -> None:
|
|
117
|
+
"""Restore UI state from stored segmentation data."""
|
|
118
|
+
label_name = self.data.label
|
|
119
|
+
if label_name:
|
|
120
|
+
self._labels_combo.setCurrentText(label_name)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class MarkerChannelRow(QGroupBox):
|
|
124
|
+
"""Channel row widget for marker feature channels."""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self, dialog: MarkerChannelsDialog, data: MarkerChannelConfig
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Initialize a channel row widget.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
dialog : MarkerChannelsDialog
|
|
134
|
+
Parent dialog instance.
|
|
135
|
+
data : MarkerChannelConfig
|
|
136
|
+
Channel configuration data.
|
|
137
|
+
"""
|
|
138
|
+
super().__init__()
|
|
139
|
+
self._dialog = dialog
|
|
140
|
+
self._feature = dialog._feature
|
|
141
|
+
self._tab = dialog._tab
|
|
142
|
+
self.data = data
|
|
143
|
+
self._threshold_updating = False
|
|
144
|
+
self._thresholds_enabled = getattr(self._tab, "_enable_thresholds", True)
|
|
145
|
+
|
|
146
|
+
self.setFlat(True)
|
|
147
|
+
self.setStyleSheet(
|
|
148
|
+
"QGroupBox {"
|
|
149
|
+
" margin-top: 6px;"
|
|
150
|
+
"}"
|
|
151
|
+
"QGroupBox::title {"
|
|
152
|
+
" subcontrol-origin: margin;"
|
|
153
|
+
" subcontrol-position: top left;"
|
|
154
|
+
" padding: 0 6px;"
|
|
155
|
+
"}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
layout = QVBoxLayout()
|
|
159
|
+
layout.setContentsMargins(8, 8, 8, 8)
|
|
160
|
+
layout.setSpacing(6)
|
|
161
|
+
|
|
162
|
+
channel_form = QFormLayout()
|
|
163
|
+
channel_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
|
|
164
|
+
name_input = QLineEdit()
|
|
165
|
+
name_input.setPlaceholderText("Channel name")
|
|
166
|
+
name_input.setMinimumWidth(160)
|
|
167
|
+
name_input.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
168
|
+
name_input.textChanged.connect(
|
|
169
|
+
lambda text: self._set_data("name", text)
|
|
170
|
+
)
|
|
171
|
+
channel_combo = RefreshingComboBox(
|
|
172
|
+
refresh_callback=lambda combo_ref=None: self._dialog._refresh_image_combo(
|
|
173
|
+
channel_combo
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
self._tab._configure_combo(channel_combo)
|
|
177
|
+
channel_combo.currentTextChanged.connect(self._on_channel_changed)
|
|
178
|
+
channel_form.addRow("Name", name_input)
|
|
179
|
+
channel_form.addRow("Channel", channel_combo)
|
|
180
|
+
layout.addLayout(channel_form)
|
|
181
|
+
|
|
182
|
+
threshold_checkbox = QCheckBox("Set threshold")
|
|
183
|
+
threshold_checkbox.setEnabled(False)
|
|
184
|
+
threshold_checkbox.toggled.connect(self._toggle_threshold)
|
|
185
|
+
layout.addWidget(threshold_checkbox)
|
|
186
|
+
|
|
187
|
+
threshold_container = QWidget()
|
|
188
|
+
threshold_layout = QHBoxLayout()
|
|
189
|
+
threshold_layout.setContentsMargins(0, 0, 0, 0)
|
|
190
|
+
threshold_slider = self._make_range_slider()
|
|
191
|
+
if hasattr(threshold_slider, "valueChanged"):
|
|
192
|
+
threshold_slider.valueChanged.connect(self._on_threshold_slider_changed)
|
|
193
|
+
threshold_min_spin = QDoubleSpinBox()
|
|
194
|
+
threshold_min_spin.setDecimals(2)
|
|
195
|
+
threshold_min_spin.setMinimumWidth(80)
|
|
196
|
+
threshold_min_spin.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
197
|
+
threshold_min_spin.valueChanged.connect(
|
|
198
|
+
lambda value: self._on_threshold_spin_changed("min", value)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
threshold_max_spin = QDoubleSpinBox()
|
|
202
|
+
threshold_max_spin.setDecimals(2)
|
|
203
|
+
threshold_max_spin.setMinimumWidth(80)
|
|
204
|
+
threshold_max_spin.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
205
|
+
threshold_max_spin.valueChanged.connect(
|
|
206
|
+
lambda value: self._on_threshold_spin_changed("max", value)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
threshold_slider.setEnabled(False)
|
|
210
|
+
threshold_slider.setVisible(False)
|
|
211
|
+
threshold_min_spin.setEnabled(False)
|
|
212
|
+
threshold_max_spin.setEnabled(False)
|
|
213
|
+
threshold_layout.addWidget(threshold_min_spin)
|
|
214
|
+
threshold_layout.addWidget(threshold_slider, 1)
|
|
215
|
+
threshold_layout.addWidget(threshold_max_spin)
|
|
216
|
+
threshold_container.setLayout(threshold_layout)
|
|
217
|
+
threshold_container.setVisible(False)
|
|
218
|
+
layout.addWidget(threshold_container)
|
|
219
|
+
|
|
220
|
+
auto_threshold_container = QWidget()
|
|
221
|
+
auto_threshold_layout = QHBoxLayout()
|
|
222
|
+
auto_threshold_layout.setContentsMargins(0, 0, 0, 0)
|
|
223
|
+
auto_threshold_combo = QComboBox()
|
|
224
|
+
auto_threshold_combo.addItems(
|
|
225
|
+
["Manual", *list(THRESHOLD_METHODS.keys())]
|
|
226
|
+
)
|
|
227
|
+
self._tab._configure_combo(auto_threshold_combo)
|
|
228
|
+
auto_threshold_combo.currentTextChanged.connect(
|
|
229
|
+
self._on_threshold_method_changed
|
|
230
|
+
)
|
|
231
|
+
auto_threshold_button = QPushButton("Auto threshold")
|
|
232
|
+
auto_threshold_button.clicked.connect(self._run_auto_threshold)
|
|
233
|
+
auto_threshold_layout.addWidget(auto_threshold_combo, 1)
|
|
234
|
+
auto_threshold_layout.addWidget(auto_threshold_button)
|
|
235
|
+
auto_threshold_container.setLayout(auto_threshold_layout)
|
|
236
|
+
auto_threshold_container.setVisible(False)
|
|
237
|
+
layout.addWidget(auto_threshold_container)
|
|
238
|
+
|
|
239
|
+
delete_button = QPushButton("Delete")
|
|
240
|
+
delete_button.clicked.connect(lambda: self._dialog._remove_channel(self))
|
|
241
|
+
layout.addWidget(delete_button)
|
|
242
|
+
|
|
243
|
+
self.setLayout(layout)
|
|
244
|
+
|
|
245
|
+
self._channel_combo = channel_combo
|
|
246
|
+
self._name_input = name_input
|
|
247
|
+
self._threshold_checkbox = threshold_checkbox
|
|
248
|
+
self._threshold_slider = threshold_slider
|
|
249
|
+
self._threshold_container = threshold_container
|
|
250
|
+
self._threshold_min_spin = threshold_min_spin
|
|
251
|
+
self._threshold_max_spin = threshold_max_spin
|
|
252
|
+
self._auto_threshold_container = auto_threshold_container
|
|
253
|
+
self._auto_threshold_combo = auto_threshold_combo
|
|
254
|
+
self._auto_threshold_button = auto_threshold_button
|
|
255
|
+
self._auto_thresholding = False
|
|
256
|
+
self._threshold_min_bound: float | None = None
|
|
257
|
+
self._threshold_max_bound: float | None = None
|
|
258
|
+
|
|
259
|
+
if not self._thresholds_enabled:
|
|
260
|
+
threshold_checkbox.setVisible(False)
|
|
261
|
+
threshold_container.setVisible(False)
|
|
262
|
+
auto_threshold_container.setVisible(False)
|
|
263
|
+
threshold_checkbox.setEnabled(False)
|
|
264
|
+
auto_threshold_button.setEnabled(False)
|
|
265
|
+
|
|
266
|
+
self._restore_state()
|
|
267
|
+
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
268
|
+
|
|
269
|
+
def update_title(self, index: int) -> None:
|
|
270
|
+
"""Update the title label for the channel row.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
index : int
|
|
275
|
+
0-based index used in the title.
|
|
276
|
+
"""
|
|
277
|
+
self.setTitle(f"Channel {index}")
|
|
278
|
+
|
|
279
|
+
def _set_data(self, key: str, value) -> None:
|
|
280
|
+
"""Update the channel data model.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
key : str
|
|
285
|
+
Data key to update.
|
|
286
|
+
value : object
|
|
287
|
+
New value to store.
|
|
288
|
+
"""
|
|
289
|
+
setattr(self.data, key, value)
|
|
290
|
+
|
|
291
|
+
def _restore_state(self) -> None:
|
|
292
|
+
"""Restore UI state from stored channel data."""
|
|
293
|
+
channel_label = self.data.name
|
|
294
|
+
if channel_label:
|
|
295
|
+
self._name_input.setText(channel_label)
|
|
296
|
+
channel_name = self.data.channel
|
|
297
|
+
if channel_name:
|
|
298
|
+
self._channel_combo.setCurrentText(channel_name)
|
|
299
|
+
method = self.data.threshold_method or "Manual"
|
|
300
|
+
self._auto_threshold_combo.setCurrentText(method)
|
|
301
|
+
enabled = bool(self.data.threshold_enabled)
|
|
302
|
+
self._threshold_checkbox.setChecked(enabled)
|
|
303
|
+
self._on_channel_changed(self._channel_combo.currentText())
|
|
304
|
+
if not self._thresholds_enabled:
|
|
305
|
+
self._set_data("threshold_enabled", False)
|
|
306
|
+
self._set_data("threshold_method", "Manual")
|
|
307
|
+
self._set_data("threshold_min", None)
|
|
308
|
+
self._set_data("threshold_max", None)
|
|
309
|
+
|
|
310
|
+
def _layer_has_data(self, layer) -> bool:
|
|
311
|
+
data = getattr(layer, "data", None)
|
|
312
|
+
if data is None:
|
|
313
|
+
return False
|
|
314
|
+
try:
|
|
315
|
+
array = np.asarray(data)
|
|
316
|
+
except Exception:
|
|
317
|
+
return False
|
|
318
|
+
if array.size == 0:
|
|
319
|
+
return False
|
|
320
|
+
if array.dtype == object and array.size == 1 and array.flat[0] is None:
|
|
321
|
+
return False
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
def _disable_threshold_controls(self) -> None:
|
|
325
|
+
self._threshold_min_bound = None
|
|
326
|
+
self._threshold_max_bound = None
|
|
327
|
+
self._threshold_checkbox.blockSignals(True)
|
|
328
|
+
self._threshold_checkbox.setChecked(False)
|
|
329
|
+
self._threshold_checkbox.blockSignals(False)
|
|
330
|
+
self._set_data("threshold_enabled", False)
|
|
331
|
+
self._threshold_checkbox.setEnabled(False)
|
|
332
|
+
self._auto_threshold_button.setEnabled(False)
|
|
333
|
+
self._set_threshold_controls(False)
|
|
334
|
+
|
|
335
|
+
def _on_channel_changed(self, text: str | None = None) -> None:
|
|
336
|
+
"""Update threshold controls when channel selection changes.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
text : str
|
|
341
|
+
Newly selected channel name.
|
|
342
|
+
"""
|
|
343
|
+
if text is None:
|
|
344
|
+
text = self._channel_combo.currentText()
|
|
345
|
+
self._set_data("channel", text)
|
|
346
|
+
if not self._thresholds_enabled:
|
|
347
|
+
self._disable_threshold_controls()
|
|
348
|
+
return
|
|
349
|
+
layer = self._feature._get_image_layer_by_name(text)
|
|
350
|
+
if layer is None or not self._layer_has_data(layer):
|
|
351
|
+
self._disable_threshold_controls()
|
|
352
|
+
return
|
|
353
|
+
self._threshold_checkbox.setEnabled(True)
|
|
354
|
+
self._set_threshold_range(
|
|
355
|
+
self._threshold_slider,
|
|
356
|
+
layer,
|
|
357
|
+
self._threshold_min_spin,
|
|
358
|
+
self._threshold_max_spin,
|
|
359
|
+
)
|
|
360
|
+
self._set_threshold_controls(self._threshold_checkbox.isChecked())
|
|
361
|
+
|
|
362
|
+
def _toggle_threshold(self, enabled: bool) -> None:
|
|
363
|
+
"""Toggle threshold controls for this channel.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
enabled : bool
|
|
368
|
+
Whether threshold controls should be enabled.
|
|
369
|
+
"""
|
|
370
|
+
if not self._thresholds_enabled:
|
|
371
|
+
return
|
|
372
|
+
self._set_data("threshold_enabled", enabled)
|
|
373
|
+
self._set_threshold_controls(enabled)
|
|
374
|
+
|
|
375
|
+
def _set_threshold_controls(self, enabled: bool) -> None:
|
|
376
|
+
"""Show or hide threshold controls.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
enabled : bool
|
|
381
|
+
Whether to show threshold controls.
|
|
382
|
+
"""
|
|
383
|
+
if not self._thresholds_enabled:
|
|
384
|
+
enabled = False
|
|
385
|
+
self._threshold_slider.setEnabled(enabled)
|
|
386
|
+
self._threshold_slider.setVisible(enabled)
|
|
387
|
+
self._threshold_min_spin.setEnabled(enabled)
|
|
388
|
+
self._threshold_max_spin.setEnabled(enabled)
|
|
389
|
+
self._threshold_container.setVisible(enabled)
|
|
390
|
+
self._auto_threshold_container.setVisible(enabled)
|
|
391
|
+
self._auto_threshold_combo.setEnabled(enabled)
|
|
392
|
+
self._auto_threshold_button.setEnabled(
|
|
393
|
+
enabled and self._auto_threshold_combo.currentText() != "Manual"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _on_threshold_method_changed(self, text: str) -> None:
|
|
397
|
+
"""Handle changes to the thresholding method selection."""
|
|
398
|
+
if not self._thresholds_enabled:
|
|
399
|
+
return
|
|
400
|
+
self._set_data("threshold_method", text)
|
|
401
|
+
if text == "Manual":
|
|
402
|
+
self._auto_threshold_button.setEnabled(False)
|
|
403
|
+
return
|
|
404
|
+
self._auto_threshold_button.setEnabled(
|
|
405
|
+
self._threshold_checkbox.isChecked()
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def _on_threshold_slider_changed(self, values) -> None:
|
|
409
|
+
"""Sync spin boxes when the slider range changes.
|
|
410
|
+
|
|
411
|
+
Parameters
|
|
412
|
+
----------
|
|
413
|
+
values : tuple
|
|
414
|
+
Updated (min, max) slider values.
|
|
415
|
+
"""
|
|
416
|
+
if values is None or self._threshold_updating:
|
|
417
|
+
return
|
|
418
|
+
self._threshold_updating = True
|
|
419
|
+
self._threshold_min_spin.blockSignals(True)
|
|
420
|
+
self._threshold_max_spin.blockSignals(True)
|
|
421
|
+
self._threshold_min_spin.setValue(values[0])
|
|
422
|
+
self._threshold_max_spin.setValue(values[1])
|
|
423
|
+
self._threshold_min_spin.blockSignals(False)
|
|
424
|
+
self._threshold_max_spin.blockSignals(False)
|
|
425
|
+
self._threshold_updating = False
|
|
426
|
+
self._set_data("threshold_min", float(values[0]))
|
|
427
|
+
self._set_data("threshold_max", float(values[1]))
|
|
428
|
+
self._update_layer_contrast_limits(values)
|
|
429
|
+
self._ensure_manual_threshold_mode()
|
|
430
|
+
|
|
431
|
+
def _on_threshold_spin_changed(self, which: str, value: float) -> None:
|
|
432
|
+
"""Sync the slider when a spin box value changes.
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
which : str
|
|
437
|
+
Identifier for the spin box ("min" or "max").
|
|
438
|
+
value : float
|
|
439
|
+
New spin box value.
|
|
440
|
+
"""
|
|
441
|
+
if self._threshold_updating:
|
|
442
|
+
return
|
|
443
|
+
min_val = self._threshold_min_spin.value()
|
|
444
|
+
max_val = self._threshold_max_spin.value()
|
|
445
|
+
if min_val > max_val:
|
|
446
|
+
if which == "min":
|
|
447
|
+
max_val = min_val
|
|
448
|
+
self._threshold_max_spin.blockSignals(True)
|
|
449
|
+
self._threshold_max_spin.setValue(max_val)
|
|
450
|
+
self._threshold_max_spin.blockSignals(False)
|
|
451
|
+
else:
|
|
452
|
+
min_val = max_val
|
|
453
|
+
self._threshold_min_spin.blockSignals(True)
|
|
454
|
+
self._threshold_min_spin.setValue(min_val)
|
|
455
|
+
self._threshold_min_spin.blockSignals(False)
|
|
456
|
+
self._threshold_updating = True
|
|
457
|
+
self._set_slider_values(
|
|
458
|
+
self._threshold_slider, (min_val, max_val)
|
|
459
|
+
)
|
|
460
|
+
self._threshold_updating = False
|
|
461
|
+
self._set_data("threshold_min", float(min_val))
|
|
462
|
+
self._set_data("threshold_max", float(max_val))
|
|
463
|
+
self._update_layer_contrast_limits((min_val, max_val))
|
|
464
|
+
self._ensure_manual_threshold_mode()
|
|
465
|
+
|
|
466
|
+
def _run_auto_threshold(self) -> None:
|
|
467
|
+
"""Compute an automatic threshold and update the range controls."""
|
|
468
|
+
if not self._thresholds_enabled:
|
|
469
|
+
return
|
|
470
|
+
layer = self._feature._get_image_layer_by_name(
|
|
471
|
+
self._channel_combo.currentText()
|
|
472
|
+
)
|
|
473
|
+
if layer is None or not self._layer_has_data(layer):
|
|
474
|
+
return
|
|
475
|
+
method = self._auto_threshold_combo.currentText() or "Otsu"
|
|
476
|
+
if method == "Manual":
|
|
477
|
+
return
|
|
478
|
+
try:
|
|
479
|
+
threshold = compute_threshold(layer_data_asarray(layer), method)
|
|
480
|
+
except Exception:
|
|
481
|
+
return
|
|
482
|
+
min_val = self._threshold_min_bound
|
|
483
|
+
max_val = self._threshold_max_bound
|
|
484
|
+
if min_val is None or max_val is None:
|
|
485
|
+
self._set_threshold_range(
|
|
486
|
+
self._threshold_slider,
|
|
487
|
+
layer,
|
|
488
|
+
self._threshold_min_spin,
|
|
489
|
+
self._threshold_max_spin,
|
|
490
|
+
)
|
|
491
|
+
min_val = self._threshold_min_bound
|
|
492
|
+
max_val = self._threshold_max_bound
|
|
493
|
+
if min_val is None or max_val is None:
|
|
494
|
+
return
|
|
495
|
+
threshold = min(max(threshold, min_val), max_val)
|
|
496
|
+
self._auto_thresholding = True
|
|
497
|
+
try:
|
|
498
|
+
self._threshold_updating = True
|
|
499
|
+
self._set_slider_values(
|
|
500
|
+
self._threshold_slider, (threshold, max_val)
|
|
501
|
+
)
|
|
502
|
+
self._threshold_min_spin.blockSignals(True)
|
|
503
|
+
self._threshold_min_spin.setValue(threshold)
|
|
504
|
+
self._threshold_min_spin.blockSignals(False)
|
|
505
|
+
self._threshold_max_spin.blockSignals(True)
|
|
506
|
+
self._threshold_max_spin.setValue(max_val)
|
|
507
|
+
self._threshold_max_spin.blockSignals(False)
|
|
508
|
+
self._threshold_updating = False
|
|
509
|
+
self._set_data("threshold_min", float(threshold))
|
|
510
|
+
self._set_data("threshold_max", float(max_val))
|
|
511
|
+
self._update_layer_contrast_limits((threshold, max_val))
|
|
512
|
+
finally:
|
|
513
|
+
self._auto_thresholding = False
|
|
514
|
+
|
|
515
|
+
def _ensure_manual_threshold_mode(self) -> None:
|
|
516
|
+
"""Switch to manual mode after user-adjusted threshold changes."""
|
|
517
|
+
if not self._thresholds_enabled:
|
|
518
|
+
return
|
|
519
|
+
if not self._threshold_checkbox.isChecked():
|
|
520
|
+
return
|
|
521
|
+
if self._auto_thresholding:
|
|
522
|
+
return
|
|
523
|
+
if self._auto_threshold_combo.currentText() == "Manual":
|
|
524
|
+
return
|
|
525
|
+
self._auto_threshold_combo.blockSignals(True)
|
|
526
|
+
self._auto_threshold_combo.setCurrentText("Manual")
|
|
527
|
+
self._auto_threshold_combo.blockSignals(False)
|
|
528
|
+
self._set_data("threshold_method", "Manual")
|
|
529
|
+
self._auto_threshold_button.setEnabled(False)
|
|
530
|
+
|
|
531
|
+
def _update_layer_contrast_limits(self, values) -> None:
|
|
532
|
+
"""Sync the image layer contrast limits with the threshold values.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
values : tuple
|
|
537
|
+
(min, max) values to apply as contrast limits.
|
|
538
|
+
"""
|
|
539
|
+
layer = self._feature._get_image_layer_by_name(
|
|
540
|
+
self._channel_combo.currentText()
|
|
541
|
+
)
|
|
542
|
+
if layer is None:
|
|
543
|
+
return
|
|
544
|
+
try:
|
|
545
|
+
layer.contrast_limits = [float(values[0]), float(values[1])]
|
|
546
|
+
except Exception:
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
def _make_range_slider(self):
|
|
550
|
+
"""Create a horizontal range slider if available.
|
|
551
|
+
|
|
552
|
+
Returns
|
|
553
|
+
-------
|
|
554
|
+
QWidget
|
|
555
|
+
Range slider widget or a placeholder QWidget when unavailable.
|
|
556
|
+
"""
|
|
557
|
+
if RangeSlider is None:
|
|
558
|
+
return QWidget()
|
|
559
|
+
try:
|
|
560
|
+
return RangeSlider(Qt.Horizontal)
|
|
561
|
+
except TypeError:
|
|
562
|
+
slider = RangeSlider()
|
|
563
|
+
slider.setOrientation(Qt.Horizontal)
|
|
564
|
+
return slider
|
|
565
|
+
|
|
566
|
+
def _set_slider_values(self, slider, values) -> None:
|
|
567
|
+
"""Set the range values on a slider.
|
|
568
|
+
|
|
569
|
+
Parameters
|
|
570
|
+
----------
|
|
571
|
+
slider : QWidget
|
|
572
|
+
Range slider widget.
|
|
573
|
+
values : tuple
|
|
574
|
+
(min, max) values to apply to the slider.
|
|
575
|
+
"""
|
|
576
|
+
if hasattr(slider, "setValue"):
|
|
577
|
+
try:
|
|
578
|
+
slider.setValue(values)
|
|
579
|
+
return
|
|
580
|
+
except TypeError:
|
|
581
|
+
pass
|
|
582
|
+
if hasattr(slider, "setValues"):
|
|
583
|
+
slider.setValues(values)
|
|
584
|
+
|
|
585
|
+
def _set_threshold_range(
|
|
586
|
+
self, slider, layer, min_spin: QDoubleSpinBox | None,
|
|
587
|
+
max_spin: QDoubleSpinBox | None
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Set slider bounds using the selected image layer.
|
|
590
|
+
|
|
591
|
+
Parameters
|
|
592
|
+
----------
|
|
593
|
+
slider : QWidget
|
|
594
|
+
Range slider widget.
|
|
595
|
+
layer : object
|
|
596
|
+
Napari image layer providing intensity bounds.
|
|
597
|
+
min_spin : QDoubleSpinBox or None
|
|
598
|
+
Spin box that displays the minimum threshold value.
|
|
599
|
+
max_spin : QDoubleSpinBox or None
|
|
600
|
+
Spin box that displays the maximum threshold value.
|
|
601
|
+
"""
|
|
602
|
+
if not hasattr(slider, "setMinimum"):
|
|
603
|
+
return
|
|
604
|
+
if not self._layer_has_data(layer):
|
|
605
|
+
self._disable_threshold_controls()
|
|
606
|
+
return
|
|
607
|
+
min_val, max_val = self._get_threshold_bounds(layer)
|
|
608
|
+
if hasattr(slider, "setRange"):
|
|
609
|
+
slider.setRange(min_val, max_val)
|
|
610
|
+
else:
|
|
611
|
+
slider.setMinimum(min_val)
|
|
612
|
+
slider.setMaximum(max_val)
|
|
613
|
+
self._set_slider_values(slider, (min_val, max_val))
|
|
614
|
+
if min_spin is not None:
|
|
615
|
+
min_spin.blockSignals(True)
|
|
616
|
+
min_spin.setRange(min_val, max_val)
|
|
617
|
+
min_spin.setValue(min_val)
|
|
618
|
+
min_spin.blockSignals(False)
|
|
619
|
+
if max_spin is not None:
|
|
620
|
+
max_spin.blockSignals(True)
|
|
621
|
+
max_spin.setRange(min_val, max_val)
|
|
622
|
+
max_spin.setValue(max_val)
|
|
623
|
+
max_spin.blockSignals(False)
|
|
624
|
+
|
|
625
|
+
def _get_threshold_bounds(self, layer) -> tuple[float, float]:
|
|
626
|
+
"""Return threshold bounds based on the layer contrast range.
|
|
627
|
+
|
|
628
|
+
Parameters
|
|
629
|
+
----------
|
|
630
|
+
layer : object
|
|
631
|
+
Napari image layer providing contrast bounds and data.
|
|
632
|
+
|
|
633
|
+
Returns
|
|
634
|
+
-------
|
|
635
|
+
tuple of float
|
|
636
|
+
Minimum and maximum bounds for the threshold controls.
|
|
637
|
+
|
|
638
|
+
Notes
|
|
639
|
+
-----
|
|
640
|
+
The computed bounds are cached on the row instance to avoid repeated
|
|
641
|
+
scans of large images when auto-thresholding runs.
|
|
642
|
+
"""
|
|
643
|
+
contrast = getattr(layer, "contrast_limits_range", None)
|
|
644
|
+
if contrast is not None and len(contrast) == 2:
|
|
645
|
+
min_val, max_val = float(contrast[0]), float(contrast[1])
|
|
646
|
+
else:
|
|
647
|
+
data = layer_data_asarray(layer)
|
|
648
|
+
min_val = float(np.nanmin(data))
|
|
649
|
+
max_val = float(np.nanmax(data))
|
|
650
|
+
if min_val == max_val:
|
|
651
|
+
max_val = min_val + 1.0
|
|
652
|
+
self._threshold_min_bound = min_val
|
|
653
|
+
self._threshold_max_bound = max_val
|
|
654
|
+
return min_val, max_val
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Thresholding helpers for marker features."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from skimage import filters
|
|
7
|
+
|
|
8
|
+
THRESHOLD_METHODS = {
|
|
9
|
+
"Otsu": filters.threshold_otsu,
|
|
10
|
+
"Yen": filters.threshold_yen,
|
|
11
|
+
"Li": filters.threshold_li,
|
|
12
|
+
"Isodata": filters.threshold_isodata,
|
|
13
|
+
"Triangle": filters.threshold_triangle,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def compute_threshold(data, method: str) -> float:
|
|
18
|
+
"""Compute a threshold value for the given image data.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
data : array-like
|
|
23
|
+
Image data to threshold.
|
|
24
|
+
method : str
|
|
25
|
+
Thresholding method name.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
float
|
|
30
|
+
Threshold value.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If the method is unknown or the data is empty.
|
|
36
|
+
"""
|
|
37
|
+
if method not in THRESHOLD_METHODS:
|
|
38
|
+
raise ValueError(f"Unknown threshold method: {method}")
|
|
39
|
+
array = np.asarray(data)
|
|
40
|
+
if array.size == 0:
|
|
41
|
+
raise ValueError("No image data available for thresholding.")
|
|
42
|
+
if not np.isfinite(array).all():
|
|
43
|
+
array = array[np.isfinite(array)]
|
|
44
|
+
if array.size == 0:
|
|
45
|
+
raise ValueError("No finite image data available for thresholding.")
|
|
46
|
+
return float(THRESHOLD_METHODS[method](array))
|