phasorpy 0.7__cp314-cp314-win_amd64.whl → 0.8__cp314-cp314-win_amd64.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_amd64.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/filter.py
ADDED
@@ -0,0 +1,966 @@
|
|
1
|
+
"""Filter signals and phasor coordinates.
|
2
|
+
|
3
|
+
The ``phasorpy.filter`` module provides functions to filter
|
4
|
+
|
5
|
+
- phasor coordinates:
|
6
|
+
|
7
|
+
- :py:func:`phasor_filter_gaussian` (not implemented yet)
|
8
|
+
- :py:func:`phasor_filter_median`
|
9
|
+
- :py:func:`phasor_filter_pawflim`
|
10
|
+
- :py:func:`phasor_threshold`
|
11
|
+
|
12
|
+
- signals:
|
13
|
+
|
14
|
+
- :py:func:`signal_filter_ncpca`
|
15
|
+
(noise-corrected principal component analysis)
|
16
|
+
- :py:func:`signal_filter_svd` (spectral vector denoise)
|
17
|
+
- :py:func:`signal_filter_median` (not implemented yet)
|
18
|
+
|
19
|
+
"""
|
20
|
+
|
21
|
+
from __future__ import annotations
|
22
|
+
|
23
|
+
__all__ = [
|
24
|
+
# 'signal_filter_gaussian',
|
25
|
+
'phasor_filter_median',
|
26
|
+
'phasor_filter_pawflim',
|
27
|
+
'phasor_threshold',
|
28
|
+
# 'signal_filter_median',
|
29
|
+
'signal_filter_ncpca',
|
30
|
+
'signal_filter_svd',
|
31
|
+
]
|
32
|
+
|
33
|
+
import math
|
34
|
+
from collections.abc import Sequence
|
35
|
+
from typing import TYPE_CHECKING
|
36
|
+
|
37
|
+
if TYPE_CHECKING:
|
38
|
+
from ._typing import Any, NDArray, ArrayLike, DTypeLike, Literal
|
39
|
+
|
40
|
+
import numpy
|
41
|
+
|
42
|
+
from ._phasorpy import (
|
43
|
+
_median_filter_2d,
|
44
|
+
_phasor_from_signal_vector,
|
45
|
+
_phasor_threshold_closed,
|
46
|
+
_phasor_threshold_mean_closed,
|
47
|
+
_phasor_threshold_mean_open,
|
48
|
+
_phasor_threshold_nan,
|
49
|
+
_phasor_threshold_open,
|
50
|
+
_signal_denoise_vector,
|
51
|
+
)
|
52
|
+
from ._utils import parse_harmonic, parse_skip_axis
|
53
|
+
from .utils import number_threads
|
54
|
+
|
55
|
+
|
56
|
+
def phasor_filter_median(
|
57
|
+
mean: ArrayLike,
|
58
|
+
real: ArrayLike,
|
59
|
+
imag: ArrayLike,
|
60
|
+
/,
|
61
|
+
*,
|
62
|
+
repeat: int = 1,
|
63
|
+
size: int = 3,
|
64
|
+
skip_axis: int | Sequence[int] | None = None,
|
65
|
+
use_scipy: bool = False,
|
66
|
+
num_threads: int | None = None,
|
67
|
+
**kwargs: Any,
|
68
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
69
|
+
"""Return median-filtered phasor coordinates.
|
70
|
+
|
71
|
+
By default, apply a NaN-aware median filter independently to the real
|
72
|
+
and imaginary components of phasor coordinates once with a kernel size of 3
|
73
|
+
multiplied by the number of dimensions of the input arrays. Return the
|
74
|
+
intensity unchanged.
|
75
|
+
|
76
|
+
Parameters
|
77
|
+
----------
|
78
|
+
mean : array_like
|
79
|
+
Intensity of phasor coordinates.
|
80
|
+
real : array_like
|
81
|
+
Real component of phasor coordinates to be filtered.
|
82
|
+
imag : array_like
|
83
|
+
Imaginary component of phasor coordinates to be filtered.
|
84
|
+
repeat : int, optional
|
85
|
+
Number of times to apply median filter. The default is 1.
|
86
|
+
size : int, optional
|
87
|
+
Size of median filter kernel. The default is 3.
|
88
|
+
skip_axis : int or sequence of int, optional
|
89
|
+
Axes in `mean` to exclude from filter.
|
90
|
+
By default, all axes except harmonics are included.
|
91
|
+
use_scipy : bool, optional
|
92
|
+
Use :py:func:`scipy.ndimage.median_filter`.
|
93
|
+
This function has undefined behavior if the input arrays contain
|
94
|
+
NaN values but is faster when filtering more than 2 dimensions.
|
95
|
+
See `issue #87 <https://github.com/phasorpy/phasorpy/issues/87>`_.
|
96
|
+
num_threads : int, optional
|
97
|
+
Number of OpenMP threads to use for parallelization.
|
98
|
+
Applies to filtering in two dimensions when not using scipy.
|
99
|
+
By default, multi-threading is disabled.
|
100
|
+
If zero, up to half of logical CPUs are used.
|
101
|
+
OpenMP may not be available on all platforms.
|
102
|
+
**kwargs
|
103
|
+
Optional arguments passed to :py:func:`scipy.ndimage.median_filter`.
|
104
|
+
|
105
|
+
Returns
|
106
|
+
-------
|
107
|
+
mean : ndarray
|
108
|
+
Unchanged intensity of phasor coordinates.
|
109
|
+
real : ndarray
|
110
|
+
Filtered real component of phasor coordinates.
|
111
|
+
imag : ndarray
|
112
|
+
Filtered imaginary component of phasor coordinates.
|
113
|
+
|
114
|
+
Raises
|
115
|
+
------
|
116
|
+
ValueError
|
117
|
+
If `repeat` is less than 0.
|
118
|
+
If `size` is less than 1.
|
119
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
120
|
+
|
121
|
+
Examples
|
122
|
+
--------
|
123
|
+
Apply three times a median filter with a kernel size of three:
|
124
|
+
|
125
|
+
>>> mean, real, imag = phasor_filter_median(
|
126
|
+
... [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
|
127
|
+
... [[0.0, 0.0, 0.0], [0.5, 0.5, 0.5], [0.2, 0.2, 0.2]],
|
128
|
+
... [[0.3, 0.3, 0.3], [0.6, math.nan, 0.6], [0.4, 0.4, 0.4]],
|
129
|
+
... size=3,
|
130
|
+
... repeat=3,
|
131
|
+
... )
|
132
|
+
>>> mean
|
133
|
+
array([[1, 2, 3],
|
134
|
+
[4, 5, 6],
|
135
|
+
[7, 8, 9]])
|
136
|
+
>>> real
|
137
|
+
array([[0, 0, 0],
|
138
|
+
[0.2, 0.2, 0.2],
|
139
|
+
[0.2, 0.2, 0.2]])
|
140
|
+
>>> imag
|
141
|
+
array([[0.3, 0.3, 0.3],
|
142
|
+
[0.4, nan, 0.4],
|
143
|
+
[0.4, 0.4, 0.4]])
|
144
|
+
|
145
|
+
"""
|
146
|
+
if repeat < 0:
|
147
|
+
raise ValueError(f'{repeat=} < 0')
|
148
|
+
if size < 1:
|
149
|
+
raise ValueError(f'{size=} < 1')
|
150
|
+
if size == 1:
|
151
|
+
# no need to filter
|
152
|
+
repeat = 0
|
153
|
+
|
154
|
+
mean = numpy.asarray(mean)
|
155
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
156
|
+
real = numpy.asarray(real)
|
157
|
+
elif isinstance(real, numpy.ndarray) and real.dtype == numpy.float32:
|
158
|
+
real = real.copy()
|
159
|
+
else:
|
160
|
+
real = numpy.asarray(real, dtype=numpy.float64, copy=True)
|
161
|
+
if use_scipy or repeat == 0: # or using nD numpy filter
|
162
|
+
imag = numpy.asarray(imag)
|
163
|
+
elif isinstance(imag, numpy.ndarray) and imag.dtype == numpy.float32:
|
164
|
+
imag = imag.copy()
|
165
|
+
else:
|
166
|
+
imag = numpy.asarray(imag, dtype=numpy.float64, copy=True)
|
167
|
+
|
168
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
169
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
170
|
+
if real.shape != imag.shape:
|
171
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
172
|
+
|
173
|
+
prepend_axis = mean.ndim + 1 == real.ndim
|
174
|
+
_, axes = parse_skip_axis(skip_axis, mean.ndim, prepend_axis)
|
175
|
+
|
176
|
+
# in case mean is also filtered
|
177
|
+
# if prepend_axis:
|
178
|
+
# mean = numpy.expand_dims(mean, axis=0)
|
179
|
+
# ...
|
180
|
+
# if prepend_axis:
|
181
|
+
# mean = numpy.asarray(mean[0])
|
182
|
+
|
183
|
+
if repeat == 0:
|
184
|
+
# no need to call filter
|
185
|
+
return mean, real, imag
|
186
|
+
|
187
|
+
if use_scipy:
|
188
|
+
# use scipy NaN-unaware fallback
|
189
|
+
from scipy.ndimage import median_filter
|
190
|
+
|
191
|
+
kwargs.pop('axes', None)
|
192
|
+
|
193
|
+
for _ in range(repeat):
|
194
|
+
real = median_filter(real, size=size, axes=axes, **kwargs)
|
195
|
+
imag = median_filter(imag, size=size, axes=axes, **kwargs)
|
196
|
+
|
197
|
+
return mean, numpy.asarray(real), numpy.asarray(imag)
|
198
|
+
|
199
|
+
if len(axes) != 2:
|
200
|
+
# n-dimensional median filter using numpy
|
201
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
202
|
+
|
203
|
+
kernel_shape = tuple(
|
204
|
+
size if i in axes else 1 for i in range(real.ndim)
|
205
|
+
)
|
206
|
+
pad_width = [
|
207
|
+
(s // 2, s // 2) if s > 1 else (0, 0) for s in kernel_shape
|
208
|
+
]
|
209
|
+
axis = tuple(range(-real.ndim, 0))
|
210
|
+
|
211
|
+
nan_mask = numpy.isnan(real)
|
212
|
+
for _ in range(repeat):
|
213
|
+
real = numpy.pad(real, pad_width, mode='edge')
|
214
|
+
real = sliding_window_view(real, kernel_shape)
|
215
|
+
real = numpy.nanmedian(real, axis=axis)
|
216
|
+
real = numpy.where(nan_mask, numpy.nan, real)
|
217
|
+
|
218
|
+
nan_mask = numpy.isnan(imag)
|
219
|
+
for _ in range(repeat):
|
220
|
+
imag = numpy.pad(imag, pad_width, mode='edge')
|
221
|
+
imag = sliding_window_view(imag, kernel_shape)
|
222
|
+
imag = numpy.nanmedian(imag, axis=axis)
|
223
|
+
imag = numpy.where(nan_mask, numpy.nan, imag)
|
224
|
+
|
225
|
+
return mean, real, imag
|
226
|
+
|
227
|
+
# 2-dimensional median filter using optimized Cython implementation
|
228
|
+
num_threads = number_threads(num_threads)
|
229
|
+
|
230
|
+
buffer = numpy.empty(
|
231
|
+
tuple(real.shape[axis] for axis in axes), dtype=real.dtype
|
232
|
+
)
|
233
|
+
|
234
|
+
for index in numpy.ndindex(
|
235
|
+
*[real.shape[ax] for ax in range(real.ndim) if ax not in axes]
|
236
|
+
):
|
237
|
+
index_list: list[int | slice] = list(index)
|
238
|
+
for ax in axes:
|
239
|
+
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
240
|
+
full_index = tuple(index_list)
|
241
|
+
|
242
|
+
_median_filter_2d(real[full_index], buffer, size, repeat, num_threads)
|
243
|
+
_median_filter_2d(imag[full_index], buffer, size, repeat, num_threads)
|
244
|
+
|
245
|
+
return mean, real, imag
|
246
|
+
|
247
|
+
|
248
|
+
def phasor_filter_pawflim(
|
249
|
+
mean: ArrayLike,
|
250
|
+
real: ArrayLike,
|
251
|
+
imag: ArrayLike,
|
252
|
+
/,
|
253
|
+
*,
|
254
|
+
sigma: float = 2.0,
|
255
|
+
levels: int = 1,
|
256
|
+
harmonic: Sequence[int] | None = None,
|
257
|
+
skip_axis: int | Sequence[int] | None = None,
|
258
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
259
|
+
"""Return pawFLIM wavelet-filtered phasor coordinates.
|
260
|
+
|
261
|
+
This function must only be used with calibrated, unprocessed phasor
|
262
|
+
coordinates obtained from FLIM data. The coordinates must not be filtered,
|
263
|
+
thresholded, or otherwise pre-processed.
|
264
|
+
|
265
|
+
The pawFLIM wavelet filter is described in [1]_.
|
266
|
+
|
267
|
+
Parameters
|
268
|
+
----------
|
269
|
+
mean : array_like
|
270
|
+
Intensity of phasor coordinates.
|
271
|
+
real : array_like
|
272
|
+
Real component of phasor coordinates to be filtered.
|
273
|
+
Must have at least two harmonics in the first axis.
|
274
|
+
imag : array_like
|
275
|
+
Imaginary component of phasor coordinates to be filtered.
|
276
|
+
Must have at least two harmonics in the first axis.
|
277
|
+
sigma : float, optional
|
278
|
+
Significance level to test difference between two phasors.
|
279
|
+
Given in terms of the equivalent 1D standard deviations.
|
280
|
+
sigma=2 corresponds to ~95% (or 5%) significance.
|
281
|
+
levels : int, optional
|
282
|
+
Number of levels for wavelet decomposition.
|
283
|
+
Controls the maximum averaging area, which has a length of
|
284
|
+
:math:`2^level`.
|
285
|
+
harmonic : sequence of int or None, optional
|
286
|
+
Harmonics included in first axis of `real` and `imag`.
|
287
|
+
If None (default), the first axis of `real` and `imag` contains lower
|
288
|
+
harmonics starting at and increasing by one.
|
289
|
+
All harmonics must have a corresponding half or double harmonic.
|
290
|
+
skip_axis : int or sequence of int, optional
|
291
|
+
Axes in `mean` to exclude from filter.
|
292
|
+
By default, all axes except harmonics are included.
|
293
|
+
|
294
|
+
Returns
|
295
|
+
-------
|
296
|
+
mean : ndarray
|
297
|
+
Unchanged intensity of phasor coordinates.
|
298
|
+
real : ndarray
|
299
|
+
Filtered real component of phasor coordinates.
|
300
|
+
imag : ndarray
|
301
|
+
Filtered imaginary component of phasor coordinates.
|
302
|
+
|
303
|
+
Raises
|
304
|
+
------
|
305
|
+
ValueError
|
306
|
+
If `level` is less than 0.
|
307
|
+
The array shapes of `mean`, `real`, and `imag` do not match.
|
308
|
+
If `real` and `imag` have no harmonic axis.
|
309
|
+
Number of harmonics in `harmonic` is less than 2 or does not match
|
310
|
+
the first axis of `real` and `imag`.
|
311
|
+
Not all harmonics in `harmonic` have a corresponding half
|
312
|
+
or double harmonic.
|
313
|
+
|
314
|
+
References
|
315
|
+
----------
|
316
|
+
.. [1] Silberberg M, and Grecco H. `pawFLIM: reducing bias and
|
317
|
+
uncertainty to enable lower photon count in FLIM experiments
|
318
|
+
<https://doi.org/10.1088/2050-6120/aa72ab>`_.
|
319
|
+
*Methods Appl Fluoresc*, 5(2): 024016 (2017)
|
320
|
+
|
321
|
+
Examples
|
322
|
+
--------
|
323
|
+
Apply a pawFLIM wavelet filter with four significance levels (sigma)
|
324
|
+
and three decomposition levels:
|
325
|
+
|
326
|
+
>>> mean, real, imag = phasor_filter_pawflim(
|
327
|
+
... [[1, 1], [1, 1]],
|
328
|
+
... [[[0.5, 0.8], [0.5, 0.8]], [[0.2, 0.4], [0.2, 0.4]]],
|
329
|
+
... [[[0.5, 0.4], [0.5, 0.4]], [[0.4, 0.5], [0.4, 0.5]]],
|
330
|
+
... sigma=4,
|
331
|
+
... levels=3,
|
332
|
+
... harmonic=[1, 2],
|
333
|
+
... )
|
334
|
+
>>> mean
|
335
|
+
array([[1, 1],
|
336
|
+
[1, 1]])
|
337
|
+
>>> real
|
338
|
+
array([[[0.65, 0.65],
|
339
|
+
[0.65, 0.65]],
|
340
|
+
[[0.3, 0.3],
|
341
|
+
[0.3, 0.3]]])
|
342
|
+
>>> imag
|
343
|
+
array([[[0.45, 0.45],
|
344
|
+
[0.45, 0.45]],
|
345
|
+
[[0.45, 0.45],
|
346
|
+
[0.45, 0.45]]])
|
347
|
+
|
348
|
+
"""
|
349
|
+
from pawflim import pawflim # type: ignore[import-untyped]
|
350
|
+
|
351
|
+
if levels < 0:
|
352
|
+
raise ValueError(f'{levels=} < 0')
|
353
|
+
if levels == 0:
|
354
|
+
return numpy.asarray(mean), numpy.asarray(real), numpy.asarray(imag)
|
355
|
+
|
356
|
+
mean = numpy.asarray(mean, dtype=numpy.float64, copy=True)
|
357
|
+
real = numpy.asarray(real, dtype=numpy.float64, copy=True)
|
358
|
+
imag = numpy.asarray(imag, dtype=numpy.float64, copy=True)
|
359
|
+
|
360
|
+
if mean.shape != real.shape[-mean.ndim if mean.ndim else 1 :]:
|
361
|
+
raise ValueError(f'{mean.shape=} != {real.shape=}')
|
362
|
+
if real.shape != imag.shape:
|
363
|
+
raise ValueError(f'{real.shape=} != {imag.shape=}')
|
364
|
+
|
365
|
+
has_harmonic_axis = mean.ndim + 1 == real.ndim
|
366
|
+
if not has_harmonic_axis:
|
367
|
+
raise ValueError('no harmonic axis')
|
368
|
+
if harmonic is None:
|
369
|
+
harmonics, _ = parse_harmonic('all', real.shape[0])
|
370
|
+
else:
|
371
|
+
harmonics, _ = parse_harmonic(harmonic, None)
|
372
|
+
if len(harmonics) < 2:
|
373
|
+
raise ValueError(
|
374
|
+
'at least two harmonics required, ' f'got {len(harmonics)}'
|
375
|
+
)
|
376
|
+
if len(harmonics) != real.shape[0]:
|
377
|
+
raise ValueError(
|
378
|
+
'number of harmonics does not match first axis of real and imag'
|
379
|
+
)
|
380
|
+
|
381
|
+
mean = numpy.asarray(numpy.nan_to_num(mean, copy=False))
|
382
|
+
real = numpy.asarray(numpy.nan_to_num(real, copy=False))
|
383
|
+
imag = numpy.asarray(numpy.nan_to_num(imag, copy=False))
|
384
|
+
real *= mean
|
385
|
+
imag *= mean
|
386
|
+
|
387
|
+
mean_expanded = numpy.broadcast_to(mean, real.shape).copy()
|
388
|
+
original_mean_expanded = mean_expanded.copy()
|
389
|
+
real_filtered = real.copy()
|
390
|
+
imag_filtered = imag.copy()
|
391
|
+
|
392
|
+
_, axes = parse_skip_axis(skip_axis, mean.ndim, True)
|
393
|
+
|
394
|
+
for index in numpy.ndindex(
|
395
|
+
*(
|
396
|
+
real.shape[ax]
|
397
|
+
for ax in range(real.ndim)
|
398
|
+
if ax not in axes and ax != 0
|
399
|
+
)
|
400
|
+
):
|
401
|
+
index_list: list[int | slice] = list(index)
|
402
|
+
for ax in axes:
|
403
|
+
index_list = index_list[:ax] + [slice(None)] + index_list[ax:]
|
404
|
+
full_index = tuple(index_list)
|
405
|
+
|
406
|
+
processed_harmonics = set()
|
407
|
+
|
408
|
+
for h in harmonics:
|
409
|
+
if h in processed_harmonics and (
|
410
|
+
h * 4 in harmonics or h * 2 not in harmonics
|
411
|
+
):
|
412
|
+
continue
|
413
|
+
if h * 2 not in harmonics:
|
414
|
+
raise ValueError(
|
415
|
+
f'harmonic {h} does not have a corresponding half '
|
416
|
+
f'or double harmonic in {harmonics}'
|
417
|
+
)
|
418
|
+
n = harmonics.index(h)
|
419
|
+
n2 = harmonics.index(h * 2)
|
420
|
+
|
421
|
+
complex_phasor = numpy.empty(
|
422
|
+
(3, *original_mean_expanded[n][full_index].shape),
|
423
|
+
dtype=numpy.complex128,
|
424
|
+
)
|
425
|
+
complex_phasor[0] = original_mean_expanded[n][full_index]
|
426
|
+
complex_phasor[1] = real[n][full_index] + 1j * imag[n][full_index]
|
427
|
+
complex_phasor[2] = (
|
428
|
+
real[n2][full_index] + 1j * imag[n2][full_index]
|
429
|
+
)
|
430
|
+
|
431
|
+
complex_phasor = pawflim(
|
432
|
+
complex_phasor, n_sigmas=sigma, levels=levels
|
433
|
+
)
|
434
|
+
|
435
|
+
for i, idx in enumerate([n, n2]):
|
436
|
+
if harmonics[idx] in processed_harmonics:
|
437
|
+
continue
|
438
|
+
mean_expanded[idx][full_index] = complex_phasor[0].real
|
439
|
+
real_filtered[idx][full_index] = complex_phasor[i + 1].real
|
440
|
+
imag_filtered[idx][full_index] = complex_phasor[i + 1].imag
|
441
|
+
|
442
|
+
processed_harmonics.add(h)
|
443
|
+
processed_harmonics.add(h * 2)
|
444
|
+
|
445
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
446
|
+
real = numpy.asarray(numpy.divide(real_filtered, mean_expanded))
|
447
|
+
imag = numpy.asarray(numpy.divide(imag_filtered, mean_expanded))
|
448
|
+
|
449
|
+
return mean, real, imag
|
450
|
+
|
451
|
+
|
452
|
+
def phasor_threshold(
|
453
|
+
mean: ArrayLike,
|
454
|
+
real: ArrayLike,
|
455
|
+
imag: ArrayLike,
|
456
|
+
/,
|
457
|
+
mean_min: ArrayLike | None = None,
|
458
|
+
mean_max: ArrayLike | None = None,
|
459
|
+
*,
|
460
|
+
real_min: ArrayLike | None = None,
|
461
|
+
real_max: ArrayLike | None = None,
|
462
|
+
imag_min: ArrayLike | None = None,
|
463
|
+
imag_max: ArrayLike | None = None,
|
464
|
+
phase_min: ArrayLike | None = None,
|
465
|
+
phase_max: ArrayLike | None = None,
|
466
|
+
modulation_min: ArrayLike | None = None,
|
467
|
+
modulation_max: ArrayLike | None = None,
|
468
|
+
open_interval: bool = False,
|
469
|
+
detect_harmonics: bool = True,
|
470
|
+
**kwargs: Any,
|
471
|
+
) -> tuple[NDArray[Any], NDArray[Any], NDArray[Any]]:
|
472
|
+
"""Return phasor coordinates with values outside interval replaced by NaN.
|
473
|
+
|
474
|
+
Interval thresholds can be set for mean intensity, real and imaginary
|
475
|
+
coordinates, and phase and modulation.
|
476
|
+
Phasor coordinates smaller than minimum thresholds or larger than maximum
|
477
|
+
thresholds are replaced with NaN.
|
478
|
+
No threshold is applied by default.
|
479
|
+
NaNs in `mean` or any `real` and `imag` harmonic are propagated to
|
480
|
+
`mean` and all harmonics in `real` and `imag`.
|
481
|
+
|
482
|
+
Parameters
|
483
|
+
----------
|
484
|
+
mean : array_like
|
485
|
+
Intensity of phasor coordinates.
|
486
|
+
real : array_like
|
487
|
+
Real component of phasor coordinates.
|
488
|
+
imag : array_like
|
489
|
+
Imaginary component of phasor coordinates.
|
490
|
+
mean_min : array_like, optional
|
491
|
+
Lower threshold for mean intensity.
|
492
|
+
mean_max : array_like, optional
|
493
|
+
Upper threshold for mean intensity.
|
494
|
+
real_min : array_like, optional
|
495
|
+
Lower threshold for real coordinates.
|
496
|
+
real_max : array_like, optional
|
497
|
+
Upper threshold for real coordinates.
|
498
|
+
imag_min : array_like, optional
|
499
|
+
Lower threshold for imaginary coordinates.
|
500
|
+
imag_max : array_like, optional
|
501
|
+
Upper threshold for imaginary coordinates.
|
502
|
+
phase_min : array_like, optional
|
503
|
+
Lower threshold for phase angle.
|
504
|
+
phase_max : array_like, optional
|
505
|
+
Upper threshold for phase angle.
|
506
|
+
modulation_min : array_like, optional
|
507
|
+
Lower threshold for modulation.
|
508
|
+
modulation_max : array_like, optional
|
509
|
+
Upper threshold for modulation.
|
510
|
+
open_interval : bool, optional
|
511
|
+
If true, the interval is open, and the threshold values are
|
512
|
+
not included in the interval.
|
513
|
+
If false (default), the interval is closed, and the threshold values
|
514
|
+
are included in the interval.
|
515
|
+
detect_harmonics : bool, optional
|
516
|
+
By default, detect presence of multiple harmonics from array shapes.
|
517
|
+
If false, no harmonics are assumed to be present, and the function
|
518
|
+
behaves like a numpy universal function.
|
519
|
+
**kwargs
|
520
|
+
Optional `arguments passed to numpy universal functions
|
521
|
+
<https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
|
522
|
+
|
523
|
+
Returns
|
524
|
+
-------
|
525
|
+
mean : ndarray
|
526
|
+
Thresholded intensity of phasor coordinates.
|
527
|
+
real : ndarray
|
528
|
+
Thresholded real component of phasor coordinates.
|
529
|
+
imag : ndarray
|
530
|
+
Thresholded imaginary component of phasor coordinates.
|
531
|
+
|
532
|
+
Examples
|
533
|
+
--------
|
534
|
+
Set phasor coordinates to NaN if mean intensity is smaller than 1.1:
|
535
|
+
|
536
|
+
>>> phasor_threshold([1, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, 0.6], 1.1)
|
537
|
+
(array([nan, 2, 3]), array([nan, 0.2, 0.3]), array([nan, 0.5, 0.6]))
|
538
|
+
|
539
|
+
Set phasor coordinates to NaN if real component is smaller than 0.15 or
|
540
|
+
larger than 0.25:
|
541
|
+
|
542
|
+
>>> phasor_threshold(
|
543
|
+
... [1.0, 2.0, 3.0],
|
544
|
+
... [0.1, 0.2, 0.3],
|
545
|
+
... [0.4, 0.5, 0.6],
|
546
|
+
... real_min=0.15,
|
547
|
+
... real_max=0.25,
|
548
|
+
... )
|
549
|
+
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
550
|
+
|
551
|
+
Apply NaNs to other input arrays:
|
552
|
+
|
553
|
+
>>> phasor_threshold(
|
554
|
+
... [numpy.nan, 2, 3], [0.1, 0.2, 0.3], [0.4, 0.5, numpy.nan]
|
555
|
+
... )
|
556
|
+
(array([nan, 2, nan]), array([nan, 0.2, nan]), array([nan, 0.5, nan]))
|
557
|
+
|
558
|
+
"""
|
559
|
+
threshold_mean_only = None
|
560
|
+
if mean_min is None:
|
561
|
+
mean_min = numpy.nan
|
562
|
+
else:
|
563
|
+
threshold_mean_only = True
|
564
|
+
if mean_max is None:
|
565
|
+
mean_max = numpy.nan
|
566
|
+
else:
|
567
|
+
threshold_mean_only = True
|
568
|
+
if real_min is None:
|
569
|
+
real_min = numpy.nan
|
570
|
+
else:
|
571
|
+
threshold_mean_only = False
|
572
|
+
if real_max is None:
|
573
|
+
real_max = numpy.nan
|
574
|
+
else:
|
575
|
+
threshold_mean_only = False
|
576
|
+
if imag_min is None:
|
577
|
+
imag_min = numpy.nan
|
578
|
+
else:
|
579
|
+
threshold_mean_only = False
|
580
|
+
if imag_max is None:
|
581
|
+
imag_max = numpy.nan
|
582
|
+
else:
|
583
|
+
threshold_mean_only = False
|
584
|
+
if phase_min is None:
|
585
|
+
phase_min = numpy.nan
|
586
|
+
else:
|
587
|
+
threshold_mean_only = False
|
588
|
+
if phase_max is None:
|
589
|
+
phase_max = numpy.nan
|
590
|
+
else:
|
591
|
+
threshold_mean_only = False
|
592
|
+
if modulation_min is None:
|
593
|
+
modulation_min = numpy.nan
|
594
|
+
else:
|
595
|
+
threshold_mean_only = False
|
596
|
+
if modulation_max is None:
|
597
|
+
modulation_max = numpy.nan
|
598
|
+
else:
|
599
|
+
threshold_mean_only = False
|
600
|
+
|
601
|
+
if detect_harmonics:
|
602
|
+
mean = numpy.asarray(mean)
|
603
|
+
real = numpy.asarray(real)
|
604
|
+
imag = numpy.asarray(imag)
|
605
|
+
|
606
|
+
shape = numpy.broadcast_shapes(mean.shape, real.shape, imag.shape)
|
607
|
+
ndim = len(shape)
|
608
|
+
|
609
|
+
has_harmonic_axis = (
|
610
|
+
# detect multi-harmonic in axis 0
|
611
|
+
mean.ndim + 1 == ndim
|
612
|
+
and real.shape == shape
|
613
|
+
and imag.shape == shape
|
614
|
+
and mean.shape == shape[-mean.ndim if mean.ndim else 1 :]
|
615
|
+
)
|
616
|
+
else:
|
617
|
+
has_harmonic_axis = False
|
618
|
+
|
619
|
+
if threshold_mean_only is None:
|
620
|
+
mean, real, imag = _phasor_threshold_nan(mean, real, imag, **kwargs)
|
621
|
+
|
622
|
+
elif threshold_mean_only:
|
623
|
+
mean_func = (
|
624
|
+
_phasor_threshold_mean_open
|
625
|
+
if open_interval
|
626
|
+
else _phasor_threshold_mean_closed
|
627
|
+
)
|
628
|
+
mean, real, imag = mean_func(
|
629
|
+
mean, real, imag, mean_min, mean_max, **kwargs
|
630
|
+
)
|
631
|
+
|
632
|
+
else:
|
633
|
+
func = (
|
634
|
+
_phasor_threshold_open
|
635
|
+
if open_interval
|
636
|
+
else _phasor_threshold_closed
|
637
|
+
)
|
638
|
+
mean, real, imag = func(
|
639
|
+
mean,
|
640
|
+
real,
|
641
|
+
imag,
|
642
|
+
mean_min,
|
643
|
+
mean_max,
|
644
|
+
real_min,
|
645
|
+
real_max,
|
646
|
+
imag_min,
|
647
|
+
imag_max,
|
648
|
+
phase_min,
|
649
|
+
phase_max,
|
650
|
+
modulation_min,
|
651
|
+
modulation_max,
|
652
|
+
**kwargs,
|
653
|
+
)
|
654
|
+
|
655
|
+
mean = numpy.asarray(mean)
|
656
|
+
real = numpy.asarray(real)
|
657
|
+
imag = numpy.asarray(imag)
|
658
|
+
if has_harmonic_axis and mean.ndim > 0:
|
659
|
+
# propagate NaN to all dimensions
|
660
|
+
mean = numpy.mean(mean, axis=0, keepdims=True)
|
661
|
+
mask = numpy.where(numpy.isnan(mean), numpy.nan, 1.0)
|
662
|
+
numpy.multiply(real, mask, out=real)
|
663
|
+
numpy.multiply(imag, mask, out=imag)
|
664
|
+
# remove harmonic dimension created by broadcasting
|
665
|
+
mean = numpy.asarray(numpy.asarray(mean)[0])
|
666
|
+
|
667
|
+
return mean, real, imag
|
668
|
+
|
669
|
+
|
670
|
+
def signal_filter_svd(
|
671
|
+
signal: ArrayLike,
|
672
|
+
/,
|
673
|
+
spectral_vector: ArrayLike | None = None,
|
674
|
+
*,
|
675
|
+
axis: int = -1,
|
676
|
+
harmonic: int | Sequence[int] | Literal['all'] | str | None = None,
|
677
|
+
sigma: float = 0.05,
|
678
|
+
vmin: float | None = None,
|
679
|
+
dtype: DTypeLike | None = None,
|
680
|
+
num_threads: int | None = None,
|
681
|
+
) -> NDArray[Any]:
|
682
|
+
"""Return spectral-vector-denoised signal.
|
683
|
+
|
684
|
+
The spectral vector denoising algorithm is based on a Gaussian weighted
|
685
|
+
average calculation, with weights obtained in n-dimensional Chebyshev or
|
686
|
+
Fourier space [2]_.
|
687
|
+
|
688
|
+
Parameters
|
689
|
+
----------
|
690
|
+
signal : array_like
|
691
|
+
Hyperspectral data to be denoised.
|
692
|
+
A minimum of three samples are required along `axis`.
|
693
|
+
The samples must be uniformly spaced.
|
694
|
+
spectral_vector : array_like, optional
|
695
|
+
Spectral vector.
|
696
|
+
For example, phasor coordinates, PCA projected phasor coordinates,
|
697
|
+
or Chebyshev coefficients.
|
698
|
+
Must be of the same shape as `signal` with `axis` removed and an axis
|
699
|
+
containing spectral space appended.
|
700
|
+
If None (default), phasor coordinates are calculated at specified
|
701
|
+
`harmonic`.
|
702
|
+
axis : int, optional, default: -1
|
703
|
+
Axis over which `spectral_vector` is computed if not provided.
|
704
|
+
The default is the last axis (-1).
|
705
|
+
harmonic : int, sequence of int, or 'all', optional
|
706
|
+
Harmonics to include in calculating `spectral_vector`.
|
707
|
+
If `'all'`, include all harmonics for `signal` samples along `axis`.
|
708
|
+
Else, harmonics must be at least one and no larger than half the
|
709
|
+
number of `signal` samples along `axis`.
|
710
|
+
The default is the first harmonic (fundamental frequency).
|
711
|
+
A minimum of `harmonic * 2 + 1` samples are required along `axis`
|
712
|
+
to calculate correct phasor coordinates at `harmonic`.
|
713
|
+
sigma : float, default: 0.05
|
714
|
+
Width of Gaussian filter in spectral vector space.
|
715
|
+
Weighted averages are calculated using the spectra of signal items
|
716
|
+
within a spectral vector Euclidean distance of `3 * sigma` and
|
717
|
+
intensity above `vmin`.
|
718
|
+
vmin : float, optional
|
719
|
+
Signal intensity along `axis` below which spectra are excluded from
|
720
|
+
denoising.
|
721
|
+
dtype : dtype_like, optional
|
722
|
+
Data type of output arrays. Either float32 or float64.
|
723
|
+
The default is float64 unless the `signal` is float32.
|
724
|
+
num_threads : int, optional
|
725
|
+
Number of OpenMP threads to use for parallelization.
|
726
|
+
By default, multi-threading is disabled.
|
727
|
+
If zero, up to half of logical CPUs are used.
|
728
|
+
OpenMP may not be available on all platforms.
|
729
|
+
|
730
|
+
Returns
|
731
|
+
-------
|
732
|
+
ndarray
|
733
|
+
Denoised signal of `dtype`.
|
734
|
+
Spectra with integrated intensity below `vmin` are unchanged.
|
735
|
+
|
736
|
+
References
|
737
|
+
----------
|
738
|
+
.. [2] Harman RC, Lang RT, Kercher EM, Leven P, and Spring BQ.
|
739
|
+
`Denoising multiplexed microscopy images in n-dimensional spectral space
|
740
|
+
<https://doi.org/10.1364/BOE.463979>`_.
|
741
|
+
*Biomed Opt Express*, 13(8): 4298-4309 (2022)
|
742
|
+
|
743
|
+
Notes
|
744
|
+
-----
|
745
|
+
This implementation is considered experimental. It is not validated
|
746
|
+
against the reference implementation and may not be practical with
|
747
|
+
real-world data. See discussion in `issue #142
|
748
|
+
<https://github.com/phasorpy/phasorpy/issues/142#issuecomment-2499421491>`_.
|
749
|
+
|
750
|
+
Examples
|
751
|
+
--------
|
752
|
+
Denoise a hyperspectral image with a Gaussian filter width of 0.1 in
|
753
|
+
spectral vector space using first and second harmonic:
|
754
|
+
|
755
|
+
>>> signal = numpy.random.randint(0, 255, (8, 16, 16))
|
756
|
+
>>> signal_filter_svd(signal, axis=0, sigma=0.1, harmonic=[1, 2])
|
757
|
+
array([[[...]]])
|
758
|
+
|
759
|
+
"""
|
760
|
+
num_threads = number_threads(num_threads)
|
761
|
+
|
762
|
+
signal = numpy.asarray(signal)
|
763
|
+
if axis == -1 or axis == signal.ndim - 1:
|
764
|
+
axis = -1
|
765
|
+
else:
|
766
|
+
signal = numpy.moveaxis(signal, axis, -1)
|
767
|
+
shape = signal.shape
|
768
|
+
samples = shape[-1]
|
769
|
+
|
770
|
+
if harmonic is None:
|
771
|
+
harmonic = 1
|
772
|
+
harmonic, _ = parse_harmonic(harmonic, samples // 2)
|
773
|
+
num_harmonics = len(harmonic)
|
774
|
+
|
775
|
+
if vmin is None or vmin < 0.0:
|
776
|
+
vmin = 0.0
|
777
|
+
|
778
|
+
signal = numpy.ascontiguousarray(signal).reshape(-1, samples)
|
779
|
+
size = signal.shape[0]
|
780
|
+
|
781
|
+
if dtype is None:
|
782
|
+
if signal.dtype.char == 'f':
|
783
|
+
dtype = signal.dtype
|
784
|
+
else:
|
785
|
+
dtype = numpy.float64
|
786
|
+
dtype = numpy.dtype(dtype)
|
787
|
+
if dtype.char not in {'d', 'f'}:
|
788
|
+
raise ValueError('dtype is not floating point')
|
789
|
+
|
790
|
+
if spectral_vector is None:
|
791
|
+
sincos = numpy.empty((num_harmonics, samples, 2))
|
792
|
+
for i, h in enumerate(harmonic):
|
793
|
+
phase = numpy.linspace(
|
794
|
+
0,
|
795
|
+
h * math.pi * 2.0,
|
796
|
+
samples,
|
797
|
+
endpoint=False,
|
798
|
+
dtype=numpy.float64,
|
799
|
+
)
|
800
|
+
sincos[i, :, 0] = numpy.cos(phase)
|
801
|
+
sincos[i, :, 1] = numpy.sin(phase)
|
802
|
+
spectral_vector = numpy.zeros((size, num_harmonics * 2), dtype=dtype)
|
803
|
+
|
804
|
+
_phasor_from_signal_vector(
|
805
|
+
spectral_vector, signal, sincos, num_threads
|
806
|
+
)
|
807
|
+
else:
|
808
|
+
spectral_vector = numpy.ascontiguousarray(spectral_vector, dtype=dtype)
|
809
|
+
if spectral_vector.shape[:-1] != shape[:-1]:
|
810
|
+
raise ValueError('signal and spectral_vector shape mismatch')
|
811
|
+
spectral_vector = spectral_vector.reshape(
|
812
|
+
-1, spectral_vector.shape[-1]
|
813
|
+
)
|
814
|
+
|
815
|
+
if dtype == signal.dtype:
|
816
|
+
denoised = signal.copy()
|
817
|
+
else:
|
818
|
+
denoised = numpy.zeros(signal.shape, dtype=dtype)
|
819
|
+
denoised[:] = signal
|
820
|
+
integrated = numpy.zeros(size, dtype=dtype)
|
821
|
+
_signal_denoise_vector(
|
822
|
+
denoised, integrated, signal, spectral_vector, sigma, vmin, num_threads
|
823
|
+
)
|
824
|
+
|
825
|
+
denoised = denoised.reshape(shape) # type: ignore[assignment]
|
826
|
+
if axis != -1:
|
827
|
+
denoised = numpy.moveaxis(denoised, -1, axis)
|
828
|
+
return denoised
|
829
|
+
|
830
|
+
|
831
|
+
def signal_filter_ncpca(
|
832
|
+
signal: ArrayLike,
|
833
|
+
/,
|
834
|
+
n_components: int | float | str | None = 3,
|
835
|
+
*,
|
836
|
+
axis: int = -1,
|
837
|
+
dtype: DTypeLike | None = None,
|
838
|
+
**kwargs: Any,
|
839
|
+
) -> NDArray[Any]:
|
840
|
+
"""Return signal filtered by noise-corrected principal component analysis.
|
841
|
+
|
842
|
+
Apply noise-corrected Principal Component Analysis (NC-PCA) to denoise
|
843
|
+
signal containing shot noise. The signal is Poisson-normalized,
|
844
|
+
its dimensionality reduced by PCA with a specified number of components,
|
845
|
+
and then reconstructed according to [3]_.
|
846
|
+
|
847
|
+
Parameters
|
848
|
+
----------
|
849
|
+
signal : array_like
|
850
|
+
Data containing Poisson noise to be filtered.
|
851
|
+
Must have at least 3 samples along the specified `axis`.
|
852
|
+
n_components : int, float, or str, optional, default: 3
|
853
|
+
Number of principal components to retain.
|
854
|
+
The default is 3, matching the reference implementation.
|
855
|
+
If None, all components are kept (no denoising).
|
856
|
+
If 'mle', use Minka's MLE to guess the dimension.
|
857
|
+
If 0 < n_components < 1 and svd_solver == 'full', select the number
|
858
|
+
of components such that the amount of variance that needs to be
|
859
|
+
explained is greater than the percentage specified by n_components.
|
860
|
+
See `sklearn.decomposition.PCA
|
861
|
+
<https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html>`_
|
862
|
+
for more details.
|
863
|
+
axis : int, optional, default: -1
|
864
|
+
Axis containing PCA features, for example, FLIM histogram bins.
|
865
|
+
The default is the last axis (-1).
|
866
|
+
Other axes are flattened and used as PCA samples.
|
867
|
+
dtype : dtype_like, optional
|
868
|
+
Data type of computation and output arrays. Either float32 or float64.
|
869
|
+
The default is float64 unless the input `signal` is float32.
|
870
|
+
**kwargs
|
871
|
+
Optional arguments passed to :py:class:`sklearn.decomposition.PCA`.
|
872
|
+
|
873
|
+
Returns
|
874
|
+
-------
|
875
|
+
ndarray
|
876
|
+
Denoised signal of specified `dtype`. Values may be negative.
|
877
|
+
Values, where the signal mean along `axis` is zero, are set to NaN.
|
878
|
+
|
879
|
+
Raises
|
880
|
+
------
|
881
|
+
ValueError
|
882
|
+
If `dtype` is not a floating point type.
|
883
|
+
If `signal` has fewer than 3 samples along specified axis.
|
884
|
+
If `n_components` is invalid for the data size.
|
885
|
+
|
886
|
+
References
|
887
|
+
----------
|
888
|
+
.. [3] Soltani S, Paulson J, Fong E, Mumenthaler, S, and Armani A.
|
889
|
+
`Denoising of fluorescence lifetime imaging data via principal
|
890
|
+
component analysis <https://doi.org/10.21203/rs.3.rs-7143126/v1>`_.
|
891
|
+
*Preprint*, (2025)
|
892
|
+
|
893
|
+
Notes
|
894
|
+
-----
|
895
|
+
Intensities of the reconstructed signal may be negative.
|
896
|
+
Hence, the phasor coordinates calculated from the reconstructed signal
|
897
|
+
may be outside the unit circle.
|
898
|
+
Consider thresholding low intensities for further analysis.
|
899
|
+
|
900
|
+
Examples
|
901
|
+
--------
|
902
|
+
Denoise FLIM data using 3 principal components:
|
903
|
+
|
904
|
+
>>> signal = numpy.random.poisson(100, (32, 32, 64))
|
905
|
+
>>> denoised = signal_filter_ncpca(signal, n_components=3)
|
906
|
+
>>> denoised.shape
|
907
|
+
(32, 32, 64)
|
908
|
+
|
909
|
+
"""
|
910
|
+
from sklearn.decomposition import PCA
|
911
|
+
|
912
|
+
if (
|
913
|
+
dtype is None
|
914
|
+
and isinstance(signal, numpy.ndarray)
|
915
|
+
and signal.dtype == numpy.float32
|
916
|
+
):
|
917
|
+
signal = signal.copy()
|
918
|
+
dtype = signal.dtype
|
919
|
+
else:
|
920
|
+
dtype = numpy.dtype(dtype)
|
921
|
+
if dtype.char not in {'f', 'd'}:
|
922
|
+
raise ValueError(f'{dtype=} is not a floating point type')
|
923
|
+
signal = numpy.asarray(signal, dtype=dtype, copy=True)
|
924
|
+
|
925
|
+
if axis == -1 or axis == signal.ndim - 1:
|
926
|
+
axis = -1
|
927
|
+
else:
|
928
|
+
signal = numpy.moveaxis(signal, axis, -1)
|
929
|
+
|
930
|
+
shape = signal.shape
|
931
|
+
|
932
|
+
if signal.size == 0:
|
933
|
+
raise ValueError('signal array is empty')
|
934
|
+
if signal.shape[-1] < 3:
|
935
|
+
raise ValueError(f'{signal.shape[-1]=} < 3')
|
936
|
+
|
937
|
+
# flatten sample dimensions
|
938
|
+
signal = signal.reshape(-1, shape[-1])
|
939
|
+
|
940
|
+
# poisson-normalize signal
|
941
|
+
scale = numpy.sqrt(numpy.nanmean(signal, axis=0, keepdims=True))
|
942
|
+
with numpy.errstate(divide='ignore', invalid='ignore'):
|
943
|
+
signal /= scale
|
944
|
+
|
945
|
+
# replace NaN and infinite values
|
946
|
+
mask = numpy.logical_or(
|
947
|
+
numpy.any(numpy.isnan(signal), axis=-1),
|
948
|
+
numpy.any(numpy.isinf(signal), axis=-1),
|
949
|
+
)
|
950
|
+
assert isinstance(signal, numpy.ndarray) # for Mypy
|
951
|
+
signal[mask] = 0.0
|
952
|
+
|
953
|
+
# PCA transform and reconstruction with n_components
|
954
|
+
pca = PCA(n_components, **kwargs)
|
955
|
+
signal = pca.inverse_transform(pca.fit_transform(signal))
|
956
|
+
del pca
|
957
|
+
|
958
|
+
# restore original scale and shape
|
959
|
+
assert isinstance(signal, numpy.ndarray) # for Mypy
|
960
|
+
signal[mask] = numpy.nan
|
961
|
+
signal *= scale
|
962
|
+
signal = signal.reshape(shape)
|
963
|
+
if axis != -1:
|
964
|
+
signal = numpy.moveaxis(signal, -1, axis)
|
965
|
+
|
966
|
+
return signal
|