phasorpy 0.7__cp314-cp314-win_arm64.whl → 0.8__cp314-cp314-win_arm64.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.
- phasorpy/__init__.py +1 -1
- phasorpy/_phasorpy.cp314-win_arm64.pyd +0 -0
- phasorpy/_phasorpy.pyx +39 -1
- phasorpy/_utils.py +13 -5
- phasorpy/cluster.py +2 -2
- phasorpy/component.py +10 -6
- phasorpy/datasets.py +1 -1
- phasorpy/experimental.py +1 -163
- phasorpy/filter.py +966 -0
- phasorpy/io/__init__.py +2 -1
- phasorpy/io/_flimlabs.py +6 -6
- phasorpy/io/_leica.py +36 -34
- phasorpy/io/_ometiff.py +8 -6
- phasorpy/io/_other.py +3 -3
- phasorpy/io/_simfcs.py +11 -8
- phasorpy/lifetime.py +16 -16
- phasorpy/phasor.py +122 -642
- phasorpy/plot/_functions.py +6 -6
- phasorpy/plot/_lifetime_plots.py +1 -1
- phasorpy/plot/_phasorplot.py +17 -20
- phasorpy/plot/_phasorplot_fret.py +1 -1
- phasorpy/utils.py +1 -0
- {phasorpy-0.7.dist-info → phasorpy-0.8.dist-info}/METADATA +8 -7
- phasorpy-0.8.dist-info/RECORD +36 -0
- phasorpy-0.7.dist-info/RECORD +0 -35
- {phasorpy-0.7.dist-info → phasorpy-0.8.dist-info}/WHEEL +0 -0
- {phasorpy-0.7.dist-info → phasorpy-0.8.dist-info}/entry_points.txt +0 -0
- {phasorpy-0.7.dist-info → phasorpy-0.8.dist-info}/licenses/LICENSE.txt +0 -0
- {phasorpy-0.7.dist-info → phasorpy-0.8.dist-info}/top_level.txt +0 -0
phasorpy/phasor.py
CHANGED
@@ -22,17 +22,15 @@ The ``phasorpy.phasor`` module provides functions to:
|
|
22
22
|
- :py:func:`phasor_divide`
|
23
23
|
- :py:func:`phasor_normalize`
|
24
24
|
|
25
|
+
- linearly combine two phasor coordinates:
|
26
|
+
|
27
|
+
- :py:func:`phasor_combine`
|
28
|
+
|
25
29
|
- reduce dimensionality of arrays of phasor coordinates:
|
26
30
|
|
27
31
|
- :py:func:`phasor_center`
|
28
32
|
- :py:func:`phasor_to_principal_plane`
|
29
33
|
|
30
|
-
- filter phasor coordinates:
|
31
|
-
|
32
|
-
- :py:func:`phasor_filter_median`
|
33
|
-
- :py:func:`phasor_filter_pawflim`
|
34
|
-
- :py:func:`phasor_threshold`
|
35
|
-
|
36
34
|
- find nearest neighbor phasor coordinates from other phasor coordinates:
|
37
35
|
|
38
36
|
- :py:func:`phasor_nearest_neighbor`
|
@@ -44,14 +42,12 @@ from __future__ import annotations
|
|
44
42
|
__all__ = [
|
45
43
|
'phasor_center',
|
46
44
|
'phasor_divide',
|
47
|
-
'phasor_filter_median',
|
48
|
-
'phasor_filter_pawflim',
|
49
45
|
'phasor_from_polar',
|
50
46
|
'phasor_from_signal',
|
47
|
+
'phasor_combine',
|
51
48
|
'phasor_multiply',
|
52
49
|
'phasor_nearest_neighbor',
|
53
50
|
'phasor_normalize',
|
54
|
-
'phasor_threshold',
|
55
51
|
'phasor_to_complex',
|
56
52
|
'phasor_to_polar',
|
57
53
|
'phasor_to_principal_plane',
|
@@ -76,22 +72,18 @@ if TYPE_CHECKING:
|
|
76
72
|
import numpy
|
77
73
|
|
78
74
|
from ._phasorpy import (
|
79
|
-
_median_filter_2d,
|
80
75
|
_nearest_neighbor_2d,
|
76
|
+
_phasor_combine,
|
81
77
|
_phasor_divide,
|
82
78
|
_phasor_from_polar,
|
83
79
|
_phasor_from_signal,
|
84
80
|
_phasor_multiply,
|
85
|
-
_phasor_threshold_closed,
|
86
|
-
_phasor_threshold_mean_closed,
|
87
|
-
_phasor_threshold_mean_open,
|
88
|
-
_phasor_threshold_nan,
|
89
|
-
_phasor_threshold_open,
|
90
81
|
_phasor_to_polar,
|
91
82
|
_phasor_transform,
|
92
83
|
_phasor_transform_const,
|
93
84
|
)
|
94
85
|
from ._utils import parse_harmonic, parse_signal_axis, parse_skip_axis
|
86
|
+
from .filter import phasor_threshold
|
95
87
|
from .utils import number_threads
|
96
88
|
|
97
89
|
|
@@ -142,7 +134,8 @@ def phasor_from_signal(
|
|
142
134
|
calculated, or `rfft` is specified.
|
143
135
|
rfft : callable, optional
|
144
136
|
Drop-in replacement function for ``numpy.fft.rfft``.
|
145
|
-
For example, ``scipy.fft.rfft`` or
|
137
|
+
For example, ``scipy.fft.rfft`` or
|
138
|
+
``mkl_fft.interfaces.numpy_fft.rfft``.
|
146
139
|
Used to calculate the real forward FFT.
|
147
140
|
dtype : dtype_like, optional
|
148
141
|
Data type of output arrays. Either float32 or float64.
|
@@ -254,8 +247,8 @@ def phasor_from_signal(
|
|
254
247
|
raise ValueError('sample_phase cannot be used with FFT')
|
255
248
|
if num_harmonics > 1 or harmonic[0] != 1:
|
256
249
|
raise ValueError('sample_phase cannot be used with harmonic != 1')
|
257
|
-
sample_phase = numpy.
|
258
|
-
|
250
|
+
sample_phase = numpy.array(
|
251
|
+
sample_phase, dtype=numpy.float64, ndmin=1, order='C', copy=None
|
259
252
|
)
|
260
253
|
if sample_phase.ndim != 1 or sample_phase.size != samples:
|
261
254
|
raise ValueError(f'{sample_phase.shape=} != ({samples},)')
|
@@ -277,10 +270,10 @@ def phasor_from_signal(
|
|
277
270
|
|
278
271
|
mean = fft.take(0, axis=axis).real
|
279
272
|
if not mean.ndim == 0:
|
280
|
-
mean = numpy.ascontiguousarray(mean, dtype)
|
273
|
+
mean = numpy.ascontiguousarray(mean, dtype=dtype)
|
281
274
|
fft = fft.take(harmonic, axis=axis)
|
282
|
-
real = numpy.ascontiguousarray(fft.real, dtype)
|
283
|
-
imag = numpy.ascontiguousarray(fft.imag, dtype)
|
275
|
+
real = numpy.ascontiguousarray(fft.real, dtype=dtype)
|
276
|
+
imag = numpy.ascontiguousarray(fft.imag, dtype=dtype)
|
284
277
|
del fft
|
285
278
|
|
286
279
|
if not keepdims and real.shape[axis] == 1:
|
@@ -327,7 +320,7 @@ def phasor_from_signal(
|
|
327
320
|
shape1 = signal.shape[axis + 1 :]
|
328
321
|
size0 = math.prod(shape0)
|
329
322
|
size1 = math.prod(shape1)
|
330
|
-
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype)
|
323
|
+
phasor = numpy.empty((num_harmonics * 2 + 1, size0, size1), dtype=dtype)
|
331
324
|
signal = signal.reshape((size0, samples, size1))
|
332
325
|
|
333
326
|
_phasor_from_signal(phasor, signal, sincos, normalize, num_threads)
|
@@ -386,7 +379,8 @@ def phasor_to_signal(
|
|
386
379
|
The default is the last axis (-1).
|
387
380
|
irfft : callable, optional
|
388
381
|
Drop-in replacement function for ``numpy.fft.irfft``.
|
389
|
-
For example, ``scipy.fft.irfft`` or
|
382
|
+
For example, ``scipy.fft.irfft`` or
|
383
|
+
``mkl_fft.interfaces.numpy_fft.irfft``.
|
390
384
|
Used to calculate the real inverse FFT.
|
391
385
|
|
392
386
|
Returns
|
@@ -444,8 +438,9 @@ def phasor_to_signal(
|
|
444
438
|
else:
|
445
439
|
keepdims = mean.ndim > 0 or real.ndim > 0
|
446
440
|
|
447
|
-
mean = numpy.
|
448
|
-
|
441
|
+
# mean, real = numpy.atleast_1d(mean, real) not working with Mypy
|
442
|
+
mean = numpy.array(mean, ndmin=1, copy=False)
|
443
|
+
real = numpy.array(real, ndmin=1, copy=False)
|
449
444
|
|
450
445
|
if real.dtype.kind != 'f' or imag.dtype.kind != 'f':
|
451
446
|
raise ValueError(f'{real.dtype=} or {imag.dtype=} not floating point')
|
@@ -537,7 +532,7 @@ def phasor_to_complex(
|
|
537
532
|
if dtype.kind != 'c':
|
538
533
|
raise ValueError(f'{dtype=} not a complex type')
|
539
534
|
|
540
|
-
c = numpy.empty(numpy.broadcast(real, imag).shape, dtype)
|
535
|
+
c = numpy.empty(numpy.broadcast(real, imag).shape, dtype=dtype)
|
541
536
|
c.real = real
|
542
537
|
c.imag = imag
|
543
538
|
return c
|
@@ -749,9 +744,9 @@ def phasor_normalize(
|
|
749
744
|
):
|
750
745
|
real = real_unnormalized.copy()
|
751
746
|
else:
|
752
|
-
real = numpy.
|
753
|
-
imag = numpy.
|
754
|
-
mean = numpy.
|
747
|
+
real = numpy.asarray(real_unnormalized, dtype=dtype, copy=True)
|
748
|
+
imag = numpy.asarray(imag_unnormalized, dtype=real.dtype, copy=True)
|
749
|
+
mean = numpy.asarray(mean_unnormalized, dtype=real.dtype, copy=True)
|
755
750
|
|
756
751
|
with numpy.errstate(divide='ignore', invalid='ignore'):
|
757
752
|
numpy.divide(real, mean, out=real)
|
@@ -762,6 +757,103 @@ def phasor_normalize(
|
|
762
757
|
return mean, real, imag
|
763
758
|
|
764
759
|
|
760
|
+
def phasor_combine(
|
761
|
+
int0: ArrayLike,
|
762
|
+
real0: ArrayLike,
|
763
|
+
imag0: ArrayLike,
|
764
|
+
int1: ArrayLike,
|
765
|
+
real1: ArrayLike,
|
766
|
+
imag1: ArrayLike,
|
767
|
+
fraction0: ArrayLike,
|
768
|
+
fraction1: ArrayLike | None = None,
|
769
|
+
**kwargs: Any,
|
770
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
771
|
+
r"""Return linear combination of two phasor coordinates.
|
772
|
+
|
773
|
+
Combine two sets of phasor coordinates using intensity-weighted mixing.
|
774
|
+
This simulates the phasor coordinates that would result from a mixture
|
775
|
+
of two components with known individual phasor coordinates.
|
776
|
+
|
777
|
+
Parameters
|
778
|
+
----------
|
779
|
+
int0 : array_like
|
780
|
+
Intensity of first phasor coordinates.
|
781
|
+
real0 : array_like
|
782
|
+
Real component of first phasor coordinates.
|
783
|
+
imag0 : array_like
|
784
|
+
Imaginary component of first phasor coordinates.
|
785
|
+
int1 : array_like
|
786
|
+
Intensity of second phasor coordinates.
|
787
|
+
real1 : array_like
|
788
|
+
Real component of second phasor coordinates.
|
789
|
+
imag1 : array_like
|
790
|
+
Imaginary component of second phasor coordinates.
|
791
|
+
fraction0 : array_like
|
792
|
+
Fraction of first phasor coordinates.
|
793
|
+
fraction1 : array_like, optional
|
794
|
+
Fraction of second phasor coordinates.
|
795
|
+
The default is `1 - fraction0`.
|
796
|
+
|
797
|
+
**kwargs
|
798
|
+
Optional `arguments passed to numpy universal functions
|
799
|
+
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
800
|
+
|
801
|
+
Returns
|
802
|
+
-------
|
803
|
+
intensity : ndarray
|
804
|
+
Intensity of the linearly combined phasor coordinates.
|
805
|
+
real : ndarray
|
806
|
+
Real component of the linearly combined phasor coordinates.
|
807
|
+
imag : ndarray
|
808
|
+
Imaginary component of the linearly combined phasor coordinates.
|
809
|
+
|
810
|
+
Notes
|
811
|
+
-----
|
812
|
+
The phasor coordinates (:math:`I`, :math:`G`, :math:`S`) of the linear
|
813
|
+
combination of two phasor coordinates (:math:`I_{0}`, :math:`G_{0}`,
|
814
|
+
:math:`S_{0}`, and :math:`I_{1}`, :math:`G_{1}`, :math:`S_{1}`)
|
815
|
+
with fractions :math:`f_{0}` and :math:`f_{1}` of the first and second
|
816
|
+
coordinates are:
|
817
|
+
|
818
|
+
.. math::
|
819
|
+
|
820
|
+
f'_{0} &= f_{0} / (f_{0} + f_{1})
|
821
|
+
|
822
|
+
f'_{1} &= 1 - f'_{0}
|
823
|
+
|
824
|
+
I &= I_{0} \cdot f'_{0} + I_{1} \cdot f'_{1}
|
825
|
+
|
826
|
+
G &= (G_{0} \cdot I_{0} \cdot f'_{0}
|
827
|
+
+ G_{1} \cdot I_{1} \cdot f'_{1}) / I
|
828
|
+
|
829
|
+
S &= (S_{0} \cdot I_{0} \cdot f'_{0}
|
830
|
+
+ S_{1} \cdot I_{1} \cdot f'_{1}) / I
|
831
|
+
|
832
|
+
If the intensity :math:`I` is zero, the linear combined phasor coordinates
|
833
|
+
are undefined (NaN).
|
834
|
+
|
835
|
+
See Also
|
836
|
+
--------
|
837
|
+
phasorpy.component.phasor_from_component
|
838
|
+
|
839
|
+
Examples
|
840
|
+
--------
|
841
|
+
Calculate the linear combination of two phasor coordinates with equal
|
842
|
+
intensities at three fractional intensities:
|
843
|
+
|
844
|
+
>>> phasor_combine(1.0, 0.6, 0.3, 1.0, 0.4, 0.2, [1.0, 0.2, 0.9])
|
845
|
+
(array([1, 1, 1]), array([0.6, 0.44, 0.58]), array([0.3, 0.22, 0.29]))
|
846
|
+
|
847
|
+
"""
|
848
|
+
if fraction1 is None:
|
849
|
+
fraction1 = numpy.asarray(fraction0, copy=True)
|
850
|
+
numpy.subtract(1.0, fraction0, out=fraction1)
|
851
|
+
|
852
|
+
return _phasor_combine( # type: ignore[no-any-return]
|
853
|
+
int0, real0, imag0, int1, real1, imag1, fraction0, fraction1, **kwargs
|
854
|
+
)
|
855
|
+
|
856
|
+
|
765
857
|
def phasor_transform(
|
766
858
|
real: ArrayLike,
|
767
859
|
imag: ArrayLike,
|
@@ -1122,618 +1214,6 @@ def phasor_to_principal_plane(
|
|
1122
1214
|
)
|
1123
1215
|
|
1124
1216
|
|
1125
|
-
def phasor_filter_median(
|
1126
|
-
mean: ArrayLike,
|
1127
|
-
real: ArrayLike,
|
1128
|
-
imag: ArrayLike,
|
1129
|
-
/,
|
1130
|
-
*,
|
1131
|
-
repeat: int = 1,
|
1132
|
-
size: int = 3,
|
1133
|
-
skip_axis: int | Sequence[int] | None = None,
|
1134
|
-
use_scipy: bool = False,
|
1135
|
-
num_threads: int | None = None,
|
1136
|
-
**kwargs: Any,
|
1137
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
1138
|
-
"""Return median-filtered phasor coordinates.
|
1139
|
-
|
1140
|
-
By default, apply a NaN-aware median filter independently to the real
|
1141
|
-
and imaginary components of phasor coordinates once with a kernel size of 3
|
1142
|
-
multiplied by the number of dimensions of the input arrays. Return the
|
1143
|
-
intensity unchanged.
|
1144
|
-
|
1145
|
-
Parameters
|
1146
|
-
----------
|
1147
|
-
mean : array_like
|
1148
|
-
Intensity of phasor coordinates.
|
1149
|
-
real : array_like
|
1150
|
-
Real component of phasor coordinates to be filtered.
|
1151
|
-
imag : array_like
|
1152
|
-
Imaginary component of phasor coordinates to be filtered.
|
1153
|
-
repeat : int, optional
|
1154
|
-
Number of times to apply median filter. The default is 1.
|
1155
|
-
size : int, optional
|
1156
|
-
Size of median filter kernel. The default is 3.
|
1157
|
-
skip_axis : int or sequence of int, optional
|
1158
|
-
Axes in `mean` to exclude from filter.
|
1159
|
-
By default, all axes except harmonics are included.
|
1160
|
-
use_scipy : bool, optional
|
1161
|
-
Use :py:func:`scipy.ndimage.median_filter`.
|
1162
|
-
This function has undefined behavior if the input arrays contain
|
1163
|
-
NaN values but is faster when filtering more than 2 dimensions.
|
1164
|
-
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
1165
|
-
num_threads : int, optional
|
1166
|
-
Number of OpenMP threads to use for parallelization.
|
1167
|
-
Applies to filtering in two dimensions when not using scipy.
|
1168
|
-
By default, multi-threading is disabled.
|
1169
|
-
If zero, up to half of logical CPUs are used.
|
1170
|
-
OpenMP may not be available on all platforms.
|
1171
|
-
**kwargs
|
1172
|
-
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
1173
|
-
|
1174
|
-
Returns
|
1175
|
-
-------
|
1176
|
-
mean : ndarray
|
1177
|
-
Unchanged intensity of phasor coordinates.
|
1178
|
-
real : ndarray
|
1179
|
-
Filtered real component of phasor coordinates.
|
1180
|
-
imag : ndarray
|
1181
|
-
Filtered imaginary component of phasor coordinates.
|
1182
|
-
|
1183
|
-
Raises
|
1184
|
-
------
|
1185
|
-
ValueError
|
1186
|
-
If `repeat` is less than 0.
|
1187
|
-
If `size` is less than 1.
|
1188
|
-
The array shapes of `mean`, `real`, and `imag` do not match.
|
1189
|
-
|
1190
|
-
Examples
|
1191
|
-
--------
|
1192
|
-
Apply three times a median filter with a kernel size of three:
|
1193
|
-
|
1194
|
-
>>> mean, real, imag = phasor_filter_median(
|
1195
|
-
... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
1196
|
-
... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
|
1197
|
-
... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
|
1198
|
-
... size=3,
|
1199
|
-
... repeat=3,
|
1200
|
-
... )
|
1201
|
-
>>> mean
|
1202
|
-
array([[1, 2, 3],
|
1203
|
-
[4, 5, 6],
|
1204
|
-
[7, 8, 9]])
|
1205
|
-
>>> real
|
1206
|
-
array([[0, 0, 0],
|
1207
|
-
[0.2, 0.2, 0.2],
|
1208
|
-
[0.2, 0.2, 0.2]])
|
1209
|
-
>>> imag
|
1210
|
-
array([[0.3, 0.3, 0.3],
|
1211
|
-
[0.4, nan, 0.4],
|
1212
|
-
[0.4, 0.4, 0.4]])
|
1213
|
-
|
1214
|
-
"""
|
1215
|
-
if repeat < 0:
|
1216
|
-
raise ValueError(f'{repeat=} < 0')
|
1217
|
-
if size < 1:
|
1218
|
-
raise ValueError(f'{size=} < 1')
|
1219
|
-
if size == 1:
|
1220
|
-
# no need to filter
|
1221
|
-
repeat = 0
|
1222
|
-
|
1223
|
-
mean = numpy.asarray(mean)
|
1224
|
-
if use_scipy or repeat == 0: # or using nD numpy filter
|
1225
|
-
real = numpy.asarray(real)
|
1226
|
-
elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
|
1227
|
-
real = real.copy()
|
1228
|
-
else:
|
1229
|
-
real = numpy.array(real, numpy.float64, copy=True)
|
1230
|
-
if use_scipy or repeat == 0: # or using nD numpy filter
|
1231
|
-
imag = numpy.asarray(imag)
|
1232
|
-
elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
|
1233
|
-
imag = imag.copy()
|
1234
|
-
else:
|
1235
|
-
imag = numpy.array(imag, numpy.float64, copy=True)
|
1236
|
-
|
1237
|
-
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
1238
|
-
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
1239
|
-
if real.shape != imag.shape:
|
1240
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1241
|
-
|
1242
|
-
prepend_axis = mean.ndim + 1 == real.ndim
|
1243
|
-
_, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
1244
|
-
|
1245
|
-
# in case mean is also filtered
|
1246
|
-
# if prepend_axis:
|
1247
|
-
# mean = numpy.expand_dims(mean, axis=0)
|
1248
|
-
# ...
|
1249
|
-
# if prepend_axis:
|
1250
|
-
# mean = numpy.asarray(mean[0])
|
1251
|
-
|
1252
|
-
if repeat == 0:
|
1253
|
-
# no need to call filter
|
1254
|
-
return mean, real, imag
|
1255
|
-
|
1256
|
-
if use_scipy:
|
1257
|
-
# use scipy NaN-unaware fallback
|
1258
|
-
from scipy.ndimage import median_filter
|
1259
|
-
|
1260
|
-
kwargs.pop('axes', None)
|
1261
|
-
|
1262
|
-
for _ in range(repeat):
|
1263
|
-
real = median_filter(real, size=size, axes=axes, **kwargs)
|
1264
|
-
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
1265
|
-
|
1266
|
-
return mean, numpy.asarray(real), numpy.asarray(imag)
|
1267
|
-
|
1268
|
-
if len(axes) != 2:
|
1269
|
-
# n-dimensional median filter using numpy
|
1270
|
-
from numpy.lib.stride_tricks import sliding_window_view
|
1271
|
-
|
1272
|
-
kernel_shape = tuple(
|
1273
|
-
size if i in axes else 1 for i in range(real.ndim)
|
1274
|
-
)
|
1275
|
-
pad_width = [
|
1276
|
-
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
1277
|
-
]
|
1278
|
-
axis = tuple(range(-real.ndim, 0))
|
1279
|
-
|
1280
|
-
nan_mask = numpy.isnan(real)
|
1281
|
-
for _ in range(repeat):
|
1282
|
-
real = numpy.pad(real, pad_width, mode='edge')
|
1283
|
-
real = sliding_window_view(real, kernel_shape)
|
1284
|
-
real = numpy.nanmedian(real, axis=axis)
|
1285
|
-
real = numpy.where(nan_mask, numpy.nan, real)
|
1286
|
-
|
1287
|
-
nan_mask = numpy.isnan(imag)
|
1288
|
-
for _ in range(repeat):
|
1289
|
-
imag = numpy.pad(imag, pad_width, mode='edge')
|
1290
|
-
imag = sliding_window_view(imag, kernel_shape)
|
1291
|
-
imag = numpy.nanmedian(imag, axis=axis)
|
1292
|
-
imag = numpy.where(nan_mask, numpy.nan, imag)
|
1293
|
-
|
1294
|
-
return mean, real, imag
|
1295
|
-
|
1296
|
-
# 2-dimensional median filter using optimized Cython implementation
|
1297
|
-
num_threads = number_threads(num_threads)
|
1298
|
-
|
1299
|
-
buffer = numpy.empty(
|
1300
|
-
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
1301
|
-
)
|
1302
|
-
|
1303
|
-
for index in numpy.ndindex(
|
1304
|
-
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
1305
|
-
):
|
1306
|
-
index_list: list[int | slice] = list(index)
|
1307
|
-
for ax in axes:
|
1308
|
-
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
1309
|
-
full_index = tuple(index_list)
|
1310
|
-
|
1311
|
-
_median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
|
1312
|
-
_median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
|
1313
|
-
|
1314
|
-
return mean, real, imag
|
1315
|
-
|
1316
|
-
|
1317
|
-
def phasor_filter_pawflim(
|
1318
|
-
mean: ArrayLike,
|
1319
|
-
real: ArrayLike,
|
1320
|
-
imag: ArrayLike,
|
1321
|
-
/,
|
1322
|
-
*,
|
1323
|
-
sigma: float = 2.0,
|
1324
|
-
levels: int = 1,
|
1325
|
-
harmonic: Sequence[int] | None = None,
|
1326
|
-
skip_axis: int | Sequence[int] | None = None,
|
1327
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
1328
|
-
"""Return pawFLIM wavelet-filtered phasor coordinates.
|
1329
|
-
|
1330
|
-
This function must only be used with calibrated, unprocessed phasor
|
1331
|
-
coordinates obtained from FLIM data. The coordinates must not be filtered,
|
1332
|
-
thresholded, or otherwise pre-processed.
|
1333
|
-
|
1334
|
-
The pawFLIM wavelet filter is described in [2]_.
|
1335
|
-
|
1336
|
-
Parameters
|
1337
|
-
----------
|
1338
|
-
mean : array_like
|
1339
|
-
Intensity of phasor coordinates.
|
1340
|
-
real : array_like
|
1341
|
-
Real component of phasor coordinates to be filtered.
|
1342
|
-
Must have at least two harmonics in the first axis.
|
1343
|
-
imag : array_like
|
1344
|
-
Imaginary component of phasor coordinates to be filtered.
|
1345
|
-
Must have at least two harmonics in the first axis.
|
1346
|
-
sigma : float, optional
|
1347
|
-
Significance level to test difference between two phasors.
|
1348
|
-
Given in terms of the equivalent 1D standard deviations.
|
1349
|
-
sigma=2 corresponds to ~95% (or 5%) significance.
|
1350
|
-
levels : int, optional
|
1351
|
-
Number of levels for wavelet decomposition.
|
1352
|
-
Controls the maximum averaging area, which has a length of
|
1353
|
-
:math:`2^level`.
|
1354
|
-
harmonic : sequence of int or None, optional
|
1355
|
-
Harmonics included in first axis of `real` and `imag`.
|
1356
|
-
If None (default), the first axis of `real` and `imag` contains lower
|
1357
|
-
harmonics starting at and increasing by one.
|
1358
|
-
All harmonics must have a corresponding half or double harmonic.
|
1359
|
-
skip_axis : int or sequence of int, optional
|
1360
|
-
Axes in `mean` to exclude from filter.
|
1361
|
-
By default, all axes except harmonics are included.
|
1362
|
-
|
1363
|
-
Returns
|
1364
|
-
-------
|
1365
|
-
mean : ndarray
|
1366
|
-
Unchanged intensity of phasor coordinates.
|
1367
|
-
real : ndarray
|
1368
|
-
Filtered real component of phasor coordinates.
|
1369
|
-
imag : ndarray
|
1370
|
-
Filtered imaginary component of phasor coordinates.
|
1371
|
-
|
1372
|
-
Raises
|
1373
|
-
------
|
1374
|
-
ValueError
|
1375
|
-
If `level` is less than 0.
|
1376
|
-
The array shapes of `mean`, `real`, and `imag` do not match.
|
1377
|
-
If `real` and `imag` have no harmonic axis.
|
1378
|
-
Number of harmonics in `harmonic` is less than 2 or does not match
|
1379
|
-
the first axis of `real` and `imag`.
|
1380
|
-
Not all harmonics in `harmonic` have a corresponding half
|
1381
|
-
or double harmonic.
|
1382
|
-
|
1383
|
-
References
|
1384
|
-
----------
|
1385
|
-
.. [2] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
1386
|
-
uncertainty to enable lower photon count in FLIM experiments
|
1387
|
-
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
1388
|
-
*Methods Appl Fluoresc*, 5(2): 024016 (2017)
|
1389
|
-
|
1390
|
-
Examples
|
1391
|
-
--------
|
1392
|
-
Apply a pawFLIM wavelet filter with four significance levels (sigma)
|
1393
|
-
and three decomposition levels:
|
1394
|
-
|
1395
|
-
>>> mean, real, imag = phasor_filter_pawflim(
|
1396
|
-
... [[1, 1], [1, 1]],
|
1397
|
-
... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
|
1398
|
-
... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
|
1399
|
-
... sigma=4,
|
1400
|
-
... levels=3,
|
1401
|
-
... harmonic=[1, 2],
|
1402
|
-
... )
|
1403
|
-
>>> mean
|
1404
|
-
array([[1, 1],
|
1405
|
-
[1, 1]])
|
1406
|
-
>>> real
|
1407
|
-
array([[[0.65, 0.65],
|
1408
|
-
[0.65, 0.65]],
|
1409
|
-
[[0.3, 0.3],
|
1410
|
-
[0.3, 0.3]]])
|
1411
|
-
>>> imag
|
1412
|
-
array([[[0.45, 0.45],
|
1413
|
-
[0.45, 0.45]],
|
1414
|
-
[[0.45, 0.45],
|
1415
|
-
[0.45, 0.45]]])
|
1416
|
-
|
1417
|
-
"""
|
1418
|
-
from pawflim import pawflim # type: ignore[import-untyped]
|
1419
|
-
|
1420
|
-
mean = numpy.asarray(mean)
|
1421
|
-
real = numpy.asarray(real)
|
1422
|
-
imag = numpy.asarray(imag)
|
1423
|
-
|
1424
|
-
if levels < 0:
|
1425
|
-
raise ValueError(f'{levels=} < 0')
|
1426
|
-
if levels == 0:
|
1427
|
-
return mean, real, imag
|
1428
|
-
|
1429
|
-
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
1430
|
-
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
1431
|
-
if real.shape != imag.shape:
|
1432
|
-
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
1433
|
-
|
1434
|
-
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
1435
|
-
if not has_harmonic_axis:
|
1436
|
-
raise ValueError('no harmonic axis')
|
1437
|
-
if harmonic is None:
|
1438
|
-
harmonics, _ = parse_harmonic('all', real.shape[0])
|
1439
|
-
else:
|
1440
|
-
harmonics, _ = parse_harmonic(harmonic, None)
|
1441
|
-
if len(harmonics) < 2:
|
1442
|
-
raise ValueError(
|
1443
|
-
'at least two harmonics required, ' f'got {len(harmonics)}'
|
1444
|
-
)
|
1445
|
-
if len(harmonics) != real.shape[0]:
|
1446
|
-
raise ValueError(
|
1447
|
-
'number of harmonics does not match first axis of real and imag'
|
1448
|
-
)
|
1449
|
-
|
1450
|
-
mean = numpy.asarray(numpy.nan_to_num(mean), dtype=float)
|
1451
|
-
real = numpy.asarray(numpy.nan_to_num(real * mean), dtype=float)
|
1452
|
-
imag = numpy.asarray(numpy.nan_to_num(imag * mean), dtype=float)
|
1453
|
-
|
1454
|
-
mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
|
1455
|
-
original_mean_expanded = mean_expanded.copy()
|
1456
|
-
real_filtered = real.copy()
|
1457
|
-
imag_filtered = imag.copy()
|
1458
|
-
|
1459
|
-
_, axes = parse_skip_axis(skip_axis, mean.ndim, True)
|
1460
|
-
|
1461
|
-
for index in numpy.ndindex(
|
1462
|
-
*(
|
1463
|
-
real.shape[ax]
|
1464
|
-
for ax in range(real.ndim)
|
1465
|
-
if ax not in axes and ax != 0
|
1466
|
-
)
|
1467
|
-
):
|
1468
|
-
index_list: list[int | slice] = list(index)
|
1469
|
-
for ax in axes:
|
1470
|
-
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
1471
|
-
full_index = tuple(index_list)
|
1472
|
-
|
1473
|
-
processed_harmonics = set()
|
1474
|
-
|
1475
|
-
for h in harmonics:
|
1476
|
-
if h in processed_harmonics and (
|
1477
|
-
h * 4 in harmonics or h * 2 not in harmonics
|
1478
|
-
):
|
1479
|
-
continue
|
1480
|
-
if h * 2 not in harmonics:
|
1481
|
-
raise ValueError(
|
1482
|
-
f'harmonic {h} does not have a corresponding half '
|
1483
|
-
f'or double harmonic in {harmonics}'
|
1484
|
-
)
|
1485
|
-
n = harmonics.index(h)
|
1486
|
-
n2 = harmonics.index(h * 2)
|
1487
|
-
|
1488
|
-
complex_phasor = numpy.empty(
|
1489
|
-
(3, *original_mean_expanded[n][full_index].shape),
|
1490
|
-
dtype=complex,
|
1491
|
-
)
|
1492
|
-
complex_phasor[0] = original_mean_expanded[n][full_index]
|
1493
|
-
complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
|
1494
|
-
complex_phasor[2] = (
|
1495
|
-
real[n2][full_index] + 1j * imag[n2][full_index]
|
1496
|
-
)
|
1497
|
-
|
1498
|
-
complex_phasor = pawflim(
|
1499
|
-
complex_phasor, n_sigmas=sigma, levels=levels
|
1500
|
-
)
|
1501
|
-
|
1502
|
-
for i, idx in enumerate([n, n2]):
|
1503
|
-
if harmonics[idx] in processed_harmonics:
|
1504
|
-
continue
|
1505
|
-
mean_expanded[idx][full_index] = complex_phasor[0].real
|
1506
|
-
real_filtered[idx][full_index] = complex_phasor[i + 1].real
|
1507
|
-
imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
|
1508
|
-
|
1509
|
-
processed_harmonics.add(h)
|
1510
|
-
processed_harmonics.add(h * 2)
|
1511
|
-
|
1512
|
-
with numpy.errstate(divide='ignore', invalid='ignore'):
|
1513
|
-
real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
|
1514
|
-
imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
|
1515
|
-
|
1516
|
-
return mean, real, imag
|
1517
|
-
|
1518
|
-
|
1519
|
-
def phasor_threshold(
|
1520
|
-
mean: ArrayLike,
|
1521
|
-
real: ArrayLike,
|
1522
|
-
imag: ArrayLike,
|
1523
|
-
/,
|
1524
|
-
mean_min: ArrayLike | None = None,
|
1525
|
-
mean_max: ArrayLike | None = None,
|
1526
|
-
*,
|
1527
|
-
real_min: ArrayLike | None = None,
|
1528
|
-
real_max: ArrayLike | None = None,
|
1529
|
-
imag_min: ArrayLike | None = None,
|
1530
|
-
imag_max: ArrayLike | None = None,
|
1531
|
-
phase_min: ArrayLike | None = None,
|
1532
|
-
phase_max: ArrayLike | None = None,
|
1533
|
-
modulation_min: ArrayLike | None = None,
|
1534
|
-
modulation_max: ArrayLike | None = None,
|
1535
|
-
open_interval: bool = False,
|
1536
|
-
detect_harmonics: bool = True,
|
1537
|
-
**kwargs: Any,
|
1538
|
-
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
1539
|
-
"""Return phasor coordinates with values outside interval replaced by NaN.
|
1540
|
-
|
1541
|
-
Interval thresholds can be set for mean intensity, real and imaginary
|
1542
|
-
coordinates, and phase and modulation.
|
1543
|
-
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
1544
|
-
thresholds are replaced with NaN.
|
1545
|
-
No threshold is applied by default.
|
1546
|
-
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
1547
|
-
`mean` and all harmonics in `real` and `imag`.
|
1548
|
-
|
1549
|
-
Parameters
|
1550
|
-
----------
|
1551
|
-
mean : array_like
|
1552
|
-
Intensity of phasor coordinates.
|
1553
|
-
real : array_like
|
1554
|
-
Real component of phasor coordinates.
|
1555
|
-
imag : array_like
|
1556
|
-
Imaginary component of phasor coordinates.
|
1557
|
-
mean_min : array_like, optional
|
1558
|
-
Lower threshold for mean intensity.
|
1559
|
-
mean_max : array_like, optional
|
1560
|
-
Upper threshold for mean intensity.
|
1561
|
-
real_min : array_like, optional
|
1562
|
-
Lower threshold for real coordinates.
|
1563
|
-
real_max : array_like, optional
|
1564
|
-
Upper threshold for real coordinates.
|
1565
|
-
imag_min : array_like, optional
|
1566
|
-
Lower threshold for imaginary coordinates.
|
1567
|
-
imag_max : array_like, optional
|
1568
|
-
Upper threshold for imaginary coordinates.
|
1569
|
-
phase_min : array_like, optional
|
1570
|
-
Lower threshold for phase angle.
|
1571
|
-
phase_max : array_like, optional
|
1572
|
-
Upper threshold for phase angle.
|
1573
|
-
modulation_min : array_like, optional
|
1574
|
-
Lower threshold for modulation.
|
1575
|
-
modulation_max : array_like, optional
|
1576
|
-
Upper threshold for modulation.
|
1577
|
-
open_interval : bool, optional
|
1578
|
-
If true, the interval is open, and the threshold values are
|
1579
|
-
not included in the interval.
|
1580
|
-
If false (default), the interval is closed, and the threshold values
|
1581
|
-
are included in the interval.
|
1582
|
-
detect_harmonics : bool, optional
|
1583
|
-
By default, detect presence of multiple harmonics from array shapes.
|
1584
|
-
If false, no harmonics are assumed to be present, and the function
|
1585
|
-
behaves like a numpy universal function.
|
1586
|
-
**kwargs
|
1587
|
-
Optional `arguments passed to numpy universal functions
|
1588
|
-
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
1589
|
-
|
1590
|
-
Returns
|
1591
|
-
-------
|
1592
|
-
mean : ndarray
|
1593
|
-
Thresholded intensity of phasor coordinates.
|
1594
|
-
real : ndarray
|
1595
|
-
Thresholded real component of phasor coordinates.
|
1596
|
-
imag : ndarray
|
1597
|
-
Thresholded imaginary component of phasor coordinates.
|
1598
|
-
|
1599
|
-
Examples
|
1600
|
-
--------
|
1601
|
-
Set phasor coordinates to NaN if mean intensity is smaller than 1.1:
|
1602
|
-
|
1603
|
-
>>> phasor_threshold([1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], 1.1)
|
1604
|
-
(array([nan, 2, 3]), array([nan, 0.2, 0.3]), array([nan, 0.5, 0.6]))
|
1605
|
-
|
1606
|
-
Set phasor coordinates to NaN if real component is smaller than 0.15 or
|
1607
|
-
larger than 0.25:
|
1608
|
-
|
1609
|
-
>>> phasor_threshold(
|
1610
|
-
... [1.0, 2.0, 3.0],
|
1611
|
-
... [0.1, 0.2, 0.3],
|
1612
|
-
... [0.4, 0.5, 0.6],
|
1613
|
-
... real_min=0.15,
|
1614
|
-
... real_max=0.25,
|
1615
|
-
... )
|
1616
|
-
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
1617
|
-
|
1618
|
-
Apply NaNs to other input arrays:
|
1619
|
-
|
1620
|
-
>>> phasor_threshold(
|
1621
|
-
... [numpy.nan, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, numpy.nan]
|
1622
|
-
... )
|
1623
|
-
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
1624
|
-
|
1625
|
-
"""
|
1626
|
-
threshold_mean_only = None
|
1627
|
-
if mean_min is None:
|
1628
|
-
mean_min = numpy.nan
|
1629
|
-
else:
|
1630
|
-
threshold_mean_only = True
|
1631
|
-
if mean_max is None:
|
1632
|
-
mean_max = numpy.nan
|
1633
|
-
else:
|
1634
|
-
threshold_mean_only = True
|
1635
|
-
if real_min is None:
|
1636
|
-
real_min = numpy.nan
|
1637
|
-
else:
|
1638
|
-
threshold_mean_only = False
|
1639
|
-
if real_max is None:
|
1640
|
-
real_max = numpy.nan
|
1641
|
-
else:
|
1642
|
-
threshold_mean_only = False
|
1643
|
-
if imag_min is None:
|
1644
|
-
imag_min = numpy.nan
|
1645
|
-
else:
|
1646
|
-
threshold_mean_only = False
|
1647
|
-
if imag_max is None:
|
1648
|
-
imag_max = numpy.nan
|
1649
|
-
else:
|
1650
|
-
threshold_mean_only = False
|
1651
|
-
if phase_min is None:
|
1652
|
-
phase_min = numpy.nan
|
1653
|
-
else:
|
1654
|
-
threshold_mean_only = False
|
1655
|
-
if phase_max is None:
|
1656
|
-
phase_max = numpy.nan
|
1657
|
-
else:
|
1658
|
-
threshold_mean_only = False
|
1659
|
-
if modulation_min is None:
|
1660
|
-
modulation_min = numpy.nan
|
1661
|
-
else:
|
1662
|
-
threshold_mean_only = False
|
1663
|
-
if modulation_max is None:
|
1664
|
-
modulation_max = numpy.nan
|
1665
|
-
else:
|
1666
|
-
threshold_mean_only = False
|
1667
|
-
|
1668
|
-
if detect_harmonics:
|
1669
|
-
mean = numpy.asarray(mean)
|
1670
|
-
real = numpy.asarray(real)
|
1671
|
-
imag = numpy.asarray(imag)
|
1672
|
-
|
1673
|
-
shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
|
1674
|
-
ndim = len(shape)
|
1675
|
-
|
1676
|
-
has_harmonic_axis = (
|
1677
|
-
# detect multi-harmonic in axis 0
|
1678
|
-
mean.ndim + 1 == ndim
|
1679
|
-
and real.shape == shape
|
1680
|
-
and imag.shape == shape
|
1681
|
-
and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
|
1682
|
-
)
|
1683
|
-
else:
|
1684
|
-
has_harmonic_axis = False
|
1685
|
-
|
1686
|
-
if threshold_mean_only is None:
|
1687
|
-
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
1688
|
-
|
1689
|
-
elif threshold_mean_only:
|
1690
|
-
mean_func = (
|
1691
|
-
_phasor_threshold_mean_open
|
1692
|
-
if open_interval
|
1693
|
-
else _phasor_threshold_mean_closed
|
1694
|
-
)
|
1695
|
-
mean, real, imag = mean_func(
|
1696
|
-
mean, real, imag, mean_min, mean_max, **kwargs
|
1697
|
-
)
|
1698
|
-
|
1699
|
-
else:
|
1700
|
-
func = (
|
1701
|
-
_phasor_threshold_open
|
1702
|
-
if open_interval
|
1703
|
-
else _phasor_threshold_closed
|
1704
|
-
)
|
1705
|
-
mean, real, imag = func(
|
1706
|
-
mean,
|
1707
|
-
real,
|
1708
|
-
imag,
|
1709
|
-
mean_min,
|
1710
|
-
mean_max,
|
1711
|
-
real_min,
|
1712
|
-
real_max,
|
1713
|
-
imag_min,
|
1714
|
-
imag_max,
|
1715
|
-
phase_min,
|
1716
|
-
phase_max,
|
1717
|
-
modulation_min,
|
1718
|
-
modulation_max,
|
1719
|
-
**kwargs,
|
1720
|
-
)
|
1721
|
-
|
1722
|
-
mean = numpy.asarray(mean)
|
1723
|
-
real = numpy.asarray(real)
|
1724
|
-
imag = numpy.asarray(imag)
|
1725
|
-
if has_harmonic_axis and mean.ndim > 0:
|
1726
|
-
# propagate NaN to all dimensions
|
1727
|
-
mean = numpy.mean(mean, axis=0, keepdims=True)
|
1728
|
-
mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
|
1729
|
-
numpy.multiply(real, mask, out=real)
|
1730
|
-
numpy.multiply(imag, mask, out=imag)
|
1731
|
-
# remove harmonic dimension created by broadcasting
|
1732
|
-
mean = numpy.asarray(numpy.asarray(mean)[0])
|
1733
|
-
|
1734
|
-
return mean, real, imag
|
1735
|
-
|
1736
|
-
|
1737
1217
|
def phasor_nearest_neighbor(
|
1738
1218
|
real: ArrayLike,
|
1739
1219
|
imag: ArrayLike,
|
@@ -1847,7 +1327,7 @@ def phasor_nearest_neighbor(
|
|
1847
1327
|
neighbor_imag = neighbor_imag.ravel()
|
1848
1328
|
|
1849
1329
|
indices = numpy.empty(
|
1850
|
-
real.shape, numpy.min_scalar_type(-neighbor_real.size)
|
1330
|
+
real.shape, dtype=numpy.min_scalar_type(-neighbor_real.size)
|
1851
1331
|
)
|
1852
1332
|
|
1853
1333
|
if distance_max is None:
|