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,150 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import fmin
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def robust_mean(
|
|
6
|
+
data: np.ndarray, axis: int=None, k_sigma: float=3, fit_mean: bool=False):
|
|
7
|
+
'''
|
|
8
|
+
Calculate robust mean, standard deviation, and indices of inliers and outliers.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
data : numpy.ndarray
|
|
13
|
+
Input data.
|
|
14
|
+
axis : int, optional
|
|
15
|
+
Axis along which the mean is taken. Default is None.
|
|
16
|
+
k_sigma : float, optional
|
|
17
|
+
Number of sigmas at which to place the cut-off. Default is 3.
|
|
18
|
+
fit_mean : bool, optional
|
|
19
|
+
Whether or not to use fitting to robustly estimate the mean.
|
|
20
|
+
Default is False.
|
|
21
|
+
If True, mean is approximated by minimizing the median deviation.
|
|
22
|
+
If False, mean is approximated by the median.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
final_mean : float
|
|
27
|
+
Robust mean.
|
|
28
|
+
std_sample : float
|
|
29
|
+
Standard deviation of the data (divide by sqrt(n) to get std of the mean).
|
|
30
|
+
inlier_idx : numpy.ndarray
|
|
31
|
+
Index into data with the inliers.
|
|
32
|
+
outlier_idx : numpy.ndarray
|
|
33
|
+
Index into data with the outliers.
|
|
34
|
+
|
|
35
|
+
Warning
|
|
36
|
+
-------
|
|
37
|
+
NaN or Inf will be counted as neither in- nor outlier.
|
|
38
|
+
The code is based on (linear) Least Median Squares and
|
|
39
|
+
can be changed to include weights.
|
|
40
|
+
|
|
41
|
+
References
|
|
42
|
+
----------
|
|
43
|
+
J. Ries https://github.com/jries/SMAP
|
|
44
|
+
|
|
45
|
+
Example
|
|
46
|
+
-------
|
|
47
|
+
>>> import numpy as np
|
|
48
|
+
>>> from scipy.stats import norm
|
|
49
|
+
>>> data = np.concatenate([norm.rvs(loc=0, scale=1, size=100),
|
|
50
|
+
... norm.rvs(loc=10, scale=1, size=5)])
|
|
51
|
+
>>> final_mean, std_sample, inlier_idx, outlier_idx = robust_mean(data)
|
|
52
|
+
'''
|
|
53
|
+
if data.size == 0:
|
|
54
|
+
raise ValueError('Parameter data should be non-empty ndarray!')
|
|
55
|
+
|
|
56
|
+
if axis is None:
|
|
57
|
+
# make sure that the axis is correct if there's a vector
|
|
58
|
+
if np.any(np.array(data.shape) == 1) and len(data.shape) == 2:
|
|
59
|
+
axis = np.where(np.array(data.shape) > 1)[0][0]
|
|
60
|
+
else:
|
|
61
|
+
axis = 0
|
|
62
|
+
|
|
63
|
+
if k_sigma is None:
|
|
64
|
+
k_sigma = 3
|
|
65
|
+
|
|
66
|
+
if fit_mean and len(data.shape) > 1:
|
|
67
|
+
raise ValueError(f'Fitting {len(data.shape)}-D data is not supported!')
|
|
68
|
+
|
|
69
|
+
if np.sum(np.isfinite(data)) < 4:
|
|
70
|
+
print('Warning: Less than 4 finite data points!')
|
|
71
|
+
finite_data = data[np.isfinite(data)]
|
|
72
|
+
final_mean = np.nanmean(finite_data, axis=axis)
|
|
73
|
+
std_sample = np.nanstd(finite_data, axis=axis)
|
|
74
|
+
inlier_idx = np.where(np.isfinite(data))
|
|
75
|
+
outlier_idx = np.array([], dtype=int)
|
|
76
|
+
return final_mean, std_sample, inlier_idx, outlier_idx
|
|
77
|
+
|
|
78
|
+
# LEAST MEDIAN SQUARES
|
|
79
|
+
# define magic numbers:
|
|
80
|
+
magic_number2 = 1.4826**2 # see Danuser, 1992 or Rousseeuw & Leroy, 1987
|
|
81
|
+
|
|
82
|
+
# store data size and reduced data size
|
|
83
|
+
data_size = np.array(data.shape)
|
|
84
|
+
reduced_data_size = data_size.copy()
|
|
85
|
+
reduced_data_size[axis] = 1
|
|
86
|
+
|
|
87
|
+
# need this for later
|
|
88
|
+
blow_up_data_size = data_size // reduced_data_size
|
|
89
|
+
|
|
90
|
+
# count relevant dimensions besides choosen axis
|
|
91
|
+
real_dimensions = np.sum(data_size > 1)
|
|
92
|
+
|
|
93
|
+
# compute median
|
|
94
|
+
if fit_mean:
|
|
95
|
+
# minimize the median deviation from the mean
|
|
96
|
+
median_data = fmin(lambda x: np.median(np.abs(data - x)), np.median(data))
|
|
97
|
+
else:
|
|
98
|
+
median_data = np.nanmedian(data, axis=axis, keepdims=True)
|
|
99
|
+
|
|
100
|
+
# calculate statistics
|
|
101
|
+
res2 = (data - np.tile(median_data, blow_up_data_size))**2
|
|
102
|
+
med_res2 = np.maximum(
|
|
103
|
+
np.nanmedian(res2, axis=axis, keepdims=True), np.finfo(float).eps)
|
|
104
|
+
|
|
105
|
+
# test value to calculate weights
|
|
106
|
+
test_value = res2 / np.tile(magic_number2 * med_res2, blow_up_data_size)
|
|
107
|
+
|
|
108
|
+
if real_dimensions == 1:
|
|
109
|
+
# goodRows: weight 1, badRows: weight 0
|
|
110
|
+
inlier_idx = np.where(test_value <= k_sigma**2)
|
|
111
|
+
outlier_idx = np.where(test_value > k_sigma**2)
|
|
112
|
+
|
|
113
|
+
# calculate std of the sample
|
|
114
|
+
if len(inlier_idx[0]) > 4:
|
|
115
|
+
std_sample = np.sqrt(np.sum(res2[inlier_idx]) / (len(inlier_idx[0]) - 4))
|
|
116
|
+
else:
|
|
117
|
+
std_sample = np.nan
|
|
118
|
+
|
|
119
|
+
# MEAN
|
|
120
|
+
final_mean = np.mean(data[inlier_idx])
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
# goodRows: weight 1, badRows: weight 0
|
|
124
|
+
inlier_idx = np.where(test_value <= k_sigma**2)
|
|
125
|
+
outlier_idx = np.where(test_value > k_sigma**2)
|
|
126
|
+
|
|
127
|
+
# mask outliers
|
|
128
|
+
res2[outlier_idx] = np.nan
|
|
129
|
+
|
|
130
|
+
# count inliers
|
|
131
|
+
n_inliers = np.sum(~np.isnan(res2), axis=axis, keepdims=True)
|
|
132
|
+
|
|
133
|
+
# calculate std of the sample
|
|
134
|
+
|
|
135
|
+
# put NaN wherever there are not enough data points to calculate a
|
|
136
|
+
# standard deviation
|
|
137
|
+
good_idx = np.sum(np.isfinite(res2), axis=axis, keepdims=True) > 4
|
|
138
|
+
good_idx_broadcasted = np.broadcast_to(good_idx, res2.shape)
|
|
139
|
+
std_sample = np.full_like(good_idx, np.nan, dtype=float).squeeze()
|
|
140
|
+
if np.any(good_idx_broadcasted):
|
|
141
|
+
std_sample[:] = np.sqrt(
|
|
142
|
+
np.nansum(
|
|
143
|
+
res2 * good_idx_broadcasted, axis=axis) / (n_inliers[good_idx] - 4)
|
|
144
|
+
).squeeze()
|
|
145
|
+
|
|
146
|
+
# MEAN
|
|
147
|
+
data[outlier_idx] = np.nan
|
|
148
|
+
final_mean = np.nanmean(data, axis=axis)
|
|
149
|
+
|
|
150
|
+
return final_mean, std_sample, inlier_idx, outlier_idx
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
from scipy.interpolate import UnivariateSpline, interp1d
|
|
4
|
+
|
|
5
|
+
from microEye.analysis.fitting.pyfit3Dcspline.PSF.rubost_mean import robust_mean
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Localization:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.frames = None
|
|
11
|
+
self.frame_z0 = None
|
|
12
|
+
self.rois = None
|
|
13
|
+
|
|
14
|
+
self.fit_x = None
|
|
15
|
+
self.fit_y = None
|
|
16
|
+
self.fit_sigmax = None
|
|
17
|
+
self.fit_sigmay = None
|
|
18
|
+
self.fit_intensity = None
|
|
19
|
+
self.fit_background = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_spline_fits(beads: list[Localization], **kwargs):
|
|
23
|
+
# Initialize an empty list to store information about each curve
|
|
24
|
+
curves = []
|
|
25
|
+
dz = kwargs.get('dz', 10)
|
|
26
|
+
|
|
27
|
+
# Iterate over beads in reverse order
|
|
28
|
+
for bead in beads:
|
|
29
|
+
beadz0 = bead.frame_z0 * dz
|
|
30
|
+
|
|
31
|
+
if 'astig' in kwargs.get('zcorr', '') or 'corr' in kwargs.get('zcorr', ''):
|
|
32
|
+
beadz = (bead.frames * dz) - beadz0
|
|
33
|
+
else:
|
|
34
|
+
beadz = (bead.frames - kwargs['midpoint']) * dz
|
|
35
|
+
|
|
36
|
+
sx = bead.fit_sigmax
|
|
37
|
+
sy = bead.fit_sigmay
|
|
38
|
+
z = beadz
|
|
39
|
+
phot = bead.fit_intensity
|
|
40
|
+
|
|
41
|
+
# Filter values based on the specified z range
|
|
42
|
+
mask = (z >= kwargs['gaussrange'][0]) & (z <= kwargs['gaussrange'][1])
|
|
43
|
+
|
|
44
|
+
# Store information about the current curve
|
|
45
|
+
curves.append({
|
|
46
|
+
'sx': np.array(sx[mask]),
|
|
47
|
+
'sy': np.array(sy[mask]),
|
|
48
|
+
'z': np.array(z[mask]),
|
|
49
|
+
'phot': np.array(phot[mask]),
|
|
50
|
+
'xpos': np.mean(bead.fit_x),
|
|
51
|
+
'ypos': np.mean(bead.fit_y),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
# Get calibrations
|
|
55
|
+
kwargs['ax'] = kwargs['ax_z']
|
|
56
|
+
spline, indgood = clean_and_get_spline(curves, **kwargs)
|
|
57
|
+
|
|
58
|
+
return spline, indgood, curves
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def clean_and_get_spline(curves, **kwargs):
|
|
62
|
+
"""
|
|
63
|
+
Cleans the curves, fits splines, and returns the results.
|
|
64
|
+
|
|
65
|
+
Parameters:
|
|
66
|
+
- curves: List of curves containing 'sx', 'sy', and 'z'.
|
|
67
|
+
- p: Dictionary of parameters.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
- spline_result: Dictionary with 'x', 'y', 'zrange', and 'maxmaxrange'.
|
|
71
|
+
- indgood_result: Boolean array indicating 'good' curves.
|
|
72
|
+
"""
|
|
73
|
+
za = np.concatenate([curve['z'] for curve in curves])
|
|
74
|
+
Sxa = np.concatenate([curve['sx'] for curve in curves])
|
|
75
|
+
Sya = np.concatenate([curve['sy'] for curve in curves])
|
|
76
|
+
|
|
77
|
+
indz = (za > kwargs['gaussrange'][0]) & (za < kwargs['gaussrange'][2])
|
|
78
|
+
z = za[indz]
|
|
79
|
+
Sx = Sxa[indz]
|
|
80
|
+
Sy = Sya[indz]
|
|
81
|
+
|
|
82
|
+
splinex = get_spline_interpolator(Sx, z, np.ones_like(z))
|
|
83
|
+
spliney = get_spline_interpolator(Sy, z, np.ones_like(z))
|
|
84
|
+
|
|
85
|
+
indgood2 = np.ones(len(curves), dtype=bool)
|
|
86
|
+
|
|
87
|
+
zg = np.concatenate([curve['z'][indgood2[i]] for i, curve in enumerate(curves)])
|
|
88
|
+
indz = (zg > kwargs['gaussrange'][0]) & (zg < kwargs['gaussrange'][2])
|
|
89
|
+
zg = zg[indz]
|
|
90
|
+
sxg = np.concatenate([curve['sx'][indgood2[i]] for i, curve in enumerate(curves)])
|
|
91
|
+
syg = np.concatenate([curve['sy'][indgood2[i]] for i, curve in enumerate(curves)])
|
|
92
|
+
sxg = sxg[indz]
|
|
93
|
+
syg = syg[indz]
|
|
94
|
+
|
|
95
|
+
splinex2 = get_spline_interpolator(sxg, zg, 1. / (np.abs(sxg - splinex(zg)) + 0.1))
|
|
96
|
+
spliney2 = get_spline_interpolator(syg, zg, 1. / (np.abs(syg - spliney(zg)) + 0.1))
|
|
97
|
+
|
|
98
|
+
zt = np.arange(min(zg), max(zg) + 0.01, 0.01)
|
|
99
|
+
|
|
100
|
+
plot_curves_and_spline(curves, indgood2, splinex2, spliney2, zt)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
'x': splinex2, 'y': spliney2,
|
|
104
|
+
'zrange': [zt[0], zt[-1]],
|
|
105
|
+
'maxmaxrange': [min(zg), max(zg)]}, indgood2
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_spline_interpolator(S, z, weights, smoothing_param=0.96):
|
|
109
|
+
'''
|
|
110
|
+
Returns a 1D cubic spline interpolator for given data.
|
|
111
|
+
|
|
112
|
+
Parameters:
|
|
113
|
+
- S: Values to be interpolated.
|
|
114
|
+
- z: Corresponding positions.
|
|
115
|
+
- weights: Weights for each data point.
|
|
116
|
+
- smoothing_param: Smoothing parameter for the spline.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- spline: Cubic spline interpolator.
|
|
120
|
+
'''
|
|
121
|
+
sorted_indices = np.argsort(z)
|
|
122
|
+
sorted_z = np.sort(z)
|
|
123
|
+
sorted_S = S[sorted_indices]
|
|
124
|
+
sorted_weights = weights[sorted_indices]
|
|
125
|
+
|
|
126
|
+
spline = UnivariateSpline(
|
|
127
|
+
sorted_z, sorted_S, w=sorted_weights, k=3, s=smoothing_param)
|
|
128
|
+
|
|
129
|
+
return spline
|
|
130
|
+
|
|
131
|
+
def plot_curves_and_spline(curves, indgood2, spline_x, spline_y, z_range):
|
|
132
|
+
"""
|
|
133
|
+
Plots the curves, distinguished between 'good' and 'bad', and the splines.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
- curves: List of curves containing 'sx', 'sy', and 'z'.
|
|
137
|
+
- indgood2: Boolean array indicating whether each curve is 'good'.
|
|
138
|
+
- spline_x: Interpolator for the x-coordinate.
|
|
139
|
+
- spline_y: Interpolator for the y-coordinate.
|
|
140
|
+
- z_range: Range of z values for plotting.
|
|
141
|
+
"""
|
|
142
|
+
z1a, z2a, x1a, x2a = [], [], [], []
|
|
143
|
+
|
|
144
|
+
for k, curve in enumerate(curves):
|
|
145
|
+
if indgood2[k]:
|
|
146
|
+
z1a.extend(curve['z'])
|
|
147
|
+
x1a.extend(curve['sx'])
|
|
148
|
+
else:
|
|
149
|
+
z2a.extend(curve['z'])
|
|
150
|
+
x2a.extend(curve['sx'])
|
|
151
|
+
|
|
152
|
+
if not z2a: # If z2a is empty, plot a bad point to have the legend correct
|
|
153
|
+
z2a = [0]
|
|
154
|
+
x2a = [0]
|
|
155
|
+
|
|
156
|
+
plt.plot(z2a, x2a, 'g.', label='Bad Curves')
|
|
157
|
+
plt.plot(z1a, x1a, 'r.', label='Good Curves')
|
|
158
|
+
plt.plot(z_range, spline_x(z_range), 'k', label='Spline X')
|
|
159
|
+
plt.plot(z_range, spline_y(z_range), 'k', label='Spline Y')
|
|
160
|
+
|
|
161
|
+
plt.xlim([z_range[0], z_range[-1]])
|
|
162
|
+
plt.ylim([0, min(5, max(max(spline_x(z_range)), max(spline_y(z_range))))])
|
|
163
|
+
plt.xlabel('z (nm)')
|
|
164
|
+
plt.ylabel('PSFx, PSFy (pixel)')
|
|
165
|
+
plt.title('Lateral size of the PSF')
|
|
166
|
+
plt.legend()
|
|
167
|
+
plt.show()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from microEye.analysis.fitting.psf.stats.core import (
|
|
2
|
+
ConfidenceMethod,
|
|
3
|
+
StatConfig,
|
|
4
|
+
StatsCalculator,
|
|
5
|
+
)
|
|
6
|
+
from microEye.analysis.fitting.psf.stats.curve_fit import (
|
|
7
|
+
CurveAnalyzer,
|
|
8
|
+
CurveFitMethod,
|
|
9
|
+
CurveResult,
|
|
10
|
+
)
|
|
11
|
+
from microEye.analysis.fitting.psf.stats.io import export_fit_curve, import_fit_curve
|
|
12
|
+
from microEye.analysis.fitting.psf.stats.slope_fit import SlopeAnalyzer, SlopeResult
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Callable, Optional, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from scipy import stats
|
|
7
|
+
|
|
8
|
+
from microEye.analysis.fitting.results import PARAMETER_HEADERS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfidenceMethod(Enum):
|
|
12
|
+
NONE = 'None'
|
|
13
|
+
MIN_MAX = 'min_max'
|
|
14
|
+
T_DIST = 't_dist'
|
|
15
|
+
MEDIAN_STD = 'median_std'
|
|
16
|
+
PERCENTILE = 'percentile'
|
|
17
|
+
BOOTSTRAP = 'bootstrap'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StatConfig:
|
|
22
|
+
'''Configuration for a statistic calculation'''
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
required_params: list[str] # Parameters required from header
|
|
26
|
+
calculator: Callable # Function to calculate the statistic
|
|
27
|
+
multi_output: bool = False # Whether the stat returns multiple values
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StatsCalculator:
|
|
31
|
+
def __init__(self, zero_plane: int, z_step: float, fitting_method: str):
|
|
32
|
+
self.zero_plane = zero_plane
|
|
33
|
+
self.z_step = z_step
|
|
34
|
+
self.fitting_method = fitting_method
|
|
35
|
+
self.header = PARAMETER_HEADERS[fitting_method]
|
|
36
|
+
self._register_stats()
|
|
37
|
+
|
|
38
|
+
def _register_stats(self):
|
|
39
|
+
'''Register all available statistics calculations'''
|
|
40
|
+
self.available_stats: dict[str, StatConfig] = {
|
|
41
|
+
'Counts': StatConfig(
|
|
42
|
+
name='Counts', required_params=[], calculator=lambda rois, _: len(rois)
|
|
43
|
+
),
|
|
44
|
+
'Sigma': StatConfig(
|
|
45
|
+
name='Sigma',
|
|
46
|
+
required_params=['sigmax', 'sigmay'],
|
|
47
|
+
calculator=self._calc_sigma,
|
|
48
|
+
multi_output=True,
|
|
49
|
+
),
|
|
50
|
+
'Sigma (sum)': StatConfig(
|
|
51
|
+
name='Sigma (sum)',
|
|
52
|
+
required_params=['sigmax', 'sigmay'],
|
|
53
|
+
calculator=lambda data_x, data_y: data_x + data_y,
|
|
54
|
+
),
|
|
55
|
+
'Sigma (diff)': StatConfig(
|
|
56
|
+
name='Sigma (diff)',
|
|
57
|
+
required_params=['sigmax', 'sigmay'],
|
|
58
|
+
calculator=lambda data_x, data_y: data_x - data_y,
|
|
59
|
+
),
|
|
60
|
+
'Sigma (abs(diff))': StatConfig(
|
|
61
|
+
name='Sigma (abs(diff))',
|
|
62
|
+
required_params=['sigmax', 'sigmay'],
|
|
63
|
+
calculator=lambda data_x, data_y: np.abs(data_x - data_y),
|
|
64
|
+
),
|
|
65
|
+
'Sigma (x/y)': StatConfig(
|
|
66
|
+
name='Sigma (x/y)',
|
|
67
|
+
required_params=['sigmax', 'sigmay'],
|
|
68
|
+
calculator=lambda data_x, data_y: np.divide(
|
|
69
|
+
data_x, data_y, out=np.zeros_like(data_x), where=data_y != 0
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
'Sigma² (diff)': StatConfig(
|
|
73
|
+
name='Sigma² (diff)',
|
|
74
|
+
required_params=['sigmax', 'sigmay'],
|
|
75
|
+
calculator=lambda data_x, data_y: np.square(data_x) - np.square(data_y),
|
|
76
|
+
),
|
|
77
|
+
'Intensity': StatConfig(
|
|
78
|
+
name='Intensity',
|
|
79
|
+
required_params=['intensity'],
|
|
80
|
+
calculator=lambda data: data,
|
|
81
|
+
),
|
|
82
|
+
'Background': StatConfig(
|
|
83
|
+
name='Background',
|
|
84
|
+
required_params=['background'],
|
|
85
|
+
calculator=lambda data: data,
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def _calc_sigma(
|
|
90
|
+
self, data_x: Optional[np.ndarray], data_y: Optional[np.ndarray]
|
|
91
|
+
) -> list[np.ndarray]:
|
|
92
|
+
'''Calculate sigma statistics for x and y dimensions'''
|
|
93
|
+
results = []
|
|
94
|
+
for data in [data_x, data_y]:
|
|
95
|
+
if data is not None:
|
|
96
|
+
results.append(data)
|
|
97
|
+
return results
|
|
98
|
+
|
|
99
|
+
def _get_param_data(self, z_slice: dict, param_name: str) -> Optional[np.ndarray]:
|
|
100
|
+
'''Extract parameter data from z_slice if parameter exists'''
|
|
101
|
+
if param_name in self.header:
|
|
102
|
+
return z_slice['params'][:, self.header.index(param_name)]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _calculate_confidence_interval(
|
|
106
|
+
self,
|
|
107
|
+
data: np.ndarray,
|
|
108
|
+
method: ConfidenceMethod,
|
|
109
|
+
confidence_level: float = 0.95,
|
|
110
|
+
n_bootstrap: int = 1000,
|
|
111
|
+
) -> tuple[float, float]:
|
|
112
|
+
'''Calculate confidence intervals using various methods'''
|
|
113
|
+
if len(data) == 0 or method == ConfidenceMethod.NONE:
|
|
114
|
+
return np.nan, np.nan
|
|
115
|
+
|
|
116
|
+
if method == ConfidenceMethod.MIN_MAX:
|
|
117
|
+
return np.nanmin(data), np.nanmax(data)
|
|
118
|
+
|
|
119
|
+
elif method == ConfidenceMethod.T_DIST:
|
|
120
|
+
if len(data) < 2:
|
|
121
|
+
return np.nan, np.nan
|
|
122
|
+
mean = np.nanmean(data)
|
|
123
|
+
std_err = np.nanstd(data, ddof=1) / np.sqrt(len(data))
|
|
124
|
+
t_value = stats.t.ppf((1 + confidence_level) / 2, len(data) - 1)
|
|
125
|
+
margin = t_value * std_err
|
|
126
|
+
return mean - margin, mean + margin
|
|
127
|
+
|
|
128
|
+
elif method == ConfidenceMethod.MEDIAN_STD:
|
|
129
|
+
median = np.nanmedian(data)
|
|
130
|
+
std = np.nanstd(data)
|
|
131
|
+
return median - std, median + std
|
|
132
|
+
|
|
133
|
+
elif method == ConfidenceMethod.PERCENTILE:
|
|
134
|
+
lower = (1 - confidence_level) / 2 * 100
|
|
135
|
+
upper = (1 + confidence_level) / 2 * 100
|
|
136
|
+
return np.nanpercentile(data, [lower, upper])
|
|
137
|
+
|
|
138
|
+
elif method == ConfidenceMethod.BOOTSTRAP:
|
|
139
|
+
if len(data) < 2:
|
|
140
|
+
return np.nan, np.nan
|
|
141
|
+
bootstrap_means = []
|
|
142
|
+
for _ in range(n_bootstrap):
|
|
143
|
+
sample = np.random.choice(data, size=len(data), replace=True)
|
|
144
|
+
bootstrap_means.append(np.nanmean(sample))
|
|
145
|
+
return np.percentile(
|
|
146
|
+
bootstrap_means,
|
|
147
|
+
[(1 - confidence_level) / 2 * 100, (1 + confidence_level) / 2 * 100],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _calculate_stats(
|
|
151
|
+
self,
|
|
152
|
+
z_slice: dict[str, Union[int, np.ndarray, list, float]],
|
|
153
|
+
stat_config: StatConfig,
|
|
154
|
+
confidence_method: ConfidenceMethod = ConfidenceMethod.MIN_MAX,
|
|
155
|
+
confidence_level: float = 0.95,
|
|
156
|
+
) -> tuple[list[float], list[float], list[float]]:
|
|
157
|
+
'''Calculate statistics for a single z-slice'''
|
|
158
|
+
if z_slice['rois'] is None:
|
|
159
|
+
return self._get_nan_result(stat_config.multi_output)
|
|
160
|
+
|
|
161
|
+
valid_rois = z_slice['rois'][~np.isnan(z_slice['rois']).any(axis=(1, 2))]
|
|
162
|
+
|
|
163
|
+
if len(valid_rois) == 0 and stat_config.name != 'Counts':
|
|
164
|
+
return self._get_nan_result(stat_config.multi_output)
|
|
165
|
+
|
|
166
|
+
if stat_config.name == 'Counts':
|
|
167
|
+
return [len(valid_rois)], [len(valid_rois)], [len(valid_rois)]
|
|
168
|
+
|
|
169
|
+
# Extract required parameters
|
|
170
|
+
param_data = [
|
|
171
|
+
self._get_param_data(z_slice, param)
|
|
172
|
+
for param in stat_config.required_params
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
any(data is None for data in param_data)
|
|
177
|
+
and len(stat_config.required_params) > 0
|
|
178
|
+
):
|
|
179
|
+
return self._get_nan_result(stat_config.multi_output)
|
|
180
|
+
|
|
181
|
+
# Calculate statistics
|
|
182
|
+
result = stat_config.calculator(*param_data)
|
|
183
|
+
if stat_config.multi_output:
|
|
184
|
+
means = [np.nanmean(r) for r in result]
|
|
185
|
+
conf_intervals = [
|
|
186
|
+
self._calculate_confidence_interval(
|
|
187
|
+
r, confidence_method, confidence_level
|
|
188
|
+
)
|
|
189
|
+
for r in result
|
|
190
|
+
]
|
|
191
|
+
return (
|
|
192
|
+
means,
|
|
193
|
+
[ci[0] for ci in conf_intervals],
|
|
194
|
+
[ci[1] for ci in conf_intervals],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
mean = np.nanmean(result)
|
|
198
|
+
lower, upper = self._calculate_confidence_interval(
|
|
199
|
+
result, confidence_method, confidence_level
|
|
200
|
+
)
|
|
201
|
+
return [mean], [lower], [upper]
|
|
202
|
+
|
|
203
|
+
def _get_nan_result(
|
|
204
|
+
self, multi_output: bool
|
|
205
|
+
) -> tuple[list[float], list[float], list[float]]:
|
|
206
|
+
'''Return NaN results based on whether multiple outputs are expected'''
|
|
207
|
+
if multi_output:
|
|
208
|
+
return [np.nan, np.nan], [np.nan, np.nan], [np.nan, np.nan]
|
|
209
|
+
return [np.nan], [np.nan], [np.nan]
|
|
210
|
+
|
|
211
|
+
def get_stats(
|
|
212
|
+
self,
|
|
213
|
+
zslices: list[dict[str, Union[int, np.ndarray, list, float]]],
|
|
214
|
+
selected_stat: str,
|
|
215
|
+
confidence_method: ConfidenceMethod = ConfidenceMethod.MIN_MAX,
|
|
216
|
+
confidence_level: float = 0.95,
|
|
217
|
+
):
|
|
218
|
+
'''
|
|
219
|
+
Get the statistic data for all z-slices.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
selected_stat : str
|
|
224
|
+
The selected statistic to return
|
|
225
|
+
|
|
226
|
+
Returns
|
|
227
|
+
-------
|
|
228
|
+
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
229
|
+
Z-indices, statistic data, minimum data, maximum
|
|
230
|
+
'''
|
|
231
|
+
if selected_stat not in self.available_stats:
|
|
232
|
+
raise ValueError(f'Unknown statistic: {selected_stat}')
|
|
233
|
+
|
|
234
|
+
z_indices = (np.arange(len(zslices)) - self.zero_plane) * self.z_step
|
|
235
|
+
stat_config = self.available_stats[selected_stat]
|
|
236
|
+
|
|
237
|
+
param_stat = []
|
|
238
|
+
param_lower = []
|
|
239
|
+
param_upper = []
|
|
240
|
+
|
|
241
|
+
# Initialize lists for multi-output statistics
|
|
242
|
+
if stat_config.multi_output:
|
|
243
|
+
param_stat = [[] for _ in range(len(stat_config.required_params))]
|
|
244
|
+
param_lower = [[] for _ in range(len(stat_config.required_params))]
|
|
245
|
+
param_upper = [[] for _ in range(len(stat_config.required_params))]
|
|
246
|
+
|
|
247
|
+
for z_slice in zslices:
|
|
248
|
+
stats, mins, maxs = self._calculate_stats(
|
|
249
|
+
z_slice, stat_config, confidence_method, confidence_level
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if stat_config.multi_output:
|
|
253
|
+
for i, (stat, min_val, max_val) in enumerate(zip(stats, mins, maxs)):
|
|
254
|
+
param_stat[i].append(stat)
|
|
255
|
+
param_lower[i].append(min_val)
|
|
256
|
+
param_upper[i].append(max_val)
|
|
257
|
+
else:
|
|
258
|
+
param_stat.extend(stats)
|
|
259
|
+
param_lower.extend(mins)
|
|
260
|
+
param_upper.extend(maxs)
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
z_indices,
|
|
264
|
+
np.array(param_stat),
|
|
265
|
+
np.array(param_lower),
|
|
266
|
+
np.array(param_upper),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def add_statistic(
|
|
270
|
+
self,
|
|
271
|
+
name: str,
|
|
272
|
+
required_params: list[str],
|
|
273
|
+
calculator: Callable,
|
|
274
|
+
multi_output: bool = False,
|
|
275
|
+
):
|
|
276
|
+
'''
|
|
277
|
+
Add a new statistic calculation method.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
name : str
|
|
282
|
+
Name of the new statistic
|
|
283
|
+
required_params : list[str]
|
|
284
|
+
List of parameter names required from the header
|
|
285
|
+
calculator : Callable
|
|
286
|
+
Function to calculate the statistic
|
|
287
|
+
multi_output : bool, optional
|
|
288
|
+
Whether the statistic returns multiple values, by default False
|
|
289
|
+
'''
|
|
290
|
+
self.available_stats[name] = StatConfig(
|
|
291
|
+
name=name,
|
|
292
|
+
required_params=required_params,
|
|
293
|
+
calculator=calculator,
|
|
294
|
+
multi_output=multi_output,
|
|
295
|
+
)
|