microeye 2.3.2__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.
- microEye/__init__.py +47 -0
- microEye/_version.py +2 -0
- microEye/analysis/__init__.py +1 -0
- microEye/analysis/checklist_dialog.py +143 -0
- microEye/analysis/cmosMaps.py +228 -0
- microEye/analysis/filters/__init__.py +9 -0
- microEye/analysis/filters/base.py +21 -0
- microEye/analysis/filters/spatial.py +338 -0
- microEye/analysis/filters/temporal.py +76 -0
- microEye/analysis/fitting/__init__.py +0 -0
- microEye/analysis/fitting/fit.py +680 -0
- microEye/analysis/fitting/nena.py +375 -0
- microEye/analysis/fitting/phasor_fit.py +90 -0
- microEye/analysis/fitting/processing.py +317 -0
- microEye/analysis/fitting/psf/__init__.py +6 -0
- microEye/analysis/fitting/psf/extract.py +1129 -0
- microEye/analysis/fitting/psf/rubost_mean.py +150 -0
- microEye/analysis/fitting/psf/spline.py +167 -0
- microEye/analysis/fitting/psf/stats/__init__.py +12 -0
- microEye/analysis/fitting/psf/stats/core.py +295 -0
- microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
- microEye/analysis/fitting/psf/stats/io.py +104 -0
- microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
- microEye/analysis/fitting/psf/temp.py +147 -0
- microEye/analysis/fitting/psf/test.py +47 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
- microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
- microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
- microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
- microEye/analysis/fitting/results.py +917 -0
- microEye/analysis/fitting/results_stats.py +251 -0
- microEye/analysis/fitting/tardis.py +209 -0
- microEye/analysis/multi_viewer.py +568 -0
- microEye/analysis/processing/__init__.py +1 -0
- microEye/analysis/processing/frc.py +120 -0
- microEye/analysis/rendering/__init__.py +3 -0
- microEye/analysis/rendering/base.py +452 -0
- microEye/analysis/rendering/cloud.py +215 -0
- microEye/analysis/rendering/core.py +47 -0
- microEye/analysis/rendering/volumetric.py +198 -0
- microEye/analysis/tools/__init__.py +0 -0
- microEye/analysis/tools/kymograms.py +1079 -0
- microEye/analysis/tools/roi_selectors.py +556 -0
- microEye/analysis/utils/__init__.py +3 -0
- microEye/analysis/utils/coordinates.py +18 -0
- microEye/analysis/utils/images.py +68 -0
- microEye/analysis/utils/windows.py +19 -0
- microEye/analysis/viewer/__init__.py +3 -0
- microEye/analysis/viewer/image_options_widget.py +615 -0
- microEye/analysis/viewer/images.py +1225 -0
- microEye/analysis/viewer/layers_widget.py +415 -0
- microEye/analysis/viewer/localizations.py +1304 -0
- microEye/analysis/viewer/psf.py +1003 -0
- microEye/analysis/viewer/volume.py +456 -0
- microEye/hardware/__init__.py +3 -0
- microEye/hardware/cams/__init__.py +29 -0
- microEye/hardware/cams/camera_calibration.py +99 -0
- microEye/hardware/cams/camera_list.py +528 -0
- microEye/hardware/cams/camera_options.py +694 -0
- microEye/hardware/cams/camera_panel.py +941 -0
- microEye/hardware/cams/dummy/__init__.py +1 -0
- microEye/hardware/cams/dummy/dummy_panel.py +759 -0
- microEye/hardware/cams/jobs.py +497 -0
- microEye/hardware/cams/line_profiler.py +99 -0
- microEye/hardware/cams/linescan/IR_Cam.py +474 -0
- microEye/hardware/cams/linescan/__init__.py +1 -0
- microEye/hardware/cams/micam.py +524 -0
- microEye/hardware/cams/pco/__init__.py +58 -0
- microEye/hardware/cams/pco/enums.py +382 -0
- microEye/hardware/cams/pco/pco_cam.py +761 -0
- microEye/hardware/cams/pco/pco_panel.py +477 -0
- microEye/hardware/cams/shortcuts.py +312 -0
- microEye/hardware/cams/thorlabs/__init__.py +1 -0
- microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
- microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
- microEye/hardware/cams/ueye/__init__.py +1 -0
- microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
- microEye/hardware/cams/ueye/ueye_panel.py +861 -0
- microEye/hardware/cams/vimba/__init__.py +1 -0
- microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
- microEye/hardware/cams/vimba/vimba_panel.py +813 -0
- microEye/hardware/device.py +60 -0
- microEye/hardware/lasers/__init__.py +13 -0
- microEye/hardware/lasers/io_matchbox.py +791 -0
- microEye/hardware/lasers/io_params.py +85 -0
- microEye/hardware/lasers/io_single_laser.py +742 -0
- microEye/hardware/lasers/laser_relay.py +594 -0
- microEye/hardware/mieye/__init__.py +1 -0
- microEye/hardware/mieye/acquisition_manager.py +467 -0
- microEye/hardware/mieye/devices_manager.py +533 -0
- microEye/hardware/mieye/miEye.py +659 -0
- microEye/hardware/misc/__init__.py +0 -0
- microEye/hardware/misc/acquisition_view.py +71 -0
- microEye/hardware/misc/reglo.py +761 -0
- microEye/hardware/misc/temp.py +188 -0
- microEye/hardware/port_config.py +59 -0
- microEye/hardware/protocols/__init__.py +2 -0
- microEye/hardware/protocols/actions.py +402 -0
- microEye/hardware/protocols/actions_items.py +703 -0
- microEye/hardware/protocols/designer.py +244 -0
- microEye/hardware/protocols/scene_manager.py +191 -0
- microEye/hardware/protocols/serialization.py +97 -0
- microEye/hardware/pycromanager/__init__.py +16 -0
- microEye/hardware/pycromanager/core.py +1433 -0
- microEye/hardware/pycromanager/devices.py +461 -0
- microEye/hardware/pycromanager/enums.py +107 -0
- microEye/hardware/pycromanager/headless.py +153 -0
- microEye/hardware/pycromanager/utils.py +34 -0
- microEye/hardware/pycromanager/widgets/__init__.py +5 -0
- microEye/hardware/pycromanager/widgets/bridges.py +407 -0
- microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
- microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
- microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
- microEye/hardware/stages/__init__.py +18 -0
- microEye/hardware/stages/elliptec/__init__.py +5 -0
- microEye/hardware/stages/elliptec/baseDevice.py +314 -0
- microEye/hardware/stages/elliptec/device.py +384 -0
- microEye/hardware/stages/elliptec/deviceID.py +212 -0
- microEye/hardware/stages/elliptec/devicePort.py +379 -0
- microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
- microEye/hardware/stages/elliptec/devicesView.py +706 -0
- microEye/hardware/stages/elliptec/ellDevices.py +134 -0
- microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
- microEye/hardware/stages/elliptec/motorInfo.py +153 -0
- microEye/hardware/stages/elliptec/stage.py +62 -0
- microEye/hardware/stages/elliptec/test.py +139 -0
- microEye/hardware/stages/kinesis/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
- microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
- microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
- microEye/hardware/stages/kinesis/kinesis.py +776 -0
- microEye/hardware/stages/piezo_concept.py +607 -0
- microEye/hardware/stages/stabilizer.py +785 -0
- microEye/hardware/stages/stage.py +89 -0
- microEye/hardware/widgets/__init__.py +10 -0
- microEye/hardware/widgets/controller.py +246 -0
- microEye/hardware/widgets/devices.py +133 -0
- microEye/hardware/widgets/focusWidget.py +264 -0
- microEye/hardware/widgets/qlist_slider.py +113 -0
- microEye/hardware/widgets/scan_acquisition.py +424 -0
- microEye/icons/1024.png +0 -0
- microEye/icons/128.png +0 -0
- microEye/icons/16.png +0 -0
- microEye/icons/24.png +0 -0
- microEye/icons/256.png +0 -0
- microEye/icons/32.png +0 -0
- microEye/icons/48.png +0 -0
- microEye/icons/512.png +0 -0
- microEye/icons/64.png +0 -0
- microEye/icons/__init__.py +0 -0
- microEye/icons/close.svg +88 -0
- microEye/icons/mieye.png +0 -0
- microEye/icons/min.svg +83 -0
- microEye/icons/viewer.png +0 -0
- microEye/launcher.py +42 -0
- microEye/qt.py +181 -0
- microEye/utils/__init__.py +2 -0
- microEye/utils/enum_encoder.py +10 -0
- microEye/utils/expandable_groupbox.py +93 -0
- microEye/utils/gui_helper.py +457 -0
- microEye/utils/hid/__init__.py +8 -0
- microEye/utils/hid/controller.py +153 -0
- microEye/utils/hid/device.py +63 -0
- microEye/utils/hid/enums.py +140 -0
- microEye/utils/hid/utils.py +58 -0
- microEye/utils/labelled_slider.py +134 -0
- microEye/utils/metadata.py +599 -0
- microEye/utils/metadata_tree.py +718 -0
- microEye/utils/micro_launcher.py +245 -0
- microEye/utils/parameter_tree.py +325 -0
- microEye/utils/pyscripting.py +444 -0
- microEye/utils/retry_exec.py +35 -0
- microEye/utils/start_gui.py +112 -0
- microEye/utils/thread_worker.py +115 -0
- microEye/utils/uImage.py +1512 -0
- microeye-2.3.2.dist-info/METADATA +401 -0
- microeye-2.3.2.dist-info/RECORD +190 -0
- microeye-2.3.2.dist-info/WHEEL +5 -0
- microeye-2.3.2.dist-info/entry_points.txt +2 -0
- microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
- microeye-2.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from pprint import pprint
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pyqtgraph as pg
|
|
9
|
+
import pyqtgraph.opengl as gl
|
|
10
|
+
from pyqtgraph import functions as fn
|
|
11
|
+
from tabulate import tabulate
|
|
12
|
+
|
|
13
|
+
from microEye.analysis.fitting.psf import ConfidenceMethod, PSFdata, stats
|
|
14
|
+
from microEye.analysis.fitting.results import PARAMETER_HEADERS, FittingMethod
|
|
15
|
+
from microEye.qt import QtCore, QtWidgets, getOpenFileName, getSaveFileName
|
|
16
|
+
from microEye.utils.enum_encoder import EnumEncoder
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PSFView(QtWidgets.QWidget):
|
|
20
|
+
'''
|
|
21
|
+
A class for viewing and interacting with PSF data
|
|
22
|
+
'''
|
|
23
|
+
|
|
24
|
+
def __init__(self, psf_data: Union['PSFdata', str] = None):
|
|
25
|
+
'''Initialize the PSFView.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
psf_data : PSFdata or str, optional
|
|
30
|
+
PSF data to visualize. Can be a PSFdata object or a path to an HDF5 file
|
|
31
|
+
'''
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
if isinstance(psf_data, str) and os.path.exists(psf_data):
|
|
35
|
+
self.psf_data = PSFdata.load_hdf(psf_data)
|
|
36
|
+
elif isinstance(psf_data, PSFdata):
|
|
37
|
+
self.psf_data = psf_data
|
|
38
|
+
else:
|
|
39
|
+
raise TypeError(
|
|
40
|
+
'PSF data must be a PSFdata object or a path to an HDF5 file.'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.fit_result = None
|
|
44
|
+
|
|
45
|
+
self.setWindowTitle('PSF Data Viewer')
|
|
46
|
+
self._threadpool = QtCore.QThreadPool.globalInstance()
|
|
47
|
+
self.main_layout = QtWidgets.QHBoxLayout()
|
|
48
|
+
self.setLayout(self.main_layout)
|
|
49
|
+
|
|
50
|
+
# Tab Widget for different views
|
|
51
|
+
self.view_tabs = QtWidgets.QTabWidget()
|
|
52
|
+
self.main_layout.addWidget(self.view_tabs)
|
|
53
|
+
|
|
54
|
+
# Default view mode
|
|
55
|
+
self.view_mode = 'XY'
|
|
56
|
+
|
|
57
|
+
# Setup different view tabs
|
|
58
|
+
self.setup_merged_view_tab()
|
|
59
|
+
self.setup_3d_view_tab()
|
|
60
|
+
self.setup_statistics_tab()
|
|
61
|
+
|
|
62
|
+
if psf_data:
|
|
63
|
+
self.update_all_views(False)
|
|
64
|
+
|
|
65
|
+
def setup_merged_view_tab(self):
|
|
66
|
+
'''Set up the merged Slice/Field View tab with enhanced controls.'''
|
|
67
|
+
merged_widget = QtWidgets.QWidget()
|
|
68
|
+
merged_layout = QtWidgets.QHBoxLayout()
|
|
69
|
+
merged_widget.setLayout(merged_layout)
|
|
70
|
+
|
|
71
|
+
# Visualization area
|
|
72
|
+
visual_layout = QtWidgets.QVBoxLayout()
|
|
73
|
+
|
|
74
|
+
self.image_widget = pg.GraphicsLayoutWidget()
|
|
75
|
+
self.view_box: pg.ViewBox = self.image_widget.addViewBox(row=0, col=0)
|
|
76
|
+
self.view_box.setAspectLocked(True)
|
|
77
|
+
self.view_box.setAutoVisible(True)
|
|
78
|
+
self.view_box.enableAutoRange()
|
|
79
|
+
self.view_box.invertY(True)
|
|
80
|
+
|
|
81
|
+
# Create two image items: one for PSF data and one for grid overlay
|
|
82
|
+
self.psf_image_item: pg.ImageItem = pg.ImageItem(axisOrder='row-major')
|
|
83
|
+
self.grid_overlay_item: pg.ImageItem = pg.ImageItem(axisOrder='row-major')
|
|
84
|
+
self.view_box.addItem(self.psf_image_item)
|
|
85
|
+
self.view_box.addItem(self.grid_overlay_item)
|
|
86
|
+
|
|
87
|
+
# Create histogram with enhanced controls
|
|
88
|
+
self.histogram = pg.HistogramLUTItem(
|
|
89
|
+
gradientPosition='right', orientation='vertical'
|
|
90
|
+
)
|
|
91
|
+
self.histogram.setImageItem(self.psf_image_item)
|
|
92
|
+
self.histogram.gradient.setColorMap(pg.colormap.get('jet', 'matplotlib'))
|
|
93
|
+
self.image_widget.addItem(self.histogram, row=0, col=1)
|
|
94
|
+
|
|
95
|
+
visual_layout.addWidget(self.image_widget)
|
|
96
|
+
|
|
97
|
+
# Modify Z-slice selection to be more generic
|
|
98
|
+
slice_layout = QtWidgets.QHBoxLayout()
|
|
99
|
+
self.slice_label = QtWidgets.QLabel('Z-Slice:')
|
|
100
|
+
self.slice_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
|
|
101
|
+
self.slice_spinbox = QtWidgets.QSpinBox()
|
|
102
|
+
self.slice_slider.setMinimum(0)
|
|
103
|
+
self.slice_spinbox.setMinimum(0)
|
|
104
|
+
self.slice_spinbox.setMinimumWidth(75)
|
|
105
|
+
|
|
106
|
+
max_slice = len(self.psf_data) - 1
|
|
107
|
+
self.slice_slider.setMaximum(max_slice)
|
|
108
|
+
self.slice_spinbox.setMaximum(max_slice)
|
|
109
|
+
|
|
110
|
+
self.slice_slider.valueChanged.connect(self.slice_spinbox.setValue)
|
|
111
|
+
self.slice_spinbox.valueChanged.connect(self.slice_slider.setValue)
|
|
112
|
+
self.slice_slider.valueChanged.connect(self.update_merged_view)
|
|
113
|
+
|
|
114
|
+
slice_layout.addWidget(self.slice_label)
|
|
115
|
+
slice_layout.addWidget(self.slice_slider)
|
|
116
|
+
slice_layout.addWidget(self.slice_spinbox)
|
|
117
|
+
|
|
118
|
+
visual_layout.addLayout(slice_layout)
|
|
119
|
+
|
|
120
|
+
merged_layout.addLayout(visual_layout, stretch=2)
|
|
121
|
+
|
|
122
|
+
# Controls area
|
|
123
|
+
controls_widget = QtWidgets.QWidget()
|
|
124
|
+
controls_layout = QtWidgets.QVBoxLayout()
|
|
125
|
+
controls_widget.setLayout(controls_layout)
|
|
126
|
+
controls_widget.setMaximumWidth(300)
|
|
127
|
+
|
|
128
|
+
# Grid settings
|
|
129
|
+
grid_group = QtWidgets.QGroupBox('Grid Settings')
|
|
130
|
+
grid_layout = QtWidgets.QFormLayout()
|
|
131
|
+
|
|
132
|
+
self.grid_size = QtWidgets.QSpinBox()
|
|
133
|
+
self.grid_size.setMinimum(1)
|
|
134
|
+
self.grid_size.setMaximum(5)
|
|
135
|
+
self.grid_size.setValue(1)
|
|
136
|
+
self.grid_size.valueChanged.connect(self.update_merged_view)
|
|
137
|
+
grid_layout.addRow('Grid Size:', self.grid_size)
|
|
138
|
+
|
|
139
|
+
# Grid overlay opacity
|
|
140
|
+
self.grid_opacity = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
|
|
141
|
+
self.grid_opacity.setMinimum(0)
|
|
142
|
+
self.grid_opacity.setMaximum(100)
|
|
143
|
+
self.grid_opacity.setValue(50)
|
|
144
|
+
self.grid_opacity.valueChanged.connect(self.update_grid_opacity)
|
|
145
|
+
grid_layout.addRow('Grid Opacity:', self.grid_opacity)
|
|
146
|
+
|
|
147
|
+
grid_group.setLayout(grid_layout)
|
|
148
|
+
controls_layout.addWidget(grid_group)
|
|
149
|
+
|
|
150
|
+
# Display options
|
|
151
|
+
display_group = QtWidgets.QGroupBox('Display Options')
|
|
152
|
+
display_layout = QtWidgets.QVBoxLayout()
|
|
153
|
+
|
|
154
|
+
# Add view mode selector
|
|
155
|
+
display_layout.addWidget(QtWidgets.QLabel('View Mode:'))
|
|
156
|
+
self.view_mode_combo = QtWidgets.QComboBox()
|
|
157
|
+
self.view_mode_combo.addItems(['XY', 'XZ', 'YZ'])
|
|
158
|
+
self.view_mode_combo.currentTextChanged.connect(self.change_view_mode)
|
|
159
|
+
display_layout.addWidget(self.view_mode_combo)
|
|
160
|
+
|
|
161
|
+
self.display_mean = QtWidgets.QRadioButton('Mean')
|
|
162
|
+
self.display_median = QtWidgets.QRadioButton('Median')
|
|
163
|
+
self.display_std = QtWidgets.QRadioButton('Standard Deviation')
|
|
164
|
+
self.display_single = QtWidgets.QRadioButton('Single ROI')
|
|
165
|
+
self.display_mean.setChecked(True)
|
|
166
|
+
|
|
167
|
+
for radio in [
|
|
168
|
+
self.display_mean,
|
|
169
|
+
self.display_median,
|
|
170
|
+
self.display_std,
|
|
171
|
+
self.display_single,
|
|
172
|
+
]:
|
|
173
|
+
display_layout.addWidget(radio)
|
|
174
|
+
radio.toggled.connect(self.update_merged_view)
|
|
175
|
+
|
|
176
|
+
# Add Single ROI selection
|
|
177
|
+
single_roi_layout = QtWidgets.QHBoxLayout()
|
|
178
|
+
self.single_roi_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
|
|
179
|
+
self.single_roi_spinbox = QtWidgets.QSpinBox()
|
|
180
|
+
|
|
181
|
+
self.single_roi_slider.setMinimum(0)
|
|
182
|
+
self.single_roi_spinbox.setMinimum(0)
|
|
183
|
+
self.single_roi_spinbox.setMinimumWidth(75)
|
|
184
|
+
|
|
185
|
+
# Initial maximum will be set when updating the view
|
|
186
|
+
self.single_roi_slider.setMaximum(0)
|
|
187
|
+
self.single_roi_spinbox.setMaximum(0)
|
|
188
|
+
|
|
189
|
+
self.single_roi_slider.valueChanged.connect(self.single_roi_spinbox.setValue)
|
|
190
|
+
self.single_roi_spinbox.valueChanged.connect(self.single_roi_slider.setValue)
|
|
191
|
+
self.single_roi_slider.valueChanged.connect(self.update_merged_view)
|
|
192
|
+
|
|
193
|
+
single_roi_layout.addWidget(QtWidgets.QLabel('ROI:'))
|
|
194
|
+
single_roi_layout.addWidget(self.single_roi_slider)
|
|
195
|
+
single_roi_layout.addWidget(self.single_roi_spinbox)
|
|
196
|
+
|
|
197
|
+
# Create a widget to contain the ROI selection controls
|
|
198
|
+
roi_selection_widget = QtWidgets.QWidget()
|
|
199
|
+
roi_selection_widget.setLayout(single_roi_layout)
|
|
200
|
+
roi_selection_widget.setEnabled(False) # Initially disabled
|
|
201
|
+
|
|
202
|
+
# Store reference to enable/disable later
|
|
203
|
+
self.roi_selection_widget = roi_selection_widget
|
|
204
|
+
|
|
205
|
+
# Connect radio button to enable/disable ROI selection
|
|
206
|
+
self.display_single.toggled.connect(roi_selection_widget.setEnabled)
|
|
207
|
+
|
|
208
|
+
display_layout.addWidget(roi_selection_widget)
|
|
209
|
+
display_group.setLayout(display_layout)
|
|
210
|
+
controls_layout.addWidget(display_group)
|
|
211
|
+
|
|
212
|
+
# Auto level control
|
|
213
|
+
auto_level_group = QtWidgets.QGroupBox('Level Control')
|
|
214
|
+
auto_level_layout = QtWidgets.QVBoxLayout()
|
|
215
|
+
self.auto_level_checkbox = QtWidgets.QCheckBox('Auto Level')
|
|
216
|
+
self.auto_level_checkbox.setChecked(True)
|
|
217
|
+
self.auto_level_checkbox.stateChanged.connect(self.update_merged_view)
|
|
218
|
+
self.normalization_checkbox = QtWidgets.QCheckBox('Normalize (Field)')
|
|
219
|
+
self.normalization_checkbox.setChecked(True)
|
|
220
|
+
self.normalization_checkbox.stateChanged.connect(self.update_merged_view)
|
|
221
|
+
auto_level_layout.addWidget(self.auto_level_checkbox)
|
|
222
|
+
auto_level_layout.addWidget(self.normalization_checkbox)
|
|
223
|
+
auto_level_group.setLayout(auto_level_layout)
|
|
224
|
+
controls_layout.addWidget(auto_level_group)
|
|
225
|
+
|
|
226
|
+
# Metadata display
|
|
227
|
+
metadata_group = QtWidgets.QGroupBox('Metadata')
|
|
228
|
+
metadata_layout = QtWidgets.QVBoxLayout()
|
|
229
|
+
self.metadata_text = QtWidgets.QTextEdit()
|
|
230
|
+
self.metadata_text.setReadOnly(True)
|
|
231
|
+
metadata_layout.addWidget(self.metadata_text)
|
|
232
|
+
metadata_group.setLayout(metadata_layout)
|
|
233
|
+
controls_layout.addWidget(metadata_group)
|
|
234
|
+
|
|
235
|
+
# Refresh button
|
|
236
|
+
self.refresh_btn = QtWidgets.QPushButton('Refresh View')
|
|
237
|
+
self.refresh_btn.clicked.connect(self.update_merged_view)
|
|
238
|
+
self.zero_btn = QtWidgets.QPushButton(f'Zero Plane {self.psf_data.zero_plane}')
|
|
239
|
+
self.zero_btn.clicked.connect(self.update_slice_controls)
|
|
240
|
+
controls_layout.addWidget(self.refresh_btn)
|
|
241
|
+
controls_layout.addWidget(self.zero_btn)
|
|
242
|
+
|
|
243
|
+
# Export PSF data
|
|
244
|
+
self.export_btn = QtWidgets.QPushButton('Export PSF Data')
|
|
245
|
+
self.export_btn.clicked.connect(self._export_psf_data)
|
|
246
|
+
controls_layout.addWidget(self.export_btn)
|
|
247
|
+
|
|
248
|
+
# Add stretch to push controls to the top
|
|
249
|
+
controls_layout.addStretch()
|
|
250
|
+
|
|
251
|
+
merged_layout.addWidget(controls_widget)
|
|
252
|
+
self.view_tabs.addTab(merged_widget, 'PSF View')
|
|
253
|
+
|
|
254
|
+
def setup_statistics_tab(self):
|
|
255
|
+
'''
|
|
256
|
+
Set up the Statistics tab with separate left and right graphs and controls.
|
|
257
|
+
'''
|
|
258
|
+
stats_widget = QtWidgets.QWidget()
|
|
259
|
+
stats_layout = QtWidgets.QHBoxLayout()
|
|
260
|
+
stats_widget.setLayout(stats_layout)
|
|
261
|
+
|
|
262
|
+
# Left graph (ROI plot)
|
|
263
|
+
left_widget = QtWidgets.QWidget()
|
|
264
|
+
left_layout = QtWidgets.QVBoxLayout()
|
|
265
|
+
left_widget.setLayout(left_layout)
|
|
266
|
+
|
|
267
|
+
self.roi_plot = pg.PlotWidget(title='Statistics Over Z')
|
|
268
|
+
self.roi_plot.setLabel('left', 'Value')
|
|
269
|
+
self.roi_plot.setLabel('bottom', 'Z-Slice')
|
|
270
|
+
left_layout.addWidget(self.roi_plot)
|
|
271
|
+
|
|
272
|
+
# Add range ROI to the plot
|
|
273
|
+
self.range_roi = pg.LinearRegionItem()
|
|
274
|
+
self.range_roi.setZValue(10)
|
|
275
|
+
self.roi_plot.addItem(self.range_roi)
|
|
276
|
+
self.range_roi.sigRegionChanged.connect(self.update_range_spinboxes)
|
|
277
|
+
|
|
278
|
+
left_options = QtWidgets.QFormLayout()
|
|
279
|
+
left_layout.addLayout(left_options)
|
|
280
|
+
|
|
281
|
+
# Statistic selection for ROI plot
|
|
282
|
+
self.stat_combo = QtWidgets.QComboBox()
|
|
283
|
+
for stat in self.psf_data.available_stats:
|
|
284
|
+
self.stat_combo.addItem(stat)
|
|
285
|
+
self.stat_combo.currentTextChanged.connect(self.update_roi_statistics)
|
|
286
|
+
left_options.addRow(
|
|
287
|
+
'Select statistic:',
|
|
288
|
+
self.stat_combo,
|
|
289
|
+
)
|
|
290
|
+
# add confidence enum combo:
|
|
291
|
+
self.confidence_combo = QtWidgets.QComboBox()
|
|
292
|
+
for c in ConfidenceMethod:
|
|
293
|
+
self.confidence_combo.addItem(c.name, c)
|
|
294
|
+
self.confidence_combo.currentIndexChanged.connect(self.update_roi_statistics)
|
|
295
|
+
left_options.addRow(
|
|
296
|
+
'Confidence method:',
|
|
297
|
+
self.confidence_combo,
|
|
298
|
+
)
|
|
299
|
+
# add confedence interval spin box 0 to 100
|
|
300
|
+
self.confidence_spinbox = QtWidgets.QSpinBox()
|
|
301
|
+
self.confidence_spinbox.setMinimum(0)
|
|
302
|
+
self.confidence_spinbox.setMaximum(100)
|
|
303
|
+
self.confidence_spinbox.setValue(95)
|
|
304
|
+
self.confidence_spinbox.valueChanged.connect(self.update_roi_statistics)
|
|
305
|
+
left_options.addRow(
|
|
306
|
+
'Confidence interval:',
|
|
307
|
+
self.confidence_spinbox,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Refresh button for left graph
|
|
311
|
+
self.refresh_roi_btn = QtWidgets.QPushButton('Refresh ROI Statistics')
|
|
312
|
+
self.refresh_roi_btn.clicked.connect(self.update_roi_statistics)
|
|
313
|
+
left_options.addWidget(self.refresh_roi_btn)
|
|
314
|
+
|
|
315
|
+
# Add zero plane adjustment controls
|
|
316
|
+
self.zero_plane_method = QtWidgets.QComboBox()
|
|
317
|
+
self.zero_plane_method.addItems(
|
|
318
|
+
['Manual', 'Peak', 'Valley', 'Gaussian Fit', 'Gaussian Fit (Inverted)']
|
|
319
|
+
)
|
|
320
|
+
left_options.addRow('Zero Plane Method:', self.zero_plane_method)
|
|
321
|
+
|
|
322
|
+
# Add range widgets
|
|
323
|
+
range_controls_layout = QtWidgets.QHBoxLayout()
|
|
324
|
+
|
|
325
|
+
# Update spinboxes for range selection
|
|
326
|
+
self.z_range_start = QtWidgets.QSpinBox()
|
|
327
|
+
self.z_range_start.setMinimumWidth(75)
|
|
328
|
+
self.z_range_end = QtWidgets.QSpinBox()
|
|
329
|
+
self.z_range_end.setMinimumWidth(75)
|
|
330
|
+
range_controls_layout.addWidget(QtWidgets.QLabel('Range:'))
|
|
331
|
+
range_controls_layout.addWidget(self.z_range_start)
|
|
332
|
+
range_controls_layout.addWidget(QtWidgets.QLabel('to'))
|
|
333
|
+
range_controls_layout.addWidget(self.z_range_end)
|
|
334
|
+
|
|
335
|
+
self.z_range_start.valueChanged.connect(self.update_range_roi)
|
|
336
|
+
self.z_range_end.valueChanged.connect(self.update_range_roi)
|
|
337
|
+
# Add range controls to left options
|
|
338
|
+
left_options.addRow('Range:', range_controls_layout)
|
|
339
|
+
|
|
340
|
+
# Add CurveFitMethods Combo
|
|
341
|
+
self.curve_fit_combo = QtWidgets.QComboBox()
|
|
342
|
+
for m in stats.CurveFitMethod:
|
|
343
|
+
self.curve_fit_combo.addItem(m.name, m)
|
|
344
|
+
|
|
345
|
+
left_options.addRow('Curve Fit Method:', self.curve_fit_combo)
|
|
346
|
+
|
|
347
|
+
# Add restore button
|
|
348
|
+
buttons_layout = QtWidgets.QHBoxLayout()
|
|
349
|
+
self.restore_zero_plane_btn = QtWidgets.QPushButton(
|
|
350
|
+
'Restore Zero Plane', clicked=self.restore_zero_plane
|
|
351
|
+
)
|
|
352
|
+
buttons_layout.addWidget(self.restore_zero_plane_btn)
|
|
353
|
+
|
|
354
|
+
self.adjust_zero_plane_btn = QtWidgets.QPushButton(
|
|
355
|
+
'Adjust Zero Plane', clicked=self.adjust_zero_plane
|
|
356
|
+
)
|
|
357
|
+
buttons_layout.addWidget(self.adjust_zero_plane_btn)
|
|
358
|
+
|
|
359
|
+
self.slope_btn = QtWidgets.QPushButton('Fit Curve', clicked=self.fit_stat_curve)
|
|
360
|
+
buttons_layout.addWidget(self.slope_btn)
|
|
361
|
+
self.export_fit_btn = QtWidgets.QPushButton(
|
|
362
|
+
'Export', clicked=self.export_fit_curve
|
|
363
|
+
)
|
|
364
|
+
buttons_layout.addWidget(self.export_fit_btn)
|
|
365
|
+
self.import_fit_btn = QtWidgets.QPushButton(
|
|
366
|
+
'Import', clicked=self.import_fit_curve
|
|
367
|
+
)
|
|
368
|
+
buttons_layout.addWidget(self.import_fit_btn)
|
|
369
|
+
|
|
370
|
+
left_options.addRow(buttons_layout)
|
|
371
|
+
|
|
372
|
+
# Right graph (Intensity plot)
|
|
373
|
+
right_widget = QtWidgets.QWidget()
|
|
374
|
+
right_layout = QtWidgets.QVBoxLayout()
|
|
375
|
+
right_widget.setLayout(right_layout)
|
|
376
|
+
|
|
377
|
+
self.intensity_plot = pg.PlotWidget(title='Intensity Statistics Over Z')
|
|
378
|
+
self.intensity_plot.setLabel('left', 'Intensity')
|
|
379
|
+
self.intensity_plot.setLabel('bottom', 'Z-Slice')
|
|
380
|
+
right_layout.addWidget(self.intensity_plot)
|
|
381
|
+
|
|
382
|
+
# Refresh button for right graph
|
|
383
|
+
self.refresh_intensity_btn = QtWidgets.QPushButton(
|
|
384
|
+
'Refresh Intensity Statistics'
|
|
385
|
+
)
|
|
386
|
+
self.refresh_intensity_btn.clicked.connect(self.update_intensity_statistics)
|
|
387
|
+
right_layout.addWidget(self.refresh_intensity_btn)
|
|
388
|
+
|
|
389
|
+
stats_layout.addWidget(left_widget, 2)
|
|
390
|
+
stats_layout.addWidget(right_widget, 1)
|
|
391
|
+
|
|
392
|
+
self.view_tabs.addTab(stats_widget, 'Statistics')
|
|
393
|
+
|
|
394
|
+
def setup_3d_view_tab(self):
|
|
395
|
+
'''Set up the 3D View tab with enhanced controls.'''
|
|
396
|
+
view_3d_widget = QtWidgets.QWidget()
|
|
397
|
+
layout_3d = QtWidgets.QHBoxLayout()
|
|
398
|
+
view_3d_widget.setLayout(layout_3d)
|
|
399
|
+
|
|
400
|
+
# 3D visualization area
|
|
401
|
+
self.gl_widget = gl.GLViewWidget()
|
|
402
|
+
self.gl_widget.setCameraPosition(distance=180, elevation=0, azimuth=90)
|
|
403
|
+
layout_3d.addWidget(self.gl_widget, stretch=2)
|
|
404
|
+
|
|
405
|
+
# Controls for 3D visualization
|
|
406
|
+
controls_widget = QtWidgets.QWidget()
|
|
407
|
+
controls_layout = QtWidgets.QVBoxLayout()
|
|
408
|
+
controls_widget.setLayout(controls_layout)
|
|
409
|
+
controls_widget.setMaximumWidth(300)
|
|
410
|
+
|
|
411
|
+
# View controls
|
|
412
|
+
view_controls = QtWidgets.QGroupBox('View Controls')
|
|
413
|
+
view_layout = QtWidgets.QVBoxLayout()
|
|
414
|
+
|
|
415
|
+
view_buttons = {
|
|
416
|
+
'Top': (90, 0),
|
|
417
|
+
'Bottom': (-90, 0),
|
|
418
|
+
'Front': (0, 0),
|
|
419
|
+
'Back': (0, 180),
|
|
420
|
+
'Left': (0, 90),
|
|
421
|
+
'Right': (0, -90),
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for label, rotation in view_buttons.items():
|
|
425
|
+
btn = QtWidgets.QPushButton(label)
|
|
426
|
+
btn.clicked.connect(lambda checked, r=rotation: self.set_3d_view(r))
|
|
427
|
+
view_layout.addWidget(btn)
|
|
428
|
+
|
|
429
|
+
view_controls.setLayout(view_layout)
|
|
430
|
+
controls_layout.addWidget(view_controls)
|
|
431
|
+
|
|
432
|
+
# Data type selection
|
|
433
|
+
type_group = QtWidgets.QGroupBox('Display Type')
|
|
434
|
+
type_layout = QtWidgets.QVBoxLayout()
|
|
435
|
+
self.view_3d_type = QtWidgets.QComboBox()
|
|
436
|
+
display_types = [
|
|
437
|
+
{
|
|
438
|
+
'name': 'Mean',
|
|
439
|
+
'value': 'mean',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
'name': 'Median',
|
|
443
|
+
'value': 'median',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
'name': 'Standard Deviation',
|
|
447
|
+
'value': 'std',
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
for t in display_types:
|
|
451
|
+
self.view_3d_type.addItem(t['name'], t['value'])
|
|
452
|
+
self.view_3d_type.currentTextChanged.connect(self.update_3d_view)
|
|
453
|
+
type_layout.addWidget(self.view_3d_type)
|
|
454
|
+
type_group.setLayout(type_layout)
|
|
455
|
+
controls_layout.addWidget(type_group)
|
|
456
|
+
|
|
457
|
+
# Opacity control
|
|
458
|
+
opacity_group = QtWidgets.QGroupBox('Opacity')
|
|
459
|
+
opacity_layout = QtWidgets.QVBoxLayout()
|
|
460
|
+
self.opacity_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal)
|
|
461
|
+
self.opacity_slider.setMinimum(0)
|
|
462
|
+
self.opacity_slider.setMaximum(100)
|
|
463
|
+
self.opacity_slider.setValue(10)
|
|
464
|
+
self.opacity_slider.valueChanged.connect(self.update_3d_view)
|
|
465
|
+
opacity_layout.addWidget(self.opacity_slider)
|
|
466
|
+
opacity_group.setLayout(opacity_layout)
|
|
467
|
+
controls_layout.addWidget(opacity_group)
|
|
468
|
+
|
|
469
|
+
# Colormap preview
|
|
470
|
+
colormap_group = QtWidgets.QGroupBox('Colormap')
|
|
471
|
+
colormap_layout = QtWidgets.QVBoxLayout()
|
|
472
|
+
self.colormap_preview = pg.GradientWidget(orientation='bottom')
|
|
473
|
+
self.colormap_preview.setMinimumHeight(40)
|
|
474
|
+
self.colormap_preview.item.setColorMap(pg.colormap.get('jet', 'matplotlib'))
|
|
475
|
+
colormap_layout.addWidget(self.colormap_preview)
|
|
476
|
+
colormap_group.setLayout(colormap_layout)
|
|
477
|
+
controls_layout.addWidget(colormap_group)
|
|
478
|
+
|
|
479
|
+
# Add axis labels checkbox
|
|
480
|
+
self.show_labels = QtWidgets.QCheckBox('Show Axis Labels')
|
|
481
|
+
self.show_labels.setChecked(True)
|
|
482
|
+
self.show_labels.stateChanged.connect(self.update_3d_view)
|
|
483
|
+
controls_layout.addWidget(self.show_labels)
|
|
484
|
+
|
|
485
|
+
layout_3d.addWidget(controls_widget)
|
|
486
|
+
self.view_tabs.addTab(view_3d_widget, '3D View')
|
|
487
|
+
|
|
488
|
+
def set_3d_view(self, args: tuple[int]):
|
|
489
|
+
'''Set the 3D view to a specific angle.'''
|
|
490
|
+
self.gl_widget.opts['elevation'] = args[0]
|
|
491
|
+
self.gl_widget.opts['azimuth'] = args[1]
|
|
492
|
+
self.gl_widget.opts['distance'] = 180
|
|
493
|
+
self.gl_widget.update()
|
|
494
|
+
|
|
495
|
+
def get_display_type(self) -> str:
|
|
496
|
+
'''Get the display type for the merged view.'''
|
|
497
|
+
if self.display_mean.isChecked():
|
|
498
|
+
return 'mean'
|
|
499
|
+
elif self.display_median.isChecked():
|
|
500
|
+
return 'median'
|
|
501
|
+
elif self.display_std.isChecked():
|
|
502
|
+
return 'std'
|
|
503
|
+
else:
|
|
504
|
+
return 'roi'
|
|
505
|
+
|
|
506
|
+
def update_merged_view(self):
|
|
507
|
+
if self.psf_data is None:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
slice_index = self.slice_slider.value()
|
|
511
|
+
grid_size = self.grid_size.value()
|
|
512
|
+
# get display type (mean, median ... etc)
|
|
513
|
+
display_type = self.get_display_type()
|
|
514
|
+
norm = self.normalization_checkbox.isChecked()
|
|
515
|
+
|
|
516
|
+
if self.view_mode == 'XY':
|
|
517
|
+
roi_index = self.single_roi_slider.value()
|
|
518
|
+
|
|
519
|
+
psf_image, grid_overlay = self.psf_data.get_z_slice(
|
|
520
|
+
slice_index, display_type, roi_index, grid_size, norm
|
|
521
|
+
)
|
|
522
|
+
elif self.view_mode == 'XZ':
|
|
523
|
+
psf_image, grid_overlay = self.psf_data.get_y_slice(
|
|
524
|
+
slice_index, display_type, grid_size
|
|
525
|
+
)
|
|
526
|
+
else: # YZ
|
|
527
|
+
psf_image, grid_overlay = self.psf_data.get_x_slice(
|
|
528
|
+
slice_index, display_type, grid_size
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Update the visualization
|
|
532
|
+
if self.auto_level_checkbox.isChecked():
|
|
533
|
+
self.psf_image_item.setImage(psf_image, autoLevels=True)
|
|
534
|
+
else:
|
|
535
|
+
self.psf_image_item.setImage(psf_image, autoLevels=False)
|
|
536
|
+
|
|
537
|
+
if grid_overlay is not None:
|
|
538
|
+
self.grid_overlay_item.setImage(grid_overlay)
|
|
539
|
+
self.update_grid_opacity()
|
|
540
|
+
else:
|
|
541
|
+
self.grid_overlay_item.clear()
|
|
542
|
+
|
|
543
|
+
# Update metadata
|
|
544
|
+
self.update_metadata()
|
|
545
|
+
|
|
546
|
+
def change_view_mode(self, mode):
|
|
547
|
+
self.view_mode = mode
|
|
548
|
+
self.update_slice_controls()
|
|
549
|
+
self.update_merged_view()
|
|
550
|
+
|
|
551
|
+
def update_metadata(self):
|
|
552
|
+
'''Update the metadata display in the PSF view tab.'''
|
|
553
|
+
if self.psf_data is None:
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
total_rois = sum([z['count'] for z in self.psf_data.zslices])
|
|
557
|
+
max_rois_per_slice = max([z['count'] for z in self.psf_data.zslices])
|
|
558
|
+
min_rois_per_slice = min([z['count'] for z in self.psf_data.zslices])
|
|
559
|
+
metadata = f'''
|
|
560
|
+
Total ROIs: {total_rois}
|
|
561
|
+
Total Z-slices: {len(self.psf_data)}
|
|
562
|
+
Average ROIs per slice: {total_rois / len(self.psf_data):.2f}
|
|
563
|
+
Maximum ROIs in a slice: {max_rois_per_slice}
|
|
564
|
+
Minimum ROIs in a slice: {min_rois_per_slice}
|
|
565
|
+
Shape: {self.psf_data.shape}
|
|
566
|
+
Pixel Size: {self.psf_data.pixel_size}
|
|
567
|
+
Z Step Size: {self.psf_data.z_step} nm
|
|
568
|
+
ROI Size: {self.psf_data.roi_size}
|
|
569
|
+
Upsample: {self.psf_data.upsample}
|
|
570
|
+
Stack: {self.psf_data.path}
|
|
571
|
+
ROI Info: {self.psf_data.roi_info}
|
|
572
|
+
Fit Method: {FittingMethod(self.psf_data.fitting_method)}
|
|
573
|
+
Zero Plane: {self.psf_data.zero_plane}
|
|
574
|
+
'''
|
|
575
|
+
self.metadata_text.setText(metadata)
|
|
576
|
+
|
|
577
|
+
def update_grid_opacity(self):
|
|
578
|
+
'''Update the opacity of the grid overlay.'''
|
|
579
|
+
self.grid_overlay_item.setOpacity(self.grid_opacity.value() / 100)
|
|
580
|
+
|
|
581
|
+
def update_3d_view(self):
|
|
582
|
+
'''Update the 3D visualization with enhanced features.'''
|
|
583
|
+
self.gl_widget.clear()
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
# Add coordinate axes with labels if enabled
|
|
587
|
+
if self.show_labels.isChecked():
|
|
588
|
+
ax = gl.GLAxisItem()
|
|
589
|
+
ax.setSize(x=10, y=10, z=10)
|
|
590
|
+
self.gl_widget.addItem(ax)
|
|
591
|
+
|
|
592
|
+
# Add grid
|
|
593
|
+
g = gl.GLGridItem()
|
|
594
|
+
g.scale(10, 10, 1)
|
|
595
|
+
self.gl_widget.addItem(g)
|
|
596
|
+
|
|
597
|
+
zero = self.psf_data.zero_plane
|
|
598
|
+
|
|
599
|
+
# Prepare volumetric data based on selected type
|
|
600
|
+
view_type = self.view_3d_type.currentData()
|
|
601
|
+
volume_data = self.psf_data.get_volume(view_type)
|
|
602
|
+
|
|
603
|
+
# Normalize data
|
|
604
|
+
if np.all(volume_data == 0):
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
normalized_data = (volume_data - volume_data.min()) / (
|
|
608
|
+
volume_data.max() - volume_data.min()
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
# Create RGBA volume with colormap
|
|
612
|
+
d2 = np.empty(volume_data.shape + (4,), dtype=np.ubyte)
|
|
613
|
+
jet_colors = self.colormap_preview.item.colorMap().getLookupTable(
|
|
614
|
+
0.0, 1.0, 256
|
|
615
|
+
)
|
|
616
|
+
color_indices = (normalized_data * 255).astype(np.uint8)
|
|
617
|
+
|
|
618
|
+
# Set RGB channels using jet colormap
|
|
619
|
+
d2[..., 0] = jet_colors[color_indices, 0] # Red channel
|
|
620
|
+
d2[..., 1] = jet_colors[color_indices, 1] # Green channel
|
|
621
|
+
d2[..., 2] = jet_colors[color_indices, 2] # Blue channel
|
|
622
|
+
|
|
623
|
+
# Set alpha channel based on opacity slider and data intensity
|
|
624
|
+
opacity = self.opacity_slider.value() / 100.0
|
|
625
|
+
d2[..., 3] = (normalized_data * 255 * opacity).astype(np.ubyte)
|
|
626
|
+
|
|
627
|
+
# Set scaling ratios for X and Y axes
|
|
628
|
+
x_ratio = y_ratio = 1 # X and Y are the reference, Z scaled
|
|
629
|
+
# z_ratio
|
|
630
|
+
z_ratio = self.psf_data.get_ratio()
|
|
631
|
+
|
|
632
|
+
# Create and add volume item
|
|
633
|
+
v = gl.GLVolumeItem(d2)
|
|
634
|
+
|
|
635
|
+
v.scale(z_ratio, y_ratio, x_ratio) # Apply scaling in (z, y, x) order
|
|
636
|
+
|
|
637
|
+
v.translate(
|
|
638
|
+
-zero * z_ratio,
|
|
639
|
+
-volume_data.shape[1] // 2 * y_ratio,
|
|
640
|
+
-volume_data.shape[2] // 2 * x_ratio,
|
|
641
|
+
)
|
|
642
|
+
self.gl_widget.addItem(v)
|
|
643
|
+
|
|
644
|
+
# Add tick labels if enabled
|
|
645
|
+
if self.show_labels.isChecked():
|
|
646
|
+
for i in range(volume_data.shape[0]):
|
|
647
|
+
if i % 5 == 0: # Add label every 5 slices
|
|
648
|
+
text = gl.GLTextItem(
|
|
649
|
+
pos=np.array(
|
|
650
|
+
[
|
|
651
|
+
-volume_data.shape[0] // 2,
|
|
652
|
+
-volume_data.shape[1] // 2,
|
|
653
|
+
i - volume_data.shape[2] // 2,
|
|
654
|
+
]
|
|
655
|
+
),
|
|
656
|
+
text=str(i),
|
|
657
|
+
color=(1, 1, 1, 1),
|
|
658
|
+
)
|
|
659
|
+
self.gl_widget.addItem(text)
|
|
660
|
+
|
|
661
|
+
except Exception as e:
|
|
662
|
+
print(f'Error updating 3D view: {str(e)}')
|
|
663
|
+
|
|
664
|
+
def update_range_spinboxes(self):
|
|
665
|
+
mn, mx = self.range_roi.getRegion()
|
|
666
|
+
zero = self.psf_data.zero_plane
|
|
667
|
+
self.z_range_start.setValue(int(mn / self.psf_data.z_step + zero))
|
|
668
|
+
self.z_range_end.setValue(int(mx / self.psf_data.z_step + zero))
|
|
669
|
+
|
|
670
|
+
def update_range_roi(self):
|
|
671
|
+
zero = self.psf_data.zero_plane
|
|
672
|
+
start = (self.z_range_start.value() - zero) * self.psf_data.z_step
|
|
673
|
+
end = (self.z_range_end.value() - zero) * self.psf_data.z_step
|
|
674
|
+
self.range_roi.setRegion((start, end))
|
|
675
|
+
|
|
676
|
+
def restore_zero_plane(self):
|
|
677
|
+
if hasattr(self, 'old_zero_plane'):
|
|
678
|
+
self.psf_data.zero_plane = self.psf_data.old_zero_plane
|
|
679
|
+
self.update_all_views()
|
|
680
|
+
|
|
681
|
+
def adjust_zero_plane(self):
|
|
682
|
+
if self.psf_data is None:
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
method = self.zero_plane_method.currentText()
|
|
686
|
+
selected_stat = self.stat_combo.currentText()
|
|
687
|
+
|
|
688
|
+
self.psf_data.adjust_zero_plane(
|
|
689
|
+
selected_stat, method, self.range_roi.getRegion()
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# Update the plots
|
|
693
|
+
self.update_roi_statistics()
|
|
694
|
+
self.update_intensity_statistics()
|
|
695
|
+
|
|
696
|
+
def fit_stat_curve(self):
|
|
697
|
+
if self.psf_data is None:
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
selected_stat = self.stat_combo.currentText()
|
|
701
|
+
self.fit_result = self.psf_data.get_z_cal(
|
|
702
|
+
selected_stat=selected_stat,
|
|
703
|
+
region=self.range_roi.getRegion(),
|
|
704
|
+
confidence_method=self.confidence_combo.currentData(),
|
|
705
|
+
confidence_level=self.confidence_spinbox.value() / 100,
|
|
706
|
+
method=self.curve_fit_combo.currentData(),
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
self.plot_curve_fit()
|
|
710
|
+
|
|
711
|
+
def plot_curve_fit(self):
|
|
712
|
+
if self.fit_result:
|
|
713
|
+
# Plot the curve fit
|
|
714
|
+
if isinstance(self.fit_result, stats.SlopeResult):
|
|
715
|
+
x_fit = (
|
|
716
|
+
np.arange(len(self.psf_data)) - self.psf_data.zero_plane
|
|
717
|
+
) * self.psf_data.z_step
|
|
718
|
+
y_fit = self.fit_result.slope * x_fit + self.fit_result.intercept
|
|
719
|
+
|
|
720
|
+
y_lower = y_upper = None
|
|
721
|
+
if self.fit_result.slope_ci:
|
|
722
|
+
y_lower = (
|
|
723
|
+
self.fit_result.slope_ci[0] * x_fit + self.fit_result.intercept
|
|
724
|
+
)
|
|
725
|
+
y_upper = (
|
|
726
|
+
self.fit_result.slope_ci[1] * x_fit + self.fit_result.intercept
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
self._add_stat_with_confidence(
|
|
730
|
+
x_fit,
|
|
731
|
+
y_fit,
|
|
732
|
+
y_lower,
|
|
733
|
+
y_upper,
|
|
734
|
+
'Calibration Slope',
|
|
735
|
+
(0, 255, 0, 150),
|
|
736
|
+
)
|
|
737
|
+
elif isinstance(self.fit_result, stats.CurveResult):
|
|
738
|
+
x_fit = (
|
|
739
|
+
np.arange(len(self.psf_data)) - self.psf_data.zero_plane
|
|
740
|
+
) * self.psf_data.z_step
|
|
741
|
+
y_fit = self.fit_result.get_data(x_fit)
|
|
742
|
+
|
|
743
|
+
y_lower = y_upper = None
|
|
744
|
+
|
|
745
|
+
self._add_stat_with_confidence(
|
|
746
|
+
x_fit,
|
|
747
|
+
y_fit,
|
|
748
|
+
y_lower,
|
|
749
|
+
y_upper,
|
|
750
|
+
'Calibration Curve',
|
|
751
|
+
(0, 255, 0, 150),
|
|
752
|
+
)
|
|
753
|
+
elif isinstance(self.fit_result, dict):
|
|
754
|
+
x_fit = self.fit_result['fitted_curves']['z']
|
|
755
|
+
y_fit = [
|
|
756
|
+
self.fit_result['fitted_curves']['sigma_x'],
|
|
757
|
+
self.fit_result['fitted_curves']['sigma_y'],
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
y_lower = y_upper = None
|
|
761
|
+
|
|
762
|
+
self._add_stat_with_confidence(
|
|
763
|
+
x_fit,
|
|
764
|
+
y_fit[0],
|
|
765
|
+
y_lower,
|
|
766
|
+
y_upper,
|
|
767
|
+
'Sigma X Curve',
|
|
768
|
+
(0, 255, 0, 150),
|
|
769
|
+
)
|
|
770
|
+
self._add_stat_with_confidence(
|
|
771
|
+
x_fit,
|
|
772
|
+
y_fit[1],
|
|
773
|
+
y_lower,
|
|
774
|
+
y_upper,
|
|
775
|
+
'Sigma X Curve',
|
|
776
|
+
(0, 255, 255, 150),
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# use tabulate to print out the slope results info in stats text
|
|
780
|
+
# pprint(asdict(self.fit_result))
|
|
781
|
+
|
|
782
|
+
def export_fit_curve(self):
|
|
783
|
+
if self.fit_result is None:
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
if stats.export_fit_curve(
|
|
787
|
+
self.fit_result, self, os.path.dirname(self.psf_data.path)
|
|
788
|
+
):
|
|
789
|
+
print(
|
|
790
|
+
f'Fit curve exported!'
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
def import_fit_curve(self):
|
|
794
|
+
fit_result, _ = stats.import_fit_curve(
|
|
795
|
+
self,
|
|
796
|
+
os.path.dirname(self.psf_data.path),
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
if fit_result:
|
|
800
|
+
self.fit_result = fit_result
|
|
801
|
+
self.range_roi.setRegion(self.fit_result.significant_region)
|
|
802
|
+
self.plot_curve_fit()
|
|
803
|
+
|
|
804
|
+
def update_intensity_statistics(self):
|
|
805
|
+
'''Update intensity statistics plot.'''
|
|
806
|
+
if self.psf_data is None:
|
|
807
|
+
return
|
|
808
|
+
|
|
809
|
+
self.intensity_plot.clear()
|
|
810
|
+
|
|
811
|
+
_indices, _mean, _median, _std = self.psf_data.get_intensity_stats()
|
|
812
|
+
|
|
813
|
+
# Plot intensity statistics
|
|
814
|
+
self.intensity_plot.plot(_indices, _mean, pen='r', name='Mean')
|
|
815
|
+
self.intensity_plot.plot(_indices, _median, pen='g', name='Median')
|
|
816
|
+
self.intensity_plot.plot(_indices, _std, pen='y', name='Std Dev')
|
|
817
|
+
|
|
818
|
+
# Update zero plane line
|
|
819
|
+
zero_line_intensity = pg.InfiniteLine(pos=0, angle=90, pen='w', movable=False)
|
|
820
|
+
self.intensity_plot.addItem(zero_line_intensity)
|
|
821
|
+
|
|
822
|
+
# Add legend
|
|
823
|
+
self.intensity_plot.addLegend()
|
|
824
|
+
|
|
825
|
+
# Set plot title and labels
|
|
826
|
+
self.intensity_plot.setTitle('Intensity Statistics Over Z')
|
|
827
|
+
self.intensity_plot.setLabel('left', 'Intensity')
|
|
828
|
+
self.intensity_plot.setLabel('bottom', 'Z-Slice')
|
|
829
|
+
|
|
830
|
+
def update_all_views(self, extra=True):
|
|
831
|
+
'''Update all views when data changes.'''
|
|
832
|
+
self.update_merged_view()
|
|
833
|
+
self.update_metadata()
|
|
834
|
+
self.update_3d_view()
|
|
835
|
+
if extra:
|
|
836
|
+
self.update_roi_statistics()
|
|
837
|
+
self.update_intensity_statistics()
|
|
838
|
+
|
|
839
|
+
def update_slice_controls(self):
|
|
840
|
+
if self.view_mode == 'XY':
|
|
841
|
+
self.slice_label.setText('Z-Slice:')
|
|
842
|
+
max_slice = len(self.psf_data) - 1
|
|
843
|
+
|
|
844
|
+
# set viewbox aspect ratio
|
|
845
|
+
ratio = 1
|
|
846
|
+
zero = self.psf_data.zero_plane
|
|
847
|
+
else:
|
|
848
|
+
if self.view_mode == 'XZ':
|
|
849
|
+
self.slice_label.setText('Y-Slice:')
|
|
850
|
+
else: # YZ
|
|
851
|
+
self.slice_label.setText('X-Slice:')
|
|
852
|
+
|
|
853
|
+
max_slice = self.psf_data.roi_size - 1
|
|
854
|
+
|
|
855
|
+
# set viewbox aspect ratio
|
|
856
|
+
ratio = self.psf_data.get_ratio()
|
|
857
|
+
zero = max_slice // 2
|
|
858
|
+
|
|
859
|
+
# set viewbox aspect ratio
|
|
860
|
+
self.view_box.setAspectLocked(True, ratio)
|
|
861
|
+
|
|
862
|
+
self.slice_slider.setMaximum(max_slice)
|
|
863
|
+
self.slice_spinbox.setMaximum(max_slice)
|
|
864
|
+
self.zero_btn.setText(f'Zero Plane {zero}')
|
|
865
|
+
self.slice_slider.setValue(zero)
|
|
866
|
+
|
|
867
|
+
def set_psf_data(self, psf_data: 'PSFdata'):
|
|
868
|
+
'''Set new PSF data and update all views.
|
|
869
|
+
|
|
870
|
+
Parameters
|
|
871
|
+
----------
|
|
872
|
+
psf_data : PSFdata
|
|
873
|
+
New PSF data to visualize
|
|
874
|
+
'''
|
|
875
|
+
self.psf_data = psf_data
|
|
876
|
+
|
|
877
|
+
# Update slice controls
|
|
878
|
+
self.update_slice_controls()
|
|
879
|
+
# Update all views
|
|
880
|
+
self.update_all_views()
|
|
881
|
+
|
|
882
|
+
def update_roi_statistics(self):
|
|
883
|
+
'''Update ROI statistics plot with confidence intervals where applicable.'''
|
|
884
|
+
if self.psf_data is None:
|
|
885
|
+
return
|
|
886
|
+
|
|
887
|
+
self.roi_plot.clear()
|
|
888
|
+
selected_stat = self.stat_combo.currentText()
|
|
889
|
+
z_indices, param_stat, param_min, param_max = self.psf_data.get_stats(
|
|
890
|
+
selected_stat,
|
|
891
|
+
self.confidence_combo.currentData(),
|
|
892
|
+
self.confidence_spinbox.value() / 100,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Plot based on statistic type
|
|
896
|
+
if selected_stat == 'Sigma':
|
|
897
|
+
self._plot_sigma_stats(z_indices, param_stat, param_min, param_max)
|
|
898
|
+
else:
|
|
899
|
+
self._plot_single_stat(
|
|
900
|
+
z_indices, param_stat, param_min, param_max, selected_stat
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
self._setup_plot_elements(z_indices, selected_stat)
|
|
904
|
+
|
|
905
|
+
def _plot_sigma_stats(self, z_indices, param_stat, param_min, param_max):
|
|
906
|
+
'''Plot sigma statistics with confidence intervals.'''
|
|
907
|
+
colors = {
|
|
908
|
+
'x': (0, 0, 255, 150),
|
|
909
|
+
'y': (255, 0, 0, 150),
|
|
910
|
+
} # Semi-transparent blue and red
|
|
911
|
+
|
|
912
|
+
if 1 <= len(param_stat) <= 2:
|
|
913
|
+
# Plot X sigma
|
|
914
|
+
self._add_stat_with_confidence(
|
|
915
|
+
z_indices,
|
|
916
|
+
param_stat[0],
|
|
917
|
+
param_min[0],
|
|
918
|
+
param_max[0],
|
|
919
|
+
'Sigma X',
|
|
920
|
+
colors['x'],
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
# Plot Y sigma if available
|
|
924
|
+
if len(param_stat) == 2:
|
|
925
|
+
self._add_stat_with_confidence(
|
|
926
|
+
z_indices,
|
|
927
|
+
param_stat[1],
|
|
928
|
+
param_min[1],
|
|
929
|
+
param_max[1],
|
|
930
|
+
'Sigma Y',
|
|
931
|
+
colors['y'],
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
def _plot_single_stat(self, z_indices, param_stat, param_min, param_max, stat_name):
|
|
935
|
+
'''Plot a single statistic with confidence interval.'''
|
|
936
|
+
self._add_stat_with_confidence(
|
|
937
|
+
z_indices,
|
|
938
|
+
param_stat,
|
|
939
|
+
param_min,
|
|
940
|
+
param_max,
|
|
941
|
+
stat_name,
|
|
942
|
+
(0, 0, 255, 150), # Semi-transparent blue
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
def _add_stat_with_confidence(self, x, y, y_min, y_max, name, color):
|
|
946
|
+
'''Add a statistic line with confidence interval fill.'''
|
|
947
|
+
# Main line
|
|
948
|
+
pen = pg.mkPen(color=color, width=2)
|
|
949
|
+
self.roi_plot.plot(x, y, pen=pen, name=name)
|
|
950
|
+
|
|
951
|
+
# Confidence interval fill
|
|
952
|
+
if y_min is not None and y_max is not None:
|
|
953
|
+
fill = pg.FillBetweenItem(
|
|
954
|
+
pg.PlotCurveItem(x, y_max),
|
|
955
|
+
pg.PlotCurveItem(x, y_min),
|
|
956
|
+
brush=pg.mkBrush((*color[:3], 50)), # More transparent for fill
|
|
957
|
+
)
|
|
958
|
+
self.roi_plot.addItem(fill)
|
|
959
|
+
|
|
960
|
+
def _setup_plot_elements(self, z_indices, selected_stat):
|
|
961
|
+
'''Setup common plot elements.'''
|
|
962
|
+
# Zero plane line
|
|
963
|
+
zero_line_roi = pg.InfiniteLine(pos=0, angle=90, pen='w', movable=False)
|
|
964
|
+
self.roi_plot.addItem(zero_line_roi)
|
|
965
|
+
|
|
966
|
+
# Range ROI setup
|
|
967
|
+
z_range = (0, (len(self.psf_data) - 1))
|
|
968
|
+
self.range_roi.setRegion((z_indices.min(), z_indices.max()))
|
|
969
|
+
|
|
970
|
+
# Update range controls
|
|
971
|
+
self._update_range_controls(z_range)
|
|
972
|
+
|
|
973
|
+
# Add plot elements
|
|
974
|
+
self.roi_plot.addItem(self.range_roi)
|
|
975
|
+
self.roi_plot.addLegend()
|
|
976
|
+
|
|
977
|
+
# Set labels
|
|
978
|
+
self._set_plot_labels(selected_stat)
|
|
979
|
+
|
|
980
|
+
def _update_range_controls(self, z_range):
|
|
981
|
+
'''Update range control values and limits.'''
|
|
982
|
+
self.z_range_start.setRange(*z_range)
|
|
983
|
+
self.z_range_end.setRange(*z_range)
|
|
984
|
+
self.z_range_start.setValue(0)
|
|
985
|
+
self.z_range_end.setValue(z_range[1])
|
|
986
|
+
|
|
987
|
+
def _set_plot_labels(self, selected_stat):
|
|
988
|
+
'''Set plot title and axis labels.'''
|
|
989
|
+
self.roi_plot.setTitle(f'{selected_stat} Over Z')
|
|
990
|
+
self.roi_plot.setLabel('left', selected_stat)
|
|
991
|
+
self.roi_plot.setLabel('bottom', 'Z-Slice')
|
|
992
|
+
|
|
993
|
+
def _export_psf_data(self):
|
|
994
|
+
'''Export PSF data to an HDF5 file.'''
|
|
995
|
+
filename, _ = getSaveFileName(
|
|
996
|
+
self,
|
|
997
|
+
'Save PSF Data',
|
|
998
|
+
os.path.dirname(self.psf_data.path),
|
|
999
|
+
'HDF5 PSF Files (*.psf.h5)',
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
if filename:
|
|
1003
|
+
self.psf_data.save_hdf(filename)
|