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,1129 @@
1
+ import json
2
+ import os
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from typing import Any, Optional, Union
5
+
6
+ import cv2
7
+ import h5py
8
+ import numba as nb
9
+ import numpy as np
10
+ import pyqtgraph as pg
11
+ from scipy.optimize import curve_fit
12
+ from scipy.signal import find_peaks, peak_prominences
13
+ from tqdm import tqdm
14
+
15
+ from microEye.analysis.fitting.psf.stats import *
16
+ from microEye.analysis.fitting.results import PARAMETER_HEADERS
17
+ from microEye.utils.uImage import TiffSeqHandler, ZarrImageSequence, uImage
18
+
19
+
20
+ def gaussian(x, a, x0, sigma, offset):
21
+ return a * np.exp(-((x - x0) ** 2) / (2 * sigma**2)) + offset
22
+
23
+
24
+ class PSFdata:
25
+ def __init__(
26
+ self, data_dict: dict[str, Union[int, np.ndarray, list, float]]
27
+ ) -> None:
28
+ self._data = data_dict
29
+ self._last_field = 0
30
+
31
+ self.stats_calculator = StatsCalculator(
32
+ zero_plane=self.zero_plane,
33
+ z_step=self.z_step,
34
+ fitting_method=self.fitting_method,
35
+ )
36
+
37
+ def get_field_psf(self, grid_size: int = 3):
38
+ '''
39
+ Get PSF field for the given grid size.
40
+ '''
41
+ if self._last_field == grid_size:
42
+ return
43
+
44
+ width, height = self.dim
45
+ width *= self.upsample
46
+ height *= self.upsample
47
+ offset = self.roi_size / 2
48
+
49
+ def get_grid_cell_index(x_start, y_start):
50
+ if not np.isfinite(x_start) or not np.isfinite(y_start):
51
+ return -1
52
+ cell_width = width // grid_size
53
+ cell_height = height // grid_size
54
+ i = (y_start + offset) // cell_height
55
+ j = (x_start + offset) // cell_width
56
+ return int(i * grid_size + j)
57
+
58
+ for zslice in self._data['zslices']:
59
+ zslice['field'] = [[] for _ in range(grid_size**2)]
60
+ zslice['field_idx'] = np.zeros(zslice['rois'].shape[0], dtype=np.uint16)
61
+ for i in range(zslice['rois'].shape[0]):
62
+ idx = get_grid_cell_index(*zslice['coords'][i])
63
+ if idx >= 0:
64
+ zslice['field'][idx].append(zslice['rois'][i].copy())
65
+ zslice['field_idx'][i] = idx
66
+
67
+ self._last_field = grid_size
68
+
69
+ @property
70
+ def zslices(self) -> list[dict[str, Union[int, np.ndarray, list, float]]]:
71
+ return self._data.get('zslices')
72
+
73
+ @property
74
+ def fitting_method(self) -> float:
75
+ return self._data.get('fit_method')
76
+
77
+ @property
78
+ def available_stats(self) -> list[str]:
79
+ # self.stats_calculator.available_stats keys
80
+ return list(self.stats_calculator.available_stats.keys())
81
+
82
+ @property
83
+ def pixel_size(self) -> float:
84
+ return self._data.get('pixel_size')
85
+
86
+ @property
87
+ def z_step(self) -> float:
88
+ step = self._data.get('z_step', 10)
89
+ if step:
90
+ return step
91
+ else:
92
+ return 10
93
+
94
+ @property
95
+ def roi_size(self) -> int:
96
+ return self._data.get('roi_size')
97
+
98
+ @property
99
+ def roi_info(self):
100
+ return self._data.get('roi_info')
101
+
102
+ @property
103
+ def origin(self):
104
+ if self.roi_info:
105
+ return self.roi_info[0]
106
+ else:
107
+ return (0, 0)
108
+
109
+ @property
110
+ def dim(self):
111
+ if self.roi_info:
112
+ return tuple(self.roi_info[1])
113
+ else:
114
+ return self.shape[-1], self.shape[-2]
115
+
116
+ @property
117
+ def upsample(self) -> int:
118
+ return self._data.get('upsample')
119
+
120
+ @property
121
+ def zero_plane(self) -> Optional[int]:
122
+ return self._data.get('zero_plane')
123
+
124
+ @zero_plane.setter
125
+ def zero_plane(self, value: int):
126
+ self._data['zero_plane'] = value
127
+
128
+ @property
129
+ def shape(self) -> tuple:
130
+ return self._data.get('shape')
131
+
132
+ @property
133
+ def path(self) -> str:
134
+ return self._data.get('stack')
135
+
136
+ @property
137
+ def headers(self) -> str:
138
+ return PARAMETER_HEADERS[self.fitting_method]
139
+
140
+ @property
141
+ def rois(self):
142
+ return [zslice['rois'] for zslice in self.zslices]
143
+
144
+ @property
145
+ def counts(self) -> np.ndarray:
146
+ return np.array([zslice['count'] for zslice in self.zslices])
147
+
148
+ @property
149
+ def mean(self) -> np.ndarray:
150
+ return np.array([zslice['mean'] for zslice in self.zslices])
151
+
152
+ @property
153
+ def median(self) -> np.ndarray:
154
+ return np.array([zslice['median'] for zslice in self.zslices])
155
+
156
+ @property
157
+ def std(self) -> np.ndarray:
158
+ return np.array([zslice['std'] for zslice in self.zslices])
159
+
160
+ @property
161
+ def coords(self) -> np.ndarray:
162
+ return np.array([zslice['coords'] for zslice in self.zslices])
163
+
164
+ @property
165
+ def params(self) -> np.ndarray:
166
+ return np.array([zslice['params'] for zslice in self.zslices])
167
+
168
+ @property
169
+ def crlbs(self) -> np.ndarray:
170
+ return np.array([zslice['crlbs'] for zslice in self.zslices])
171
+
172
+ @property
173
+ def loglike(self) -> np.ndarray:
174
+ return np.array([zslice['loglike'] for zslice in self.zslices])
175
+
176
+ @property
177
+ def field_rois(self) -> list:
178
+ return [zslice.get('field') for zslice in self.zslices]
179
+
180
+ @property
181
+ def field_indecies(self) -> np.ndarray:
182
+ return np.array([zslice['field_idx'] for zslice in self.zslices])
183
+
184
+ def __getitem__(self, key: str) -> Any:
185
+ return self._data.get(key)
186
+
187
+ def __setitem__(self, key: str, value: Any) -> None:
188
+ self._data[key] = value
189
+
190
+ def __repr__(self) -> str:
191
+ return f'PSFdata({self._data})'
192
+
193
+ def __str__(self) -> str:
194
+ return f'PSFdata({self._data})'
195
+
196
+ def __len__(self) -> int:
197
+ return len(self.zslices)
198
+
199
+ def __iter__(self):
200
+ self._index = 0
201
+ return self
202
+
203
+ def __next__(self):
204
+ if self._index < len(self.zslices):
205
+ result = self.zslices[self._index]
206
+ self._index += 1
207
+ return result
208
+ else:
209
+ raise StopIteration
210
+
211
+ def get_z_slice(
212
+ self,
213
+ z_index: int,
214
+ type: str = 'mean',
215
+ roi_index: int = 0,
216
+ grid_size: int = 1,
217
+ normalize: bool = False,
218
+ ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
219
+ """
220
+ Get the XY slice for the given z-index.
221
+
222
+ Parameters:
223
+ -----------
224
+ z_index : int
225
+ The z-index of the slice
226
+ type : str, optional
227
+ The type of data to return, by default 'mean'
228
+ Options: 'mean', 'std', 'median', 'roi'
229
+ roi_index : int, optional
230
+ The index of the ROI to return, by default 0
231
+ grid_size : int, optional
232
+ The grid size for field PSF, by default 1
233
+ normalize : bool, optional
234
+ Whether to normalize the data, by default False
235
+
236
+ Returns:
237
+ --------
238
+ tuple[np.ndarray, np.ndarray]
239
+ The data array and grid overlay
240
+ """
241
+ # check if Z index is in bounds
242
+ if z_index >= len(self.zslices):
243
+ raise IndexError(f'Z index {z_index} out of bounds')
244
+
245
+ # get the z-slice
246
+ zslice = self.zslices[z_index]
247
+
248
+ # check if roi index is in bounds
249
+ if roi_index >= zslice['count']:
250
+ raise IndexError(
251
+ f'ROI index {roi_index} out of bounds for z-slice {z_index}'
252
+ )
253
+
254
+ # limit grid_size from 1 to 5
255
+ grid_size = max(1, min(grid_size, 5))
256
+
257
+ # get the ROI
258
+ if type in ['mean', 'median', 'std', 'roi']:
259
+ if grid_size == 1:
260
+ if type == 'roi':
261
+ data = zslice['rois'][roi_index]
262
+ return None if np.isnan(data).any() else data, None
263
+ return zslice[type], None
264
+ else:
265
+ self.get_field_psf(grid_size)
266
+ colors = [
267
+ pg.intColor(i, grid_size**2).getRgb() for i in range(grid_size**2)
268
+ ]
269
+
270
+ roi_size = self.roi_size
271
+ size = roi_size * grid_size
272
+
273
+ # Create empty arrays for visualization
274
+ psf_image = np.zeros((size, size))
275
+ # RGBA for grid overlay
276
+ grid_overlay = np.zeros((size, size, 4))
277
+
278
+ for field_idx, field_rois in enumerate(zslice['field']):
279
+ if not field_rois:
280
+ continue
281
+
282
+ field_rois = np.array(field_rois)
283
+
284
+ if type == 'mean':
285
+ stat = np.nanmean(field_rois, axis=0)
286
+ elif type == 'median':
287
+ stat = np.nanmedian(field_rois, axis=0)
288
+ elif type == 'std':
289
+ stat = np.nanstd(field_rois, axis=0)
290
+ else: # Single ROI
291
+ stat = (
292
+ field_rois[roi_index]
293
+ if roi_index < len(field_rois)
294
+ else None
295
+ )
296
+
297
+ if stat is not None:
298
+ y = field_idx // grid_size
299
+ x = field_idx % grid_size
300
+ psf_image[
301
+ y * roi_size : (y + 1) * roi_size,
302
+ x * roi_size : (x + 1) * roi_size,
303
+ ] = 2**16 * stat / np.sum(stat) if normalize else stat
304
+
305
+ # Add colored overlay for grid visualization
306
+ color = colors[field_idx]
307
+ grid_overlay[
308
+ y * roi_size : (y + 1) * roi_size,
309
+ x * roi_size : (x + 1) * roi_size,
310
+ ] = [color[0], color[1], color[2], 128]
311
+ return psf_image, grid_overlay
312
+ else:
313
+ raise ValueError(f'Invalid type {type}')
314
+
315
+ def get_longitudinal_slice(
316
+ self,
317
+ index: int,
318
+ type: str = 'mean',
319
+ grid_size: int = 1,
320
+ sagittal: bool = True,
321
+ normalize: bool = False,
322
+ ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
323
+ """
324
+ Get the longitudinal slice for the given index.
325
+
326
+ Parameters
327
+ ----------
328
+ index : int
329
+ The index of the slice
330
+ type : str, optional
331
+ The type of data to return, by default 'mean'
332
+ Options: 'mean', 'median', 'std'
333
+ grid_size : int, optional
334
+ The grid size for field PSF, by default 1
335
+ sagittal : bool, optional
336
+ Whether to get the sagittal slice, or coronal, by default True
337
+ normalize : bool, optional
338
+ Whether to normalize the data, by default False
339
+
340
+ Returns
341
+ -------
342
+ tuple[np.ndarray, np.ndarray]
343
+ The data array and grid overlay
344
+ """
345
+ # check if index is in bounds
346
+ if index >= self.roi_size:
347
+ raise IndexError(f'Y index {index} out of bounds')
348
+
349
+ # limit grid_size from 1 to 5
350
+ grid_size = np.clip(grid_size, 1, 5)
351
+
352
+ roi_height, roi_width = self.roi_size, len(self.zslices)
353
+
354
+ if type not in ['mean', 'median', 'std']:
355
+ raise ValueError(f'Invalid type {type}')
356
+
357
+ if grid_size == 1:
358
+ data = np.array(
359
+ [
360
+ zslice[type][index] if sagittal else zslice[type][:, index]
361
+ for zslice in self.zslices
362
+ ]
363
+ ).T
364
+ return data, None
365
+
366
+ self.get_field_psf(grid_size)
367
+ colors = [pg.intColor(i, grid_size**2).getRgb() for i in range(grid_size**2)]
368
+
369
+ height, width = roi_height * grid_size, roi_width * grid_size
370
+
371
+ psf_image = np.zeros((height, width))
372
+ grid_overlay = np.zeros((height, width, 4), dtype=np.uint8)
373
+
374
+ stat_func = {'mean': np.nanmean, 'median': np.nanmedian, 'std': np.nanstd}[type]
375
+
376
+ grid_painted = []
377
+ for i, zslice in enumerate(self.zslices):
378
+ for field_idx, field_rois in enumerate(zslice['field']):
379
+ if not field_rois:
380
+ continue
381
+
382
+ field_rois = (
383
+ np.array(field_rois)[:, index, :]
384
+ if sagittal
385
+ else np.array(field_rois)[..., index]
386
+ )
387
+ stat = stat_func(field_rois, axis=0)
388
+
389
+ if stat is not None:
390
+ y, x = divmod(field_idx, grid_size)
391
+ psf_image[
392
+ y * roi_height : (y + 1) * roi_height, x * roi_width + i
393
+ ] = stat
394
+
395
+ if field_idx not in grid_painted:
396
+ color = colors[field_idx]
397
+ grid_overlay[
398
+ y * roi_height : (y + 1) * roi_height,
399
+ x * roi_width : (x + 1) * roi_width,
400
+ ] = [*color[:3], 128]
401
+ grid_painted.append(field_idx)
402
+
403
+ return psf_image, grid_overlay
404
+
405
+ def get_x_slice(
406
+ self, x_index: int, type: str = 'mean', grid_size: int = 1
407
+ ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
408
+ """
409
+ Get the YZ slice for the given x-index.
410
+
411
+ Parameters
412
+ ----------
413
+ x_index : int
414
+ The x-index of the slice
415
+ type : str, optional
416
+ The type of data to return, by default 'mean'
417
+ Options: 'mean', 'median', 'std'
418
+ grid_size : int, optional
419
+ The grid size for field PSF, by default 1
420
+
421
+ Returns
422
+ -------
423
+ tuple[np.ndarray, np.ndarray]
424
+ The data array and grid overlay
425
+ """
426
+ return self.get_longitudinal_slice(
427
+ x_index, type=type, grid_size=grid_size, sagittal=False
428
+ )
429
+
430
+ def get_y_slice(
431
+ self, y_index: int, type: str = 'mean', grid_size: int = 1
432
+ ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
433
+ """
434
+ Get the XZ slice for the given y-index.
435
+
436
+ Parameters
437
+ ----------
438
+ y_index : int
439
+ The y-index of the slice
440
+ type : str, optional
441
+ The type of data to return, by default 'mean'
442
+ Options: 'mean', 'median', 'std'
443
+ grid_size : int, optional
444
+ The grid size for field PSF, by default 1
445
+
446
+ Returns
447
+ -------
448
+ tuple[np.ndarray, np.ndarray]
449
+ The data array and grid overlay
450
+ """
451
+ return self.get_longitudinal_slice(
452
+ y_index, type=type, grid_size=grid_size, sagittal=True
453
+ )
454
+
455
+ def get_volume(self, type: str = 'mean'):
456
+ """
457
+ Get the 3D volume for the given type.
458
+
459
+ Parameters
460
+ ----------
461
+ type : str, optional
462
+ The type of data to return, by default 'mean'
463
+
464
+ Options:
465
+ 'mean' - Mean intensity
466
+
467
+ 'median' - Median intensity
468
+
469
+ 'std' - Standard deviation of intensity
470
+
471
+ Returns
472
+ -------
473
+ np.ndarray
474
+ The 3D volume data
475
+ """
476
+ if type not in ['mean', 'median', 'std']:
477
+ raise ValueError(f'Invalid type {type}')
478
+
479
+ volume_data = np.zeros((len(self), self.roi_size, self.roi_size))
480
+
481
+ for i, z_slice in enumerate(self.zslices):
482
+ volume_data[i] = (
483
+ z_slice[type]
484
+ if z_slice[type] is not None
485
+ else np.zeros((self.roi_size, self.roi_size))
486
+ )
487
+
488
+ return volume_data
489
+
490
+ def get_ratio(self) -> float:
491
+ '''
492
+ Get the ratio of Z step to lateral pixel size.
493
+
494
+ Returns
495
+ -------
496
+ float
497
+ The ratio of Z step to lateral pixel size
498
+ '''
499
+ return self.z_step * self.upsample / self.pixel_size
500
+
501
+ def get_intensity_stats(self):
502
+ '''
503
+ Get the intensity statistics for all z-slices.
504
+
505
+ Returns
506
+ -------
507
+ _indices, _mean, _median, _std: tuple[np.ndarray, list, list, list]
508
+ The indices, mean, median, and standard deviation of the intensity values
509
+ '''
510
+ _indices = (np.arange(len(self)) - self.zero_plane) * self.z_step
511
+ _mean = []
512
+ _median = []
513
+ _std = []
514
+
515
+ for z_slice in self.zslices:
516
+ if z_slice['rois'] is not None:
517
+ valid_rois = z_slice['rois'][
518
+ ~np.isnan(z_slice['rois']).any(axis=(1, 2))
519
+ ]
520
+ if len(valid_rois) > 0:
521
+ _mean.append(np.mean(valid_rois))
522
+ _median.append(np.median(valid_rois))
523
+ _std.append(np.std(valid_rois))
524
+ else:
525
+ _mean.append(np.nan)
526
+ _median.append(np.nan)
527
+ _std.append(np.nan)
528
+ else:
529
+ _mean.append(np.nan)
530
+ _median.append(np.nan)
531
+ _std.append(np.nan)
532
+
533
+ return _indices, _mean, _median, _std
534
+
535
+ def get_stats(
536
+ self,
537
+ selected_stat: str,
538
+ confidence_method: ConfidenceMethod = ConfidenceMethod.NONE,
539
+ confidence_level: float = 0.95,
540
+ ):
541
+ """
542
+ Get the statistic data for all z-slices.
543
+
544
+ Parameters
545
+ ----------
546
+ selected_stat : str
547
+ The selected statistic to return
548
+ Options:
549
+ 'Counts', 'Sigma', 'Sigma (sum)', 'Sigma (diff)', 'Sigma (abs(diff))',
550
+ 'Intensity', 'Background'
551
+ confidence_method : ConfidenceMethod, optional
552
+ The method to use for confidence interval calculation,
553
+ by default ConfidenceMethod.NONE
554
+
555
+ Returns
556
+ -------
557
+ z_indices, param_stat: tuple[np.ndarray, np.ndarray]
558
+ The z-indices and the statistic data for all z-slices
559
+ """
560
+ # Simply delegate to the calculator
561
+ return self.stats_calculator.get_stats(
562
+ self.zslices,
563
+ selected_stat,
564
+ confidence_method,
565
+ confidence_level,
566
+ )
567
+
568
+ def get_z_cal(
569
+ self,
570
+ selected_stat: str,
571
+ region: tuple[int, int],
572
+ confidence_method: ConfidenceMethod = ConfidenceMethod.NONE,
573
+ confidence_level: float = 0.95,
574
+ **kwargs,
575
+ ) -> Union[SlopeResult, CurveResult, dict]:
576
+ '''
577
+ Get the slope or curve fit for the selected statistic.
578
+
579
+ Parameters
580
+ ----------
581
+ selected_stat : str
582
+ The selected statistic to use for slope or curve fit
583
+ region : tuple[int, int]
584
+ The region to use for slope or curve fit
585
+ confidence_method : ConfidenceMethod, optional
586
+ The method to use for confidence interval calculation,
587
+ by default ConfidenceMethod.NONE
588
+ confidence_level : float, optional
589
+ The confidence level for the confidence interval calculation,
590
+ by default 0.95
591
+
592
+ Keyword Arguments
593
+ -----------------
594
+ method : CurveFitMethod, optional
595
+ The method to use for curve fitting, by default CurveFitMethod.CSPLINE
596
+ derivative_threshold : float, optional
597
+ The threshold for derivative-based zero crossing detection,
598
+ by default 0.01
599
+ smoothing : float, optional
600
+ The smoothing factor for curve fitting, by default None
601
+
602
+ Returns
603
+ -------
604
+ Union[SlopeResult, CurveResult]
605
+ The slope or curve fit result
606
+ '''
607
+ method: CurveFitMethod = kwargs.get('method', CurveFitMethod.LINEAR)
608
+
609
+ if method == CurveFitMethod.LINEAR:
610
+ return SlopeAnalyzer.fit_stat_slope(
611
+ selected_stat,
612
+ lambda: self.get_stats(
613
+ selected_stat, confidence_method, confidence_level
614
+ ),
615
+ region,
616
+ confidence_method,
617
+ confidence_level,
618
+ )
619
+ elif method == CurveFitMethod.ASTIGMATIC_PSF:
620
+ return None
621
+ else:
622
+ derivative_threshold: float = kwargs.get('derivative_threshold', 0.01)
623
+ smoothing_factor: float = kwargs.get('smoothing', 0.1)
624
+ return CurveAnalyzer.fit_stat_curve(
625
+ selected_stat,
626
+ lambda: self.get_stats(
627
+ selected_stat, confidence_method, confidence_level
628
+ ),
629
+ region,
630
+ method,
631
+ derivative_threshold,
632
+ smoothing_factor,
633
+ )
634
+
635
+ def adjust_zero_plane(
636
+ self, selected_stat: str, method: str, region: tuple[int, int]
637
+ ):
638
+ """
639
+ Adjust the zero plane based on the selected statistic and method.
640
+
641
+ Parameters
642
+ ----------
643
+ selected_stat : str
644
+ The selected statistic to use for zero plane adjustment
645
+
646
+ Options:
647
+
648
+ 'Counts', 'Sigma', 'Sigma (sum)', 'Sigma (diff)', 'Sigma (abs(diff))',
649
+ 'Intensity', 'Background'
650
+ method : str
651
+ The method to use for zero plane adjustment
652
+
653
+ Options: 'Peak', 'Valley', 'Gaussian Fit', 'Gaussian Fit (Inverted)',
654
+ 'Manual'
655
+ region : tuple[int, int]
656
+ The region to use for zero plane adjustment
657
+
658
+ Returns
659
+ -------
660
+ int
661
+ The new zero plane
662
+ """
663
+ if selected_stat == 'Sigma':
664
+ selected_stat = 'Sigma (sum)'
665
+
666
+ start, end = map(
667
+ int,
668
+ [v / self.z_step + self.zero_plane for v in region],
669
+ )
670
+
671
+ # Store the old zero plane for potential restoration
672
+ self.old_zero_plane = self.zero_plane
673
+
674
+ _, stat_data, _, _ = self.get_stats(selected_stat, ConfidenceMethod.NONE)
675
+
676
+ # Slice the data according to the specified range
677
+ x = np.arange(start, end)
678
+ y = stat_data[start:end]
679
+
680
+ if method == 'Peak':
681
+ # Find peaks
682
+ peaks, _ = find_peaks(y)
683
+ if len(peaks) > 0:
684
+ # Get peak prominences
685
+ prominences = peak_prominences(y, peaks)[0]
686
+ # Select the most prominent peak
687
+ max_peak = peaks[np.argmax(prominences)]
688
+ new_zero_plane = start + max_peak
689
+ else:
690
+ new_zero_plane = start + np.argmax(y)
691
+ elif method == 'Valley':
692
+ # Invert the data to find valleys as peaks
693
+ inverted_y = -y
694
+ valleys, _ = find_peaks(inverted_y)
695
+ if len(valleys) > 0:
696
+ # Get valley prominences
697
+ prominences = peak_prominences(inverted_y, valleys)[0]
698
+ # Select the most prominent valley
699
+ max_valley = valleys[np.argmax(prominences)]
700
+ new_zero_plane = start + max_valley
701
+ else:
702
+ new_zero_plane = start + np.argmin(y)
703
+ elif 'Gaussian Fit' in method:
704
+ if 'Inverted' in method:
705
+ y = -y
706
+ try:
707
+ # Initial guess for Gaussian parameters
708
+ a_init = np.max(y) - np.min(y)
709
+ x0_init = x[np.argmax(y)]
710
+ sigma_init = (end - start) / 4
711
+ offset_init = np.min(y)
712
+
713
+ # Perform Gaussian fit
714
+ popt, _ = curve_fit(
715
+ gaussian, x, y, p0=[a_init, x0_init, sigma_init, offset_init]
716
+ )
717
+ new_zero_plane = int(popt[1]) # x0 is the center of the Gaussian
718
+ except Exception:
719
+ # Fallback to original plane if fitting fails
720
+ new_zero_plane = self.zero_plane
721
+ else:
722
+ return self.zero_plane # Keep current zero plane for 'Manual' method
723
+
724
+ # Update the zero plane
725
+ self.zero_plane = new_zero_plane
726
+ self.stats_calculator.zero_plane = new_zero_plane
727
+
728
+ return self.zero_plane
729
+
730
+ def get_consistent_rois(self) -> 'PSFdata':
731
+ '''
732
+ Creates a new PSFdata object with consistent ROIs across all z-slices.
733
+ Only keeps coordinates that exist in all slices and ensures consistent ordering.
734
+
735
+ Returns
736
+ -------
737
+ PSFdata
738
+ A new PSFdata object with consistent ROIs
739
+ '''
740
+ # Create a copy of the original data
741
+ new_data = self._data.copy()
742
+
743
+ # Tolerance for matching coordinates
744
+ tolerance = 2.0 * self.upsample
745
+
746
+ # First, identify coordinates that exist in all slices
747
+ all_coords = []
748
+ for zslice in self.zslices:
749
+ params = zslice['params']
750
+ # Only consider non-NaN coordinates
751
+ valid_coords = params[~np.isnan(params[:, 0])][:, :2]
752
+ all_coords.append(valid_coords)
753
+
754
+ # Function to find matching coordinates within a tolerance
755
+ def find_matching_coords(coord, coords_list):
756
+ matches = []
757
+ for slice_idx, slice_coords in enumerate(coords_list):
758
+ distances = np.sqrt(np.sum((slice_coords - coord) ** 2, axis=1))
759
+ match_idx = np.argmin(distances)
760
+ if distances[match_idx] <= tolerance:
761
+ matches.append((slice_idx, match_idx))
762
+ return matches if len(matches) == len(coords_list) else None
763
+
764
+ # Find coordinates that exist in all slices
765
+ consistent_coords = []
766
+ consistent_indices = [[] for _ in range(len(all_coords))]
767
+
768
+ for _, coord in enumerate(all_coords[0]):
769
+ matches = find_matching_coords(coord, all_coords)
770
+ if matches:
771
+ consistent_coords.append(coord)
772
+ for slice_idx, match_idx in matches:
773
+ consistent_indices[slice_idx].append(match_idx)
774
+
775
+ # Create new zslices with only consistent ROIs
776
+ new_data['zslices'] = []
777
+ for slice_idx, zslice in enumerate(self.zslices):
778
+ indices = consistent_indices[slice_idx]
779
+
780
+ new_zslice = {
781
+ 'index': zslice['index'],
782
+ 'rois': zslice['rois'][indices] if zslice['rois'] is not None else None,
783
+ 'coords': zslice['coords'][indices]
784
+ if zslice['coords'] is not None
785
+ else None,
786
+ 'params': zslice['params'][indices],
787
+ 'crlbs': zslice['crlbs'][indices],
788
+ 'loglike': zslice['loglike'][indices],
789
+ 'count': len(indices),
790
+ }
791
+
792
+ # Recalculate statistics for the consistent ROIs
793
+ if new_zslice['rois'] is not None:
794
+ new_zslice['mean'] = np.nanmean(new_zslice['rois'], axis=0)
795
+ new_zslice['median'] = np.nanmedian(new_zslice['rois'], axis=0)
796
+ new_zslice['std'] = np.nanstd(new_zslice['rois'], axis=0)
797
+ else:
798
+ new_zslice['mean'] = None
799
+ new_zslice['median'] = None
800
+ new_zslice['std'] = None
801
+
802
+ new_data['zslices'].append(new_zslice)
803
+
804
+ return PSFdata(new_data)
805
+
806
+ def save_hdf(self, filename: str):
807
+ '''
808
+ Save PSF data to HDF5 file.
809
+ '''
810
+ with h5py.File(filename, 'w') as hdf:
811
+ # Save scalar attributes
812
+ for key in ['pixel_size', 'roi_size', 'upsample', 'z_step']:
813
+ hdf.attrs[key] = self._data[key]
814
+
815
+ hdf.attrs['shape'] = json.dumps(self.shape)
816
+ hdf.attrs['stack'] = self.path
817
+ hdf.attrs['fit_method'] = self.fitting_method
818
+ hdf.attrs['zero_plane'] = self.zero_plane
819
+ hdf.attrs['roi_info'] = json.dumps(self.roi_info)
820
+ hdf.attrs['params_headers'] = json.dumps(self.headers)
821
+
822
+ # Save zslices data
823
+ zslices_group = hdf.create_group('zslices')
824
+ for i, zslice in enumerate(self.zslices):
825
+ slice_group = zslices_group.create_group(f'slice_{i}')
826
+ for key, value in zslice.items():
827
+ if isinstance(value, np.ndarray):
828
+ slice_group.create_dataset(key, data=value)
829
+ elif value is None:
830
+ slice_group.attrs[key] = 'None'
831
+ else:
832
+ slice_group.attrs[key] = value
833
+
834
+ print(f'PSF data saved to {filename}')
835
+
836
+ @staticmethod
837
+ def load_hdf(filename: str):
838
+ '''
839
+ Load PSF data from HDF5 file.
840
+ '''
841
+ if not os.path.exists(filename):
842
+ raise FileNotFoundError(f'File not found: {filename}')
843
+
844
+ with h5py.File(filename, 'r') as hdf:
845
+ data = {
846
+ 'zslices': [],
847
+ }
848
+
849
+ for key in [
850
+ 'pixel_size',
851
+ 'z_step',
852
+ 'roi_size',
853
+ 'upsample',
854
+ 'stack',
855
+ 'fit_method',
856
+ 'zero_plane',
857
+ ]:
858
+ data[key] = hdf.attrs.get(key, None)
859
+
860
+ for key in ['shape', 'roi_info', 'params_headers']:
861
+ data[key] = json.loads(hdf.attrs.get(key, '[]'))
862
+
863
+ zslices_group = hdf['zslices']
864
+ # Sort slice names numerically
865
+ slice_names = sorted(
866
+ zslices_group.keys(), key=lambda x: int(x.split('_')[1])
867
+ )
868
+
869
+ for slice_name in slice_names:
870
+ slice_group = zslices_group[slice_name]
871
+ zslice_data = {}
872
+ for key in slice_group:
873
+ zslice_data[key] = slice_group[key][()]
874
+ for key in slice_group.attrs:
875
+ if slice_group.attrs[key] == 'None':
876
+ zslice_data[key] = None
877
+ else:
878
+ zslice_data[key] = slice_group.attrs[key]
879
+ data['zslices'].append(zslice_data)
880
+
881
+ # Validate required attributes
882
+ required_attrs = ['pixel_size', 'roi_size', 'upsample', 'shape', 'stack']
883
+ missing_attrs = [attr for attr in required_attrs if attr not in data]
884
+ if missing_attrs:
885
+ raise ValueError(f'Missing required attributes: {missing_attrs}')
886
+
887
+ return PSFdata(data)
888
+
889
+
890
+ @nb.njit(cache=True)
891
+ def get_roi_list(image: np.ndarray, points: np.ndarray, roi_size=7):
892
+ '''
893
+ Gets the roi list of specific size around the supplied (x, y) points
894
+
895
+ Parameters
896
+ ----------
897
+ image : np.ndarray
898
+ The single channel image
899
+ points : np.ndarray
900
+ The points list of preliminary detection
901
+ roi_size : int, optional
902
+ roi size, by default 7
903
+
904
+ Returns
905
+ -------
906
+ tuple[np.ndarray, np.ndarray]
907
+ roi_list array of shape (nRoi, roi_size**2),
908
+ coord_list of roi top left corner
909
+ '''
910
+ if len(points) < 1:
911
+ return None
912
+
913
+ assert len(image.shape) == 2, 'image should be a 2D ndarray!'
914
+
915
+ roi_list = np.zeros((points.shape[0], roi_size, roi_size), np.float32)
916
+ coord_list = np.zeros_like(points)
917
+ mask = np.zeros(points.shape[0])
918
+
919
+ half_size = roi_size // 2
920
+ y_max, x_max = image.shape
921
+
922
+ for r in nb.prange(points.shape[0]):
923
+ x, y = points[r, :]
924
+
925
+ x_start = int(x - half_size)
926
+ y_start = int(y - half_size)
927
+ x_end = x_start + roi_size
928
+ y_end = y_start + roi_size
929
+
930
+ # Ensure ROI is within image bounds
931
+ # if x_start < 0:
932
+ # x_start = 0
933
+ # x_end = roi_size
934
+ # elif x_end > x_max:
935
+ # x_end = x_max
936
+ # x_start = x_end - roi_size
937
+
938
+ # if y_start < 0:
939
+ # y_start = 0
940
+ # y_end = roi_size
941
+ # elif y_end > y_max:
942
+ # y_end = y_max
943
+ # y_start = y_end - roi_size
944
+
945
+ if x_start < 0 or x_end > x_max or y_start < 0 or y_end > y_max:
946
+ coord_list[r, :] = [np.nan, np.nan]
947
+ roi_list[r] = np.nan
948
+ else:
949
+ coord_list[r, :] = [x_start, y_start]
950
+ roi_list[r] = image[y_start:y_end, x_start:x_end]
951
+ mask[r] = 1
952
+
953
+ return roi_list, coord_list, mask
954
+
955
+
956
+ def find_best_z(point_params: np.ndarray, z0_criteria: str, headers: list[str]):
957
+ '''
958
+ Find the best z0 index based on the given criteria.
959
+ '''
960
+ z0_criteria = z0_criteria.lower().replace(' ', '_')
961
+
962
+ def get_column(header: str):
963
+ return point_params[:, headers.index(header)]
964
+
965
+ if z0_criteria == 'intensity':
966
+ intensities = get_column('intensity')
967
+ return np.argmax(intensities)
968
+ elif z0_criteria == 'min_sigma' and 'sigmax' in headers:
969
+ sigmax = get_column('sigmax')
970
+ return np.argmin(sigmax)
971
+ elif z0_criteria == 'min_sum_sigma' and 'sigmax' in headers and 'sigmay' in headers:
972
+ sigmax = get_column('sigmax')
973
+ sigmay = get_column('sigmay')
974
+ return np.argmin(sigmax + sigmay)
975
+ else:
976
+ argmins = []
977
+ if 'sigmax' in headers and 'sigmay' in headers:
978
+ sigmax = get_column('sigmax')
979
+ sigmay = get_column('sigmay')
980
+ intensities = get_column('intensity')
981
+ argmins.append(np.argmin(sigmax + sigmay))
982
+ argmins.append(np.argmax(intensities))
983
+ else:
984
+ argmins.append(np.argmax(get_column('intensity')))
985
+ if 'sigmax' in headers:
986
+ argmins.append(np.argmin(get_column('sigmax')))
987
+
988
+ # return average index
989
+ return int(np.mean(argmins))
990
+
991
+
992
+ def get_psf_rois(
993
+ stack_handler: Union[TiffSeqHandler, ZarrImageSequence],
994
+ frame_list,
995
+ params,
996
+ crlbs,
997
+ loglike,
998
+ fit_method: int,
999
+ pixel_size: float = 114.17,
1000
+ z_step: float = 10,
1001
+ roi_size: int = 13,
1002
+ upsample: int = 1,
1003
+ roi_info: Optional[tuple] = None,
1004
+ find_z0: bool = False,
1005
+ z0_criteria: str = 'all',
1006
+ channel: int = 0,
1007
+ ) -> 'PSFdata':
1008
+ '''
1009
+ Get PSF ROIs for the given frame list.
1010
+ '''
1011
+ if upsample > 1:
1012
+ roi_size = roi_size * upsample | 1
1013
+ # This uses the bitwise OR operator to ensure the least significant bit is
1014
+ # set to 1, making the number odd.
1015
+ else:
1016
+ upsample = 1
1017
+
1018
+ headers = PARAMETER_HEADERS[fit_method]
1019
+ frame_ids = np.cumsum(np.bincount(np.array(frame_list, np.int64) - 1))
1020
+ frames = np.arange(0, max(frame_list), 1, dtype=np.uint32)
1021
+
1022
+ if find_z0:
1023
+ frame_params = np.zeros((len(frames), len(headers)))
1024
+ for i in range(len(frames)):
1025
+ start_idx = frame_ids[i - 1] if i > 0 else 0
1026
+ end_idx = frame_ids[i]
1027
+ frame_params[i] = np.nanmean(params[slice(start_idx, end_idx)], axis=0)
1028
+
1029
+ z0 = find_best_z(frame_params, z0_criteria, headers)
1030
+ else:
1031
+ # z0 = max(frame_list) // 2
1032
+ z0 = None
1033
+
1034
+ def process_frame(index: int) -> dict[str, Union[int, np.ndarray, list, float]]:
1035
+ image = stack_handler.getSlice(index, channel, 0)
1036
+
1037
+ if roi_info is not None:
1038
+ origin, dim = roi_info
1039
+ slice_y = slice(int(origin[1]), int(origin[1] + dim[1]))
1040
+ slice_x = slice(int(origin[0]), int(origin[0] + dim[0]))
1041
+ image = image[slice_y, slice_x]
1042
+ else:
1043
+ origin = (0, 0)
1044
+ dim = (image.shape[1], image.shape[0])
1045
+
1046
+ if upsample > 1:
1047
+ image = cv2.resize(
1048
+ image,
1049
+ (0, 0),
1050
+ fx=upsample,
1051
+ fy=upsample,
1052
+ interpolation=cv2.INTER_NEAREST,
1053
+ )
1054
+
1055
+ param = params[
1056
+ slice(frame_ids[index - 1] if index > 0 else 0, frame_ids[index])
1057
+ ]
1058
+ crlb = crlbs[slice(frame_ids[index - 1] if index > 0 else 0, frame_ids[index])]
1059
+ logl = loglike[
1060
+ slice(frame_ids[index - 1] if index > 0 else 0, frame_ids[index])
1061
+ ]
1062
+
1063
+ if z0 is None:
1064
+ points = upsample * (param[..., :2] - origin) + (upsample - 1) / 2
1065
+ else:
1066
+ points = (
1067
+ upsample
1068
+ * (
1069
+ params[
1070
+ slice(frame_ids[z0 - 1] if z0 > 0 else 0, frame_ids[z0]),
1071
+ :2,
1072
+ ]
1073
+ - origin
1074
+ )
1075
+ + (upsample - 1) / 2
1076
+ )
1077
+
1078
+ res = get_roi_list(image, points, roi_size)
1079
+
1080
+ if res:
1081
+ rois, coords, mask = res
1082
+ count = rois.shape[0]
1083
+
1084
+ mask = mask.astype(bool)
1085
+
1086
+ param = param[mask]
1087
+ crlb = crlb[mask]
1088
+ logl = logl[mask]
1089
+ else:
1090
+ rois, coords = None, None
1091
+ count = 0
1092
+
1093
+ return {
1094
+ 'index': index,
1095
+ 'rois': rois,
1096
+ 'count': count,
1097
+ 'mean': np.nanmean(rois, axis=0) if count else None,
1098
+ 'median': np.nanmedian(rois, axis=0) if count else None,
1099
+ 'std': np.nanstd(rois, axis=0) if count else None,
1100
+ 'coords': coords,
1101
+ 'params': param,
1102
+ 'crlbs': crlb,
1103
+ 'loglike': logl,
1104
+ }
1105
+
1106
+ with ThreadPoolExecutor() as executor:
1107
+ futures = [executor.submit(process_frame, i) for i in frames]
1108
+ results = [None] * len(frames)
1109
+ for future in tqdm(
1110
+ as_completed(futures), total=len(futures), desc='Extrating PSF ROIs'
1111
+ ):
1112
+ idx = futures.index(future)
1113
+ results[idx] = future.result()
1114
+
1115
+ return PSFdata(
1116
+ {
1117
+ 'zslices': results,
1118
+ 'shape': stack_handler.shapeTCZYX(),
1119
+ 'pixel_size': pixel_size,
1120
+ 'roi_size': roi_size,
1121
+ 'upsample': upsample,
1122
+ 'stack': stack_handler.path,
1123
+ 'roi_info': roi_info,
1124
+ 'fit_method': fit_method,
1125
+ 'zero_plane': z0 if z0 else max(frame_list) // 2,
1126
+ 'z_step': z_step,
1127
+ 'params_headers': headers,
1128
+ }
1129
+ )