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.
Files changed (190) hide show
  1. microEye/__init__.py +47 -0
  2. microEye/_version.py +2 -0
  3. microEye/analysis/__init__.py +1 -0
  4. microEye/analysis/checklist_dialog.py +143 -0
  5. microEye/analysis/cmosMaps.py +228 -0
  6. microEye/analysis/filters/__init__.py +9 -0
  7. microEye/analysis/filters/base.py +21 -0
  8. microEye/analysis/filters/spatial.py +338 -0
  9. microEye/analysis/filters/temporal.py +76 -0
  10. microEye/analysis/fitting/__init__.py +0 -0
  11. microEye/analysis/fitting/fit.py +680 -0
  12. microEye/analysis/fitting/nena.py +375 -0
  13. microEye/analysis/fitting/phasor_fit.py +90 -0
  14. microEye/analysis/fitting/processing.py +317 -0
  15. microEye/analysis/fitting/psf/__init__.py +6 -0
  16. microEye/analysis/fitting/psf/extract.py +1129 -0
  17. microEye/analysis/fitting/psf/rubost_mean.py +150 -0
  18. microEye/analysis/fitting/psf/spline.py +167 -0
  19. microEye/analysis/fitting/psf/stats/__init__.py +12 -0
  20. microEye/analysis/fitting/psf/stats/core.py +295 -0
  21. microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
  22. microEye/analysis/fitting/psf/stats/io.py +104 -0
  23. microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
  24. microEye/analysis/fitting/psf/temp.py +147 -0
  25. microEye/analysis/fitting/psf/test.py +47 -0
  26. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
  27. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
  28. microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
  29. microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
  30. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
  31. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
  32. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
  33. microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
  34. microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
  35. microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
  36. microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
  37. microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
  38. microEye/analysis/fitting/results.py +917 -0
  39. microEye/analysis/fitting/results_stats.py +251 -0
  40. microEye/analysis/fitting/tardis.py +209 -0
  41. microEye/analysis/multi_viewer.py +568 -0
  42. microEye/analysis/processing/__init__.py +1 -0
  43. microEye/analysis/processing/frc.py +120 -0
  44. microEye/analysis/rendering/__init__.py +3 -0
  45. microEye/analysis/rendering/base.py +452 -0
  46. microEye/analysis/rendering/cloud.py +215 -0
  47. microEye/analysis/rendering/core.py +47 -0
  48. microEye/analysis/rendering/volumetric.py +198 -0
  49. microEye/analysis/tools/__init__.py +0 -0
  50. microEye/analysis/tools/kymograms.py +1079 -0
  51. microEye/analysis/tools/roi_selectors.py +556 -0
  52. microEye/analysis/utils/__init__.py +3 -0
  53. microEye/analysis/utils/coordinates.py +18 -0
  54. microEye/analysis/utils/images.py +68 -0
  55. microEye/analysis/utils/windows.py +19 -0
  56. microEye/analysis/viewer/__init__.py +3 -0
  57. microEye/analysis/viewer/image_options_widget.py +615 -0
  58. microEye/analysis/viewer/images.py +1225 -0
  59. microEye/analysis/viewer/layers_widget.py +415 -0
  60. microEye/analysis/viewer/localizations.py +1304 -0
  61. microEye/analysis/viewer/psf.py +1003 -0
  62. microEye/analysis/viewer/volume.py +456 -0
  63. microEye/hardware/__init__.py +3 -0
  64. microEye/hardware/cams/__init__.py +29 -0
  65. microEye/hardware/cams/camera_calibration.py +99 -0
  66. microEye/hardware/cams/camera_list.py +528 -0
  67. microEye/hardware/cams/camera_options.py +694 -0
  68. microEye/hardware/cams/camera_panel.py +941 -0
  69. microEye/hardware/cams/dummy/__init__.py +1 -0
  70. microEye/hardware/cams/dummy/dummy_panel.py +759 -0
  71. microEye/hardware/cams/jobs.py +497 -0
  72. microEye/hardware/cams/line_profiler.py +99 -0
  73. microEye/hardware/cams/linescan/IR_Cam.py +474 -0
  74. microEye/hardware/cams/linescan/__init__.py +1 -0
  75. microEye/hardware/cams/micam.py +524 -0
  76. microEye/hardware/cams/pco/__init__.py +58 -0
  77. microEye/hardware/cams/pco/enums.py +382 -0
  78. microEye/hardware/cams/pco/pco_cam.py +761 -0
  79. microEye/hardware/cams/pco/pco_panel.py +477 -0
  80. microEye/hardware/cams/shortcuts.py +312 -0
  81. microEye/hardware/cams/thorlabs/__init__.py +1 -0
  82. microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
  83. microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
  84. microEye/hardware/cams/ueye/__init__.py +1 -0
  85. microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
  86. microEye/hardware/cams/ueye/ueye_panel.py +861 -0
  87. microEye/hardware/cams/vimba/__init__.py +1 -0
  88. microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
  89. microEye/hardware/cams/vimba/vimba_panel.py +813 -0
  90. microEye/hardware/device.py +60 -0
  91. microEye/hardware/lasers/__init__.py +13 -0
  92. microEye/hardware/lasers/io_matchbox.py +791 -0
  93. microEye/hardware/lasers/io_params.py +85 -0
  94. microEye/hardware/lasers/io_single_laser.py +742 -0
  95. microEye/hardware/lasers/laser_relay.py +594 -0
  96. microEye/hardware/mieye/__init__.py +1 -0
  97. microEye/hardware/mieye/acquisition_manager.py +467 -0
  98. microEye/hardware/mieye/devices_manager.py +533 -0
  99. microEye/hardware/mieye/miEye.py +659 -0
  100. microEye/hardware/misc/__init__.py +0 -0
  101. microEye/hardware/misc/acquisition_view.py +71 -0
  102. microEye/hardware/misc/reglo.py +761 -0
  103. microEye/hardware/misc/temp.py +188 -0
  104. microEye/hardware/port_config.py +59 -0
  105. microEye/hardware/protocols/__init__.py +2 -0
  106. microEye/hardware/protocols/actions.py +402 -0
  107. microEye/hardware/protocols/actions_items.py +703 -0
  108. microEye/hardware/protocols/designer.py +244 -0
  109. microEye/hardware/protocols/scene_manager.py +191 -0
  110. microEye/hardware/protocols/serialization.py +97 -0
  111. microEye/hardware/pycromanager/__init__.py +16 -0
  112. microEye/hardware/pycromanager/core.py +1433 -0
  113. microEye/hardware/pycromanager/devices.py +461 -0
  114. microEye/hardware/pycromanager/enums.py +107 -0
  115. microEye/hardware/pycromanager/headless.py +153 -0
  116. microEye/hardware/pycromanager/utils.py +34 -0
  117. microEye/hardware/pycromanager/widgets/__init__.py +5 -0
  118. microEye/hardware/pycromanager/widgets/bridges.py +407 -0
  119. microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
  120. microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
  121. microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
  122. microEye/hardware/stages/__init__.py +18 -0
  123. microEye/hardware/stages/elliptec/__init__.py +5 -0
  124. microEye/hardware/stages/elliptec/baseDevice.py +314 -0
  125. microEye/hardware/stages/elliptec/device.py +384 -0
  126. microEye/hardware/stages/elliptec/deviceID.py +212 -0
  127. microEye/hardware/stages/elliptec/devicePort.py +379 -0
  128. microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
  129. microEye/hardware/stages/elliptec/devicesView.py +706 -0
  130. microEye/hardware/stages/elliptec/ellDevices.py +134 -0
  131. microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
  132. microEye/hardware/stages/elliptec/motorInfo.py +153 -0
  133. microEye/hardware/stages/elliptec/stage.py +62 -0
  134. microEye/hardware/stages/elliptec/test.py +139 -0
  135. microEye/hardware/stages/kinesis/__init__.py +1 -0
  136. microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
  137. microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
  138. microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
  139. microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
  140. microEye/hardware/stages/kinesis/kinesis.py +776 -0
  141. microEye/hardware/stages/piezo_concept.py +607 -0
  142. microEye/hardware/stages/stabilizer.py +785 -0
  143. microEye/hardware/stages/stage.py +89 -0
  144. microEye/hardware/widgets/__init__.py +10 -0
  145. microEye/hardware/widgets/controller.py +246 -0
  146. microEye/hardware/widgets/devices.py +133 -0
  147. microEye/hardware/widgets/focusWidget.py +264 -0
  148. microEye/hardware/widgets/qlist_slider.py +113 -0
  149. microEye/hardware/widgets/scan_acquisition.py +424 -0
  150. microEye/icons/1024.png +0 -0
  151. microEye/icons/128.png +0 -0
  152. microEye/icons/16.png +0 -0
  153. microEye/icons/24.png +0 -0
  154. microEye/icons/256.png +0 -0
  155. microEye/icons/32.png +0 -0
  156. microEye/icons/48.png +0 -0
  157. microEye/icons/512.png +0 -0
  158. microEye/icons/64.png +0 -0
  159. microEye/icons/__init__.py +0 -0
  160. microEye/icons/close.svg +88 -0
  161. microEye/icons/mieye.png +0 -0
  162. microEye/icons/min.svg +83 -0
  163. microEye/icons/viewer.png +0 -0
  164. microEye/launcher.py +42 -0
  165. microEye/qt.py +181 -0
  166. microEye/utils/__init__.py +2 -0
  167. microEye/utils/enum_encoder.py +10 -0
  168. microEye/utils/expandable_groupbox.py +93 -0
  169. microEye/utils/gui_helper.py +457 -0
  170. microEye/utils/hid/__init__.py +8 -0
  171. microEye/utils/hid/controller.py +153 -0
  172. microEye/utils/hid/device.py +63 -0
  173. microEye/utils/hid/enums.py +140 -0
  174. microEye/utils/hid/utils.py +58 -0
  175. microEye/utils/labelled_slider.py +134 -0
  176. microEye/utils/metadata.py +599 -0
  177. microEye/utils/metadata_tree.py +718 -0
  178. microEye/utils/micro_launcher.py +245 -0
  179. microEye/utils/parameter_tree.py +325 -0
  180. microEye/utils/pyscripting.py +444 -0
  181. microEye/utils/retry_exec.py +35 -0
  182. microEye/utils/start_gui.py +112 -0
  183. microEye/utils/thread_worker.py +115 -0
  184. microEye/utils/uImage.py +1512 -0
  185. microeye-2.3.2.dist-info/METADATA +401 -0
  186. microeye-2.3.2.dist-info/RECORD +190 -0
  187. microeye-2.3.2.dist-info/WHEEL +5 -0
  188. microeye-2.3.2.dist-info/entry_points.txt +2 -0
  189. microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
  190. 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)