phasor-handler 2.2.0__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.
- phasor_handler/__init__.py +9 -0
- phasor_handler/app.py +249 -0
- phasor_handler/img/icons/chevron-down.svg +3 -0
- phasor_handler/img/icons/chevron-up.svg +3 -0
- phasor_handler/img/logo.ico +0 -0
- phasor_handler/models/dir_manager.py +100 -0
- phasor_handler/scripts/contrast.py +131 -0
- phasor_handler/scripts/convert.py +155 -0
- phasor_handler/scripts/meta_reader.py +467 -0
- phasor_handler/scripts/plot.py +110 -0
- phasor_handler/scripts/register.py +86 -0
- phasor_handler/themes/__init__.py +8 -0
- phasor_handler/themes/dark_theme.py +330 -0
- phasor_handler/tools/__init__.py +1 -0
- phasor_handler/tools/check_stylesheet.py +15 -0
- phasor_handler/tools/misc.py +20 -0
- phasor_handler/widgets/__init__.py +5 -0
- phasor_handler/widgets/analysis/components/__init__.py +9 -0
- phasor_handler/widgets/analysis/components/bnc.py +426 -0
- phasor_handler/widgets/analysis/components/circle_roi.py +850 -0
- phasor_handler/widgets/analysis/components/image_view.py +667 -0
- phasor_handler/widgets/analysis/components/meta_info.py +481 -0
- phasor_handler/widgets/analysis/components/roi_list.py +659 -0
- phasor_handler/widgets/analysis/components/trace_plot.py +621 -0
- phasor_handler/widgets/analysis/view.py +1735 -0
- phasor_handler/widgets/conversion/view.py +83 -0
- phasor_handler/widgets/registration/view.py +110 -0
- phasor_handler/workers/__init__.py +2 -0
- phasor_handler/workers/analysis_worker.py +0 -0
- phasor_handler/workers/histogram_worker.py +55 -0
- phasor_handler/workers/registration_worker.py +242 -0
- phasor_handler-2.2.0.dist-info/METADATA +134 -0
- phasor_handler-2.2.0.dist-info/RECORD +37 -0
- phasor_handler-2.2.0.dist-info/WHEEL +5 -0
- phasor_handler-2.2.0.dist-info/entry_points.txt +5 -0
- phasor_handler-2.2.0.dist-info/licenses/LICENSE.md +21 -0
- phasor_handler-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Brightness & Contrast (BnC) Widget with histogram display.
|
|
3
|
+
|
|
4
|
+
Provides percentile-based contrast adjustment with live histogram visualization
|
|
5
|
+
showing min/max cutoff lines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox,
|
|
11
|
+
QPushButton, QDoubleSpinBox
|
|
12
|
+
)
|
|
13
|
+
from PyQt6.QtCore import pyqtSignal, QThread
|
|
14
|
+
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
from ....workers import HistogramWorker
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BnCWidget(QWidget):
|
|
20
|
+
"""Brightness & Contrast widget with histogram display and percentile controls."""
|
|
21
|
+
|
|
22
|
+
# Signal emitted when percentile values change
|
|
23
|
+
percentileChanged = pyqtSignal()
|
|
24
|
+
|
|
25
|
+
# Signal emitted when channel selection changes
|
|
26
|
+
channelChanged = pyqtSignal(int) # Emits 1 for Ch1, 2 for Ch2
|
|
27
|
+
|
|
28
|
+
# Signal emitted when reset button is clicked
|
|
29
|
+
resetRequested = pyqtSignal()
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent=None):
|
|
32
|
+
super().__init__(parent)
|
|
33
|
+
self._ch1_data = None
|
|
34
|
+
self._ch2_data = None
|
|
35
|
+
self._active_channel = 1 # 1 for Ch1, 2 for Ch2
|
|
36
|
+
|
|
37
|
+
# Thread management for histogram computation
|
|
38
|
+
self._histogram_thread = None
|
|
39
|
+
self._histogram_worker = None
|
|
40
|
+
self._pending_histogram_update = False
|
|
41
|
+
|
|
42
|
+
self._setup_ui()
|
|
43
|
+
|
|
44
|
+
def _setup_ui(self):
|
|
45
|
+
"""Setup the UI components."""
|
|
46
|
+
main_layout = QVBoxLayout()
|
|
47
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
48
|
+
|
|
49
|
+
# Create the group box
|
|
50
|
+
self.group_box = QGroupBox("Brightness and Contrast")
|
|
51
|
+
self.group_box.setObjectName('bnc_group')
|
|
52
|
+
group_layout = QVBoxLayout()
|
|
53
|
+
|
|
54
|
+
# Channel selection buttons (mutually exclusive)
|
|
55
|
+
channel_buttons_layout = QHBoxLayout()
|
|
56
|
+
|
|
57
|
+
self.channel1_button = QPushButton("Channel 1")
|
|
58
|
+
self.channel1_button.setCheckable(True)
|
|
59
|
+
self.channel1_button.setChecked(True)
|
|
60
|
+
self.channel1_button.clicked.connect(lambda: self._on_channel_selected(1))
|
|
61
|
+
self.channel1_button.setMaximumWidth(80)
|
|
62
|
+
|
|
63
|
+
self.channel2_button = QPushButton("Channel 2")
|
|
64
|
+
self.channel2_button.setCheckable(True)
|
|
65
|
+
self.channel2_button.setChecked(False)
|
|
66
|
+
self.channel2_button.setEnabled(False)
|
|
67
|
+
self.channel2_button.clicked.connect(lambda: self._on_channel_selected(2))
|
|
68
|
+
self.channel2_button.setMaximumWidth(80)
|
|
69
|
+
|
|
70
|
+
channel_buttons_layout.addWidget(self.channel1_button)
|
|
71
|
+
channel_buttons_layout.addWidget(self.channel2_button)
|
|
72
|
+
|
|
73
|
+
# Labels
|
|
74
|
+
labels_layout = QHBoxLayout()
|
|
75
|
+
labels_layout.addWidget(QLabel("Min (pth):"))
|
|
76
|
+
labels_layout.addWidget(QLabel("Max (pth):"))
|
|
77
|
+
|
|
78
|
+
# Spinboxes
|
|
79
|
+
spinbox_layout = QHBoxLayout()
|
|
80
|
+
|
|
81
|
+
self.spinbox_min = QDoubleSpinBox()
|
|
82
|
+
self.spinbox_min.setRange(0.0, 100.0)
|
|
83
|
+
self.spinbox_min.setSingleStep(0.2)
|
|
84
|
+
self.spinbox_min.setValue(0.5)
|
|
85
|
+
self.spinbox_min.setMaximumWidth(80)
|
|
86
|
+
self.spinbox_min.setToolTip("Lower percentile cutoff")
|
|
87
|
+
self.spinbox_min.valueChanged.connect(self._on_percentile_changed)
|
|
88
|
+
|
|
89
|
+
self.spinbox_max = QDoubleSpinBox()
|
|
90
|
+
self.spinbox_max.setRange(0.0, 100.0)
|
|
91
|
+
self.spinbox_max.setSingleStep(0.2)
|
|
92
|
+
self.spinbox_max.setValue(99.5)
|
|
93
|
+
self.spinbox_max.setMaximumWidth(80)
|
|
94
|
+
self.spinbox_max.setToolTip("Upper percentile cutoff")
|
|
95
|
+
self.spinbox_max.valueChanged.connect(self._on_percentile_changed)
|
|
96
|
+
|
|
97
|
+
spinbox_layout.addWidget(self.spinbox_min)
|
|
98
|
+
spinbox_layout.addWidget(self.spinbox_max)
|
|
99
|
+
spinbox_layout.addStretch()
|
|
100
|
+
|
|
101
|
+
# Reset button and histogram toggle
|
|
102
|
+
buttons_layout = QHBoxLayout()
|
|
103
|
+
|
|
104
|
+
self.reset_button = QPushButton("Reset")
|
|
105
|
+
self.reset_button.setMaximumWidth(80)
|
|
106
|
+
self.reset_button.setToolTip("Reset to default range (0.5-99.5)")
|
|
107
|
+
self.reset_button.clicked.connect(self._on_reset)
|
|
108
|
+
|
|
109
|
+
self.histogram_toggle = QPushButton("Show Histogram")
|
|
110
|
+
self.histogram_toggle.setCheckable(True)
|
|
111
|
+
self.histogram_toggle.setChecked(False)
|
|
112
|
+
self.histogram_toggle.setMaximumWidth(120)
|
|
113
|
+
self.histogram_toggle.setToolTip("Toggle histogram display")
|
|
114
|
+
self.histogram_toggle.clicked.connect(self._on_histogram_toggle)
|
|
115
|
+
|
|
116
|
+
buttons_layout.addWidget(self.reset_button)
|
|
117
|
+
buttons_layout.addWidget(self.histogram_toggle)
|
|
118
|
+
buttons_layout.addStretch()
|
|
119
|
+
|
|
120
|
+
# Histogram display - smaller size
|
|
121
|
+
self.histogram_figure = Figure(figsize=(2.5, 0.8), dpi=80)
|
|
122
|
+
self.histogram_figure.patch.set_facecolor('#31363b') # Match dark theme
|
|
123
|
+
self.histogram_canvas = FigureCanvas(self.histogram_figure)
|
|
124
|
+
self.histogram_canvas.setMinimumHeight(60)
|
|
125
|
+
self.histogram_canvas.setMaximumHeight(80)
|
|
126
|
+
|
|
127
|
+
self.histogram_ax = self.histogram_figure.add_subplot(111)
|
|
128
|
+
self.histogram_ax.set_facecolor('#232629') # Darker background for plot area
|
|
129
|
+
|
|
130
|
+
# Hide axes
|
|
131
|
+
self.histogram_ax.set_xticks([])
|
|
132
|
+
self.histogram_ax.set_yticks([])
|
|
133
|
+
self.histogram_ax.spines['top'].set_visible(False)
|
|
134
|
+
self.histogram_ax.spines['right'].set_visible(False)
|
|
135
|
+
self.histogram_ax.spines['bottom'].set_visible(False)
|
|
136
|
+
self.histogram_ax.spines['left'].set_visible(False)
|
|
137
|
+
|
|
138
|
+
# Initialize histogram lines
|
|
139
|
+
self._min_line = None
|
|
140
|
+
self._max_line = None
|
|
141
|
+
|
|
142
|
+
# Add components to group layout
|
|
143
|
+
group_layout.addLayout(channel_buttons_layout)
|
|
144
|
+
group_layout.addLayout(labels_layout)
|
|
145
|
+
group_layout.addLayout(spinbox_layout)
|
|
146
|
+
group_layout.addLayout(buttons_layout)
|
|
147
|
+
group_layout.addWidget(self.histogram_canvas)
|
|
148
|
+
|
|
149
|
+
# Initially hide the histogram
|
|
150
|
+
self.histogram_canvas.setVisible(False)
|
|
151
|
+
|
|
152
|
+
self.group_box.setLayout(group_layout)
|
|
153
|
+
main_layout.addWidget(self.group_box)
|
|
154
|
+
|
|
155
|
+
self.setLayout(main_layout)
|
|
156
|
+
|
|
157
|
+
# Don't update histogram initially since it's hidden
|
|
158
|
+
# self._update_histogram()
|
|
159
|
+
|
|
160
|
+
def _on_channel_selected(self, channel):
|
|
161
|
+
"""Handle channel selection."""
|
|
162
|
+
self._active_channel = channel
|
|
163
|
+
|
|
164
|
+
# Update button states
|
|
165
|
+
if channel == 1:
|
|
166
|
+
self.channel1_button.setChecked(True)
|
|
167
|
+
self.channel2_button.setChecked(False)
|
|
168
|
+
else:
|
|
169
|
+
self.channel1_button.setChecked(False)
|
|
170
|
+
self.channel2_button.setChecked(True)
|
|
171
|
+
|
|
172
|
+
# Update histogram only if it's visible
|
|
173
|
+
if self.histogram_toggle.isChecked():
|
|
174
|
+
self._update_histogram()
|
|
175
|
+
|
|
176
|
+
# Emit signal
|
|
177
|
+
self.channelChanged.emit(channel)
|
|
178
|
+
|
|
179
|
+
def _on_percentile_changed(self):
|
|
180
|
+
"""Handle percentile value changes."""
|
|
181
|
+
# Update histogram only if it's visible
|
|
182
|
+
if self.histogram_toggle.isChecked():
|
|
183
|
+
self._update_histogram()
|
|
184
|
+
|
|
185
|
+
# Emit signal
|
|
186
|
+
self.percentileChanged.emit()
|
|
187
|
+
|
|
188
|
+
def _on_histogram_toggle(self):
|
|
189
|
+
"""Handle histogram visibility toggle."""
|
|
190
|
+
is_visible = self.histogram_toggle.isChecked()
|
|
191
|
+
|
|
192
|
+
# Update button text
|
|
193
|
+
if is_visible:
|
|
194
|
+
self.histogram_toggle.setText("Hide Histogram")
|
|
195
|
+
self.histogram_canvas.setVisible(True)
|
|
196
|
+
# Update histogram when showing
|
|
197
|
+
self._update_histogram()
|
|
198
|
+
else:
|
|
199
|
+
self.histogram_toggle.setText("Show Histogram")
|
|
200
|
+
self.histogram_canvas.setVisible(False)
|
|
201
|
+
# Cancel any pending histogram update
|
|
202
|
+
self._pending_histogram_update = False
|
|
203
|
+
|
|
204
|
+
def _on_reset(self):
|
|
205
|
+
"""Reset percentile values to defaults."""
|
|
206
|
+
self.spinbox_min.setValue(0.5)
|
|
207
|
+
self.spinbox_max.setValue(99.5)
|
|
208
|
+
|
|
209
|
+
# Emit signal
|
|
210
|
+
self.resetRequested.emit()
|
|
211
|
+
|
|
212
|
+
def enable_controls(self, enabled, has_channel2=True):
|
|
213
|
+
"""Enable or disable all BnC controls.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
enabled: Whether to enable the controls
|
|
217
|
+
has_channel2: Whether channel 2 is available
|
|
218
|
+
"""
|
|
219
|
+
self.channel1_button.setEnabled(enabled)
|
|
220
|
+
self.channel2_button.setEnabled(enabled and has_channel2)
|
|
221
|
+
self.spinbox_min.setEnabled(enabled)
|
|
222
|
+
self.spinbox_max.setEnabled(enabled)
|
|
223
|
+
self.reset_button.setEnabled(enabled)
|
|
224
|
+
|
|
225
|
+
def get_min_percentile(self):
|
|
226
|
+
"""Get the current minimum percentile value."""
|
|
227
|
+
return self.spinbox_min.value()
|
|
228
|
+
|
|
229
|
+
def get_max_percentile(self):
|
|
230
|
+
"""Get the current maximum percentile value."""
|
|
231
|
+
return self.spinbox_max.value()
|
|
232
|
+
|
|
233
|
+
def set_min_percentile(self, value):
|
|
234
|
+
"""Set the minimum percentile value."""
|
|
235
|
+
self.spinbox_min.setValue(value)
|
|
236
|
+
|
|
237
|
+
def set_max_percentile(self, value):
|
|
238
|
+
"""Set the maximum percentile value."""
|
|
239
|
+
self.spinbox_max.setValue(value)
|
|
240
|
+
|
|
241
|
+
def set_image_data(self, ch1_data, ch2_data=None):
|
|
242
|
+
"""Set image data for histogram display.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
ch1_data: NumPy array for channel 1 (green)
|
|
246
|
+
ch2_data: NumPy array for channel 2 (red), optional
|
|
247
|
+
"""
|
|
248
|
+
self._ch1_data = ch1_data
|
|
249
|
+
self._ch2_data = ch2_data
|
|
250
|
+
|
|
251
|
+
# Enable/disable Channel 2 button based on data availability
|
|
252
|
+
self.channel2_button.setEnabled(ch2_data is not None)
|
|
253
|
+
|
|
254
|
+
# If Ch2 becomes unavailable, switch to Ch1
|
|
255
|
+
if ch2_data is None and self._active_channel == 2:
|
|
256
|
+
self._on_channel_selected(1)
|
|
257
|
+
|
|
258
|
+
# Update histogram only if it's visible
|
|
259
|
+
if self.histogram_toggle.isChecked():
|
|
260
|
+
self._update_histogram()
|
|
261
|
+
|
|
262
|
+
def _normalize_to_255(self, data):
|
|
263
|
+
"""Normalize data to 0-255 range for histogram display."""
|
|
264
|
+
if data is None:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
data_flat = data.flatten()
|
|
268
|
+
data_min = np.min(data_flat)
|
|
269
|
+
data_max = np.max(data_flat)
|
|
270
|
+
|
|
271
|
+
if data_max > data_min:
|
|
272
|
+
normalized = ((data_flat - data_min) / (data_max - data_min) * 255.0).astype(np.float32)
|
|
273
|
+
return normalized
|
|
274
|
+
else:
|
|
275
|
+
return np.zeros_like(data_flat, dtype=np.float32)
|
|
276
|
+
|
|
277
|
+
def _update_histogram(self):
|
|
278
|
+
"""Update histogram display for current channel using background thread."""
|
|
279
|
+
# If a histogram computation is already running, mark that we need another update
|
|
280
|
+
if self._histogram_thread is not None and self._histogram_thread.isRunning():
|
|
281
|
+
self._pending_histogram_update = True
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Get current channel data
|
|
285
|
+
if self._active_channel == 1:
|
|
286
|
+
current_data = self._ch1_data
|
|
287
|
+
self._current_hist_color = 'green'
|
|
288
|
+
else:
|
|
289
|
+
current_data = self._ch2_data
|
|
290
|
+
self._current_hist_color = 'red'
|
|
291
|
+
|
|
292
|
+
if current_data is None:
|
|
293
|
+
self._clear_histogram()
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Normalize data to 0-255
|
|
297
|
+
norm_data = self._normalize_to_255(current_data)
|
|
298
|
+
|
|
299
|
+
if norm_data is None:
|
|
300
|
+
self._clear_histogram()
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
# Get percentile values
|
|
304
|
+
min_percentile = self.spinbox_min.value()
|
|
305
|
+
max_percentile = self.spinbox_max.value()
|
|
306
|
+
|
|
307
|
+
# Create thread and worker
|
|
308
|
+
self._histogram_thread = QThread()
|
|
309
|
+
self._histogram_worker = HistogramWorker(norm_data, min_percentile, max_percentile)
|
|
310
|
+
self._histogram_worker.moveToThread(self._histogram_thread)
|
|
311
|
+
|
|
312
|
+
# Connect signals
|
|
313
|
+
self._histogram_thread.started.connect(self._histogram_worker.run)
|
|
314
|
+
self._histogram_worker.finished.connect(self._on_histogram_computed)
|
|
315
|
+
self._histogram_worker.error.connect(self._on_histogram_error)
|
|
316
|
+
|
|
317
|
+
# Cleanup on finish
|
|
318
|
+
def cleanup():
|
|
319
|
+
self._histogram_thread.quit()
|
|
320
|
+
self._histogram_thread.wait()
|
|
321
|
+
self._histogram_worker.deleteLater()
|
|
322
|
+
self._histogram_thread.deleteLater()
|
|
323
|
+
self._histogram_thread = None
|
|
324
|
+
self._histogram_worker = None
|
|
325
|
+
|
|
326
|
+
# If another update was requested while computing, trigger it now
|
|
327
|
+
if self._pending_histogram_update:
|
|
328
|
+
self._pending_histogram_update = False
|
|
329
|
+
self._update_histogram()
|
|
330
|
+
|
|
331
|
+
self._histogram_worker.finished.connect(cleanup)
|
|
332
|
+
self._histogram_worker.error.connect(cleanup)
|
|
333
|
+
|
|
334
|
+
# Start computation
|
|
335
|
+
self._histogram_thread.start()
|
|
336
|
+
|
|
337
|
+
def _clear_histogram(self):
|
|
338
|
+
"""Clear the histogram display."""
|
|
339
|
+
self.histogram_ax.clear()
|
|
340
|
+
|
|
341
|
+
# Hide axes
|
|
342
|
+
self.histogram_ax.set_xticks([])
|
|
343
|
+
self.histogram_ax.set_yticks([])
|
|
344
|
+
self.histogram_ax.spines['top'].set_visible(False)
|
|
345
|
+
self.histogram_ax.spines['right'].set_visible(False)
|
|
346
|
+
self.histogram_ax.spines['bottom'].set_visible(False)
|
|
347
|
+
self.histogram_ax.spines['left'].set_visible(False)
|
|
348
|
+
|
|
349
|
+
self.histogram_canvas.draw()
|
|
350
|
+
|
|
351
|
+
def _on_histogram_computed(self, counts, bins, min_val, max_val):
|
|
352
|
+
"""Handle histogram computation results from worker thread."""
|
|
353
|
+
try:
|
|
354
|
+
self.histogram_ax.clear()
|
|
355
|
+
|
|
356
|
+
# Hide axes
|
|
357
|
+
self.histogram_ax.set_xticks([])
|
|
358
|
+
self.histogram_ax.set_yticks([])
|
|
359
|
+
self.histogram_ax.spines['top'].set_visible(False)
|
|
360
|
+
self.histogram_ax.spines['right'].set_visible(False)
|
|
361
|
+
self.histogram_ax.spines['bottom'].set_visible(False)
|
|
362
|
+
self.histogram_ax.spines['left'].set_visible(False)
|
|
363
|
+
|
|
364
|
+
# Plot histogram using bar
|
|
365
|
+
bin_centers = (bins[:-1] + bins[1:]) / 2
|
|
366
|
+
self.histogram_ax.bar(
|
|
367
|
+
bin_centers, counts, width=1.0,
|
|
368
|
+
color=self._current_hist_color, alpha=0.7, edgecolor='none'
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Add vertical lines for min/max percentiles
|
|
372
|
+
self._min_line = self.histogram_ax.axvline(
|
|
373
|
+
min_val, color='cyan', linewidth=1.5, linestyle='--', alpha=0.8
|
|
374
|
+
)
|
|
375
|
+
self._max_line = self.histogram_ax.axvline(
|
|
376
|
+
max_val, color='magenta', linewidth=1.5, linestyle='--', alpha=0.8
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Set limits
|
|
380
|
+
self.histogram_ax.set_xlim(0, 255)
|
|
381
|
+
|
|
382
|
+
# Adjust layout to prevent label cutoff
|
|
383
|
+
self.histogram_figure.tight_layout(pad=0.05)
|
|
384
|
+
|
|
385
|
+
# Redraw canvas
|
|
386
|
+
self.histogram_canvas.draw()
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print(f"Error drawing histogram: {e}")
|
|
390
|
+
|
|
391
|
+
def _on_histogram_error(self, error_msg):
|
|
392
|
+
"""Handle histogram computation error."""
|
|
393
|
+
print(f"Histogram computation error: {error_msg}")
|
|
394
|
+
self._clear_histogram()
|
|
395
|
+
|
|
396
|
+
def get_min_percentile(self):
|
|
397
|
+
"""Get current minimum percentile value."""
|
|
398
|
+
return self.spinbox_min.value()
|
|
399
|
+
|
|
400
|
+
def get_max_percentile(self):
|
|
401
|
+
"""Get current maximum percentile value."""
|
|
402
|
+
return self.spinbox_max.value()
|
|
403
|
+
|
|
404
|
+
def get_active_channel(self):
|
|
405
|
+
"""Get currently active channel (1 or 2)."""
|
|
406
|
+
return self._active_channel
|
|
407
|
+
|
|
408
|
+
def set_min_percentile(self, value):
|
|
409
|
+
"""Set minimum percentile value."""
|
|
410
|
+
self.spinbox_min.setValue(value)
|
|
411
|
+
|
|
412
|
+
def set_max_percentile(self, value):
|
|
413
|
+
"""Set maximum percentile value."""
|
|
414
|
+
self.spinbox_max.setValue(value)
|
|
415
|
+
|
|
416
|
+
def cleanup(self):
|
|
417
|
+
"""Cleanup resources, especially running threads."""
|
|
418
|
+
# Stop any running histogram computation
|
|
419
|
+
if self._histogram_thread is not None and self._histogram_thread.isRunning():
|
|
420
|
+
self._histogram_thread.quit()
|
|
421
|
+
self._histogram_thread.wait()
|
|
422
|
+
if self._histogram_worker is not None:
|
|
423
|
+
self._histogram_worker.deleteLater()
|
|
424
|
+
self._histogram_thread.deleteLater()
|
|
425
|
+
self._histogram_thread = None
|
|
426
|
+
self._histogram_worker = None
|