microeye 2.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- microEye/__init__.py +47 -0
- microEye/_version.py +2 -0
- microEye/analysis/__init__.py +1 -0
- microEye/analysis/checklist_dialog.py +143 -0
- microEye/analysis/cmosMaps.py +228 -0
- microEye/analysis/filters/__init__.py +9 -0
- microEye/analysis/filters/base.py +21 -0
- microEye/analysis/filters/spatial.py +338 -0
- microEye/analysis/filters/temporal.py +76 -0
- microEye/analysis/fitting/__init__.py +0 -0
- microEye/analysis/fitting/fit.py +680 -0
- microEye/analysis/fitting/nena.py +375 -0
- microEye/analysis/fitting/phasor_fit.py +90 -0
- microEye/analysis/fitting/processing.py +317 -0
- microEye/analysis/fitting/psf/__init__.py +6 -0
- microEye/analysis/fitting/psf/extract.py +1129 -0
- microEye/analysis/fitting/psf/rubost_mean.py +150 -0
- microEye/analysis/fitting/psf/spline.py +167 -0
- microEye/analysis/fitting/psf/stats/__init__.py +12 -0
- microEye/analysis/fitting/psf/stats/core.py +295 -0
- microEye/analysis/fitting/psf/stats/curve_fit.py +708 -0
- microEye/analysis/fitting/psf/stats/io.py +104 -0
- microEye/analysis/fitting/psf/stats/slope_fit.py +171 -0
- microEye/analysis/fitting/psf/temp.py +147 -0
- microEye/analysis/fitting/psf/test.py +47 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUfunctions.py +657 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUmleFit_LM.py +1336 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/CPUsplineLib.py +270 -0
- microEye/analysis/fitting/pyfit3Dcspline/CPU/__init__.py +1 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUfunctions.py +609 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_EMCCD.py +1396 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUmleFit_LM_sCMOS.py +1426 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/GPUsplineLib.py +231 -0
- microEye/analysis/fitting/pyfit3Dcspline/GPU/__init__.py +2 -0
- microEye/analysis/fitting/pyfit3Dcspline/__init__.py +10 -0
- microEye/analysis/fitting/pyfit3Dcspline/constants.py +27 -0
- microEye/analysis/fitting/pyfit3Dcspline/mainfunctions.py +903 -0
- microEye/analysis/fitting/results.py +917 -0
- microEye/analysis/fitting/results_stats.py +251 -0
- microEye/analysis/fitting/tardis.py +209 -0
- microEye/analysis/multi_viewer.py +568 -0
- microEye/analysis/processing/__init__.py +1 -0
- microEye/analysis/processing/frc.py +120 -0
- microEye/analysis/rendering/__init__.py +3 -0
- microEye/analysis/rendering/base.py +452 -0
- microEye/analysis/rendering/cloud.py +215 -0
- microEye/analysis/rendering/core.py +47 -0
- microEye/analysis/rendering/volumetric.py +198 -0
- microEye/analysis/tools/__init__.py +0 -0
- microEye/analysis/tools/kymograms.py +1079 -0
- microEye/analysis/tools/roi_selectors.py +556 -0
- microEye/analysis/utils/__init__.py +3 -0
- microEye/analysis/utils/coordinates.py +18 -0
- microEye/analysis/utils/images.py +68 -0
- microEye/analysis/utils/windows.py +19 -0
- microEye/analysis/viewer/__init__.py +3 -0
- microEye/analysis/viewer/image_options_widget.py +615 -0
- microEye/analysis/viewer/images.py +1225 -0
- microEye/analysis/viewer/layers_widget.py +415 -0
- microEye/analysis/viewer/localizations.py +1304 -0
- microEye/analysis/viewer/psf.py +1003 -0
- microEye/analysis/viewer/volume.py +456 -0
- microEye/hardware/__init__.py +3 -0
- microEye/hardware/cams/__init__.py +29 -0
- microEye/hardware/cams/camera_calibration.py +99 -0
- microEye/hardware/cams/camera_list.py +528 -0
- microEye/hardware/cams/camera_options.py +694 -0
- microEye/hardware/cams/camera_panel.py +941 -0
- microEye/hardware/cams/dummy/__init__.py +1 -0
- microEye/hardware/cams/dummy/dummy_panel.py +759 -0
- microEye/hardware/cams/jobs.py +497 -0
- microEye/hardware/cams/line_profiler.py +99 -0
- microEye/hardware/cams/linescan/IR_Cam.py +474 -0
- microEye/hardware/cams/linescan/__init__.py +1 -0
- microEye/hardware/cams/micam.py +524 -0
- microEye/hardware/cams/pco/__init__.py +58 -0
- microEye/hardware/cams/pco/enums.py +382 -0
- microEye/hardware/cams/pco/pco_cam.py +761 -0
- microEye/hardware/cams/pco/pco_panel.py +477 -0
- microEye/hardware/cams/shortcuts.py +312 -0
- microEye/hardware/cams/thorlabs/__init__.py +1 -0
- microEye/hardware/cams/thorlabs/thorlabs.py +1508 -0
- microEye/hardware/cams/thorlabs/thorlabs_panel.py +850 -0
- microEye/hardware/cams/ueye/__init__.py +1 -0
- microEye/hardware/cams/ueye/ueye_camera.py +1023 -0
- microEye/hardware/cams/ueye/ueye_panel.py +861 -0
- microEye/hardware/cams/vimba/__init__.py +1 -0
- microEye/hardware/cams/vimba/vimba_cam.py +1000 -0
- microEye/hardware/cams/vimba/vimba_panel.py +813 -0
- microEye/hardware/device.py +60 -0
- microEye/hardware/lasers/__init__.py +13 -0
- microEye/hardware/lasers/io_matchbox.py +791 -0
- microEye/hardware/lasers/io_params.py +85 -0
- microEye/hardware/lasers/io_single_laser.py +742 -0
- microEye/hardware/lasers/laser_relay.py +594 -0
- microEye/hardware/mieye/__init__.py +1 -0
- microEye/hardware/mieye/acquisition_manager.py +467 -0
- microEye/hardware/mieye/devices_manager.py +533 -0
- microEye/hardware/mieye/miEye.py +659 -0
- microEye/hardware/misc/__init__.py +0 -0
- microEye/hardware/misc/acquisition_view.py +71 -0
- microEye/hardware/misc/reglo.py +761 -0
- microEye/hardware/misc/temp.py +188 -0
- microEye/hardware/port_config.py +59 -0
- microEye/hardware/protocols/__init__.py +2 -0
- microEye/hardware/protocols/actions.py +402 -0
- microEye/hardware/protocols/actions_items.py +703 -0
- microEye/hardware/protocols/designer.py +244 -0
- microEye/hardware/protocols/scene_manager.py +191 -0
- microEye/hardware/protocols/serialization.py +97 -0
- microEye/hardware/pycromanager/__init__.py +16 -0
- microEye/hardware/pycromanager/core.py +1433 -0
- microEye/hardware/pycromanager/devices.py +461 -0
- microEye/hardware/pycromanager/enums.py +107 -0
- microEye/hardware/pycromanager/headless.py +153 -0
- microEye/hardware/pycromanager/utils.py +34 -0
- microEye/hardware/pycromanager/widgets/__init__.py +5 -0
- microEye/hardware/pycromanager/widgets/bridges.py +407 -0
- microEye/hardware/pycromanager/widgets/headless_manager.py +258 -0
- microEye/hardware/pycromanager/widgets/headless_options.py +224 -0
- microEye/hardware/pycromanager/widgets/pycro_panel.py +455 -0
- microEye/hardware/stages/__init__.py +18 -0
- microEye/hardware/stages/elliptec/__init__.py +5 -0
- microEye/hardware/stages/elliptec/baseDevice.py +314 -0
- microEye/hardware/stages/elliptec/device.py +384 -0
- microEye/hardware/stages/elliptec/deviceID.py +212 -0
- microEye/hardware/stages/elliptec/devicePort.py +379 -0
- microEye/hardware/stages/elliptec/deviceStatus.py +65 -0
- microEye/hardware/stages/elliptec/devicesView.py +706 -0
- microEye/hardware/stages/elliptec/ellDevices.py +134 -0
- microEye/hardware/stages/elliptec/messageUpdater.py +34 -0
- microEye/hardware/stages/elliptec/motorInfo.py +153 -0
- microEye/hardware/stages/elliptec/stage.py +62 -0
- microEye/hardware/stages/elliptec/test.py +139 -0
- microEye/hardware/stages/kinesis/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/__init__.py +1 -0
- microEye/hardware/stages/kinesis/kdc101/enums.py +1002 -0
- microEye/hardware/stages/kinesis/kdc101/factory.py +171 -0
- microEye/hardware/stages/kinesis/kdc101/kdc101.py +718 -0
- microEye/hardware/stages/kinesis/kinesis.py +776 -0
- microEye/hardware/stages/piezo_concept.py +607 -0
- microEye/hardware/stages/stabilizer.py +785 -0
- microEye/hardware/stages/stage.py +89 -0
- microEye/hardware/widgets/__init__.py +10 -0
- microEye/hardware/widgets/controller.py +246 -0
- microEye/hardware/widgets/devices.py +133 -0
- microEye/hardware/widgets/focusWidget.py +264 -0
- microEye/hardware/widgets/qlist_slider.py +113 -0
- microEye/hardware/widgets/scan_acquisition.py +424 -0
- microEye/icons/1024.png +0 -0
- microEye/icons/128.png +0 -0
- microEye/icons/16.png +0 -0
- microEye/icons/24.png +0 -0
- microEye/icons/256.png +0 -0
- microEye/icons/32.png +0 -0
- microEye/icons/48.png +0 -0
- microEye/icons/512.png +0 -0
- microEye/icons/64.png +0 -0
- microEye/icons/__init__.py +0 -0
- microEye/icons/close.svg +88 -0
- microEye/icons/mieye.png +0 -0
- microEye/icons/min.svg +83 -0
- microEye/icons/viewer.png +0 -0
- microEye/launcher.py +42 -0
- microEye/qt.py +181 -0
- microEye/utils/__init__.py +2 -0
- microEye/utils/enum_encoder.py +10 -0
- microEye/utils/expandable_groupbox.py +93 -0
- microEye/utils/gui_helper.py +457 -0
- microEye/utils/hid/__init__.py +8 -0
- microEye/utils/hid/controller.py +153 -0
- microEye/utils/hid/device.py +63 -0
- microEye/utils/hid/enums.py +140 -0
- microEye/utils/hid/utils.py +58 -0
- microEye/utils/labelled_slider.py +134 -0
- microEye/utils/metadata.py +599 -0
- microEye/utils/metadata_tree.py +718 -0
- microEye/utils/micro_launcher.py +245 -0
- microEye/utils/parameter_tree.py +325 -0
- microEye/utils/pyscripting.py +444 -0
- microEye/utils/retry_exec.py +35 -0
- microEye/utils/start_gui.py +112 -0
- microEye/utils/thread_worker.py +115 -0
- microEye/utils/uImage.py +1512 -0
- microeye-2.3.2.dist-info/METADATA +401 -0
- microeye-2.3.2.dist-info/RECORD +190 -0
- microeye-2.3.2.dist-info/WHEEL +5 -0
- microeye-2.3.2.dist-info/entry_points.txt +2 -0
- microeye-2.3.2.dist-info/licenses/LICENSE +674 -0
- microeye-2.3.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
)
|