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.
Files changed (37) hide show
  1. phasor_handler/__init__.py +9 -0
  2. phasor_handler/app.py +249 -0
  3. phasor_handler/img/icons/chevron-down.svg +3 -0
  4. phasor_handler/img/icons/chevron-up.svg +3 -0
  5. phasor_handler/img/logo.ico +0 -0
  6. phasor_handler/models/dir_manager.py +100 -0
  7. phasor_handler/scripts/contrast.py +131 -0
  8. phasor_handler/scripts/convert.py +155 -0
  9. phasor_handler/scripts/meta_reader.py +467 -0
  10. phasor_handler/scripts/plot.py +110 -0
  11. phasor_handler/scripts/register.py +86 -0
  12. phasor_handler/themes/__init__.py +8 -0
  13. phasor_handler/themes/dark_theme.py +330 -0
  14. phasor_handler/tools/__init__.py +1 -0
  15. phasor_handler/tools/check_stylesheet.py +15 -0
  16. phasor_handler/tools/misc.py +20 -0
  17. phasor_handler/widgets/__init__.py +5 -0
  18. phasor_handler/widgets/analysis/components/__init__.py +9 -0
  19. phasor_handler/widgets/analysis/components/bnc.py +426 -0
  20. phasor_handler/widgets/analysis/components/circle_roi.py +850 -0
  21. phasor_handler/widgets/analysis/components/image_view.py +667 -0
  22. phasor_handler/widgets/analysis/components/meta_info.py +481 -0
  23. phasor_handler/widgets/analysis/components/roi_list.py +659 -0
  24. phasor_handler/widgets/analysis/components/trace_plot.py +621 -0
  25. phasor_handler/widgets/analysis/view.py +1735 -0
  26. phasor_handler/widgets/conversion/view.py +83 -0
  27. phasor_handler/widgets/registration/view.py +110 -0
  28. phasor_handler/workers/__init__.py +2 -0
  29. phasor_handler/workers/analysis_worker.py +0 -0
  30. phasor_handler/workers/histogram_worker.py +55 -0
  31. phasor_handler/workers/registration_worker.py +242 -0
  32. phasor_handler-2.2.0.dist-info/METADATA +134 -0
  33. phasor_handler-2.2.0.dist-info/RECORD +37 -0
  34. phasor_handler-2.2.0.dist-info/WHEEL +5 -0
  35. phasor_handler-2.2.0.dist-info/entry_points.txt +5 -0
  36. phasor_handler-2.2.0.dist-info/licenses/LICENSE.md +21 -0
  37. 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