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,1304 @@
1
+ import os
2
+ import re
3
+ import traceback
4
+
5
+ import cv2
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pyqtgraph as pg
9
+ import tifffile as tf
10
+
11
+ from microEye.analysis.checklist_dialog import ChecklistDialog
12
+ from microEye.analysis.fitting.nena import NeNA_Widget
13
+ from microEye.analysis.fitting.processing import plot_animation_stats, plot_drift
14
+ from microEye.analysis.fitting.psf import stats
15
+ from microEye.analysis.fitting.results import (
16
+ UNIQUE_COLUMNS,
17
+ DataColumns,
18
+ FittingResults,
19
+ )
20
+ from microEye.analysis.fitting.results_stats import resultsStatsWidget
21
+ from microEye.analysis.fitting.tardis import TARDIS_Widget
22
+ from microEye.analysis.processing import FRC_resolution_binomial, plot_frc
23
+ from microEye.analysis.rendering import *
24
+ from microEye.analysis.viewer.layers_widget import ImageItemsWidget
25
+ from microEye.analysis.viewer.volume import PointCloudViewer, VolumeViewerWindow
26
+ from microEye.qt import Qt, QtCore, QtGui, QtWidgets, getOpenFileName, getSaveFileName
27
+ from microEye.utils.gui_helper import *
28
+ from microEye.utils.thread_worker import QThreadWorker
29
+
30
+
31
+ class LocalizationsView(QtWidgets.QWidget):
32
+ '''
33
+ A class for viewing and interacting with SMLM localizations in a PyQt5 application.
34
+ '''
35
+
36
+ __INSTANCES = 0
37
+
38
+ def __init__(self, fittingResults: FittingResults = None):
39
+ '''Initialize the StackView.
40
+
41
+ Parameters
42
+ ----------
43
+ path : str
44
+ The path to the image stack.
45
+ fittingResults : FittingResults, optional
46
+ Fitting results, by default None.
47
+ '''
48
+ super().__init__()
49
+
50
+ LocalizationsView.__INSTANCES += 1
51
+ self.setWindowTitle(f'Loc Viewer {LocalizationsView.__INSTANCES:04d}')
52
+
53
+ self._threadpool = QtCore.QThreadPool.globalInstance()
54
+ # Initialize variables
55
+ self.fittingResults: list[FittingResults] = []
56
+
57
+ self.main_layout = QtWidgets.QHBoxLayout()
58
+ self.setLayout(self.main_layout)
59
+
60
+ # Graphics layout
61
+ self.init_graphics()
62
+
63
+ # Tab Widget
64
+ self.tab_widget = QtWidgets.QTabWidget()
65
+
66
+ self.main_layout.addWidget(self.tab_widget, 1)
67
+
68
+ # Localization / Render tab layout
69
+ self.setup_localization_tab()
70
+
71
+ # Results stats tab layout
72
+ self.setup_data_filters_tab()
73
+
74
+ # Layers tab
75
+ self.setup_layers_tab()
76
+
77
+ if fittingResults:
78
+ self.add_data_layer(fittingResults)
79
+
80
+ @property
81
+ def currentResults(self) -> FittingResults:
82
+ '''Get the current fitting results.'''
83
+ if self.image_layers.currentIndex < 0:
84
+ return None
85
+
86
+ return self.fittingResults[self.image_layers.currentIndex]
87
+
88
+ @currentResults.setter
89
+ def currentResults(self, value: FittingResults):
90
+ '''Set the current fitting results.'''
91
+ if self.image_layers.currentIndex < 0:
92
+ return
93
+
94
+ value.colormap = self.currentResults.colormap
95
+ value.intensity_range = self.currentResults.intensity_range
96
+
97
+ self.fittingResults[self.image_layers.currentIndex] = value
98
+
99
+ def setImage(self, image: np.ndarray, autoLevels: bool = True, **kwargs):
100
+ '''Set the image data.'''
101
+ if image is None or self.image_layers.currentLayer is None:
102
+ return
103
+
104
+ index = kwargs.get('index', self.image_layers.currentIndex)
105
+
106
+ colorMap = self.fittingResults[index].colormap
107
+ if colorMap is not None:
108
+ kwargs['colorMap'] = colorMap
109
+
110
+ self.image_layers.getImageItemAt(index).setImage(
111
+ image, autoLevels=autoLevels, **kwargs
112
+ )
113
+
114
+ def init_graphics(self):
115
+ '''Initialize the graphics layout for the main window.'''
116
+ # A plot area (ViewBox + axes) for displaying the image
117
+ self.image_widget = pg.GraphicsLayoutWidget()
118
+ self.image_widget.setMinimumWidth(600)
119
+
120
+ # Create the ViewBox
121
+ self.view_box: pg.ViewBox = self.image_widget.addViewBox(row=0, col=0)
122
+
123
+ self.view_box.setAspectLocked(True)
124
+ self.view_box.setAutoVisible(True)
125
+ self.view_box.enableAutoRange()
126
+ self.view_box.invertY(True)
127
+
128
+ self.empty_image = np.zeros((128, 128), dtype=np.uint8)
129
+ self.empty_alpha = np.zeros((128, 128, 4), dtype=np.uint8)
130
+ self.empty_image[0, 0] = 255
131
+
132
+ # histogram item
133
+ self.histogram_item = pg.HistogramLUTItem(
134
+ gradientPosition='bottom', orientation='horizontal'
135
+ )
136
+
137
+ self.image_widget.addItem(self.histogram_item, row=1, col=0)
138
+
139
+ # Add ROI
140
+ self.roi = pg.RectROI(
141
+ [-8, 14], [6, 5], scaleSnap=True, translateSnap=True, movable=False
142
+ )
143
+ self.roi.addTranslateHandle([0, 0], [0.5, 0.5])
144
+ self.view_box.addItem(self.roi)
145
+ self.roi.setZValue(1000)
146
+ self.roi.setVisible(False)
147
+
148
+ # Add LinePlotItem for trajectories
149
+ self.trajectory_item = pg.PlotDataItem(pen=pg.mkPen(color='r', width=2))
150
+ self.view_box.addItem(self.trajectory_item)
151
+ self.trajectory_item.setZValue(999) # Ensure lines are on top of image
152
+
153
+ # Add the two sub-main layouts
154
+ self.main_layout.addWidget(self.image_widget, 4)
155
+
156
+ def add_data_layer(self, data: FittingResults):
157
+ if data is None:
158
+ return
159
+
160
+ self.fittingResults.append(data)
161
+
162
+ self.updateStatsLabel()
163
+
164
+ count = self.image_layers.count()
165
+ # Get the last part of the path
166
+ path = (
167
+ data.path
168
+ if len(data.path.split('/')) < 3
169
+ else '/'.join(data.path.split('/')[-2:])
170
+ )
171
+
172
+ image_item = self.add_image_item(
173
+ self.empty_image if count < 1 else self.empty_alpha,
174
+ compMode='SourceOver' if count < 1 else 'Plus',
175
+ name=path,
176
+ )
177
+
178
+ self.render_loc()
179
+
180
+ def add_image_item(
181
+ self,
182
+ image: np.ndarray,
183
+ opacity: float = 1.0,
184
+ compMode='SourceOver',
185
+ name='Layer',
186
+ ):
187
+ """Add an image item to the view.
188
+
189
+ Parameters
190
+ ----------
191
+ image : np.ndarray
192
+ The image data.
193
+ opacity : float, optional
194
+ The opacity of the image, by default 1.0.
195
+ compMode : str, optional
196
+ The composition mode, by default 'SourceOver'.
197
+ name : str, optional
198
+ The name of the image item layer, by default 'Layer'.
199
+
200
+ Returns
201
+ -------
202
+ pg.ImageItem
203
+ The added image item.
204
+ """
205
+ # Create the ImageItem and set its view to self.view_box
206
+ image_item = self.image_layers.add_layer(image, opacity, compMode, name=name)
207
+
208
+ # Add the ImageItem to the ViewBox
209
+ self.view_box.addItem(image_item)
210
+
211
+ return image_item
212
+
213
+ def setup_layers_tab(self):
214
+ '''Set up the Layers tab.'''
215
+ # Layers tab
216
+ self.image_layers = ImageItemsWidget()
217
+
218
+ self.image_layers.layerChanged.connect(self.update_layer)
219
+ self.image_layers.layerRemoved.connect(
220
+ lambda index: self.fittingResults.pop(index)
221
+ )
222
+
223
+ self.tab_widget.addTab(self.image_layers, 'Layers')
224
+
225
+ def update_layer(self, current: int, previous: int, imageItem: pg.ImageItem):
226
+ '''Update the layer settings.
227
+
228
+ Parameters
229
+ ----------
230
+ current : int
231
+ The current layer index.
232
+ previous : int
233
+ The previous layer index.
234
+ imageItem : pg.ImageItem
235
+ The image item.
236
+ '''
237
+ if current == -1 or imageItem is None:
238
+ return
239
+
240
+ if previous >= 0 and previous < len(self.fittingResults):
241
+ self.fittingResults[
242
+ previous
243
+ ].colormap = self.histogram_item.gradient.colorMap()
244
+ self.fittingResults[
245
+ previous
246
+ ].intensity_range = self.histogram_item.getLevels()
247
+
248
+ self.histogram_item.setImageItem(imageItem)
249
+
250
+ if self.currentResults.colormap is not None:
251
+ self.histogram_item.gradient.setColorMap(self.currentResults.colormap)
252
+ self.histogram_item.setLevels(*self.currentResults.intensity_range)
253
+
254
+ self.results_plot.clear()
255
+
256
+ def update_position(self):
257
+ '''Update the position label.'''
258
+ if self.projection_cbox.currentData() in [3, 4, 5]:
259
+ self.position_label.setVisible(True)
260
+ self.position.setVisible(True)
261
+
262
+ projection = self.projection_cbox.currentData() - 3
263
+ if projection == 0:
264
+ column = DataColumns.Z
265
+ elif projection == 1:
266
+ column = DataColumns.Y
267
+ else:
268
+ column = DataColumns.X
269
+
270
+ self.position_label.setText('Z Position [nm]:')
271
+ self.position.setRange(
272
+ self.currentResults.data[column].min(),
273
+ self.currentResults.data[column].max(),
274
+ )
275
+ self.position.setValue(self.position.minimum())
276
+ else:
277
+ self.position_label.setVisible(False)
278
+ self.position.setVisible(False)
279
+
280
+ def render_loc(self):
281
+ '''Update the rendered super-res image.
282
+
283
+ Returns
284
+ -------
285
+ ndarray or None
286
+ Rendered super-res image or None.
287
+ '''
288
+ for idx, localizations in enumerate(self.fittingResults):
289
+ if localizations is None:
290
+ continue
291
+
292
+ if len(localizations) <= 0:
293
+ continue
294
+
295
+ render_idx = self.render_cbox.currentData()
296
+ projection = self.projection_cbox.currentData()
297
+
298
+ renderClass = BaseRenderer(
299
+ self.xy_binsize.value(),
300
+ self.z_binsize.value(),
301
+ RenderModes(render_idx),
302
+ )
303
+ if 0 <= projection < 3:
304
+ img = renderClass.render(
305
+ Projection(projection),
306
+ localizations.data[DataColumns.X],
307
+ localizations.data[DataColumns.Y],
308
+ localizations.data[DataColumns.Z],
309
+ localizations.data[DataColumns.INTENSITY],
310
+ )
311
+
312
+ self.setImage(
313
+ img, index=idx, autoLevels=self.auto_level.isChecked()
314
+ )
315
+
316
+ if projection == 0:
317
+ self.view_box.setAspectLocked(True, 1)
318
+ else:
319
+ self.view_box.setAspectLocked(
320
+ True, self.xy_binsize.value() / self.z_binsize.value()
321
+ )
322
+ elif 3 <= projection < 6:
323
+ projection -= 3
324
+
325
+ if projection == 0:
326
+ self.view_box.setAspectLocked(True, 1)
327
+ width = self.z_binsize.value()
328
+ else:
329
+ self.view_box.setAspectLocked(
330
+ True, self.xy_binsize.value() / self.z_binsize.value()
331
+ )
332
+ width = self.xy_binsize.value()
333
+
334
+ img = renderClass.render_slice(
335
+ Projection(projection),
336
+ localizations.data[DataColumns.X],
337
+ localizations.data[DataColumns.Y],
338
+ localizations.data[DataColumns.Z],
339
+ localizations.data[DataColumns.INTENSITY],
340
+ self.position.value(),
341
+ width,
342
+ )
343
+
344
+ self.setImage(
345
+ img, index=idx, autoLevels=self.auto_level.isChecked()
346
+ )
347
+
348
+ def extract_z(self):
349
+ '''Extract Z values from the fitting results.'''
350
+ if self.currentResults is None:
351
+ return None
352
+ if len(self.currentResults) <= 0:
353
+ return
354
+
355
+ if self.cal_curve_cbox.currentData() == stats.CurveFitMethod.LINEAR:
356
+ self.currentResults.data[DataColumns.Z] = (
357
+ (
358
+ self.currentResults.data[DataColumns.X_SIGMA]
359
+ / self.currentResults.data[DataColumns.Y_SIGMA]
360
+ )
361
+ - self.z_cal_intercept.value()
362
+ ) / self.z_cal_slope.value()
363
+ self.results_plot.setData(self.currentResults.dataFrame())
364
+ elif self.cal_curve_cbox.currentData() == stats.CurveFitMethod.CSPLINE:
365
+ if self.z_cal_curve:
366
+ self.currentResults.data[DataColumns.Z] = (
367
+ stats.CurveAnalyzer.get_z_from_y_array_optimized(
368
+ self.currentResults.data[DataColumns.X_SIGMA]
369
+ / self.currentResults.data[DataColumns.Y_SIGMA],
370
+ self.z_cal_curve,
371
+ region=[-450, 450],
372
+ )
373
+ )
374
+ self.results_plot.setData(self.currentResults.dataFrame())
375
+
376
+ def _load_calibration(self):
377
+ '''Load a calibration file.'''
378
+ self.z_cal_curve, path = stats.import_fit_curve(
379
+ self,
380
+ os.path.dirname(self.currentResults.path),
381
+ )
382
+ if self.z_cal_curve is not None:
383
+ self.cal_file_path.setText(path)
384
+ if self.z_cal_curve.method == stats.CurveFitMethod.LINEAR:
385
+ self.cal_file_path.setText(path)
386
+ self.cal_curve_cbox.setCurrentIndex(0)
387
+ self.z_cal_slope.setValue(self.z_cal_curve.slope)
388
+ self.z_cal_intercept.setValue(self.z_cal_curve.intercept)
389
+ elif self.z_cal_curve.method == stats.CurveFitMethod.CSPLINE:
390
+ self.cal_file_path.setText(path)
391
+ self.cal_curve_cbox.setCurrentIndex(1)
392
+
393
+ def _plot_calibration(self):
394
+ '''Plot the calibration curve.'''
395
+ if self.z_cal_curve is not None:
396
+ plt = pg.plot()
397
+ plt.showGrid(x=True, y=True)
398
+ legend = plt.addLegend()
399
+ legend.anchor(itemPos=(1, 0), parentPos=(1, 0))
400
+ plt.setWindowTitle('Z Calibration')
401
+ plt.setLabel('left', 'Sigma Ratio (X/Y)')
402
+ plt.setLabel('bottom', 'Z [nm]')
403
+
404
+ # Zero plane line
405
+ plt.plotItem.addLine(x=0, pen='w')
406
+
407
+ if self.z_cal_curve.method == stats.CurveFitMethod.LINEAR:
408
+ pen = pg.mkPen(color=(0, 0, 255, 150), width=2)
409
+ plt.plot(
410
+ self.z_cal_curve.data['z'],
411
+ self.z_cal_curve.data['ratio'],
412
+ pen=pen,
413
+ name='data',
414
+ )
415
+ pen = pg.mkPen(color=(0, 255, 0, 150), width=2)
416
+ plt.plot(
417
+ self.z_cal_curve.data['z'],
418
+ np.array(self.z_cal_curve.data['z']) * self.z_cal_curve.slope
419
+ + self.z_cal_curve.intercept,
420
+ pen=pen,
421
+ name='fit',
422
+ )
423
+
424
+ # Confidence interval fill
425
+ fill = pg.FillBetweenItem(
426
+ pg.PlotCurveItem(
427
+ self.z_cal_curve.data['z'],
428
+ self.z_cal_curve.data['lower_bounds'],
429
+ ),
430
+ pg.PlotCurveItem(
431
+ self.z_cal_curve.data['z'],
432
+ self.z_cal_curve.data['upper_bounds'],
433
+ ),
434
+ brush=pg.mkBrush((0, 255, 0, 50)), # More transparent for fill
435
+ )
436
+ plt.addItem(fill)
437
+
438
+ plt.show()
439
+ else:
440
+ pen = pg.mkPen(color=(0, 0, 255, 150), width=2)
441
+ plt.plot(
442
+ self.z_cal_curve.parameters['x_data'],
443
+ self.z_cal_curve.parameters['y_data'],
444
+ pen=pen,
445
+ name='data',
446
+ )
447
+ pen = pg.mkPen(color=(0, 255, 0, 150), width=2)
448
+ plt.plot(
449
+ self.z_cal_curve.parameters['x_data'],
450
+ self.z_cal_curve.get_data(self.z_cal_curve.parameters['x_data']),
451
+ pen=pen,
452
+ name='fit',
453
+ )
454
+
455
+ plt.show()
456
+
457
+ def render_3D(self):
458
+ '''
459
+ Render a 3D view of the super-res image.
460
+ '''
461
+ if (
462
+ self.currentResults is None
463
+ or self.currentResults.data[DataColumns.Z] is None
464
+ ):
465
+ return None
466
+
467
+ if len(self.currentResults) <= 0:
468
+ return
469
+
470
+ renderClass = PointCloudRenderer(
471
+ self.xy_binsize.value(), self.z_binsize.value()
472
+ )
473
+ # Render point cloud
474
+ points, intensities, metadata = renderClass.render(
475
+ self.currentResults.data[DataColumns.X],
476
+ self.currentResults.data[DataColumns.Y],
477
+ self.currentResults.data[DataColumns.Z],
478
+ self.currentResults.data[DataColumns.INTENSITY],
479
+ )
480
+ volume_viewer = PointCloudViewer(points, intensities, metadata)
481
+
482
+ def toAnimation(self):
483
+ '''
484
+ Render an animation of the super-res image.
485
+ '''
486
+ if self.currentResults is None:
487
+ return
488
+
489
+ if len(self.currentResults) <= 0:
490
+ return
491
+
492
+ filename = self.currentResults.path
493
+
494
+ if filename is None:
495
+ return
496
+
497
+ def work_func(**kwargs):
498
+ try:
499
+ return self.currentResults.toAnimation(
500
+ self.frame_bins.value(), self.xy_binsize.value(),
501
+ z_pixel_size=self.z_binsize.value(),
502
+ projection=Projection(self.projection_cbox.currentData()),
503
+ renderMode=RenderModes(self.render_cbox.currentData())
504
+ )
505
+ except Exception:
506
+ traceback.print_exc()
507
+ return None
508
+
509
+ def done(results):
510
+ self.animation_btn.setDisabled(False)
511
+ # results might be tuple of three nones
512
+ if results is not None and all([r is not None for r in results]):
513
+ stack, frame_bins, locs_per_bin = results
514
+
515
+ # save stack to tiff to the same directory as the fitting results
516
+ tf.imwrite(
517
+ re.sub(r'(\.h5|\.tsv)$', '_animation.tif', filename),
518
+ stack,
519
+ photometric='minisblack',
520
+ bigtiff=True,
521
+ ome=False,
522
+ )
523
+
524
+ plot_animation_stats(frame_bins, locs_per_bin)
525
+
526
+ self.worker = QThreadWorker(work_func)
527
+ self.worker.signals.result.connect(done)
528
+ # Execute
529
+ self.animation_btn.setDisabled(True)
530
+ self._threadpool.start(self.worker)
531
+
532
+ self.currentResults.toAnimation()
533
+
534
+ def render_tracks(self):
535
+ # self.trajectory_item.setData(
536
+ # x=np.linspace(0, 10, 100),
537
+ # y=np.sin(np.linspace(0, 10, 100))
538
+ # )
539
+ pass
540
+
541
+ def setup_localization_tab(self):
542
+ '''Set up the Localization tab.'''
543
+ # Render tab layout
544
+ self.localization_widget, self.localization_form = create_widget(
545
+ QtWidgets.QVBoxLayout
546
+ )
547
+ self.tab_widget.addTab(self.localization_widget, 'Localization')
548
+
549
+ # Render layout
550
+
551
+ self.render_cbox = QtWidgets.QComboBox()
552
+ self.render_cbox.addItem('2D Histogram (Intensity)', 0)
553
+ self.render_cbox.addItem('2D Histogram (Events)', 1)
554
+ self.render_cbox.addItem('2D Gaussian Histogram', 2)
555
+
556
+ self.projection_cbox = QtWidgets.QComboBox()
557
+ self.projection_cbox.addItem('XY', 0)
558
+ self.projection_cbox.addItem('XY (slice)', 3)
559
+ self.projection_cbox.addItem('XZ', 1)
560
+ self.projection_cbox.addItem('XZ (slice)', 4)
561
+ self.projection_cbox.addItem('YZ', 2)
562
+ self.projection_cbox.addItem('YZ (slice)', 5)
563
+
564
+ self.projection_cbox.currentIndexChanged.connect(lambda: self.update_position())
565
+
566
+ self.auto_level = QtWidgets.QCheckBox('Auto-stretch')
567
+ self.auto_level.setChecked(True)
568
+
569
+ self.frame_bins = create_spin_box(min_value=2, max_value=1000, initial_value=10)
570
+ self.xy_binsize = create_spin_box(min_value=0, max_value=250, initial_value=10)
571
+ self.z_binsize = create_spin_box(min_value=0, max_value=250, initial_value=50)
572
+
573
+ self.position_label = QtWidgets.QLabel('Position [nm]:')
574
+ self.position = create_double_spin_box(
575
+ min_value=0, max_value=200, single_step=10, decimals=6, initial_value=0
576
+ )
577
+ self.position_label.setVisible(False)
578
+ self.position.setVisible(False)
579
+ self.position.valueChanged.connect(lambda: self.render_loc())
580
+
581
+ self.refresh_btn = QtWidgets.QPushButton(
582
+ 'Refresh 2D View', clicked=lambda: self.render_loc()
583
+ )
584
+
585
+ self.display_3d_btn = QtWidgets.QPushButton(
586
+ '3D View', clicked=lambda: self.render_3D()
587
+ )
588
+
589
+ self.animation_btn = QtWidgets.QPushButton(
590
+ 'Animation', clicked=lambda: self.toAnimation()
591
+ )
592
+
593
+ # Render GroupBox
594
+ localization = QtWidgets.QGroupBox('Render')
595
+ flocalization = QtWidgets.QFormLayout()
596
+ localization.setLayout(flocalization)
597
+
598
+ flocalization.addRow('Rendering Method:', self.render_cbox)
599
+ flocalization.addRow('Projection:', self.projection_cbox)
600
+ flocalization.addRow('Frame bins:', self.frame_bins)
601
+ flocalization.addRow('XY binsize [nm]:', self.xy_binsize)
602
+ flocalization.addRow('Z binsize [nm]:', self.z_binsize)
603
+ flocalization.addRow(self.position_label, self.position)
604
+ flocalization.addRow(
605
+ create_hbox_layout(
606
+ self.auto_level,
607
+ self.refresh_btn,
608
+ self.display_3d_btn,
609
+ self.animation_btn,
610
+ )
611
+ )
612
+ # End Localization GroupBox
613
+
614
+ # Z Extraction
615
+ self.z_cal_curve = None
616
+ z_extract = QtWidgets.QGroupBox('Z Extraction')
617
+ z_extract_form = QtWidgets.QFormLayout()
618
+ z_extract.setLayout(z_extract_form)
619
+
620
+ self.cal_curve_cbox = QtWidgets.QComboBox()
621
+ for c in [stats.CurveFitMethod.LINEAR, stats.CurveFitMethod.CSPLINE]:
622
+ self.cal_curve_cbox.addItem(c.name, c)
623
+ self.cal_curve_cbox.currentIndexChanged.connect(
624
+ lambda: [
625
+ z_extract_form.setRowVisible(
626
+ idx, self.cal_curve_cbox.currentIndex() == 0
627
+ )
628
+ for idx in [1, 2]
629
+ ]
630
+ )
631
+
632
+ self.z_cal_slope = create_double_spin_box(
633
+ min_value=0, max_value=200, decimals=8, initial_value=0.000800867
634
+ )
635
+ self.z_cal_intercept = create_double_spin_box(
636
+ min_value=0, max_value=200, decimals=8, initial_value=1.00927
637
+ )
638
+ self.load_cal_btn = QtWidgets.QPushButton(
639
+ 'Load Z Calibration', clicked=lambda: self._load_calibration()
640
+ )
641
+ self.plot_cal_btn = QtWidgets.QPushButton(
642
+ 'Calibration Curve', clicked=lambda: self._plot_calibration()
643
+ )
644
+ # display loaded file path in multiline readonly edit box
645
+ self.cal_file_path = QtWidgets.QTextEdit()
646
+ self.cal_file_path.setReadOnly(True)
647
+ self.cal_file_path.setFixedHeight(60)
648
+ self.cal_file_path.setLineWrapMode(QtWidgets.QTextEdit.LineWrapMode.WidgetWidth)
649
+ self.cal_file_path.setPlaceholderText('No file loaded')
650
+
651
+ self.extract_z_btn = QtWidgets.QPushButton(
652
+ 'Extract Z', clicked=lambda: self.extract_z()
653
+ )
654
+ z_extract_form.addRow(
655
+ 'Calibration Method:',
656
+ self.cal_curve_cbox,
657
+ )
658
+ z_extract_form.addRow(
659
+ 'Z Calibration (slope):',
660
+ self.z_cal_slope,
661
+ )
662
+ z_extract_form.addRow(
663
+ 'Z Calibration (intercept):',
664
+ self.z_cal_intercept,
665
+ )
666
+ z_extract_form.addRow(
667
+ create_hbox_layout(self.extract_z_btn, self.load_cal_btn, self.plot_cal_btn)
668
+ )
669
+ z_extract_form.addRow(self.cal_file_path)
670
+
671
+ # End Z Extraction
672
+
673
+ self.drift_cross_args = QtWidgets.QHBoxLayout()
674
+
675
+ self.drift_cross_bins = create_spin_box(initial_value=10)
676
+ self.drift_cross_px = create_spin_box(initial_value=10)
677
+ self.drift_cross_up = create_spin_box(
678
+ min_value=0, max_value=1000, initial_value=100
679
+ )
680
+
681
+ self.drift_cross_args.addWidget(self.drift_cross_bins)
682
+ self.drift_cross_args.addWidget(self.drift_cross_px)
683
+ self.drift_cross_args.addWidget(self.drift_cross_up)
684
+
685
+ self.drift_cross_btn = QtWidgets.QPushButton(
686
+ 'Drift cross-correlation', clicked=lambda: self.drift_cross()
687
+ )
688
+ self.drift_fdm_btn = QtWidgets.QPushButton(
689
+ 'Fiducial marker drift correction', clicked=lambda: self.drift_fdm()
690
+ )
691
+
692
+ # Drift GroupBox
693
+ drift = QtWidgets.QGroupBox('Drift Correction')
694
+ fdrift = QtWidgets.QFormLayout()
695
+ drift.setLayout(fdrift)
696
+
697
+ fdrift.addRow(QtWidgets.QLabel('Drift X-Corr. (bins, pixelSize, upsampling):'))
698
+ fdrift.addRow(self.drift_cross_args)
699
+ fdrift.addRow(create_hbox_layout(self.drift_cross_btn, self.drift_fdm_btn))
700
+ # End Drift GroupBox
701
+
702
+ self.frc_cbox = QtWidgets.QComboBox()
703
+ self.frc_cbox.addItem('Binomial')
704
+ self.frc_cbox.addItem('Odd/Even')
705
+ self.frc_cbox.addItem('Halves')
706
+
707
+ precision_btns = QtWidgets.QHBoxLayout()
708
+ self.frc_res_btn = QtWidgets.QPushButton(
709
+ 'FRC Resolution', clicked=lambda: self.FRC_estimate()
710
+ )
711
+
712
+ self.NeNA_widget = None
713
+ self.NeNA_btn = QtWidgets.QPushButton(
714
+ 'NeNA Loc. Prec. Estimate', clicked=lambda: self.NeNA_estimate()
715
+ )
716
+ self.tardis_btn = QtWidgets.QPushButton(
717
+ 'TARDIS', clicked=lambda: self.TARDIS_analysis()
718
+ )
719
+
720
+ # Precision GroupBox
721
+ precision = QtWidgets.QGroupBox('Loc. Precision')
722
+ fprecision = QtWidgets.QFormLayout()
723
+ precision.setLayout(fprecision)
724
+
725
+ fprecision.addRow(QtWidgets.QLabel('FRC Method:'), self.frc_cbox)
726
+ precision_btns.addWidget(self.frc_res_btn)
727
+ precision_btns.addWidget(self.NeNA_btn)
728
+ precision_btns.addWidget(self.tardis_btn)
729
+ fprecision.addRow(precision_btns)
730
+ # End Precision GroupBox
731
+
732
+ self.nneigh_merge_args = QtWidgets.QHBoxLayout()
733
+
734
+ self.nn_neighbors = create_spin_box(max_value=20000, initial_value=1)
735
+ self.nn_min_distance = create_double_spin_box(max_value=20000, initial_value=0)
736
+ self.nn_max_distance = create_double_spin_box(max_value=20000, initial_value=30)
737
+ self.nn_max_off = create_spin_box(max_value=20000, initial_value=1)
738
+ self.nn_max_length = create_spin_box(max_value=20000, initial_value=500)
739
+
740
+ self.nneigh_merge_args.addWidget(self.nn_neighbors)
741
+ self.nneigh_merge_args.addWidget(self.nn_min_distance)
742
+ self.nneigh_merge_args.addWidget(self.nn_max_distance)
743
+ self.nneigh_merge_args.addWidget(self.nn_max_off)
744
+ self.nneigh_merge_args.addWidget(self.nn_max_length)
745
+
746
+ self.nn_layout = QtWidgets.QHBoxLayout()
747
+ self.nneigh_btn = QtWidgets.QPushButton(
748
+ 'Nearest-neighbour', clicked=lambda: self.nneigh()
749
+ )
750
+ self.merge_btn = QtWidgets.QPushButton(
751
+ 'Merge Tracks', clicked=lambda: self.merge()
752
+ )
753
+ self.nneigh_merge_btn = QtWidgets.QPushButton(
754
+ 'NM + Merging', clicked=lambda: self.nneigh_merge()
755
+ )
756
+
757
+ self.nn_layout.addWidget(self.nneigh_btn)
758
+ self.nn_layout.addWidget(self.merge_btn)
759
+ self.nn_layout.addWidget(self.nneigh_merge_btn)
760
+
761
+ # Precision GroupBox
762
+ nearestN = QtWidgets.QGroupBox('NN Analysis')
763
+ fnearestN = QtWidgets.QFormLayout()
764
+ nearestN.setLayout(fnearestN)
765
+
766
+ fnearestN.addRow(
767
+ QtWidgets.QLabel('NN (n-neighbor, min, max-distance, max-off, max-len):')
768
+ )
769
+ fnearestN.addRow(self.nneigh_merge_args)
770
+ fnearestN.addRow(self.nn_layout)
771
+ # End Precision GroupBox
772
+
773
+ self.export_options = ChecklistDialog(
774
+ 'Exported Columns',
775
+ [
776
+ 'Super-res image',
777
+ ]
778
+ + UNIQUE_COLUMNS,
779
+ checked=True,
780
+ parent=self,
781
+ )
782
+
783
+ self.import_loc_btn = QtWidgets.QPushButton(
784
+ 'Import', clicked=lambda: self.import_loc()
785
+ )
786
+ self.export_loc_btn = QtWidgets.QPushButton(
787
+ 'Export', clicked=lambda: self.export_loc()
788
+ )
789
+
790
+ self.localization_form.addWidget(localization)
791
+ self.localization_form.addWidget(z_extract)
792
+ self.localization_form.addWidget(drift)
793
+ self.localization_form.addWidget(precision)
794
+ self.localization_form.addWidget(nearestN)
795
+
796
+ self.localization_form.addLayout(
797
+ create_hbox_layout(self.import_loc_btn, self.export_loc_btn)
798
+ )
799
+ self.localization_form.addStretch()
800
+
801
+ def setup_data_filters_tab(self):
802
+ '''Set up the Data Filters tab.'''
803
+ # Results stats tab layout
804
+ self.data_filters_widget, self.data_filters_layout = create_widget(
805
+ QtWidgets.QVBoxLayout
806
+ )
807
+ self.tab_widget.addTab(self.data_filters_widget, 'Data Filters')
808
+
809
+ # results stats widget
810
+ self.results_plot_scroll = QtWidgets.QScrollArea()
811
+ self.results_plot = resultsStatsWidget()
812
+ self.results_plot.dataFilterUpdated.connect(self.filter_updated)
813
+ self.results_plot_scroll.setHorizontalScrollBarPolicy(
814
+ Qt.ScrollBarPolicy.ScrollBarAlwaysOff
815
+ )
816
+ self.results_plot_scroll.setWidgetResizable(True)
817
+ self.results_plot_scroll.setWidget(self.results_plot)
818
+
819
+ self.populate_data_btn = QtWidgets.QPushButton(
820
+ 'Populate Data', clicked=lambda: self.populate_data()
821
+ )
822
+ self.apply_filters_btn = QtWidgets.QPushButton(
823
+ 'Apply Filters', clicked=lambda: self.apply_filters()
824
+ )
825
+ self.apply_filters_btn.setToolTip(
826
+ 'Applies the filters permanently to fitting results.'
827
+ )
828
+ self.zero_coords_btn = QtWidgets.QPushButton(
829
+ 'Zero Coordinates', clicked=lambda: self.zero_coordinates()
830
+ )
831
+ self.toggle_tracking_btn = QtWidgets.QPushButton(
832
+ 'Track Plots', clicked=lambda: self.toggle_track_plots()
833
+ )
834
+
835
+ data_btn_layout = create_hbox_layout(
836
+ self.populate_data_btn,
837
+ self.apply_filters_btn,
838
+ self.zero_coords_btn,
839
+ self.toggle_tracking_btn,
840
+ )
841
+
842
+ # create x/y/z coordinate shift
843
+ self.data_shift = create_double_spin_box(
844
+ min_value=1, max_value=10000, decimals=0, initial_value=200
845
+ )
846
+ self.data_shift_label = QtWidgets.QLabel('Data Shift (nm)')
847
+ # axis to shift
848
+ self.data_shift_axis = QtWidgets.QComboBox()
849
+ self.data_shift_axis.addItem('X', DataColumns.X)
850
+ self.data_shift_axis.addItem('Y', DataColumns.Y)
851
+ self.data_shift_axis.addItem('Z', DataColumns.Z)
852
+ self.data_shift_axis.setToolTip('Select the axis to shift the data.')
853
+ # direction to shift
854
+ self.data_shift_dir = QtWidgets.QCheckBox('Negative')
855
+ self.data_shift_dir.setToolTip('Shift the data in the negative direction.')
856
+ # apply shift button
857
+ self.data_shift_btn = QtWidgets.QPushButton(
858
+ 'Shift Data', clicked=lambda: self.shift_data()
859
+ )
860
+ self.data_shift_btn.setToolTip('Shift the data by the specified value.')
861
+
862
+ data_shift_layout = create_hbox_layout(
863
+ self.data_shift_label,
864
+ self.data_shift,
865
+ self.data_shift_axis,
866
+ self.data_shift_dir,
867
+ self.data_shift_btn,
868
+ )
869
+
870
+ # Create stats label with HTML formatting
871
+ self.stats_label = QtWidgets.QLabel()
872
+
873
+ self.data_filters_layout.addWidget(self.results_plot_scroll)
874
+ self.data_filters_layout.addLayout(data_btn_layout)
875
+ self.data_filters_layout.addLayout(data_shift_layout)
876
+ self.data_filters_layout.addWidget(self.stats_label)
877
+
878
+ def updateStatsLabel(self):
879
+ total = len(self.currentResults) if self.currentResults is not None else 0
880
+ filtered = (
881
+ len(self.results_plot.filtered)
882
+ if hasattr(self.results_plot, 'filtered')
883
+ and self.results_plot.filtered is not None
884
+ else total
885
+ )
886
+
887
+ self.stats_label.setText(
888
+ f'''<div style='text-align: center;'>
889
+ <b>Localizations:</b> {filtered:,} / {total:,}
890
+ </div>'''
891
+ )
892
+
893
+ def FRC_estimate(self):
894
+ '''Estimate FRC (Fourier Ring Correlation) resolution.'''
895
+ if self.currentResults is None:
896
+ return
897
+
898
+ if len(self.currentResults) <= 0:
899
+ return
900
+
901
+ frc_method = self.frc_cbox.currentText()
902
+ data = self.currentResults.toRender()
903
+
904
+ def work_func(**kwargs):
905
+ try:
906
+ return FRC_resolution_binomial(
907
+ np.c_[data[0], data[1], data[2]],
908
+ self.xy_binsize.value(),
909
+ frc_method,
910
+ )
911
+ except Exception:
912
+ traceback.print_exc()
913
+ return None
914
+
915
+ def done(results):
916
+ self.frc_res_btn.setDisabled(False)
917
+ if results is not None:
918
+ plot_frc(*results)
919
+
920
+ self.worker = QThreadWorker(work_func)
921
+ self.worker.signals.result.connect(done)
922
+ # Execute
923
+ self.frc_res_btn.setDisabled(True)
924
+ self._threadpool.start(self.worker)
925
+
926
+ def NeNA_estimate(self):
927
+ '''Estimate Nearest-Neighbor Analysis for localization precision.'''
928
+ if self.currentResults is None:
929
+ return
930
+
931
+ if len(self.currentResults) <= 0:
932
+ return
933
+
934
+ def work_func(**kwargs):
935
+ try:
936
+ return self.currentResults.nn_trajectories(0, 200, 0, 1)
937
+ except Exception:
938
+ traceback.print_exc()
939
+ return None
940
+
941
+ def done(results):
942
+ self.NeNA_btn.setDisabled(False)
943
+ if results is not None:
944
+ self.currentResults = results
945
+ self.results_plot.setData(self.currentResults.dataFrame())
946
+
947
+ self.NeNA_widget = NeNA_Widget(
948
+ self.currentResults.neighbour_dist, self.currentResults.trackID
949
+ )
950
+
951
+ res = self.NeNA_widget.exec()
952
+
953
+ self.worker = QThreadWorker(work_func)
954
+ self.worker.signals.result.connect(done)
955
+ # Execute
956
+ self.NeNA_btn.setDisabled(True)
957
+ self._threadpool.start(self.worker)
958
+
959
+ def TARDIS_analysis(self):
960
+ '''Perform TARDIS analysis.'''
961
+ if self.currentResults is None:
962
+ return
963
+
964
+ if len(self.currentResults) <= 0:
965
+ return
966
+
967
+ self.tardis = TARDIS_Widget(
968
+ self.currentResults.frames,
969
+ self.currentResults.locX,
970
+ self.currentResults.locY,
971
+ self.currentResults.locZ,
972
+ )
973
+ self.tardis.startWorker.connect(
974
+ lambda worker: self._threadpool.start(worker)
975
+ )
976
+ self.tardis.show()
977
+
978
+ def drift_cross(self):
979
+ '''Perform drift cross-correlation.'''
980
+ if self.currentResults is None:
981
+ return
982
+
983
+ if len(self.currentResults) <= 0:
984
+ return
985
+
986
+ def work_func(**kwargs):
987
+ try:
988
+ return self.currentResults.drift_cross_correlation(
989
+ self.drift_cross_bins.value(),
990
+ self.drift_cross_px.value(),
991
+ self.drift_cross_up.value(),
992
+ )
993
+ except Exception:
994
+ traceback.print_exc()
995
+ return None
996
+
997
+ def done(results):
998
+ self.drift_cross_btn.setDisabled(False)
999
+ if results is not None:
1000
+ self.render_loc()
1001
+ self.currentResults = results[0]
1002
+ self.results_plot.setData(self.currentResults.dataFrame())
1003
+ plot_drift(*results[2])
1004
+
1005
+ self.worker = QThreadWorker(work_func)
1006
+ self.worker.signals.result.connect(done)
1007
+ # Execute
1008
+ self.drift_cross_btn.setDisabled(True)
1009
+ self._threadpool.start(self.worker)
1010
+
1011
+ def drift_fdm(self):
1012
+ '''Perform drift correction using fiducial markers.'''
1013
+ if self.currentResults is None:
1014
+ return
1015
+
1016
+ if len(self.currentResults) <= 0:
1017
+ return
1018
+
1019
+ def work_func(**kwargs):
1020
+ try:
1021
+ return self.currentResults.drift_fiducial_marker()
1022
+ except Exception:
1023
+ traceback.print_exc()
1024
+ return None
1025
+
1026
+ def done(results):
1027
+ self.drift_fdm_btn.setDisabled(False)
1028
+ if results is not None:
1029
+ self.currentResults = results[0]
1030
+ self.results_plot.setData(self.currentResults.dataFrame())
1031
+ plot_drift(*results[1])
1032
+ self.render_loc()
1033
+
1034
+ self.worker = QThreadWorker(work_func)
1035
+ self.worker.signals.result.connect(done)
1036
+ # Execute
1037
+ self.drift_fdm_btn.setDisabled(True)
1038
+ self._threadpool.start(self.worker)
1039
+
1040
+ def nneigh_merge(self):
1041
+ '''Perform nearest-neighbor merging.'''
1042
+ if self.currentResults is None:
1043
+ return
1044
+
1045
+ if len(self.currentResults) <= 0:
1046
+ return
1047
+
1048
+ def work_func(**kwargs):
1049
+ try:
1050
+ return self.currentResults.nearest_neighbour_merging(
1051
+ self.nn_min_distance.value(),
1052
+ self.nn_max_distance.value(),
1053
+ self.nn_max_off.value(),
1054
+ self.nn_max_length.value(),
1055
+ self.nn_neighbors.value(),
1056
+ )
1057
+ except Exception:
1058
+ traceback.print_exc()
1059
+ return None
1060
+
1061
+ def done(results):
1062
+ self.nneigh_merge_btn.setDisabled(False)
1063
+ if results is not None:
1064
+ self.currentResults = results
1065
+ self.results_plot.setData(self.currentResults.dataFrame())
1066
+
1067
+ self.worker = QThreadWorker(work_func)
1068
+ self.worker.signals.result.connect(done)
1069
+ # Execute
1070
+ self.nneigh_merge_btn.setDisabled(True)
1071
+ self._threadpool.start(self.worker)
1072
+
1073
+ def nneigh(self):
1074
+ '''Perform nearest-neighbor analysis.'''
1075
+ if self.currentResults is None:
1076
+ return
1077
+
1078
+ if len(self.currentResults) <= 0:
1079
+ return
1080
+
1081
+ def work_func(**kwargs):
1082
+ try:
1083
+ return self.currentResults.nn_trajectories(
1084
+ self.nn_min_distance.value(),
1085
+ self.nn_max_distance.value(),
1086
+ self.nn_max_off.value(),
1087
+ self.nn_neighbors.value(),
1088
+ )
1089
+ except Exception:
1090
+ traceback.print_exc()
1091
+ return None
1092
+
1093
+ def done(results):
1094
+ self.nneigh_btn.setDisabled(False)
1095
+ if results is not None:
1096
+ self.currentResults = results
1097
+ self.results_plot.setData(self.currentResults.dataFrame())
1098
+
1099
+ self.worker = QThreadWorker(work_func)
1100
+ self.worker.signals.result.connect(done)
1101
+ # Execute
1102
+ self.nneigh_btn.setDisabled(True)
1103
+ self._threadpool.start(self.worker)
1104
+
1105
+ def merge(self):
1106
+ '''Merge tracks.'''
1107
+ if self.currentResults is None:
1108
+ return
1109
+
1110
+ if len(self.currentResults) <= 0:
1111
+ return
1112
+
1113
+ def work_func(**kwargs):
1114
+ try:
1115
+ return self.currentResults.merge_tracks(self.nn_max_length.value())
1116
+ except Exception:
1117
+ traceback.print_exc()
1118
+ return None
1119
+
1120
+ def done(results):
1121
+ self.merge_btn.setDisabled(False)
1122
+ if results is not None:
1123
+ self.currentResults = results
1124
+ self.results_plot.setData(self.currentResults.dataFrame())
1125
+
1126
+ self.worker = QThreadWorker(work_func)
1127
+ self.worker.signals.result.connect(done)
1128
+ # Execute
1129
+ self.merge_btn.setDisabled(True)
1130
+ self._threadpool.start(self.worker)
1131
+
1132
+ def populate_data(self):
1133
+ '''Populate the data filters.'''
1134
+ self.results_plot.setData(self.currentResults.dataFrame())
1135
+
1136
+ def apply_filters(self):
1137
+ '''Apply data filters to the fitting results.'''
1138
+ if (
1139
+ not hasattr(self.results_plot, 'filtered')
1140
+ or self.results_plot.filtered is None
1141
+ ):
1142
+ self.results_plot.update()
1143
+
1144
+ self.currentResults = FittingResults.fromDataFrame(
1145
+ self.results_plot.filtered, 1, path=self.currentResults.path
1146
+ )
1147
+ self.results_plot.setData(self.currentResults.dataFrame())
1148
+ self.render_loc()
1149
+ print('Filters applied.')
1150
+
1151
+ def zero_coordinates(self):
1152
+ '''Zero the fitting results coordinates.'''
1153
+ if self.currentResults is not None:
1154
+ self.currentResults.zero_coordinates()
1155
+ self.results_plot.setData(self.currentResults.dataFrame())
1156
+ self.render_loc()
1157
+ print('Coordinates reset.')
1158
+
1159
+ def toggle_track_plots(self):
1160
+ '''Toggle the track plots.'''
1161
+ if self.currentResults is not None:
1162
+ self.results_plot.toggle_track_plots()
1163
+
1164
+ def shift_data(self):
1165
+ '''Shift the data by a specified amount.'''
1166
+ if self.currentResults is None:
1167
+ return
1168
+
1169
+ if len(self.currentResults) <= 0:
1170
+ return
1171
+
1172
+ axis: DataColumns = self.data_shift_axis.currentData()
1173
+ shift = self.data_shift.value()
1174
+ direction = -1 if self.data_shift_dir.isChecked() else 1
1175
+
1176
+ self.currentResults.data[axis] += shift * direction
1177
+
1178
+ self.results_plot.clear()
1179
+ self.render_loc()
1180
+
1181
+ def filter_updated(self, df: pd.DataFrame):
1182
+ '''Update the view when data filters are updated.
1183
+
1184
+ Parameters
1185
+ ----------
1186
+ df : pd.DataFrame
1187
+ The filtered DataFrame.
1188
+ '''
1189
+ if df is not None:
1190
+ self.updateStatsLabel()
1191
+
1192
+ if df.count().min() > 1:
1193
+ render_idx = self.render_cbox.currentData()
1194
+ renderClass = BaseRenderer(
1195
+ self.xy_binsize.value(), RenderModes(render_idx)
1196
+ )
1197
+ img = renderClass.render_xy(
1198
+ df['x'].to_numpy(), df['y'].to_numpy(), df['intensity'].to_numpy()
1199
+ )
1200
+ self.setImage(img, autoLevels=False)
1201
+ else:
1202
+ # Create a black image
1203
+ img = np.zeros(
1204
+ (
1205
+ self.image_layers.currentLayer.height(),
1206
+ self.image_layers.currentLayer.width(),
1207
+ 3,
1208
+ ),
1209
+ np.uint8,
1210
+ )
1211
+
1212
+ # Write some Text
1213
+ font = cv2.FONT_HERSHEY_SIMPLEX
1214
+ bottomLeftCornerOfText = (10, 50)
1215
+ fontScale = 1
1216
+ fontColor = (255, 255, 255)
1217
+ thickness = 1
1218
+ lineType = 2
1219
+
1220
+ cv2.putText(
1221
+ img,
1222
+ 'EMPTY!',
1223
+ bottomLeftCornerOfText,
1224
+ font,
1225
+ fontScale,
1226
+ fontColor,
1227
+ thickness,
1228
+ lineType,
1229
+ )
1230
+ self.setImage(img, autoLevels=False)
1231
+
1232
+ def export_loc(self, filename=None):
1233
+ '''Export the fitting results into a file.
1234
+
1235
+ Parameters
1236
+ ----------
1237
+ filename : str, optional
1238
+ File path; if None, a save file dialog is shown, by default None.
1239
+ '''
1240
+ if self.currentResults is None:
1241
+ return
1242
+
1243
+ if filename is None:
1244
+ if not self.export_options.exec():
1245
+ return
1246
+
1247
+ filename, _ = getSaveFileName(
1248
+ self,
1249
+ 'Export localizations',
1250
+ filter='HDF5 files (*.h5);;TSV Files (*.tsv)',
1251
+ directory=os.path.dirname(self.currentResults.path),
1252
+ )
1253
+
1254
+ if len(filename) > 0:
1255
+ options = self.export_options.toList()
1256
+
1257
+ dataFrame = self.currentResults.dataFrame()
1258
+ exp_columns = []
1259
+ for col in dataFrame.columns:
1260
+ if col in options:
1261
+ exp_columns.append(col)
1262
+
1263
+ if exp_columns:
1264
+ if '.tsv' in filename:
1265
+ dataFrame.to_csv(
1266
+ filename,
1267
+ index=False,
1268
+ columns=exp_columns,
1269
+ float_format=self.export_options.export_precision.text(),
1270
+ sep='\t',
1271
+ encoding='utf-8',
1272
+ )
1273
+ elif '.h5' in filename:
1274
+ dataFrame[exp_columns].to_hdf(
1275
+ filename, key='microEye', index=False, complevel=0
1276
+ )
1277
+
1278
+ if 'Super-res image' in options:
1279
+ sres_img = self.render_loc()
1280
+ tf.imwrite(
1281
+ re.sub(r'(\.h5|\.tsv)$', '_super_res.tif', filename),
1282
+ sres_img,
1283
+ photometric='minisblack',
1284
+ bigtiff=True,
1285
+ ome=False,
1286
+ )
1287
+
1288
+ def import_loc(self):
1289
+ '''Import fitting results from a file.'''
1290
+ filename, _ = getOpenFileName(
1291
+ self,
1292
+ 'Import localizations',
1293
+ filter='HDF5 files (*.h5);;TSV Files (*.tsv)',
1294
+ directory=os.path.dirname(self.currentResults.path),
1295
+ )
1296
+
1297
+ if len(filename) > 0:
1298
+ results = FittingResults.fromFile(filename, 1)
1299
+
1300
+ if results is not None:
1301
+ self.add_data_layer(results)
1302
+ print('Done importing results.')
1303
+ else:
1304
+ print('Error importing results.')